MVC Design Pattern

The MVC design pattern was conceived in 1979 at Xerox, and since then many variants have evolved. All web frameworks, today, support some variant of the MVC pattern. But the MVC pattern has been largely ignored in GUI application development. This article describes one variant of the MVC pattern and how it can be used in GUI application development.

What are Patterns?

Software development involves writing programs to solve problems. And when developers are faced with a complex problem, they break the problem into simpler ones and solve the simpler ones, using sub-programs. The sub-programs collectively solve the complex problem.

This is very similar to what we do in mathematics. When we are faced with the mathematical problem `99 * 5`, we have been taught to break it down into `(100 - 1) * 5`. Now instead of one big problem we have three smaller problems:

  • `100 * 5`

  • `1 * 5`

  • `500 - 5`

Now for problems like multiplication it is easy to figure, how the problem should be broken down. But with problems that software developers come across, it is really hard to figure out, how to break down the problem. And many a time developers break the problem, and end up with more complex problems! This is similar to breaking down the above multiplication problem into `(93 + 6) * 5`! This is exactly what software developers end up doing, without even realizing it. And by the time they realize it, it is just too late.

As the software industry matured, software developers gradually figured out ways to break down problems, that result in smaller problems. These ways to break down problems have been documented and shared with other developers, and are called design patterns.

Model, View, Controller

The MVC pattern presented here is roughly based upon the MVC pattern used in Cocoa.

The model, is the data represented by one or more objects, along with methods to manipulate the data.

The view, is the GUI form/window/dialog that user interacts with. The view displays the data from the model, and allows the user to change the content of the model.

The controller, acts as an intermediate between the view and the model. The view, through the controller, retreives data from the model, and modifies the data present in the model. User actions on the view result in invocation of methods on the controller.

The interaction between the model, view and the controller is shown in the following diagram.

http://www.zilogic.com/static/images/mvc-pattern.png
Figure 1. Interaction between Model, View, Controller

In our variant of the MVC pattern, the controller does not directly invoke the view’s methods, to update it. The view always pulls data from the controller. The controller does not push data to the view. One of the advantages with this approach is that, it enables easy remoting of the controller in the future. The controller could be a web service, and the view could be a GUI app or javascript enabled web page.

The only way the controller invokes the view is through an event notification mechanism. This is used to notify the view that it needs to be refreshed. This is especially useful when there are multiple views, an update performed in one view, triggers an event notification to the other views. When converting to a web service, the event notifications could be polled by the views.

Address Book Example

We demonstrate the MVC design pattern using an address book example, written in Python. The address book allows the user to perform the following operations.

  • View existing contacts.

  • Modify exisiting contacts.

  • Add new contacts.

  • Delete contacts.

Model

The contact information is stored in a XML file. An example file is shown below.

<abook>
  <contact>
    <name>Alex</name>
    <email>alex@gmail.com</email>
    <phone>9444123432</phone>
  </contact>

  <contact>
    <name>Bill</name>
    <email>bill@yahoo.com</email>
    <phone>9444246321</phone>
  </contact>

  <contact>
    <name>Cindy</name>
    <email>cindy@mark.com</email>
    <phone>9444892145</phone>
  </contact>
</abook>

The XML file is parsed and represented in memory as a tree of tags. The parsing of the XML file into the in-memory tree representation is done using lxml. This tree will be used as the model. XPath expressions are used for querying and modifying the tree.

View

Two views are provided - a list view and a form view. The views are created using the wxPython.

The list view, displays a list of names in the address book. The user can select a name and the details are displayed in the form view.

http://www.zilogic.com/static/images/abook-list.png
Figure 2. List View Screenshot

The view is a form based view, that displays one contact and allows the user to modify it. The user can navigate to previous and next contact using the buttons on the toolbar. The toolbar also has buttons for add new contacts, deleting contacts, and saving it back to a file.

http://www.zilogic.com/static/images/abook-form.png
Figure 3. Form View Screeshot

Controller

The controller provides methods for various actions that the user can perform. The list of actions and the corresponding methods in the controller is listed in the following table.

Action Controller Method

Display current contact

get_contact()

Move to next contact

next_contact()

Move to prev contact

prev_contact()

Add new contact

new_contact()

Delete current contact

del_contact()

Modify current contact

update_contact()

Save to address book

save_abook()

The controller internally maintains a cursor that has the index of the currently displayed contact. The get_contact() method is used to retreive a single contact information, corresponding to the cursor position. The view after obtaining the information fills up the text controls with the information.

In keeping up with the pull model of the view, the next_contact() and prev_contact() only update the cursor. The view then invokes get_contact() to retrieve the current contact information.

The controller also notifies the views, through a callback mechanism, when the model is updated. For example, when a contact is deleted in the form view, the list view is notified and the list view removes the contact name from its list.

Advantages

The MVC design provides the following advantages:

Unit Testing

Unit testing code in GUI applications has always been a challenge. But with an MVC design, since the UI is completely isolated from the Controller into the Views, the Controller can be easily unit tested.

Orthogonality

Orthogonality is the ability to change one part of the code with little impact on the other parts of the code. The separation into Model - View - Controller offers a high degree of orthogonality. For example, if the View is written in GTK+, and it needs to moved to Qt, the View can be completely re-written without affecting the Controller and the Model.

Source Code

The complete source code for the address book application, with the definition of the View and Controller classes is listed below.

import wx
import lxml.etree as etree
from lxml.etree import ETXPath as XPath

class Event(object):
    CURSOR_UPDATED = 0
    CONTACT_ADDED = 1
    CONTACT_DELETED = 2
    CONTACT_MODIFIED = 3

class Controller(object):
    def __init__(self, model):
        self.model = model
        self.views = []
        self.contact_by_id = XPath("/abook/contact[$id]")
        self.contact_names = XPath("/abook/contact/name/text()")
        self.count_persons = XPath("count(/abook/contact)")
        self.count = int(self.count_persons(self.model))
        self.home_contact()

    def get_cursor(self, view_id=None):
        return self.cursor

    def set_cursor(self, pos, view_id=None):
        self.cursor = pos
        self.notify((Event.CURSOR_UPDATED, view_id))

    def get_count(self, view_id=None):
        return self.count

    def get_list(self, view_id=None):
        return self.contact_names(self.model)

    def get_contact(self, view_id=None):
        if self.cursor == -1:
            return { "name": "", "email": "", "phone": "" }

        contact = self.contact_by_id(self.model, id=self.cursor)
        contact = contact[0]

        name = contact.find("name").text
        email = contact.find("email").text
        phone = contact.find("phone").text
        return { "name": name, "email": email, "phone": phone }

    def new_contact(self, view_id=None):
        root = self.model.getroot()
        contact = etree.SubElement(root, "contact")
        name = etree.SubElement(contact, "name")
        name.text = ""
        email = etree.SubElement(contact, "email")
        email.text = ""
        phone = etree.SubElement(contact, "phone")
        phone.text = ""
        self.count = self.count + 1
        self.cursor = self.count
        self.notify((Event.CONTACT_ADDED, view_id, self.cursor))
        self.notify((Event.CURSOR_UPDATED, view_id))

    def home_contact(self, view_id=None):
        if self.count > 0:
            self.cursor = 1
        else:
            self.cursor = -1
        self.notify((Event.CURSOR_UPDATED, view_id))

    def end_contact(self, view_id=None):
        if self.count > 0:
            self.cursor = self.count
        else:
            self.cursor = -1
        self.notify((Event.CURSOR_UPDATED, view_id))

    def next_contact(self, view_id=None):
        if self.cursor == -1:
            return

        if self.cursor < self.count:
            self.cursor = self.cursor + 1

        self.notify((Event.CURSOR_UPDATED, view_id))

    def prev_contact(self, view_id=None):
        if self.cursor == -1:
            return

        if self.cursor > 1:
            self.cursor = self.cursor - 1

        self.notify((Event.CURSOR_UPDATED, view_id))

    def del_contact(self, view_id=None):
        if self.cursor == -1:
            return

        index = self.cursor
        contact = self.contact_by_id(self.model, id=index)
        contact = contact[0]
        parent = contact.getparent()
        parent.remove(contact)
        self.count = self.count - 1
        if self.count == 0:
            self.cursor = -1

        if self.cursor > self.count:
            self.cursor = self.count
        self.notify((Event.CONTACT_DELETED, view_id, index))
        self.notify((Event.CURSOR_UPDATED, view_id))

    def update_contact(self, contact, view_id=None):
        if self.cursor == -1:
            return

        contact_elem = self.contact_by_id(self.model, id=self.cursor)
        contact_elem = contact_elem[0]

        contact_elem.find("name").text = contact["name"]
        contact_elem.find("email").text = contact["email"]
        contact_elem.find("phone").text = contact["phone"]

        self.notify((Event.CONTACT_MODIFIED, view_id, self.cursor))

    def save_abook(self, view_id=None):
        print etree.tostring(self.model)

    def register_view(self, view):
        self.views.append(view)
        return len(self.views) - 1

    def unregister_view(self, view_id):
        self.views[view_id] = None

    def notify(self, event):
        for view in self.views:
            if view != None:
                view.handle_event(event)

class ListView(object):
    def __init__(self, controller):
        self.controller = controller
        self.view_id = self.controller.register_view(self)
        self.frame = wx.Frame(None, title="Address Book - List View")
        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.frame.SetSizer(self.sizer)

        self.list = wx.ListBox(self.frame)
        self.sizer.Add(self.list, 1, flag=wx.EXPAND)
        self.list.AppendItems(self.controller.get_list(self.view_id))
        self.list.Bind(wx.EVT_LISTBOX, self.__on_select)

        self.ignore = False
        self.__select_from_cursor()

        self.frame.Show()

    def __select_from_cursor(self):
        self.ignore = True
        self.list.Select(self.controller.get_cursor(self.view_id) - 1)
        self.ignore = False

    def __on_select(self, event):
        if self.ignore:
            return

        index = self.list.GetSelection()
        self.controller.set_cursor(index + 1, self.view_id)

    def handle_event(self, event):
        if event[1] == self.view_id:
            return

        if event[0] == Event.CURSOR_UPDATED:
            self.__select_from_cursor()

        elif event[0] == Event.CONTACT_ADDED:
            index = event[2] - 1
            contact = self.controller.get_contact(self.view_id)
            self.list.Insert(contact["name"], index)

        elif event[0] == Event.CONTACT_DELETED:
            index = event[2] - 1
            self.ignore = True
            self.list.Delete(index)
            self.ignore = False

        elif event[0] == Event.CONTACT_MODIFIED:
            index = event[2] - 1
            contact = self.controller.get_contact(self.view_id)
            self.list.SetString(index, contact["name"])

class FormView(object):
    def __init__(self, controller):
        self.controller = controller
        self.view_id = self.controller.register_view(self)
        self.ignore = False
        self.frame = wx.Frame(None, title="Address Book - Form View")
        self.sizer = wx.GridBagSizer()
        self.frame.SetSizer(self.sizer)
        self.fields = {}
        self.toolbar = self.frame.CreateToolBar()

        self.__create_field("name", "Name: ", 0)
        self.__create_field("email", "Email: ", 1)
        self.__create_field("phone", "Phone: ", 2)
        self.sizer.AddGrowableCol(1, 1)

        self.__create_tool("Save", wx.ART_FILE_SAVE, self.__on_save)
        self.__create_tool("Add", wx.ART_NEW, self.__on_add)
        self.__create_tool("Delete", wx.ART_DELETE, self.__on_delete)
        self.__create_tool("Previous", wx.ART_GO_BACK, self.__on_prev)
        self.__create_tool("Next", wx.ART_GO_FORWARD,  self.__on_next)

        self.__pull()

        self.frame.Show()
        self.frame.Fit()

    def __create_field(self, tag, label, row):
        statictext = wx.StaticText(self.frame, label=label)
        textctrl = wx.TextCtrl(self.frame)
        textctrl.Bind(wx.EVT_TEXT, self.__on_update)

        self.sizer.Add(statictext, pos=(row, 0), span=(1, 1),
                       flag=wx.EXPAND | wx.ALL, border=5)
        self.sizer.Add(textctrl, pos=(row, 1), span=(1, 1),
                       flag=wx.EXPAND | wx.ALL, border=5)

        self.fields[tag] = textctrl

    def __create_tool(self, label, bitmap_id, callback):
        bitmap = wx.ArtProvider.GetBitmap(bitmap_id, wx.ART_TOOLBAR)
        tool = self.toolbar.AddLabelTool(wx.ID_ANY, label, bitmap)
        self.toolbar.Bind(wx.EVT_TOOL, callback, tool)

    def __pull(self):
        contact = self.controller.get_contact(self.view_id)
        for tag, textctrl in self.fields.iteritems():
            self.ignore = True
            textctrl.SetValue(contact[tag])
            self.ignore = False

    def __on_save(self, event):
        self.controller.save_abook(self.view_id)

    def __on_add(self, event):
        self.controller.new_contact(self.view_id)
        self.__pull()

    def __on_next(self, event):
        self.controller.next_contact(self.view_id)
        self.__pull()

    def __on_prev(self, event):
        self.controller.prev_contact(self.view_id)
        self.__pull()

    def __on_delete(self, event):
        self.controller.del_contact(self.view_id)
        self.__pull()

    def __on_update(self, event):
        if self.ignore:
            return

        contact = {}
        for tag, textctrl in self.fields.iteritems():
            contact[tag] = textctrl.GetValue()
        self.controller.update_contact(contact, self.view_id)

    def handle_event(self, event):
        if event[1] == self.view_id:
            return

        if event[0] == Event.CURSOR_UPDATED:
            self.__pull()

if __name__ == "__main__":
    app = wx.App()
    model = etree.parse("abook.xml")
    controller = Controller(model)
    form_view = FormView(controller)
    list_view = ListView(controller)
    app.MainLoop()