原文 wxPython wiki :: ModelViewController

翻译:ZhangYunfeng

1. 模型-视图-控制器

ModelViewController 简称为 MVC

2. 简介

本页介绍 MVC (ModelViewController) 模式并演示其在Python中的实现方法

http://c2.com/cgi/wiki?ModelViewController 可以了解更多关于MVC的信息, 你也可以察看MVC的一种演化模式 ModelViewPresenter

3. 原理

(原文出处 http://mail.python.org/pipermail/python-list/2006-January/319314.html by has)

MVC模式关注于内容的分离。

Model(模型)负责管理程序中的数据(私有数据和客户数据)。View/Controller(视图/控制器)负责为外界提供与程序中的客户数据进行交互的手段。

模型提供了内部接口(API),允许程序的其他部分与之交互。视图/控制器提供外部接口(GUI/CLI/web form/high-level IPC/等等),允许程序外部的世界与程序进行交互。

模型负责保持数据的完整性,因为数据一旦被破坏,所有的东西就 game over 了。视图/控制器负责保持UI的完整性, 确保所有的文字视图显示的都是最新的数据,使不适用于当前焦点的菜单失效,等等。

模型不包含视图/控制器代码;没有GUI窗口类,没有对话框布置代码,不接受用户输入。视图/控制器不包含模型代码;没有URLs验证和SQL查询代码,也不包含数据原始状态: 窗口中的任何数据仅仅是为显示目的而存在,而且也仅仅是模型中数据的真实反映。

如何验证真正的MVC设计呢?如果程序不存在视图/控制器部分,它也应该具有完整的功能。当然,这种情况下外部世界与它进行交互会存在困难,但是 只要知道适当的模型API,程序应该能够正常的保持和操作数据。

为什么能够达到这种效果?最简单的答案:都是因为模型与视图/控制器层之间的弱耦合关系。但是这并不是它的全部内容。MVC模式的关键是 这些连接的走向:所有的指令都是从视图/控制器指向模型的。模型从不告诉视图/控制器去做什么。

为什么呢?因为在MVC中,允许视图/控制器对模型有一些了解(特别是模型的API),但是不允许模型了解视图/控制器的任何情况。

为什么呢?因为MVC就是要建立一个内容上完整分离的结构。

为什么呢?是为了防止无法控制程序的复杂性,导致你这位程序开发者精疲力尽。程序越大,其部件数量越多。这些部件之间的连接越多,开发人员就越难以控制/扩展/替换各个部件,甚至于无法搞清整个系统如何工作。问问你自己:当你看到程序结构图时,你愿意看到一颗树还是猫爪印?MVC模式拒绝回环连接(B可以连接A,但A不能连接B。在这里,A是模型,B是视图/控制器),从而避免了后者的产生。

另外,如果你够聪明,你会意识到刚才描述的单向约束中有一个问题:在模型根本不知道视图/控制器的情况下,如何通知视图/控制器其用户数据的变化呢?不要担心,这里有一个解决方案,虽然刚开始看起来有一些绕圈子,但实际上它相当的简洁,我们待会儿会讲到它。

在实际项目中,视图/控制器对象会通过模型的API,1. 让模型做一些事情(执行命令),2. 让模型给它一些东西(返回数据)。视图/控制器层向模型层发出指令并从模型层中获取信息

例如一个 MyCoolListControl 类,应该在需要时能够从下层获取所需的数据。对于list widget的情况,一般意味着询问有多少个值,然后返回每一个值,因为这是完成这种操作最简单最宽松的方式,同时将耦合保持到最低限度。而如果 widget 想向用户展示按照字母排序的结果,那就是它自己的工作,当然要它自己负责完成。

现在,最后一个迷题(也就是我们之前提到的): 在基于MVC的系统中如何保持UI的显示同步于模型的状态呢?

这就是问题所在:很多的视图对象都是状态性的,比如 checkbox 可能处于选择或者未选择状态,文字框可能包含一些可以编辑的文字。但是,MVC规定所有的用户数据保存在模型层中,所以其他层中的为了显示目的而存在的任何数据(checkbox的状态,文字框中的当前文字)必须是模型数据的辅助副本。但如果模型状态改变了,视图中的副本就不再准确而应该被刷新。

但是如何操作呢?MVC模式禁止模型向视图层发出相关信息的最新副本,甚至不允许模型向视图发出消息,表明其状态的变化。

是的,几乎就是这样。Ok,不允许模型层与其他层直接对话,因为这就要求它知道其他层的信息,而且MVC的规定也不允许这样做。但是,如果森林中的一颗树倒下了,而没有人听到,那么它发出声音了吗?

答案就是建立一套通告系统,为模型层提供一个空间,让它可以发出通告,说它已经完成了一些有趣的事情。其他层可以向通告系统发出监听器,监听它们所感兴趣的的通告。模型层根本不需要知道谁在监听(甚至是否有人在监听);它仅仅发出通告,然后就把它抛在了脑后。如果有人听到了这个通告,并且想随之做一些事情 - 比如向模型询问一些新的数据以便更新显示 - 这样就搞定了。模型只是将它所发出的通告列表作为它的API定义的一部分;其他人想拿这个知识做什么事的话,随便。

MVC is preserved,and everyone is happy. 你的应用程序框架可能已经提供了内置的通告系统,你也可以自己写一个(参考 ObserverPattern)。

4. 示例代码

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

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

5. 讨论

ModelViewController (last edited 2009-12-25 07:08:32 by localhost)