Contents
原文 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
模型代码仅是简单的数据对象。
通过创建presenter来启动程序。
View在wx.Frame内构建
将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访问下层的控件。
除了set/get方法之外,view还可以使用对象属性以及通过简单的assignment/access方法来实现。
接下来
interactor对象从presenter被安装,它安装所有必需的event handler, 这些handler将event委托给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()
当用户点击apply按钮时,将数据保存到model。
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()
要检查apply是否正常工作,你可以使用unittest模块,模仿view和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
完整的实现(包括模仿对象和测试): mvp.zip 使用 albums.pyw 运行程序。
5. 使用MVP构架扩展应用程序
假设下一步要增加功能:增加新的albums,提供将albums按照升/降方式来排序的方法。此程序应该看起来是这个样子:
扩展这个例子的最好顺序是:从测试模块开始,首先升级测试,然后是presenter,模仿的views,然后,当这些功能完整移植到真正的view中之后,增加必须的部分并替换interactor来连接view和presenter。方便起见,从View开始,然后Presenter,最后Interactor。
首先增加可视部分:
在AlbumWindow中增加两个按钮,放置于albums列表的下方,并提供访问排序按钮标签的方法。
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)
接下来,在presenter中增加相应功能。首先,增加标记来保存顺序(order):
接下来,考虑到应该根据我们选择的顺序对列表排序,对refreshAlbumList方法进行修改:
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()
剩下要做的就是更新AlbumInteractor,提供按钮点击和新功能之间的连接:
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()
全部完成!
新版的源代码: mvp2.zip
记住!编写产品代码时,从unittests开始,这样你就可以在相关的逻辑错误被view部分替换之前,尽早的发现它们。