## page was renamed from zhArticleTemplate #language:zh #pragma section-numbers on <> 原文 [[http://wiki.wxpython.org/index.cgi/ModelViewPresenter|wxPython wiki :: ModelViewPresenter]] 即将翻译,标记一下 = 前言 = 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。很多文字仅仅是复制和粘贴而来。 = 简介 = Model View Presenter 将表现行为分离出来,形成单独的presenter类。所有用户事件都被转向presenter,由presenter处理视图(view)的状态。在分离任务的同时,它也允许在没有UI的清况下测试这些行为,允许对不同的UI使用相同的基本行为。 = 如何运作 = Model View Presenter 的核心是将所有的presentation行为从view中分离出来,并放置到单独的presenter类中去。最终得到的view是几乎没有能动性的 - 除了保持GUI控件。这种方式的分离和经典的Model View Controller分离是非常相似的。 MVC与MVP的差别在于MVP中presenter不像MVC的controller那样直接处理GUI事件,而是利用interactor,通过delegation来实现。在我看来,这使得presenter的测试更加容易。 = 示例: Album Window = {{attachment:mvp.png}} 模型代码仅是简单的数据对象。 {{{ #!python class Album(object): def __init__(self, artist, title, isClassical=False, composer=None): self.artist = artist self.title = title self.isClassical = isClassical self.composer = composer }}} 通过创建presenter来启动程序。 {{{ #!python class AlbumPresenter(object): def __init__(self, albums, view, interactor): self.albums = albums self.view = view interactor.Install(self, view) self.isListening = True self.initView() view.start() }}} View在wx.Frame内构建 {{{ #!python class AlbumWindow(wx.Frame): def __init__(self): self.app = wx.App(0) wx.Frame.__init__(self, None) self.SetBackgroundColour("lightgray") ... }}} 将Frame实例化之前,我们创建应用程序的wx.App对象,当view启动时,我们开始Main Loop。在实际工作中,你可以使用wxGlade之类的工具创建布局,并从生成的类中进行继承。 presenter负责把所有的数据放入窗口。它从主类中获取数据,并通过view的接口将数据置入窗口。 {{{ #!python class AlbumPresenter(object): ... def loadViewFromModel(self): if self.isListening: self.isListening = False self.refreshAlbumList() self.view.setTitle(self.selectedAlbum.title) self.updateWindowTitle() self.view.setArtist(self.selectedAlbum.artist) self.view.setClassical(self.selectedAlbum.isClassical) if self.selectedAlbum.isClassical: self.view.setComposer(self.selectedAlbum.composer) else: self.view.setComposer("") self.view.setComposerEnabled(self.selectedAlbum.isClassical) self.enableApplyAndCancel(False) self.isListening = True def refreshAlbumList(self): currentAlbum = self.view.getSelectedAlbum() self.view.setAlbums(self.albums) self.view.setSelectedAlbum(currentAlbum) self.selectedAlbum = self.albums[currentAlbum] def updateWindowTitle(self): self.view.setWindowTitle("Album: " + self.view.getTitle()) def enableApplyAndCancel(self, enabled): self.view.setApplyEnabled(enabled) self.view.setCancelEnabled(enabled) ... }}} loadViewFromModel方法对view中所有项目进行更新。有一些更新事件会导致递归式的触发更新方法,所以在这里我使用对更新方法加入一个防护,防止递归的发生。 view中的方法允许通过fields访问下层的控件。 {{{ #!python class AlbumWindow(wx.Frame): ... def setClassical(self, isClassical): self.classical.SetValue(isClassical) def isClassical(self): return self.classical.GetValue() }}} 除了set/get方法之外,view还可以使用对象属性以及通过简单的assignment/access方法来实现。 {{{ #!python class AlbumWindow(wx.Frame): ... def _setTitle(self, title): self._title.SetValue(title) def _getTitle(self): return self._title.GetValue() title = property(_getTitle, _setTitle) }}} 接下来 {{{ #!python class AlbumPresenter(object): ... def loadViewFromModel(self): if self.isListening: ... self.view.title = self.selectedAlbum.title ... def updateWindowTitle(self): self.view.setWindowTitle("Album: " + self.view.title) }}} interactor对象从presenter被安装,它安装所有必需的event handler, 这些handler将event委托给presenter。 {{{ #!python class AlbumInteractor(object): def Install(self, presenter, view): self.presenter = presenter self.view = view view.albums.Bind(wx.EVT_LISTBOX, self.OnReloadNeeded) view.title.Bind(wx.EVT_TEXT, self.OnDataFieldUpdated) view.artist.Bind(wx.EVT_TEXT, self.OnDataFieldUpdated) view.composer.Bind(wx.EVT_TEXT, self.OnDataFieldUpdated) view.classical.Bind(wx.EVT_CHECKBOX, self.OnDataFieldUpdated) view.apply.Bind(wx.EVT_BUTTON, self.OnApply) view.cancel.Bind(wx.EVT_BUTTON, self.OnReloadNeeded) def OnApply(self, evt): self.presenter.updateModel() def OnReloadNeeded(self, evt): self.presenter.loadViewFromModel() def OnDataFieldUpdated(self, evt): self.presenter.dataFieldUpdated() }}} 当用户点击apply按钮时,将数据保存到model。 {{{ #!python def updateModel(self): self.selectedAlbum.title = self.view.getTitle() self.selectedAlbum.artist = self.view.getArtist() self.selectedAlbum.isClassical = self.view.isClassical() if self.view.isClassical: self.selectedAlbum.composer = self.view.getComposer() else: self.selectedAlbum.composer = None self.enableApplyAndCancel(False) self.loadViewFromModel() }}} 要检查apply是否正常工作,你可以使用unittest模块,模仿view和Interactor对象。 {{{ #!python class TestAlbumPresenter(unittest.TestCase): ... def testApplySavesDataToModel(self): view = mock_objects.MockAlbumWindow(); model = [models.Album(*data) for data in self.someAlbums] interactor = mock_objects.MockAlbumInteractor() presenter = presenters.AlbumPresenter(model, view, interactor); newTitle = "Some Other Title" view.title = newTitle presenter.updateModel() assert view.title == newTitle }}} 完整的实现(包括模仿对象和测试): [[attachment:mvp.zip]] 使用 albums.pyw 运行程序。 = 使用MVP构架扩展应用程序 = 假设下一步要增加功能:增加新的albums,提供将albums按照升/降方式来排序的方法。此程序应该看起来是这个样子: {{attachment:mvp2.png}} 扩展这个例子的最好顺序是:从测试模块开始,首先升级测试,然后是presenter,模仿的views,然后,当这些功能完整移植到真正的view中之后,增加必须的部分并替换interactor来连接view和presenter。方便起见,从View开始,然后Presenter,最后Interactor。 首先增加可视部分: 在AlbumWindow中增加两个按钮,放置于albums列表的下方,并提供访问排序按钮标签的方法。 {{{ #!python class AlbumWindow(wx.Frame): def __init__(self): ... self.add = wx.Button(self, label="New Album") self.order = wx.Button(self, label="A->Z") leftSizer = wx.GridBagSizer(5,5) leftSizer.Add(self.albums, (0,0), (1,2),flag=wx.EXPAND) leftSizer.Add(self.add, (1,0), (1,1),flag=wx.EXPAND) leftSizer.Add(self.order, (1,1), (1,1),flag=wx.EXPAND) ... mainSizer.Add(leftSizer, 0, wx.EXPAND|wx.ALL, 5) ... def setOrderLabel(self, label): self.order.SetLabel(label) }}} 接下来,在presenter中增加相应功能。首先,增加标记来保存顺序(order): {{{ #!python class AlbumPresenter(object): def __init__(self, albums, view, interactor): ... self.order = 1 self.albums.sort(lambda a, b: cmp(a.title, b.title)) }}} 接下来,考虑到应该根据我们选择的顺序对列表排序,对refreshAlbumList方法进行修改: {{{ #!python class AlbumPresenter(object): def refreshAlbumList(self): currentAlbum = self.view.getSelectedAlbum() self.selectedAlbum = self.albums[currentAlbum] self.albums.sort(lambda a, b:self.order*cmp(a.title, b.title)) self.view.setAlbums(self.albums) self.view.setSelectedAlbum(self.albums.index(self.selectedAlbum)) }}} Provide a way to toggle the order and a way to add a new album {{{ #!python class AlbumPresenter(object): def toggleOrder(self): self.order = -1*self.order self.loadViewFromModel() def addNewAlbum(self): newAlbum = models.Album("Unknown Artist", "New Album Title") self.albums.append(newAlbum) self.view.setAlbums(self.albums) self.view.setSelectedAlbum(self.albums.index(newAlbum)) self.loadViewFromModel() }}} 剩下要做的就是更新AlbumInteractor,提供按钮点击和新功能之间的连接: {{{ #!python class AlbumInteractor(object): def Install(self, presenter, view): ... view.add.Bind(wx.EVT_BUTTON, self.OnAddNewAlbum) view.order.Bind(wx.EVT_BUTTON, self.OnToggleOrder) def OnAddNewAlbum(self, evt): self.presenter.addNewAlbum() def OnToggleOrder(self, evt): self.presenter.toggleOrder() }}} 全部完成! 新版的源代码: [[attachment:mvp2.zip]] 记住!编写产品代码时,从unittests开始,这样你就可以在相关的逻辑错误被view部分替换之前,尽早的发现它们。