Differences between revisions 2 and 3
Revision 2 as of 2006-04-23 04:08:27
Size: 11355
Editor: ZhangYunfeng
Comment:
Revision 3 as of 2006-04-23 04:11:38
Size: 11362
Editor: ZhangYunfeng
Comment:
Deletions are marked like this. Additions are marked like this.
Line 206: Line 206:
Complete implementation together with mock objects and tests: inline:mvp.zip To start the application use albums.pyw Complete implementation together with mock objects and tests: attachment:mvp.zip To start the application use albums.pyw
Line 212: Line 212:
inline:mvp2.png attachment:mvp2.png
Line 297: Line 297:
New version of the source code: inline:mvp2.zip New version of the source code: attachment:mvp2.zip
Line 300: Line 300:
}}}

TableOfContents 原文 [http://wiki.wxpython.org/index.cgi/ModelViewPresenter wxPython wiki :: ModelViewPresenter]

即将翻译,标记一下

1. 前言

ModelViewPresenter 是ModelViewController模式的一个派生产物。其目的是提供一个Observer连接(模型与视图)的整洁实现方法。此模式更多信息可以参考 [http://c2.com/cgi/wiki?ModelViewPresenter] 和 [http://www.martinfowler.com/eaaDev/ModelViewPresenter.html]。原始文章请看 [http://www.oodesign.com.br/forum/index.php?act=Attach&type=post&id=74]。而 [http://atomicobject.com/media/files/PresenterFirst.pdf] 这篇文章展示了使用MVP来构建GUI程序测试驱动开发(TDD)的模式和过程。(Caveat: I've only briefly scanned the paper, and don't know whether it will warrant the link on closer inspection. -- Don Dwiggins)

接下来的大部分信息来源于Martin Fowler's MVP页面,但经调整可用于wxPython。很多文字仅仅是复制和粘贴而来。

2. 简介

Model View Presenter 将表现行为分离出来,形成单独的presenter类。所有用户事件都被转向presenter,由presenter处理视图(view)的状态。在分离任务的同时,它也允许在没有UI的清况下测试这些行为,允许对不同的UI使用相同的基本行为。

3. 如何运作

Model View Presenter 的核心是将所有的presentation行为从view中分离出来,并放置到单独的presenter类中去。最终得到的view是几乎没有能动性的 - 除了保持GUI控件。这种方式的分离和经典的Model View Controller分离是非常相似的。

MVC与MVP的差别在于MVP中presenter不像MVC的controller那样直接处理GUI事件,而是利用interactor,通过delegation来实现。在我看来,这使得presenter的测试更加容易。

4. 示例: Album Window

attachment:mvp.png

模型代码仅是简单的数据对象。

   1 class Album(object):
   2     def __init__(self, artist, title, isClassical=False, composer=None):
   3         self.artist = artist
   4         self.title = title
   5         self.isClassical = isClassical
   6         self.composer = composer

通过创建presenter来启动程序。

   1 class AlbumPresenter(object):
   2     def __init__(self, albums, view, interactor):
   3         self.albums = albums
   4         self.view = view
   5         interactor.Install(self, view)
   6         self.isListening = True
   7         self.initView()
   8         view.start()

View在wx.Frame内构建

   1 class AlbumWindow(wx.Frame):
   2     def __init__(self):
   3         self.app = wx.App(0)
   4         wx.Frame.__init__(self, None)
   5         self.SetBackgroundColour("lightgray")
   6         ...

将Frame实例化之前,我们创建应用程序的wx.App对象,当view启动时,我们开始Main Loop。在实际工作中,你可以使用wxGlade之类的工具创建布局,并从生成的类中进行继承。

presenter负责把所有的数据放入窗口。它从主类中获取数据,并通过view的接口将数据置入窗口。

   1 class AlbumPresenter(object):
   2     ...
   3     def loadViewFromModel(self):
   4         if self.isListening:
   5             self.isListening = False
   6             self.refreshAlbumList()
   7             self.view.setTitle(self.selectedAlbum.title)
   8             self.updateWindowTitle()
   9             self.view.setArtist(self.selectedAlbum.artist)
  10             self.view.setClassical(self.selectedAlbum.isClassical)
  11             if self.selectedAlbum.isClassical:
  12                 self.view.setComposer(self.selectedAlbum.composer)
  13             else:
  14                 self.view.setComposer("")
  15             self.view.setComposerEnabled(self.selectedAlbum.isClassical)
  16             self.enableApplyAndCancel(False)
  17             self.isListening = True
  18             
  19     def refreshAlbumList(self):
  20         currentAlbum = self.view.getSelectedAlbum()
  21         self.view.setAlbums(self.albums)
  22         self.view.setSelectedAlbum(currentAlbum)
  23         self.selectedAlbum = self.albums[currentAlbum]
  24         
  25     def updateWindowTitle(self):
  26         self.view.setWindowTitle("Album: " + self.view.getTitle())
  27         
  28     def enableApplyAndCancel(self, enabled):
  29         self.view.setApplyEnabled(enabled)
  30         self.view.setCancelEnabled(enabled)
  31     ...

loadViewFromModel方法对view中所有项目进行更新。有一些更新事件会导致递归式的触发更新方法,所以在这里我使用对更新方法加入一个防护,防止递归的发生。

view中的方法允许通过fields访问下层的控件。

   1 class AlbumWindow(wx.Frame):
   2     ...
   3     def setClassical(self, isClassical):
   4         self.classical.SetValue(isClassical)
   5     def isClassical(self):
   6         return self.classical.GetValue()

Alternatively the view could be implemented using property objects and use simple assignment/access instead of set/get methods.

   1 class AlbumWindow(wx.Frame):
   2     ...
   3     def _setTitle(self, title):
   4         self._title.SetValue(title)
   5     def _getTitle(self):
   6         return self._title.GetValue()
   7     title = property(_getTitle, _setTitle)

then

   1 class AlbumPresenter(object):
   2     ...
   3     def loadViewFromModel(self):
   4         if self.isListening:
   5             ...
   6             self.view.title = self.selectedAlbum.title
   7     ...
   8     def updateWindowTitle(self):
   9         self.view.setWindowTitle("Album: " + self.view.title)

The interactor object is installed from the presenter and it installs all the needed event handlers delegating the event to the presenter.

   1 class AlbumInteractor(object):
   2     def Install(self, presenter, view):
   3         self.presenter = presenter
   4         self.view = view
   5         view.albums.Bind(wx.EVT_LISTBOX, self.OnReloadNeeded)
   6         view.title.Bind(wx.EVT_TEXT, self.OnDataFieldUpdated)
   7         view.artist.Bind(wx.EVT_TEXT, self.OnDataFieldUpdated)
   8         view.composer.Bind(wx.EVT_TEXT, self.OnDataFieldUpdated)
   9         view.classical.Bind(wx.EVT_CHECKBOX, self.OnDataFieldUpdated)
  10         view.apply.Bind(wx.EVT_BUTTON, self.OnApply)
  11         view.cancel.Bind(wx.EVT_BUTTON, self.OnReloadNeeded)
  12         
  13     def OnApply(self, evt):
  14         self.presenter.updateModel()
  15         
  16     def OnReloadNeeded(self, evt):
  17         self.presenter.loadViewFromModel()
  18         
  19     def OnDataFieldUpdated(self, evt):
  20         self.presenter.dataFieldUpdated()

You save data to the model when the user hits the apply button.

   1     def updateModel(self):
   2         self.selectedAlbum.title = self.view.getTitle()
   3         self.selectedAlbum.artist = self.view.getArtist()
   4         self.selectedAlbum.isClassical = self.view.isClassical()
   5         if self.view.isClassical:
   6             self.selectedAlbum.composer = self.view.getComposer()
   7         else:
   8             self.selectedAlbum.composer = None
   9         self.enableApplyAndCancel(False)
  10         self.loadViewFromModel()

To test if apply works you can use the unittest module and mock objects for view and interactor

   1 class TestAlbumPresenter(unittest.TestCase):
   2     ...
   3     def testApplySavesDataToModel(self):
   4         view = mock_objects.MockAlbumWindow();
   5         model = [models.Album(*data) for data in self.someAlbums]
   6         interactor = mock_objects.MockAlbumInteractor()
   7         presenter = presenters.AlbumPresenter(model, view, interactor);
   8         newTitle = "Some Other Title"
   9         view.title = newTitle
  10         presenter.updateModel()
  11         assert view.title == newTitle

Complete implementation together with mock objects and tests: attachment:mvp.zip To start the application use albums.pyw

4.1. Expanding applications with MVP architecture

Let's say that the next step would be to add the capability to add new albums and to provide a way to sort the albums either ascending or descending. The application will look like this:

attachment:mvp2.png

The best order to expand the example is to start in the test module and first update the tests, next the presenter, the mock views, then when the functionality is done move to the real view, add the necessary bits and then alter the interactor to connect the view with the presenter. For expediency reasons will do it first the View next the Presenter and last the Interactor.

First let's add the visual bits: In the AlbumWindow we add 2 buttons, place them beneath the albums list and provide access to the order button label.

   1 class AlbumWindow(wx.Frame):
   2     def __init__(self):
   3         ...
   4         self.add = wx.Button(self, label="New Album")
   5         self.order = wx.Button(self, label="A->Z")
   6 
   7         leftSizer = wx.GridBagSizer(5,5)
   8         leftSizer.Add(self.albums, (0,0), (1,2),flag=wx.EXPAND)
   9         leftSizer.Add(self.add, (1,0), (1,1),flag=wx.EXPAND)
  10         leftSizer.Add(self.order, (1,1), (1,1),flag=wx.EXPAND)
  11         ...
  12         mainSizer.Add(leftSizer, 0, wx.EXPAND|wx.ALL, 5)
  13     ...
  14     def setOrderLabel(self, label):
  15         self.order.SetLabel(label)

Next we add the functionality in the presenter. First we add a flag to hold the order:

   1 class AlbumPresenter(object):
   2     def __init__(self, albums, view, interactor):
   3         ...
   4         self.order = 1
   5         self.albums.sort(lambda a, b: cmp(a.title, b.title))

Next we fix the refreshAlbumList method to take into account that the list must be sorted according to our selected order.

   1 class AlbumPresenter(object):
   2     def refreshAlbumList(self):
   3         currentAlbum = self.view.getSelectedAlbum()
   4         self.selectedAlbum = self.albums[currentAlbum]
   5         self.albums.sort(lambda a, b:self.order*cmp(a.title, b.title))
   6         self.view.setAlbums(self.albums)
   7         self.view.setSelectedAlbum(self.albums.index(self.selectedAlbum))        

Provide a way to toggle the order and a way to add a new album

   1 class AlbumPresenter(object):
   2     def toggleOrder(self):
   3         self.order = -1*self.order
   4         self.loadViewFromModel()
   5         
   6     def addNewAlbum(self):
   7         newAlbum = models.Album("Unknown Artist", "New Album Title")
   8         self.albums.append(newAlbum)
   9         self.view.setAlbums(self.albums)
  10         self.view.setSelectedAlbum(self.albums.index(newAlbum))
  11         self.loadViewFromModel()

Now all that remains to be done to update the AlbumInteractor to provide the connection between button presses and our new functionality:

   1 class AlbumInteractor(object):
   2     def Install(self, presenter, view):
   3         ...
   4         view.add.Bind(wx.EVT_BUTTON, self.OnAddNewAlbum)
   5         view.order.Bind(wx.EVT_BUTTON, self.OnToggleOrder)
   6         
   7     def OnAddNewAlbum(self, evt):
   8         self.presenter.addNewAlbum()
   9         
  10     def OnToggleOrder(self, evt):
  11         self.presenter.toggleOrder()

All Done! New version of the source code: attachment:mvp2.zip

Remember! When writing production code start in the unittests, this way you'll be able to catch logic related errors early on, before they are altered by the view part.

ModelViewPresenter (last edited 2009-12-25 07:11:03 by localhost)