Java开发网 Java开发网
注册 | 登录 | 帮助 | 搜索 | 排行榜 | 发帖统计  

您没有登录

» Java开发网 » Java程序分享区  

按打印兼容模式打印这个话题 打印话题    把这个话题寄给朋友 寄给朋友    该主题的所有更新都将Email到你的邮箱 订阅主题
reply to postflat modethreaded modego to previous topicgo to next topicgo to back
作者 Smalltalk-80(TM)中的应用编程:如何使用模型-视图-控制器(MVC)
ralf1999





发贴: 5
于 2004-12-12 18:07 user profilesend a private message to userreply to postsearch all posts byselect and copy to clipboard. 
ie only, sorry for netscape users:-)add this post to my favorite list
Smalltalk-80(TM)中的应用编程:
如何使用模型-视图-控制器(MVC)
作者:Steve Burbeck 博士

翻译:寒蝉退士

译者声明:译者对译文不做任何担保,译者对译文不拥有任何权利并且不负担任何责任和义务。

作者的注释: 本文最初描述了在 Smalltalk-80 v2.0 中存在的 MVC 框架。在 1992 年针对 Smalltalk-80 v2.5 的变动而做了更新。在本文中没有反映 ParcPlace 对版本 4.x 中 MVC 机制做的广泛的变动。

Copyright Coffee 1987, 1992 by S. Burbeck
permission to copy for educational or non-commercial purposes is hereby granted

(TM)Smalltalk-80 is a trademark of ParcPlace Systems, Inc.

目录
介绍
基本概念
在三元组内的通信
被动模型
模型到三元组的连接
视图-控制器连接
视图
视图/子视图层次
显示视图
对现存视图的解说
现存的视图层次
控制器
控制器之间通信
进入和离开控制流
鼠标菜单控制器
屏幕控制器
MVC 检查器
附录 A: 控制流的详情
介绍

Xerox PARC 对编程艺术的贡献之一就是多窗口的高度交互性的 Smalltalk-80 界面。这种类型的界面被 Apple Lisa 和 Macintosh 以及 Macintosh 的摹仿者所借用。在这样的界面中,输入主要使用鼠标而输出主要使用图形和文字的适当的混合。在 Smalltalk-80 用户界面背后的中心概念是模型-视图-控制器 (MVC)范例。它是典雅和简单的,但是与传统的应用程序的方式非常不同。因为新颖,所以需要做一些解释 -- 在出版的 Smalltalk-80 参考中不容易获得的解释。

如果你在类 Pen 中运行图形例子,你可能非常惊奇于这个“应用”直接绘制到屏幕上而不是象浏览器、工作空间、或记录簿这样的你很熟悉的窗口中。你当然希望你自己的应用程序通过工作空间的 easy aplomb 来共享屏幕上的空间,而不是简单的写到屏幕上。这有什么不同? 最简单的表述是,不良行为的应用程序不符合 MVC 范例,而常见的良好行为的应用程序符合 MVC 范例。

本文意图向新 Smalltalk-80 编程者提供实质的信息以便在他们自己的应用程序中开始使用 MVC。这里将介绍 MVC 的机制。一旦你消化了这个介绍你就可以创出自己的。你要充实这里给出的信息需要察看常见种类的视图和控制器 -- 比如工作空间、浏览器和文件列表 -- 的建立方式,首先并且经常是浏览器。记住这是 Smalltalk-80,鼓励你去复制。通过复制类似于你想要建立的窗口来开始你自己的窗口,接着修改它。不要害羞, 随意踩到对 Smalltalk-80 V2.5 映像做出贡献的众多程序员的肩膀上。在很大程度上,这是给你的礼物。
基本概念

在 MVC 范例中用户输入、外部世界的建模、和给用户的视觉反馈被明确的分离并由三种类型的对象处理,每类对象专门于它自己的任务。视图操控图形和/或文字输出到分配给它的应用程序的位图显示器的那一部分。控制器解释来自鼠标和键盘输入,命令模型和/或视图来做适当改变。最后,模型操控应用程序范围的行为和数据,响应对它的状态信息的请求(通常来自视图),并响应改变状态的指令(通常来自控制器)。这三个任务的形式上的分离是特别适合 Smalltalk-80 的一个重要的概念,这里的基本行为可以具体为抽象对象: 视图、控制器、模型和对象。MVC 行为就这样继承了,按需要增加和修改来提供一个灵活和强大的系统。

要有效的使用 MVC 范例你必须理解在 MVC 三元组中的劳动分工。你还必须理解三元组的三部分如何相互通信和同其他活跃的视图和控制器的通信;在需要通信和协作的多个应用程序之间共享一个单一的鼠标、键盘和显示器屏幕。要良好的使用 MVC 范例你还必须学习视图和控制器的可获得的子类,它们为你的应用程序提供了准备好了的起点。

在 Smalltalk-80 中,输入和输出在很大程度上被程式化了。视图必须管理屏幕实际区域(estate)并在这个实际区域中显示文本或图形。控制器们必须协作来确保让正确的控制器解释键盘和鼠标输入(通常依据那个试图包含光标)。因为多数应用程序的输入和输出被程式化了,其中许多是继承自一般类 -- View 和 Controller。这两个类,连同它们的子类,提供了丰富多样的行为,你的应用只须增加一些协议来完成它们的命令输入和交互性输出行为。反之,模型不能被程式化。在允许充当模型的对象的类型上的约束将限制可能在 MVC 范例中的应用程序的有用范围。有必要,任何对象都可以是一个模型。浮点数可以是空速视图的模型,它是更复杂的飞行模拟器的仪表板视图的子视图。字符串是编辑器应用程序的非常有用的模型(尽管叫做 StringHolder 的一个稍微复杂的对象通常用作这种目的)。因为任何对象都可以担任模型,对参与 MVC 范例的模型要求的基本行为是继承自类 Object,这个类是所有可能的模型的超类。

 
在 MVC 三元组内的通信

如果应用程序要与用户达成连贯的交互,在 MVC 三元组中涉及的模型、视图和控制器必须相互通信。视图和与之关联的控制器之间的通信是直接了当的,因为视图和控制器被特别设计为一起工作。在另一方面,模型以很微妙的方式进行通信。


被动模型

在最简单的情况下,模型不需要为参与 MVC 三元组而作任何准备。最简单的 WYSIWYG 文本编辑器是个好例子。这样的一个编辑器的中心特征是你总是看见文本如同它们出现在页面上一样。所以对文本的每个变更都必须明确的通知视图,这样它可以更新它的显示。然而模型(这里假定是字符串的一个实例)不需要负责传达这些改变到视图,因为只有来自用户的请求导致这些改变。控制器可以负责通知视图这些改变,因为控制器解释用户的请求。它可以简单的指示视图有个东西改变了 -- 视图可以向它的模型要求字符串的当前状态 -- 或者控制器可以指示视图哪个东西改变了。在这两种情况下,字符串模型是视图和控制器操纵的字符串数据的完全被动的持有者。它按照控制器的要求增加、删除、或替代子串并按照试图的要求反馈正确的子串。这个模型完全“没有意识到”视图或控制器的存在和它参与了 MVC 三元组。这种孤立不是这个模型的人为简化,而是这个模型的只在其他三元组成员的要求下改变。

模型到三元组的连接

但不是所有模型都这么被动。假定数据对象 -- 上个例子中的字符串 -- 改变是作为来自除了它的视图或控制器之外的对象的消息的结果。例如,子串可以被添加到字符串的尾部,比如 SystemTranscript 个例。在这种情况下,依赖于模型的状态的对象 -- 它的视图 -- 必须被通知这个模型已经改变的。因为只有模型可以跟踪对它自身状态的所有改变,这个模型必须有连接到视图的一些通信。要满足这个需要,在 Object 中的一个全局机制提供了对模型和它的视图之间这种依赖的跟踪。这个机制使用叫做 DependentFields 的一个 IdentityDictionary (Object 的一个类变量),它简单的纪录所有现存的依赖。在这个字典中的键是已经有注册的依赖的所有对象;与每个键相关联的值是依赖于这个键的对象的列表。除了这个通用机制之外,类 Model 提供了管理依赖的一个更有效的机制。当你建立在 MVC 三元组中充当活跃模型的一个新类的时候,你应当使它们成为 Model 的子类。在这个层次中的模型在一个实例变量(依赖)中保持他们的依赖,它持有 nil,或则一个单一的依赖对象,或 DependentsCollection 的一个实例。视图依靠这些机制来通知它们在模型中的改变。当一个新视图被给与它的模型,它注册自己为这个模型的一个依赖。在释放这个视图的时候,它删除模型中自己的依赖。

在类 Object 的“updating”协议中有方法提供间接依赖的通信连接。打开一个浏览器并察看这些方法。 消息“changed”向这个对象的所有依赖发起通告这个对象中已经发生了变化。changed 消息的接收者发送消息 update: self 到它的每个依赖。这样模型可以通过简单的发送消息 self changed 来指示所有依赖的视图它已经改变了。视图(和任何注册为这个模型的依赖的对象)接收消息带有这个模型作为参数的消息 update: 。[注意: 还有一个 changed:with: 消息允许你传递一个参数到依赖。] 继承自 Object 的update: 消息的缺省方法什么也不做。但是在收到 update: 消息的时候多数视图有重新显示自身的协议。这个 changed/update: 机制被选择为通信渠道,因为它在模型的结构上放置了很少的约束,通过它可以通知视图模型中的变化。

要知道在 MVC 中如何使用这个 changed/update: 机制,在 changed 消息的发送者上打开一个浏览器(Smalltalk browseAllCallsOn: #changed)并在 update: 消息的实现者上打开另一个浏览器 (Smalltalk browseAllImplementorsOf: #update)。注意几乎所有的 update: 的实现者都是 View 的变体,并且它们的行为都是更新显示。你的视图将做类似的事情。changed 和 changed: 的发送者所在的方法中改变的这个对象的一些属性对它的视图很重要。此外,你使用的 changed 消息也会与之类似。

一个对象可以同时充当多于一个 MVC 三元组的模型。考虑一个建筑物的建筑模型。我们忽略这个模型自身的结构。要点是可以有一个平面图的视图,另一个外部透视图的视图,可能还有一个外部热损失的视图(用于估计能源效率)。每个视图可以有它自己的协作控制器。当模型改变的时候,通知所有依赖它的视图。如果只有这些视图的一个子集响应给定变化,模型可以传递指示发送了哪种改变的一个参数这样只用感兴趣的视图才需要响应。这个消息每个接受者可以检查这个参数的值来决定正确的响应。
 
视图-控制器连接

不像模型可以松散的连接到多个 MVC 三元组,每个视图只能关联一个唯一的控制器反之亦然。其中的实例 变量维持这个紧密耦合。视图的实例变量 controller 指向它的控制器,而控制器的实例变量 view 指向它关联的视图。 并且,因为二者必须与它们的模型通信,每个都有一个实例变量 model 指向模型对象。所以尽管模型被限制为只能发送 self changed:,视图和控制器二者可以直接相互和向它们的模型发送消息。

在给定 MVC 三元组中视图负责建立这个内部通信。当视图收到消息 model:controller: 的时候,它注册自己为这模型的一个依赖,设置它的 controller 实例变量为指向这个控制器,并发送消息 view: self 到控制器,这样控制器可以设置它的 view 实例变量。视图还负责取消这些连接。视图 release 导致它删除自己在模型中的依赖,发送消息 release 到控制器,接着发送 release 到子视图。

视图
视图/子视图等级

视图被设计为嵌套的。多数窗口实际上涉及至少两个视图,一个嵌套到另一个中。最外部的视图叫做顶视图(topView),它是 StandardSystemView 或者它的子类的一个实例。StandardSystemView 管理常见的窗口的标签条(label tab)。它关联的控制器是 StandardSystemController 的一个实例,它管理在顶层窗口可获得的常见的移动、构架(framing)、折叠(collapsing)和关闭操作。在顶视图内部是一个或多个子视图(subView和)与之关联的管理在这些视图中可获得的控制选项的控制器。例如常见的工作空间有 StandardSystemView 作为顶视图和一个 StringHolderView 作为它的单一的子视图。一个子视图可以依次有额外的子视图,尽管多数应用不要求这样。子视图/超视图关系纪录在继承自 View 的实例变量中。每个视图有一个实例变量 superView 指向包含它和其他视图的视图,一个实例变量 subViews 是它的 subView 的一个 OrderedCollection。这样每个窗口的 topView 是能够通过 superView/subViews 实例变量跟踪的视图层次的顶端。但是要注意一些视图类(比如,BinaryChoiceView 和 FillInTheBlankView)没有标签条,并且不能改变大小或移动。这些类不使用 StandardSystemView 作为顶视图;而是使用了一个朴素的 View 作为顶视图。

我们察看建造和启动一个 MVC 三元组的一个例子。这个例子是简化版本的打开一个 methodListBrowser 的代码 -- 在一个系统浏览器的方法列表子视图中选择实现者或发送者菜单条目的时候你见到的浏览器。这个浏览器上部的子视图显示方法的一个列表。当选择其中一个方法的时候,它的代码出现在下部的子视图中。这里的代码带有用来易于引用的行号。

openListBrowserOn: aCollection label: labelString initialSelection: sel
"为 aCollection 中的方法建立并调度一个方法列表浏览器。"
| topView aBrowser |
1.aBrowser := MethodListBrowser new on: aCollection.
2.topView := BrowserView new.
3.topView model: aBrowser; controller: StandardSystemController new;
4.label: labelString asString; minimumSize: 300@100.
5. topView addSubView:
6.(SelectionInListView on: aBrowser printItems: false oneItem: false
7.aspect: #methodName change: #methodName: list: #methodList
8.menu: #methodMenu initialSelection: #methodName)
9.in: (0@0 extent: 1.0@0.25) borderWidth: 1.
10. topView addSubView:
11.(CodeView on: aBrowser aspect: #text change: #acceptText:from:
12.menu: #textMenu initialSelection: sel)
13.in: (0@0.25 extent: 1@0.75) borderWidth: 1.
14. topView controller open

我们逐行察看这些代码。在建立模型 [1] 之后,我们建立顶视图 [2]。通常这将是一个 StandardSystemView,但是这里我们使用了一个 BrowserView,它是 StandardSystemView 的一个子类。 行 [3] 指定模型和控制器。[注意: 如果没有明确的提供控制器,在第一次要求这个视图的控制器的时候,这个视图的 defaultController 方法将提供控制器。许多应用在这个缺省方法中间接的指定控制器而不是在打开视图的时候明确的提供控制器。] 下一行提供顶视图的标签和最小大小 [4]。行 [5-9] 安装上部子视图,它是一个 SelectionInListView。行 [10-13] 安装下部 CodeView。这两种类型的视图叫做“可插入视图”。 在后面章节详细讨论这些。仔细看行 [9] 和 [13] 它们指示把子视图放置在顶视图占据的矩形中。有多种方式指示一个视图如何放置它的子视图比如 addSubView:below: 和 insertSubView:above:。它们可以在 View 的子视图插入协议中找到。这里给出的位置相对于一个规范的 1.0@1.0 矩形。你的代码不需要依赖顶视图窗口的最终大小和形状。上部视图被放置到左上角(就是 0@0) 并允许占据完全的宽度但只有顶视图的上部 25% 的高度(就是 extent: 1@0.25)。下部子视图被放置到 0@0.25 并允许占据窗口的剩余部分(1@0.75)。每个都给出一个像素的边框宽度。最后,打开控制器 [14] 它导致窗口初始化架构过程 -- 光标变成为左上角光标,这样用户可以架构这个窗口。你自己的 MVC 应用的打开也将类似。

 
显示视图

你的视图可能需要自己的显示协议。这个协议将用于初始化你的视图的显示,并用于在模型通告一个改变的时候重新显示(还能用于控制器发起的重新显示)。特定的,在 View 中的 update: 方法发送 self display。视图 display 依次发送 self displayBorder. self displayView. self displaySubviews。如果你的视图要求除了它继承的之外的任何特定的显示行为,则它属于这三个方法之一。你可以浏览 displayView 的实现者来查看不同种类的显示技术。如果你做了,你将发现许多现实方法使用了显示变换。

显示变换是 WindowingTransformation 的实例。它们处理缩放和平移来连接窗口和视区(viewport)。窗口输入到变换中。它是在抽象显示空间中的一个矩形,带有你觉得最适合你的应用的任何任意的坐标系统。 视区(viewport)可以被当作抽象窗口要映像到的显示屏幕上一个特定矩形区域。类 WindowingTransformation 在可显示的对象比如点和矩形上计算和应用缩放和平移因子,所以在变换的时候 aWindow 对应于 aViewport。但是变换简化并平移一组坐标到另一组坐标,因此没有必要连接到显示器屏幕;变换可以用于其它目的。

WindowingTransformations 可以复合,并可以调用它们的逆过程。视图可以使用变换来管理子视图放置。如果你需要直接绘制在一个视图上,则也可以使用它们。视图 displayTransform: anObject 应用接收者的显示信息到 anObject 并返回结果的缩放了的、平移了的对象。它通常用到带有定义在视图中的局部坐标系统的坐标的 Rectangles、Points、和其他对象上,来获取使用显示器坐标的一个缩放了的和平移了的对象。视图 displayTransformation 返回的 WindowingTransformation 是复合在接收者的超视图链上所有局部转换和这接收者自己的局部变换的结果。控制器 redButtonActivity 使用视图 inverseDisplayTransformation: aPoint 来把 aPoint (比如,Sensor cursorPoint) 从屏幕坐标变换到视图的窗口坐标。

对现存视图的解说

你的第一个应用视图可以开始于现存的视图,本节结束处给出它们的带注释的列表。其中一些,完整的应用比如浏览器检查器和调试器可作为样例。其它的通用视图在你的应用中可以完整的用作子视图。毫无疑问你将通过建立带有更多特定行为的子视图来精通一些视图。可以通过浏览使用一个给定视图的视图建立方法来学习使用很多视图。例如,执行 Smalltalk browseAllCallsOn: (Smalltalk associationAt: #SwitchView) 将呈现给你在所有发送消息到类 SwitchView 的方法上的一个方法浏览器。它们都是使用给定视图为子视图的其它视图的实例建立方法。你可以使用这些作为你自己的视图的样例。

四个现存的通用视图 -- BooleanView、SelectionInListView、TextView 和 CodeView -- 是特别灵活的。它们叫做“可插入视图”。它们的额外的灵活性意图减少对只在方法选择子上有所区别的众多子视图的需要,它们用来做常规的任务如从模型获取数据或表现一个不同的 yellowButtonMenu。可插入视图和它们的相关的控制器通过调用在实例建立的时候传递给它们的"adaptor"选择子来进行这些任务。在前面章节展示的 openListBrowserOn: 方法的例子中 SelectionInListView 和 CodeView 用作子视图。注意传递到这些视图的建立方法的参数是选择子的名字。每个可插入视图的类注释定义要传递到这个视图的选择子。

现存的视图层次

View - 用作 BinaryChoiceView 和 FillInTneBlankView 的非标准的顶视图
BinaryChoiceView - 拇指上/下提示器
SwitchView - 用在 BitEditor 和 FormEditor 中作为工具按钮
BooleanView - [可插入] 用于浏览器实例/类切换
DisplayTextView - 用于是/否提示器的上部子视图中的消息
TextView - [可插入] 不用在 vanilla V2.5 映像中
CodeView - [可插入] 用作展示代码的浏览器子视图
OnlyWhenSelectedCodeView - 用于 FileList 下部子视图
StringHolderView - 用于工作空间
FillInTheBlankView - 带有文本答案的常见的问题提示器
ProjectView - 一个项目的描述视图
TextCollectorView - 用于记录簿
FormMenuView - 用于 BitEditor 和 FormEditor 的按钮
FormView - 用作 BitEditor 和背景屏幕灰色 InfiniteForm
FormHolderView - 用作 BitEditor 和 FormEditor 的编辑表单
ListView - 不用在 vanilla V2.5 映像中
ChangeListView - 完整的应用
SelectionInListView - [可插入] 用作浏览器列表子视图
StandardSystemView - 提供 topView 功能
BrowserView - 完整的应用
InspectorView - 完整的应用
NotifierView - 错误通知器,比如“不知道的对象”
控制器

Smalltalk-80 呈现的外观是控制驻留在鼠标中。当你移动和点击鼠标的时候,在 Smalltalk-80 屏幕上的对象表现得如同服从指挥的乐队。而实际上没有单一的独断权力,附着到各种活跃视图的控制器的协作维持着一个单一的控制线索。只有一个控制器在某个时刻实际上拥有控制。所以诀窍是确保它是正确的那个!

控制器之间通信

确保这个诀窍成为可能的主要组织原理是每个对象的活跃控制器形成一个有层次的树。这个树的根是全局变量 ScheduledControllers,它是附着到活跃项目的一个 ControlManager。源自 ScheduledControllers 的分支是每个活跃窗口的顶层控制器,加上管理在灰色屏幕背景上可获得的主系统 yellowButtonMenu 的一个额外的控制器。因为每个视图都关联着一个唯一的控制器,视图/子视图树在每个顶视图内导出一个平行的控制器树。源自每个顶层控制器的进一步的分支遵循着这个导出的树。控制沿着这个树的分支从控制器传递到控制器。

用最简单的术语,控制流要求有教养的控制器的协同动作,它们都斯文的拒绝控制除非光标在它的视图中。 并且有教养的控制器在接受控制的时候尝试交给某个子视图的控制器。顶层的 ControlManager 询问每个活跃的顶视图的控制器是否想要控制。只有包含着光标的那个视图给与肯定的回应并给与它控制。它依次询问它的子视图的控制器。再一次包含着光标的那个接受控制。这个过程找到包含光标的最内嵌套的视图,并且一般的,这个视图的控制器保持控制只要光标停留在它的视图中。(在附录 A 中有关于控制流的更详细的披露。) 在这个方案中,控制管理涉及所有活跃视图和控制器的错综复杂的协调协作。要求视图选出它的子视图的控制器。控制器询问它们的视图是否包含光标。为此,不同寻常的 -- 并且冒险的 -- 去修改你的涉及非标准控制流的视图或控制器。牢记在头脑中,在你首次尝试安装一个新应用程序控制器的时候,控制流的无意中的中断会使系统崩溃。谨慎的编程者在做这种尝试之前为系统作一个 snapshot!

控制器扮演的至关重要的角色暗示者你不能有没有控制器的模型-视图对。如果允许这样,在缺少的控制器留下的间隙上控制流会消失。但是有些情况你可能希望由一个包容控制器集体的控制一组子视图,而不是由子视图的单独的控制器。类 NoController 为这样的子视图提供了一个特殊的控制器,它被特殊的构造为拒绝控制。

当光标在浏览器的子视图之间移动时滚动条的舞蹈是控制流的最明显的结果。当光标跨越浏览器内的子视图的边界的时候,刚退出的子视图的滚动条消失而进入的子视图的滚动条出现。这是通过带有继承自 ScrollController 的控制器的那些子视图的 controlInitialize 和 controlTerminate 方法完成的。当光标退出给定子视图的时候,viewHasCursor 返回假,并且控制器执行它的 controlTerminate 方法,它重新显示以前的滚动条覆盖的区域。接着经过视图/控制器树的适当的部分来找到现在应该拥有控制的控制器。 如果这个视图应当拥有一个滚动条,新控制器的 controlInitialize 方法保存这个滚动条要覆盖的区域,接着显示这个滚动条。

进入和离开控制流

记住这种礼貌的舞蹈是一个经常进行的事情。你新建立的 MVC 三元组如何步入这个过程? 并且当作完了的时候如何退出?

首先,你的顶视图的控制器负责进入这个过程。它接着传递控制到它的子视图控制器(在典型的应用中它们实际上做主要的工作)。顶层控制器必须都是类 StandardSystemController 的后代,它们被设计为顶层视图的控制器。到 standardSystemController 的 open 消息导致你的新 MVC 成为控制树的一个顶层分支。在建立新 MVC 的方法中 open 消息应当是最后的消息,因为这个方法不返回控制。在控制器 open 消息之后的代码将不被执行。StandardSystemController 的 controlTerminate 方法负责在关闭窗口的时候取消安排。

鼠标菜单控制器

多数应用使用鼠标来定点和选菜单选项。在类层次中多数控制器都被安置 MouseMenuController 下的某个地方,它提供基本的实例变量和方法。有关的实例变量是 {red, yellow, blue}ButtonMenu 和 {red, yellow, blue}ButtonMessages,你将在其中安置你的菜单和它们关联的消息。重要的方法有 redButtonActivity、yellowButtonActivity、和 blueButtonActivity。 控制器 controlLoop 方法,象名字暗示的那样,它是主控制循环。每次通过这个循环,它都发送 self controlActivity。在 MouseMenuController 中重新实现了这个方法来检查每个鼠标按钮,例如,如果按下了红色按钮并且它的视图拥有光标,则发送 self redButtonActivity。xxxButtonActivity 检查一个非 nil xxxButtonMenu,并且如果找到了则发送消息: self menuMessageReceiver perform: (redButtonMessages at: index)。注意 : menuMessageReciever 通常返回自己 -- 就是控制器 -- 所以这个菜单消息协议通常驻留在这个控制器中。

所有顶层控制器都是 StandardSystemController 或它的子类的实例。StandardSystemController 是 MouseMenuController 的一个子类,它被专门化为处于一个窗口的控制器层次的顶层。他管理常见的 blueButtonMenu -- 架构、关闭、折叠、. . . 窗口的行为 -- 它们应用于顶视图。你的子视图控制器不应该是 StandardSystemController 的子类。它们应当在与 MouseMenuController 分离的继承分支。 blueButton 菜单功能应当仍由顶层控制器来处理。要确保如此你的控制器可以通过下面的 blueButtonActivity 把 blueButton 按下重路由到顶层:

blueButtonActivity
view topView controller blueButtonActivity.

这是非常透明的,一个人浏览你的控制器代码时可以立即见到 blueButton 是被顶视图控制器处理的。子视图控制器有一个更微妙的途径在 blueButton 按下的时候拒绝控制。这是在 isControlActive 方法中的做的:

isControlActive
^super isControlActive & sensor blueButtonPressed not.

在典型的情况下,你的控制器有它自己的 yellowButtonMenu,并可能使用红色按钮来某种定点或项目选择功能。你通常将象下面这样制作你的菜单:

PopUpMenu labels:
'foo baz
over there
file out
new gadget'
lines: #(1 3).

这提供了带有给定选项的一个菜单,并在第一个和第三个条目之后带有横线。你接着安装一个并列的消息列表。比如,#(fooBaz overThere fileOut newGadget)。菜单和消息列表必须归宿于(end up) yellowButtonMenu 和 yellowButtonMessages 实例变量中。如果需要可以迅速的(on the fly)完成,但是更典型的方式是在类 initialize 方法方法中建立菜单和消息并在实例 initialize 方法中安装它们。例如,察看 ChangeListController 类 initialize 方法。最后,你需要安装实现菜单消息的方法。它们按照惯例驻留在你的控制器的菜单消息协议中。如果你希望把 redButton 按下用做控制活动而不是菜单选择,重定义你的控制器的 redButtonActivity 到实现想要的活动。在你的新 redButtonActivity 方法中你可以检查其他条件比如 Sensor leftShiftDown 来提供更多的灵活性。你可以在 FormEditor、ListController、和 SelectionInListController 中看到非菜单的 redButtonActivity 方法的例子。

菜单, 尽管它们使用了显示屏幕,不由 MVC 三元组来处理。它们操控它们自己的屏幕显示和控制,包括保存和替代被菜单所覆盖的区域中的屏幕内容。除了上面例子中的 PopUpMenu 之外,你应该察看特别用于可插入视图 ActionMenu。

段落编辑器

所有接收键盘文本输入的控制器在控制器层次中位于 ParagraphEditor 的下面。ParagraphEditor 早于 MVC 范例的完整开发。它同时处理文本输入和文本显示功能,所以它在某些方面是视图和控制器之间的一个交叉。使用了它或它的子类作为控制器的视图倒转了在显示中的平常的角色;它们通过发送 controller display 来实现文本的显示。ParagraphEditor 所扮演的多重角色使它成为一个复合对象。它操控特殊键(例如,Ctrl T 映射为 ifTrueSmile。它还操控文本的选择,字体和点大小的选择,和格式化文本(就是,比例字符的间隔和裁减到视图宽度的断行(line break))。因为它在文本处理控制器的层次的顶部,你可以轻易的访问到它的所有能力。但是文本处理类的复杂性意味着非常大的开销。在键入一个字符和它的显示到屏幕上之间的烦人的延迟就能体现出这种开销。做多数文本处理工作的三个类是 ParagraphEditor、 Paragraph、和 CharacterScanner。一些 Smalltalk-80 编程者建立了与这几个类并列的类,它们免除了多数时间消耗特征。他们对特定应用完成文本处理的速度比标准类要快许多倍。

屏幕控制器

灰色的屏幕背景和在这个背景上可获得的 yellowButtonMenu 不是 MVC 范例的特殊例外;它们也由一个 MVC 三元组操控。模型是 InfiniteForm (有色的灰度),视图是 FormView 的一个实例,并且控制器是类 ScreenController 的唯一的实例。要向主屏幕菜单增加一个条目(比如一个 printScreen 选项),你可以编辑 ScreenController 类方法 initialize (在类初始化协议中)并在菜单消息协议中增加你的方法。不要忘记执行在类初始化方法的底部的注释来安装你的新菜单。你还可能希望察看菜单消息协议中其他方法来获知如何打开浏览器、文件列表、或工作空间。

MVC 检查器

由于 MVC 三元组非常重要,Smalltalk-80 提供了一个专门的检查器 -- MVC 检查器 -- 来一次性检查所有的三个对象。当你开始建造你自己的应用程序的时候,你可能会考虑使用这个检查器,同时用作察看其他 MVC 三元组如何工作的工具和用做察看你的程序为何不工作的无价的调试工具。你可以通过向任何视图发送 inspect 消息来打开一个 MVC 检查器。使用 MVC 检查器的最容易的方式是 fileIn Smalltalk-AT 发行提供的 BLUEINSP.ST goodie。[注意: 不是所有的 Smalltalk-80 发布都包含这个 goodie。] 它为所有顶视图安装一个蓝色按钮菜单条目 "inspect"。这个菜单条目在这个视图、它的模型和它的控制器上打开一个 MVC 检查器。一旦你理解了一些 MVC 三元组,MVC 检查器将让你任何窗口的内部闲逛来发挥你想象力。在带有多个子视图的一个复合的视图比如浏览器中,你可以沿着子视图链,在选择的子视图上打开进一步的 MVC 检查器。

作为实验,在系统记录簿窗口上打开一个 MVC 检查器(首先确定在你的屏幕上有一个 SystemTranscript)。 通过执行 DependentsCollection allInstances inspect 在 DependentsCollection 的实例上打开一个检查器。定位有两个依赖“a StandardSystemView”和“aTextCollectorView”的条目。选定 TextCollectorView 并选择 inspect。现在你将在 TextCollector、它的视图、和它的控制器上有一个 MVC 检查器。例如,在顶部模型子视图中,选择 contents 变量: 在右面窗口中,你将见到同你见到与在你的记录簿窗口中相同的文本。在底部控制器区域察看并检查按钮菜单和消息。它们将是常见的一个纪录簿。在中间的视图区域,选择 superView: 它将是一个 StandardSystemView。选定它并选择 inspect。你拥有在这个记录簿窗口的顶视图上的另一个 MVC 检查器。这两个 MVC 检查器在一起访问记录簿应用的整个结构。注意,两个 MVC 三元组共享同一个模型 -- TextCollector。

[注意: 在 Smalltalk-80 包含 Model 类之前的版本中,你需要困难的进行: 通过执行 (Object classPool at: #DependentsFields) inspect 在 DependentsFields 字典上打开一个检查器(在这个字典中维护所有对象依赖)。这让你访问在 IdentityDictionary 上的一个检查器。用键“aTextCollector”定位条目,它是记录簿的模型。选定它并在 yellowButtonMenu 中选择 inspect 选项。这在注册为TextCollector的依赖的对象的 OrderedCollection 上打开检查器。在这个新检查器中,条目之一是 TextCollectorView。选定 TextCollectorView 并选择 inspect。]

附录 A:
控制流的详情

所有顶视图的控制器的上面是 ScheduledControllers -- 附着到当前项目上的 ControlManager 的实例。这个 controlManager 决定哪个顶视图的控制器应该活跃。它使用方法 searchForActiveController,它依次发送 isControlWanted 到所有 scheduledControllers。当它发现合适的顶层控制器的时候向这个控制器发送消息 startUp,它继承自类 Controller。它发送下列三个消息: self controllInitialize, self controlLoop, self controlTerminate。controllLoop 方法如下处理控制流:

[self is ControlActive] whileTrue: [Processor yield.
self controlActivity]
self controlActivity 只是说: self controlToNextLevel。这里通过代码的方式把控制流简要的传递给视图:

aView := view subViewWantingControl.
aView ~~ nil ifTrue: [aView controller startUp]
它只是要求视图决定哪个子视图想要控制,并且如果某个需要,则传递控制到这个视图的控制器。isControlWanted 方法简单的返回消息 self viewHasCursor 的结果,它依次简单的说 view containsPoint: sensor cursorPoint。因而,在控制流处理的底部是哪个视图包含光标的问题。例外的一个是 NoController,它重新实现了 isControlWanted 来总是返回 false。

 
感谢 Charles Lloyd HTML 化了这个文档。

如果你发现了错误,请联系 Ian。
上次修改: Tue Mar 4 13:58:26 1997




话题树型展开
人气 标题 作者 字数 发贴时间
4689 Smalltalk-80(TM)中的应用编程:如何使用模型-视图-控制器(MVC) ralf1999 16726 2004-12-12 18:07

reply to postflat modethreaded modego to previous topicgo to next topicgo to back
  已读帖子
  新的帖子
  被删除的帖子
Jump to the top of page

   Powered by Jute Powerful Forum® Version Jute 1.5.6 Ent
Copyright © 2002-2021 Cjsdn Team. All Righits Reserved. 闽ICP备05005120号-1
客服电话 18559299278    客服信箱 714923@qq.com    客服QQ 714923