Introduction

This page describes the MVC (ModelViewController) design pattern and shows how to implement it in Python.

You can read more about MVC at http://c2.com/cgi/wiki?ModelViewController and you should also check out a variation on this pattern at the ModelViewPresenter page.

This feature in wxPython has been branched off into its own full fledged Pipy package. PyPubSub

The module was identical to the wxPython wx.lib.pubsub but has now grown further. The wx.lib.pubsub library is maintained ONLY for backward compatibility. Please consider using the new Package on Pypi. There are new example listed below that are from the Github repository. Thanks to Oliver Schoenborn.

Theory

(orignally posted at http://mail.python.org/pipermail/python-list/2006-January/319314.html by has)

MVC is all about separation of concerns.

The Model is responsible for managing the program's data (both private and client data). The View/Controller is responsible for providing the outside world with the means to interact with the program's client data.

The Model provides an internal interface (API) to enable other parts of the program to interact with it. The View/Controller provides an external interface (GUI/CLI/web form/high-level IPC/etc.) to enable everything outwith the program to communicate with it.

The Model is responsible for maintaining the integrity of the program's data, because if that gets corrupted then it's game over for everyone. The View/Controller is responsible for maintaining the integrity of the UI, making sure all text views are displaying up-to-date values, disabling menu items that don't apply to the current focus, etc.

The Model contains no View/Controller code; no GUI widget classes, no code for laying out dialog boxes or receiving user input. The View/Controller contains no Model code; no code for validating URLs or performing SQL queries, and no original state either: any data held by widgets is for display purposes only, and merely a reflection of the true data stored in the Model.

Now, here's the test of a true MVC design: the program should in essence be fully functional even without a View/Controller attached. OK, the outside world will have trouble interacting with it in that form, but as long as one knows the appropriate Model API incantations, the program will hold and manipulate data as normal.

Why is this possible? Well, the simple answer is that it's all thanks to the low coupling between the Model and View/Controller layers. However, this isn't the full story. What's key to the whole MVC pattern is the _direction_ in which those connection goes: ALL instructions flow _from_ the View/Controller _to_ the Model. The Model NEVER tells the View/Controller what to do.

Why? Because in MVC, while the View/Controller is permitted to know a little about the Model (specifically, the Model's API), but the Model is not allowed to know anything whatsoever about the View/Controller.

Why? Because MVC is about creating a clear separation of concerns.

Why? To help prevent program complexity spiralling out of control and burying you, the developer, under it. The bigger the program, the greater the number of components in that program. And the more connections exist between those components, the harder it is for developers to maintain/extend/replace individual components, or even just follow how the whole system works. Ask yourself this: when looking at a diagram of the program's structure, would you rather see a tree or a cat's cradle? The MVC pattern avoids the latter by disallowing circular connections: B can connect to A, but A cannot connect to B. In this case, A is the Model and B is the View/Controller.

BTW, if you're sharp, you'll notice a problem with the 'one-way' restriction just described: how can the Model inform the View/Controller of changes in the Model's user data when the Model isn't even allowed to know that the View/Controller exists, never mind send messages to it? But don't worry: there is a solution to this, and it's rather neat even if it does seem a bit roundabout at first. We'll get back to that in a moment.

In practical terms, then, a View/Controller object may, via the Model's API, 1. tell the Model to do things (execute commands), and 2. tell the Model to give it things (return data). The View/Controller layer *pushes instructions* to the Model layer and *pulls information* from the Model layer.

A MyCoolListControl class for example, should pull the data it needs from the layer below, when it needs it. In the case of a list widget, that generally means asking how many values there are and then asking for each of those items in turn, because that's about the simplest and loosest way to do it and therefore keeps what coupling there is to a minimum. And if the widget wants, say, to present those values to the user in nice alphabetical order then that's its perogative; and its responsibility, of course.

Now, one last conundrum, as I hinted at earlier: how do you keep the UI's display synchronised with the Model's state in an MVC-based system?

Here's the problem: many View objects are stateful, e.g. a checkbox may be ticked or unticked, a text field may contain some editable text. However, MVC dictates that all user data be stored in the Model layer, so any data held by other layers for display purposes (the checkbox's state, the text field's current text) must therefore be a subsidiary copy of that primary Model data. But if the Model's state changes, the View's copy of that state will no longer be accurate and needs to be refreshed.

But how? The MVC pattern prevents the Model pushing a fresh copy of that information into the View layer. Heck, it doesn't even allow the Model to send the View a message to say its state has changed.

Well, almost. Okay, the Model layer isn't allowed to talk directly to other layers, since to do so would require it knows something about those layers, and MVC rules prevent that. However, if a tree falls in a forest and nobody's around to hear it, does it make a sound?

The answer is to set up a notifications system, providing the Model layer with a place it can announce to no-one in particular that it has just done something interesting. Other layers can then post listeners with that notification system to listen for those announcements that they're actually interested in. The Model layer doesn't need to know anything about who's listening (or even if anyone is listening at all!); it just posts an announcement and then forgets about it. And if anyone hears that announcement and feels like doing something afterwards - like asking the Model for some new data so it can update its on-screen display - then great. The Model just lists what notifications it sends as part of its API definition; and what anyone else does with that knowledge is up to them.

MVC is preserved, and everyone is happy. Your application framework may well provide a built-in notifications system, or you can write your own if not (see the 'observer pattern').

One approach is to use wx.lib.pubsub as the method of communication. The below code sample is a rewrite of the sample below it, except that it uses pubsub instead of the Observable class.

Example using PyPubSub

wx_main.py

   1 """
   2 Adapted from wxPython website at http://wiki.wxpython.org/ModelViewController/.
   3 :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
   4 :license: BSD, see LICENSE.txt for details.
   5 """
   6 
   7 import wx
   8 
   9 from pubsub import pub
  10 
  11 print('pubsub API version', pub.VERSION_API)
  12 
  13 # notification
  14 from pubsub.utils.notification import useNotifyByWriteFile
  15 import sys
  16 
  17 useNotifyByWriteFile(sys.stdout)
  18 
  19 # the following two modules don't know about each other yet will
  20 # exchange data via pubsub:
  21 from wx_win1 import View
  22 from wx_win2 import ChangerWidget
  23 
  24 
  25 class Model:
  26     def __init__(self):
  27         self.myMoney = 0
  28 
  29     def addMoney(self, value):
  30         self.myMoney += value
  31         # now tell anyone who cares that the value has been changed
  32         pub.sendMessage("money_changed", money=self.myMoney)
  33 
  34     def removeMoney(self, value):
  35         self.myMoney -= value
  36         # now tell anyone who cares that the value has been changed
  37         pub.sendMessage("money_changed", money=self.myMoney)
  38 
  39 
  40 class Controller:
  41     def __init__(self):
  42         self.model = Model()
  43 
  44         # set up the first frame which displays the current Model value
  45         self.view1 = View()
  46         self.view1.setMoney(self.model.myMoney)
  47 
  48         # set up the second frame which allows the user to modify the Model's value
  49         self.view2 = ChangerWidget()
  50 
  51         self.view1.Show()
  52         self.view2.Show()
  53 
  54         pub.subscribe(self.changeMoney, 'money_changing')
  55 
  56     def changeMoney(self, amount):
  57         if amount >= 0:
  58             self.model.addMoney(amount)
  59         else:
  60             self.model.removeMoney(-amount)
  61 
  62 
  63 if __name__ == "__main__":
  64     app = wx.App()
  65     c = Controller()
  66     sys.stdout = sys.__stdout__
  67 
  68     print('---- Starting main event loop ----')
  69     app.MainLoop()
  70     print('---- Exited main event loop ----')

wx_win1.py

   1 """
   2 :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
   3 :license: BSD, see LICENSE.txt for details.
   4 """
   5 
   6 import wx
   7 from pubsub import pub
   8 
   9 
  10 class View(wx.Frame):
  11     def __init__(self, parent=None):
  12         wx.Frame.__init__(self, parent, -1, "Main View")
  13 
  14         sizer = wx.BoxSizer(wx.VERTICAL)
  15         text = wx.StaticText(self, -1, "My Money")
  16         ctrl = wx.TextCtrl(self, -1, "")
  17         sizer.Add(text, 0, wx.EXPAND | wx.ALL)
  18         sizer.Add(ctrl, 0, wx.EXPAND | wx.ALL)
  19 
  20         self.moneyCtrl = ctrl
  21         ctrl.SetEditable(False)
  22         self.SetSizer(sizer)
  23 
  24         # subscribe to all "MONEY CHANGED" messages from the Model
  25         # to subscribe to ALL messages (topics), omit the second argument below
  26         pub.subscribe(self.setMoney, "money_changed")
  27 
  28     def setMoney(self, money):
  29         self.moneyCtrl.SetValue(str(money))

wx_win2.py

   1 """
   2 Widget from which money can be added or removed from account.
   3 :copyright: Copyright since 2006 by Oliver Schoenborn, all rights reserved.
   4 :license: BSD, see LICENSE.txt for details.
   5 """
   6 
   7 import wx
   8 from pubsub import pub
   9 
  10 
  11 class ChangerWidget(wx.Frame):
  12     CHANGE = 10  # by how much money changes every time click
  13 
  14     def __init__(self, parent=None):
  15         wx.Frame.__init__(self, parent, -1, "Changer View")
  16 
  17         sizer = wx.BoxSizer(wx.VERTICAL)
  18         self.add = wx.Button(self, -1, "Add Money")
  19         self.remove = wx.Button(self, -1, "Remove Money")
  20         sizer.Add(self.add, 0, wx.EXPAND | wx.ALL)
  21         sizer.Add(self.remove, 0, wx.EXPAND | wx.ALL)
  22         self.SetSizer(sizer)
  23 
  24         self.add.Bind(wx.EVT_BUTTON, self.onAdd)
  25         self.remove.Bind(wx.EVT_BUTTON, self.onRemove)
  26 
  27     def onAdd(self, evt):
  28         print('-----')
  29         pub.sendMessage("money_changing", amount=self.CHANGE)
  30 
  31     def onRemove(self, evt):
  32         print('-----')
  33         pub.sendMessage("money_changing", amount=- self.CHANGE)

Earlier Code Sample

Posted by Mike Rooney

   1 #!/usr/bin/env python
   2 
   3 """
   4 An example of a Model-View-Controller architecture,
   5 using wx.lib.pubsub to handle proper updating of widget (View) values.
   6 """
   7 import wx
   8 from wx.lib.pubsub import Publisher as pub
   9 
  10 class Model:
  11     def __init__(self):
  12         self.myMoney = 0
  13 
  14     def addMoney(self, value):
  15         self.myMoney += value
  16         #now tell anyone who cares that the value has been changed
  17         pub.sendMessage("MONEY CHANGED", self.myMoney)
  18 
  19     def removeMoney(self, value):
  20         self.myMoney -= value
  21         #now tell anyone who cares that the value has been changed
  22         pub.sendMessage("MONEY CHANGED", self.myMoney)
  23 
  24 class View(wx.Frame):
  25     def __init__(self, parent):
  26         wx.Frame.__init__(self, parent, title="Main View")
  27 
  28         sizer = wx.BoxSizer(wx.VERTICAL)
  29         text = wx.StaticText(self, label="My Money")
  30         ctrl = wx.TextCtrl(self)
  31         sizer.Add(text, 0, wx.EXPAND | wx.ALL)
  32         sizer.Add(ctrl, 0, wx.EXPAND | wx.ALL)
  33 
  34         self.moneyCtrl = ctrl
  35         ctrl.SetEditable(False)
  36         self.SetSizer(sizer)
  37 
  38     def SetMoney(self, money):
  39         self.moneyCtrl.SetValue(str(money))
  40 
  41 class ChangerWidget(wx.Frame):
  42     def __init__(self, parent):
  43         wx.Frame.__init__(self, parent, title="Main View")
  44 
  45         sizer = wx.BoxSizer(wx.VERTICAL)
  46         self.add = wx.Button(self, label="Add Money")
  47         self.remove = wx.Button(self, label="Remove Money")
  48         sizer.Add(self.add, 0, wx.EXPAND | wx.ALL)
  49         sizer.Add(self.remove, 0, wx.EXPAND | wx.ALL)
  50         self.SetSizer(sizer)
  51 
  52 class Controller:
  53     def __init__(self, app):
  54         self.model = Model()
  55 
  56         #set up the first frame which displays the current Model value
  57         self.view1 = View(None)
  58         self.view1.SetMoney(self.model.myMoney)
  59 
  60         #set up the second frame which allows the user to modify the Model's value
  61         self.view2 = ChangerWidget(self.view1)
  62         self.view2.add.Bind(wx.EVT_BUTTON, self.AddMoney)
  63         self.view2.remove.Bind(wx.EVT_BUTTON, self.RemoveMoney)
  64         #subscribe to all "MONEY CHANGED" messages from the Model
  65         #to subscribe to ALL messages (topics), omit the second argument below
  66         pub.subscribe(self.MoneyChanged, "MONEY CHANGED")
  67 
  68         self.view1.Show()
  69         self.view2.Show()
  70 
  71     def AddMoney(self, evt):
  72         self.model.addMoney(10)
  73 
  74     def RemoveMoney(self, evt):
  75         self.model.removeMoney(10)
  76 
  77     def MoneyChanged(self, message):
  78         """
  79         This method is the handler for "MONEY CHANGED" messages,
  80         which pubsub will call as messages are sent from the model.
  81 
  82         We already know the topic is "MONEY CHANGED", but if we
  83         didn't, message.topic would tell us.
  84         """
  85         self.view1.SetMoney(message.data)
  86 
  87 if __name__ == "__main__":
  88     app = wx.App(False)
  89     controller = Controller(app)
  90     app.MainLoop()

The following is an older sample of the MVC architecture, written before pubsub existed in wxPython.

Code Sample

(originally posted by Brian Kelly at http://www.techietwo.com/detail-6332577.html)

   1 #!/usr/bin/env python
   2 
   3 import wx
   4 # an observable calls callback functions when the data has changed
   5 #o = Observable()
   6 #def func(data):
   7 # print "hello", data
   8 #o.addCallback(func)
   9 #o.set(1)
  10 # --| "hello", 1
  11 class Observable:
  12     def __init__(self, initialValue=None):
  13         self.data = initialValue
  14         self.callbacks = {}
  15 
  16     def addCallback(self, func):
  17         self.callbacks[func] = 1
  18 
  19     def delCallback(self, func):
  20         del self.callback[func]
  21 
  22     def _docallbacks(self):
  23         for func in self.callbacks:
  24             func(self.data)
  25 
  26     def set(self, data):
  27         self.data = data
  28         self._docallbacks()
  29 
  30     def get(self):
  31         return self.data
  32 
  33     def unset(self):
  34         self.data = None
  35 
  36 class Model:
  37     def __init__(self):
  38         self.myMoney = Observable(0)
  39 
  40     def addMoney(self, value):
  41         self.myMoney.set(self.myMoney.get() + value)
  42 
  43     def removeMoney(self, value):
  44         self.myMoney.set(self.myMoney.get() - value)
  45 
  46 
  47 class View(wx.Frame):
  48     def __init__(self, parent):
  49         wx.Frame.__init__(self, parent, title="Main View")
  50         sizer = wx.BoxSizer(wx.VERTICAL)
  51         text = wx.StaticText(self, label="My Money")
  52         ctrl = wx.TextCtrl(self)
  53         sizer.Add(text, 0, wx.EXPAND | wx.ALL)
  54         sizer.Add(ctrl, 0, wx.EXPAND | wx.ALL)
  55         ctrl.SetEditable(False)
  56         self.SetSizer(sizer)
  57         self.moneyCtrl = ctrl
  58 
  59     def SetMoney(self, money):
  60         self.moneyCtrl.SetValue(str(money))
  61 
  62 
  63 class ChangerWidget(wx.Frame):
  64     def __init__(self, parent):
  65         wx.Frame.__init__(self, parent, title="Main View")
  66         sizer = wx.BoxSizer(wx.VERTICAL)
  67         self.add = wx.Button(self, label="Add Money")
  68         self.remove = wx.Button(self, label="Remove Money")
  69         sizer.Add(self.add, 0, wx.EXPAND | wx.ALL)
  70         sizer.Add(self.remove, 0, wx.EXPAND | wx.ALL)
  71         self.SetSizer(sizer)
  72 
  73 class Controller:
  74     def __init__(self, app):
  75         self.model = Model()
  76         self.view1 = View(None)
  77         self.view2 = ChangerWidget(self.view1)
  78         self.MoneyChanged(self.model.myMoney.get())
  79         self.view2.add.Bind(wx.EVT_BUTTON, self.AddMoney)
  80         self.view2.remove.Bind(wx.EVT_BUTTON, self.RemoveMoney)
  81         self.model.myMoney.addCallback(self.MoneyChanged)
  82         self.view1.Show()
  83         self.view2.Show()
  84 
  85     def AddMoney(self, evt):
  86         self.model.addMoney(10)
  87 
  88     def RemoveMoney(self, evt):
  89         self.model.removeMoney(10)
  90 
  91     def MoneyChanged(self, money):
  92         self.view1.SetMoney(money)
  93 
  94 app = wx.App(False)
  95 controller = Controller(app)
  96 app.MainLoop()

ModelViewController (last edited 2017-08-15 21:01:25 by JamesKey)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.