这是我关于 Robotlegs AS3 的介绍性系列文章的第二部分。 在本系列的第一部分,你已经了解 Robotlegs AS3* 是什么以及对 Robotlegs 的 Context 和 Mediator 类的简介("HelloWorld" )。 如果你错过它,请查阅 Part 1: Context and mediators。 本文扩展了这些概念并且引入了各种模型。
你应该熟悉ActionScript 3和面向对象的术语和原理。 一些框架的经验非常有用,但不是必需的。 确保你已经阅读了第一部分:上下文与仲裁器 。
Model 类可以封装你的应用程序的数据并且提供一个访问和处理这些数据的 API。 你的应用程序的其它类将通过这一 API 发起模型请求。 当在模型中更新数据时,该模型将下发你的应用程序中的其它类能够响应的事件。 模型应该适用于捕捉域逻辑(domain logic),例如执行计算或其它处理等。 其中一个范例是购物车。 当一个条目添加到购物车模型时,购物车中的所有条目的新总价将被计算出来。 之后,该新的总价将被存储到模型中,以便应用程序中的其它类访问。 通过将这一逻辑放置到你的模型中,你可以确保它在整个应用程序中不会分散,并且知道在什么位置查看你的数据是如何及在什么时间处理的。
除了控制对数据的访问之外,模型还能够管理你的应用程序的状态。 从数据模型的意义来看,这里的 state 和 Flex state 不是相同的概念,而 Flex state 与控制你的应用程序的外观是相关的。 它们当然是相关的,但带有一个模型,并且考虑一列对象。 你可能希望跟踪这些对象中被选中的对象,因此相应的数据模型具有选中的属性,它能够通过当前选中的条目进行更新。 你的应用程序的其它部分现在能够访问该属性以便寻找被选中的条目并且作出相应的反应。
模型能够编写为可移植代码。 当你开发你的模型时,除了服务(Services)之外,你还应该记住这一点。 有许多公共数据集能够在应用程序之间方便地传送。 作为一个范例,你可以看一下UserLoginModel 或 ShoppingCartModel。 可移植性需要多花费一些思考和精力,但不会多于为每个项目重复编写相同代码的工作量。
该模型值得投入大量的关注。 它是你的应用程序的核心。 视觉组件能够获得所有的赞叹,但作为一个开发人员,你应该知道数据才是真正的幕后人物。 作为一个开发人员,编写这些数据并且准确地将它们传送到美丽的界面条目上是我们的工作。 这就是为什么在模型中对域逻辑进行隔离是如此重要。 通过对域逻辑进行隔离,你使得它更易于寻找、更新和维护。 在了解模型的基本定义之后,让我们来看一下一个小型范例应用程序。
这一纯 ActionScript 3 应用程序充分利用 Keith Peters 的很棒的 Minimal Comps 库*。 不用担心,如果你喜爱 Flex(你怎能不喜欢?),下一个范例将是一个 Flex 应用程序,但 Minimal Comps 使得利用 ActionScript 3 快速制作范例易如反掌,并且 Robotlegs 直接与 ActionScript 应用程序一起使用就像它与 Flex 应用程序一起使用一样简单。 因此,让我们看一下应用程序并且将其拆开以便仔细讨论其每个部分。
视频
上面是我们将要讨论的应用程序。 它是一个简单的作者列表,在列表中被选中的作者将显示一段引语。 你可以下载归档的 Flash Builder 项目,其中也包括 Robotlegs 1.1 和 MinimalComps SWC 文件,它们位于本文页面的顶部。
SimpleListExample.as
public class SimpleListExample extends Sprite { public function SimpleListExample() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; } }
这是应用程序的主文件,即入口点。 它是一个简单的 Sprite。 这是一个 Robotlegs 应用程序,因此我们需要做的第一个操作是创建一个 Context:
SimpleListExampleContext.as
public class SimpleListExampleContext extends Context { public function SimpleListExampleContext(contextView:DisplayObjectContainer) { super(contextView); } override public function startup():void { injector.mapSingleton(AuthorModel); mediatorMap.mapView(ListView, ListViewMediator); mediatorMap.mapView(QuoteTextArea, QuoteTextAreaMediator); mediatorMap.mapView(SimpleListExample, ApplicationMediator); } }
在 Flex 应用程序中通常完全不考虑构造器,因为contextView是在 MXML 声明语句中设置的。 由于这是一个 ActionScript 应用程序,因此你需要将contextView传递到构造器中,这样可以立即对它进行设置。 现在让我们在主应用程序视图中创建一个 SimpleListExampleContext 的实例:
SimpleListExample.as
public class SimpleListExample extends Sprite { public var context:SimpleListExampleContext; public function SimpleListExample() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; context = new SimpleListExampleContext(this); } }
你应该注意到相应的 context 变量。 你必须为保存在内存中的你的 context 建立一个引用。 如果你只简单地在构造器中创建 SimpleListExampleContext 的一个新实例,而不将它放置到一个变量中,则在启动 Flash Player 时它将作为垃圾而被回收。 这将导致进行数小时故障排查的严重混乱局面! 利用 Flex,你只需要通过 MXML 声明该 context,它可以与相应的引用关联而不需要变量。除非你在 Script tag 中创建该 context。 因此,它需要一个引用,与上述 ActionScript 范例的情形一样。
该应用程序将具有两个视图:一个是可以选中的名称列表,另一个是可以显示来自列表中被选中的条目的引语的文本区。 这两个视图被称作:
没有觉得这里的命名方案具有太多的创意。 这些视图是基础 Minimal Comps 类的简单子类。 下面是它们的代码:
ListView.as
public class ListView extends List { public function ListView(parent:DisplayObjectContainer) { super(parent); } }
QuoteTextArea.as
public class QuoteTextArea extends TextArea { public function QuoteTextArea(parent:DisplayObjectContainer) { super(parent); } }
该应用程序仅具有其中一个视图。 即使我们只使用基础List 和TextArea类,它也能够提供所有的功能。 为什么还要使用子类呢? 这是一种习惯-当处理依赖注入(dependency injection)时,这是一种良好习惯。 如果我们知道我们将对一个视图类进行调解,我们应该将其变成子类并且为其提供一个能够表示其目的的名称。 当我们以这一方式实现上述操作时,它能够方便地被隔离以便进行调解。
现在我们需要做的一切是将上述两个视图作为主程序的子视图放到Stage之上。 在ActionScript应用程序中,我喜爱的方法是将一个createChildren方法放置到主视图中,然后从一个中介者(mediator)中调用它。 下面是具有createChildren()方法的主视图:
SimpleListExample.as
public class SimpleListExample extends Sprite { private var hbox:HBox; private var list:ListView; private var quoteText:QuoteTextArea; public var context:SimpleListExampleContext; public function SimpleListExample() { stage.align = StageAlign.TOP_LEFT; stage.scaleMode = StageScaleMode.NO_SCALE; context = new SimpleListExampleContext(this); } /** * Called from ApplicationMediator's onRegister() */ public function createChildren():void { hbox = new HBox(this,0,0); addChild(hbox); list = new ListView(hbox); list.alternateRows = true; quoteText = new QuoteTextArea(hbox); quoteText.editable = false; quoteText.selectable = false; } }
你也许感到奇怪为什么不直接在构造器中添加视图? 我喜欢在添加子类之前启动 Robotlegs。 通过从 ApplicationMediator 中调用createChildren(),我们能够100%地确保所有的主要应用程序的引导指令均创建完毕,因此我们能够继续进行下一步操作。 下面是相应的 ApplicationMediator 代码:
ApplicationMediator.as
public class ApplicationMediator extends Mediator { [Inject] public var view:SimpleListExample; override public function onRegister():void { view.createChildren(); } }
在本简单范例中,中介者(mediator)不会具有超过上述的一个职责,但在你的应用程序中,调解contextView视图是一种便捷的机制。 在中介者创建之后,它需要进行映射。 这是一个容易忘记的步骤,但当你点击 Debug 而你的中介者没有响应时,开发人员会回想到这一步骤。 而奇怪的事情是它没有进行映射或它的视图组件从来都不是 ADDED_TO_STAGE。 现在,你的 context 中的 startup 方法应该与下列代码一样:
SimpleListExampleContext.as
override public function startup():void { mediatorMap.mapView(SimpleListExample, ApplicationMediator); }
当你现在运行应用程序时,你应该看到一个空的列表和文本区。 现在我们需要为视图获取一些数据。 我们将在一个Model类中放入一些静态数据,而我们的视图组件的中介者将能够访问这些数据。 下面是相应的基本模型:
AuthorModel.as
public class AuthorModel extends Actor { private var _list:Array; public function get list():Array { if(!_list) initializeList(); return _list; } protected function initializeList():void { var twain:Author = new Author("Twain"); var poe:Author = new Author("Poe"); var plato:Author = new Author("Plato"); var fowler:Author = new Author("Fowler"); twain.quote = "Why, I have known clergymen, good men, kind-hearted, liberal, sincere" + ", and all that, who did not know the meaning of a 'flush.' It is enough " + "to make one ashamed of one's species."; fowler.quote = "Any fool can write code that a computer can understand. " + "Good programmers write code that humans can understand."; poe.quote = "Deep into that darkness peering, long I stood there, wondering, " + "fearing, doubting, dreaming dreams no mortal ever dared to dream before."; plato.quote = "All things will be produced in superior quantity and quality, and with greater ease, " + "when each man works at a single occupation, in accordance with his natural gifts, " + "and at the right moment, without meddling with anything else. "; _list = [twain,fowler,poe,plato]; } }
你将立即注意到 AuthorModel 扩展了Actor 的功能。Actor 是一个提供了 MVCS 的方便的类。 它可以提供适用于注入用于在 context 中进行通信的 IeventDispatcher 的合适代码以及通过相应调度程序发送事件的一 个dispatch()方法。 扩展 Actor 不是一个必须进行的操作,但如果你不进行该操作,则你必须自己提供相应的 IeventDispatcher 注入点。 在 Robotlegs MVC+S中,Model 纯粹是一个概念;这里没有 Model 类需要扩展并且命名惯例只是用于表达类的目的。
该模型目前提供一些我钦佩的作者(Authors),其中每个作者均拥有一段引语。 Author 类是一个用于保存这些属性的简单值对象,正如你在下面看到的一样:
Author.as
public class Author { public var name:String; public var quote:String; public function Author(name:String) { this.name = name; } /** * Minimal comps took issue with toString(); * @return * */ public function get label():String { return name; } }
随着模型数据的充满,我们需要将这些数据传送到我们的显示列表。 第一步是映射用于注入操作的模型。 第二步是调解 ListView 并且将数据从我们的模型传送到相应的列表。 在对模型进行映射之后,你的 context 的startup()方法应该如下所示:
SimpleListExampleContext.as
override public function startup():void { injector.mapSingleton(AuthorModel); mediatorMap.mapView(SimpleListExample, ApplicationMediator); }
为了对模型或任何其它你希望注入的类进行映射,你必须使用context的注入器(injector)。 该注入器具有多个映射类的方法以及用于注入的值。
mapSingleton()是对简单的注入类进行映射的常用方法。 应该注意在严格的设计模式意义上,context 中的术语 singleton 不是一个 Singleton 类。 在这种情形下,mapSingleton()意味着注入器将创建和提供 AuthorModel 的一个单一实例,而无论它是否被要求这样做。 你可以拥有你期望数量的 AuthorModel 实例,但注入器将根据上述的映射只创建和注入你要求的实例。 此外,还有一些其它利用注入器进行映射的方法,关于 SwiftSuspenders Injector,我强烈推荐你阅读 Till Schneideriet的文档* 。 在默认情形下,这是 Robotlegs 使用的注入器。 另外,Robotlegs Best Practices 文档* 也讨论了该 Injector 的使用方法。 除了该文档之外,我将在未来的文章中讨论其它注入映射选项。
随着模型的注入映射,我们离将 Authors 放入他们的列表更近一步。 为了实现这一操作,我们必须调解 ListView 并且将 AuthorModel 注入到相应的中介者。 下面是用于 ListView 的中介者:
ListViewMediator.as
public class ListViewMediator extends Mediator { [Inject] public var view:ListView; [Inject] public var authorModel:AuthorModel; override public function onRegister():void { view.items = authorModel.list; } }
在已调解的视图组件的[Inject] 标记符之外,我们没有看到[Inject] 标记符的使用或讨论Robotlegs如何访问你的类的依赖注入。 在 ListViewMediator 中, you 将看到注入的视图。 除了视图之外,还有一个名称为authorModel的 public 属性,它属于带有[Inject] 标记符的 AuthorModel 类型。 当你将[Inject] 标记符放置于一个 public 属性之上并且通过 Robotlegs 创建该类时,它基于你已经映射的规则"注入"该属性。 请注意如果你没有提供注入的规则,你将收到一个运行时错误提示你注意该问题。 如果这一情况发生,你必须核查你的映射。 在这种情形下,我们已经使用mapSingleton()为注入准备 AuthorModel 。
在运行的中介者(mediator)的onRegister()方法中,当中介者已经完全构造出来并且提供注入时,我们将访问该模型并且将list属性赋给视图的 items 属性。 很棒! 我们应该在列表中看到这些条目。
...嗯...
...好,成功!...
首先,我们需要对中介者进行映射:
SimpleListExampleContext.as
override public function startup():void { injector.mapSingleton(AuthorModel); mediatorMap.mapView(ListView, ListViewMediator); mediatorMap.mapView(SimpleListExample, ApplicationMediator); }
好了。 已经对 ListView 进行调解。 请注意映射的顺序。 ListView 中介者的映射位于 SimpleListExample(主视图)的映射之上。 SimpleListExample 是主视图,contextView也是主视图。 当对contextView的类进行映射时,对 contextView 的处理方式有点不同。contextView将立即被调解,而不是侦听将要发生的 ADDED_TO_STAGE 事件。 由于contextView通常已经位于 Stage 之中,因此我们不能可靠地侦听一个可能永远不会发生的事件。 由于我们希望在 ListView 被添加到 Stage 时,它将被调解,因此我们应该确保在 ApplicationMediator 的onRegister()有机会被调用之前,它已经被映射。 如果你记得的话,ApplicationMediator 的onRegister()正是我们要求主视图添加其子视图的地方,其中包括 ListView。
因此,随着 ListView 数据的填满,现在你可以在该列表中选中 Authors 的列表。 相当令人兴奋,不是吗? 你应该自制一点。 因为你还有很长的路要走。
现在我希望使用一些文本填充 QuoteTextArea —最好是来自选中Author的一段引言。 为了实现这一目的,我们需要对 AuthorModel 添加一些功能,以便它能够跟踪选中的 Author。 当在 ListView 中选中一个 Author 时,ListViewMediator 将更新相应的 AuthorModel 。 然后,AuthorModel 将发送一个事件以通报可能正在侦听的任何模型一个 Author 已经被选中。 我们还需要为 QuoteTextArea 创建一个中介者,以便它能够侦听该事件并且利用来自选中 Author 的引语。 我们知道我们将需要该事件,因此让我们首先编写相应的代码:
SelectedAuthorEvent.as
public class SelectedAuthorEvent extends Event { public static const SELECTED:String = "authorSelected"; private var _author:Author; public function get author():Author { return _author; } public function SelectedAuthorEvent(type:String, author:Author = null, bubbles:Boolean = false, cancelable:Boolean = false) { super(type, bubbles, cancelable); _author = author; } override public function clone():Event { return new SelectedAuthorEvent(type, author, bubbles, cancelable) } }
该事件是相同普通的。 它是一个典型的自定义事件,带有一个单一常量 SELECTED,用于一个类型和一个可选 author 参数。 在我们拥有该事件之后,我们需要更新 AuthorModel 以跟踪选中的 Author 并且通报相应的应用程序它在什么时候已经改变:
AuthorModel.as
private var _selected:Author; public function get selected():Author { return _selected; } public function set selected(value:Author):void { _selected = value; dispatch(new SelectedAuthorEvent(SelectedAuthorEvent.SELECTED, selected)); }
通过将上面代码添加到 AuthorModel,我们能够有效地跟踪当前选中的 Author。 为了在模型中设置该属性,我们将更新 ListViewMediator 以侦听来自 ListView 的选择事件并且当它发生时更新该模型:
ListViewMediator.as
override public function onRegister():void { view.items = authorModel.list; addViewListener(Event.SELECT, handleSelected) } private function handleSelected(event:Event):void { authorModel.selected = view.selectedItem as Author; }
在 ListViewMediator的onRegister()的内部,我们将使用addViewListener()方法在由handleSelected方法处理的Event.SELECT 的视图中添加一个事件侦听器。 现在,当在该列表中选中一个条目,相应的事件处理程序方法将访问 AuthorModel 的选中属性,并且利用被选中的条目对它进行更新。 该值将被设置,并且 AuthorModel 将下发相应的事件通报应用程序的其它条目这一操作已经发生。
"为什么不在这里直接下发选中的事件并且对它进行处理?"
你完全可以,并且如果你的应用程序是如此的简单,则它很可能是合理可行的方法。 在我的一生中还没有遇到许多如此简单的应用程序,并且这是一个如何使用模型的范例,因此,这就是为什么我们在这里看似还有很长的路要走的原因。 尽管现在我们已经沿着这条路前行,但我们仍然需要将相应的文本添加到 QuoteTextArea 中,因此,让我们使其获得调解:
QuoteTextAreaMediator.as
public class QuoteTextAreaMediator extends Mediator { [Inject] public var view:QuoteTextArea; override public function onRegister():void { addContextListener(SelectedAuthorEvent.SELECTED, handleSelectedAuthorChanged) } private function handleSelectedAuthorChanged(event:SelectedAuthorEvent):void { var author:Author = event.author; view.text = author.quote; } }
好了,这就是相应的代码。 现在,QuoteTextArea 已经与应用程序的其它部分建立联系。 在它的onRegister中,我们将使用addContextListener()来侦听 SelectedAuthorEvent.SELECTED 并且利用handleSelectedAuthorChanged对它进行处理:
handleSelectedAuthorChanged方法从该事件中获取相应的信息,然后利用来自选中 Author 的引语更新视图的 text 属性。 在我们使得中介者映射之后,我们就已经实现了本范例的功能:
SimpleListExampleContext.as
override public function startup():void { injector.mapSingleton(AuthorModel); mediatorMap.mapView(ListView, ListViewMediator); mediatorMap.mapView(QuoteTextArea, QuoteTextAreaMediator); mediatorMap.mapView(SimpleListExample, ApplicationMediator); }
现在,你已经拥有了一个在 Robotlegs 应用程序中包含使用模型的基本知识的范例。 你的数据和模型的监护程序(guardian)在你的应用程序中扮演了极为重要的角色。 通过将模型用作你的数据的访问点,你能够对保存和管理数据的位置进行隔离。 当你准备解决一个难题时,你应该知道在什么位置寻找关于数据和它对你的应用程序状态的后续表示的问题。 如果你的数据散落在你的应用程序中,它将变成一个黑色的鱼塘,这将很难快速隔离故障点或将功能添加到应用程序中。
本文是对模型的一个简介。 我强烈推荐你对 MVC 的M进行一些深入的研究。 在下一部分,我将讨论在 Robotlegs 应用程序中如何使用各种服务。 在讨论服务之后及转向一些更高级的话题之前,我们将浏览一下完成 Robotlegs MVC+S 实施的命
如果你不能等待,那么在我的博客上*(以及因特网的大量其它博客上)有许多涉及各种 Robotleg 话题的文章,其中包括我的一段25分钟的视频录像*。John Lindquist 在他的博客中提供了一段 Hello World屏幕录像*。此外,有一份最佳方法文档*有一份最佳方法文档对许多开发人员非常有用。 另外,你可以随时点击 Robotlegs Knowledge Base* 以便获得帮助和支持。 有一个非常活跃的社区志愿者小组,其中包括我自己,能够坚持不懈地回答所有涉及Robotlegs的问题。 关于这些话题及更多其它话题的深入讨论,参见 ActionScript Developer's Guide to Robotlegs*。
接下来的文章是第三部分:服务(Part 3: Services)*.
本文基于Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License*协议发布。
查看原文:Robotlegs简介 – 第二部分:模型