Robotlegs2中文教程-1使用MVCBundle

Robotlegs2中文教程-1使用MVCBundle

本系列全部文章:using robotlegs2

目的

本章使用Robotlegs2自带的MVCBundle实现一个简单的MVC实例。

Robotlegs2在架构设计上,框架仅实现了生命周期管理、Logger、消息调度、插件管理器、配置管理器等核心功能,其他功能全部使用插件实现。而MVCBundle,就是Robotlegs2提供的一个插件和配置集合,这个集合包含所有MVC需要的插件和功能。

本章不会研究Robotlegs2在结构上的设计,而是从最终用户的角度来使用MVCBundle。若希望了解Robotlegs2的架构,请关注本系列后续文章。

本章也不会详细介绍MVCBundle的所有用法,那样会导致本文篇幅过长。下一章会进行详细介绍。

Timer-MVCBundle-Sample概述

Timer-MVCBundle-Sample是本章的实例名称。这个实例实现了一个简单的定时器。看看定时器的3个状态:

  1. 初始状态,设置时、分,类名为TimerSetView。按Start按钮开始倒计时。

TimerSetView

  1. 倒计时状态,类名为TimerActionView。按Cancel按钮取消倒计时,回到初始状态。

TimerActionView

  1. 倒计时时间到,类名为AlertView。按Dismiss按钮回到初始状态。

AlertView

项目依赖

项目结构

这里简单介绍一下项目中的包分类,以及各个部分的作用。介绍按照MVC分块进行。如果你性急,也可以跳过这部分直接看后面的项目详解。

View + Mediator

刚才看到的后缀为View的类,我叫它们“视图类”,所有的视图类,都位于 view 包中,并实现 ITimerView 接口。 ITimerView 中并没有包含任何方法签名。实现这个接口,是为了方便进行类型匹配。

视图类用于我们能看到的定时器的3个状态,每个状态对应一个视图。定时器运行的过程中所显示的状态,在这3个视图之间切换。上节 Timer-MCBundle-Sample 已经介绍了这3个状态以及对应的类名。

视图类都是显示对象的子类,3个视图也就是3个显示对象。在一个视图显示的时候,它会被加到舞台上,其他的视图则移出舞台。

在这个项目中,我使用了MinialComps组件。视图类继承其中的VBox或者HBox。3个视图类如下图:

View

每个视图类都有一个带有Mediator后缀的类,这是视图类的中介类。中介类负责管理视图事件,接收和传递系统事件等等。所有的中介类都继承 Robotlegs2 提供的 Mediator 类。如下图:

Mediator

Model

此项目有2个Model:TimerModelViewModel

TimerModel 实现 ITimerModel 接口。由于项目过于简单,ITimerModel 中不包含任何需要具体实现的方法,但是这种编程习惯是Robotlegs所推荐的。它既方便了在Robotlegs中替换Model(例如你可以在开发和发布的时候使用不同的Model),也符合中的针对接口编程的OO原则。

TimerModel 中包含了一个 flash.util.Timer 实例,用它来实现定时器的核心功能。同时它还会发布定时器 工作的事件,并暴露一些方法来停止和启动定时器。

ViewModel 保存上面提到的3个视图类的实例,以此实现对视图实例的重用。同时它还提供一个 getView() 方法根据类或者类名来获取一个新的/重用的实例。你可以把 ViewModel 理解成一个对象池,虽然它并不是对象池。如下图: Model

Command + Event

此项目有3个Commnd: ChangeStateCmdTimerStartCmdTimerStopCmd

ChangeStateCmd 负责切换视图状态。它收到切换的命令,然后移除掉当前舞台上的视图实例,再从 ViewModel 中获取一个视图实例,将它加到舞台上。

TimerStartCmd 负责用户主动开始定时器运行的事件。它只是简单的更新 TimerModel 中的定时器流逝时间,然后开启定时器。

TimerStopCmd 负责用户主动停止定时器运行的事件。它停止 TimerModel 中的定时器,然后将视图切换到 TimerSetView

TEvent 是此项目中使用的系统事件类型。上面的几个Command均使用这个类型代表的事件来触发。如下图: Command and Event

项目详解

初始化Robotlegs2

先来看看主类 Robotlegs2TimerExample 的全部内容(省略了package和import):

 1[SWF(width=200,height=200)]
 2public class Robotlegs2TimerExample extends Sprite
 3{
 4	public function Robotlegs2TimerExample() 
 5	{
 6		Style.embedFonts = true;
 7		Style.fontSize = 8;
 8		Component.initStage(this.stage);
 9		init();
10	}
11	
12	private var _context:IContext;
13	
14	private function init():void
15	{
16		_context = new Context()
17		.install(MVCSBundle)
18		.configure(AppConfig)
19		.configure(new ContextView(this));
20		
21		_context.logLevel = LogLevel.DEBUG;
22		trace("init done");
23		
24		//这里通过内置的事件框架来实现View的启动
25		(_context.injector.getInstance(IEventDispatcher) as IEventDispatcher).dispatchEvent(new TEvent(TEvent.CHANGE_STATE, TimerSetView));
26	}
27}

Style和Component都是 minimalcomps 中的类,具体用法可参考该组件源码。需要注意的是,如果需要显示中文,那么需要将 Style.embedFonts 设置为 false,具体原因可参考 MinimalComps简介-一个超轻量级的纯AS组件库

让我们来看 init 方法的具体内容。Context 类是Robotlegs2初始化的核心。在本项目的初始化当中,我们使用了 Contextinstallconfiguare 方法。这两个方法都会返回 Context 自身,因此我们可以进行链式调用。

链式调用是JAVA和JavaScript中常用的调用方式,能减少代码量,让代码看起来更干净。当然,如果你不愿意用它,依然可以使用旧的方式。例如,上面的链式调用可以改成这样:

1_context = new Context();
2_content.install(MVCSBundle);
3_content.configure(AppConfig);
4_content.configure(new ContextView(this));

install() 的作用是安装一个 IExtension 或者 IBundle。前者是一个扩展,后者是一个/一堆扩展和配置的集合。Robotlegs2根据我们的调用对 IExtensionIBundle 进行按需安装,这样可以节省资源。install() 支持无限参数,如果有多个 IBundle,可以使用链式调用进行单个安装,也可以使用一个 install() 多个参数进行安装。

注意

为了描述的统一性和准确性,后文将不对 IExtensionIBundle 进行翻译,而直接使用接口名。

MVCBundle 是一个包含许多 IExtensionIBundle,它包含了MVC框架中的所有 IExtension。除此以外,它还包含一些Logger系统等核心功能。在这个项目中,我们不需要其他的 IExtension ,一包足矣。

configuare() 的作用是对项目进行配置,通常将各种映射放在这里。与 install() 只能接收 IBundleIExtension 不同, configuare() 可以接受任何类型的对象作为参数。同时,它也接受多个参数。例如上面的 configuare() 相关代码可以写成这样:

1_content.configure(AppConfig, new ContextView(this));

需要注意的是,对于ContextView的配置,必须放在所有配置的最后。具体原因,请关注本系列后续文章。

ContextView 是Robotlegs2提供的类,它没有实现任何接口,只是用于保存根显示对象的引用。我们可以将它注入到需要的地方,以方便访问根显示对象。

Injector

init() 的最后一句直接发布 TEvent.CHANGE_STATE 事件,ChangeStateCmd 会处理这个事件,它获取 TimerSetView 的实例,将其添加到舞台上。

注意这一句的作用:

1(_context.injector.getInstance(IEventDispatcher) as IEventDispatcher)....

最外层的括号中的内容,是为了获取一个 IEventDispatcher 的实例。IEventDispatcher 是AS3原生的用于发布事件的接口。 Robotlegs2中使用它作为 MVCBundle 的事件核心。

它以单例的形式存在,我们使用 injector.getInstance 方法,就可以得到这个单例。

injector是SwiftSuspenders提供的注入器。Robotlegs2使用这个注入器来实现注入,我们也可以使用它来获取Robotlegs2中注册过的各种资源。在 Context 中包含一个它的引用。我们既可以使用 _context.injector 的方式获取它,也可以使用注入的方式获取它。

例如,我们要得到 TimerModel 的单例,可以这样做(注意我使用的是接口):

1[Inject]
2public var injector:Injector;
3
4//获取到ITimerModel的单例,调用它的start方法让计时器开始运行
5(injector.getInstance(ITimerModel) as ITimerModel).start();

那么,既然 ITimerModel 已经在Robotlegs2中注册(这个注册是在AppConfig中完成的,后面会讲到),更简单的方法是这样的:

1[Inject]
2public var timerModel:ITimerModel;
3
4timerModel.start();

可是,我们为什么不在 Robotlegs2TimerSample 中直接注入 IEventDispatcher ,而要用 injector 来获取呢?例如像这样:

1[Inject]
2public var eventDispatcher:IEventDispatcher;
3
4private function init():void
5{
6	................
7	eventDispatcher.dispatchEvent(new TEvent(TEvent.CHANGE_STATE, TimerSetView));
8}

这样的代码会出现运行时错误,原因是 eventDispatcher 的值为 null

这是因为在默认情况下, 只有被注入器初始化的类,才能被注入 。主类 Robotlegs2TimerSample 是无法被注入器初始化的,因此在主类中进行注入,在默认情况下是不会成功的。

AppConfig

让我们看看 AppConfig 的内容(同样省略了package和import)。

 1public class AppConfig implements IConfig 
 2{
 3	[Inject]
 4	public var injector:Injector;
 5	
 6	[Inject]
 7	public var mediatorMap:IMediatorMap;
 8	
 9	[Inject]
10	public var commandMap:IEventCommandMap;
11	
12	[Inject]
13	public var logger:ILogger;
14	
15	public function configure():void
16	{
17		models();
18		mediators();
19		commands();
20		logger.info("logger in AppConfig");
21	}
22	
23	private function models():void
24	{
25		injector.map(ITimerModel).toSingleton(TimerModel);
26		injector.map(ViewModel).asSingleton();
27	}
28	
29	private function mediators():void
30	{
31		mediatorMap.map(TimerSetView).toMediator(TimerSetMediator);
32		mediatorMap.map(TimerActionView).toMediator(TimerActionMediator);
33		mediatorMap.map(AlertView).toMediator(AlertViewMediator);
34	}
35	
36	private function commands():void
37	{
38		commandMap.map(TEvent.TIMER_START, TEvent).toCommand(TimerStartCmd);
39		commandMap.map(TEvent.TIMER_STOP, TEvent).toCommand(TimerStopCmd);
40		commandMap.map(TEvent.CHANGE_STATE, TEvent).toCommand(ChangeStateCmd);
41	}
42}

AppConfig 实现了 IConfig 接口。Robotlegs2在对该接口进行配置的时候,会自动调用它的 configuare 方法进行注册。

我们来仔细看看这个类中注入的4个对象。

injector上面讲过一些,这里继续。使用它,可以进行单例映射。toSingleton() 方法和 asSingleton() 方法的不同之处在于,前者针对接口映射,我们可以方便在该方法的参数中替换具体实现;后者针对具体实现映射,它不能被替换。

如果你用过Robotlegs1,你会发现injector的映射语法改变了不少。它抛弃了原来使用参数来映射的方式,改用链式调用,在我看来,这样的改动让语法更加简洁,且容易记忆。

注意

本系列文章在必要的时候会对Robotlegs1和2进行比较,这是为了方便使用过Robotlegs1的读者进行更深入的理解。如果你以前没有使用过Robotlegs1,可以跳过这些内容。

例如,在Robotlegs1中,要实现本例中的两个单例映射,需要这样调用:

1injector.mapSingletonOf(ITimerModel, TimerModel);
2injector.mapSingleton(ViewModel);

mediatorMap用来实现视图类和Mediator的映射。以 mediatorMap.map(TimerSetView).toMediator(TimerSetMediator) 为例,这句实现了在 TimerSetView 被添加到舞台的时候, TimerSetMediator 会自动创建并完成初始化,同时进行需要的注入。

在Robotlegs1中,mediatorMap的调用方式也做了与injector类似的修改。这并不奇怪,因为mediatorMap的映射就是使用injector实现的。

例如,在Robotlegs1中,实现 TimerSetViewTimerSetMediator 的映射,需要这样调用:

1mediatorMap.mapView(TimerActionView, TimerActionMediator);

commandMap用来实现事件与Command的映射。以 commandMap.map(TEvent.TIMER_START, TEvent).toCommand(TimerStartCmd) 为例,这句实现了在 TEvent.TIMER_START 事件发生的时候, TimerStartCmd 被自动创建,同时进行需要的注入,然后执行其 execute 方法。

在Robotlegs1中,要实现 TEvent.TIMER_STARTTimerStartCmd 的映射,需要这样调用:

1commandMap.mapEvent(TEvent.TIMER_START, TimerStartCmd);

logger提供一个全局的日志分析器,默认使用tarce实现。logger不但可以像上面的代码一样,直接输出文字,也可以方便的实现文本替换。例如:

1logger.info("logger in {0}, {1}", [this, "done"]);
2//输出内容
3//638 INFO Context-0-9f [class AppConfig] logger in [object AppConfig], done

AppConfig实际上是分担了一部分Context的功能。我们可以把不同模块,不同需求的Config进行分离,让程序之间的耦合更加松散。

在Robotlegs1中,我们一般把映射放在 Contextstartup 方法中,这样在项目逐渐庞大的时候,Context就无可避免的庞大起来:

 1..........
 2//========================================
 3// 注入Model和Service
 4//========================================
 5injector.mapSingletonOf(SocketServiceBase, SocketService);
 6injector.mapSingleton(SocketDataDispatcherModel);
 7injector.mapSingleton(StateModel);
 8injector.mapSingleton(HTTPService2);
 9injector.mapSingleton(UpdateService);
10
11//----界面信号
12signalCommandMap.mapSignalClass(FightEndSign, FightEndCmd);
13signalCommandMap.mapSignalClass(GuideAniEndSign, GuideAniEndCmd);
14signalCommandMap.mapSignalClass(GuideHelpSign, GuideHelpCmd);
15signalCommandMap.mapSignalClass(GuideCheckOpen8Sign, GuideCheckOpen8Cmd);
16signalCommandMap.mapSignalClass(CheckMausoleumSign, CheckMausoleumCmd);
17signalCommandMap.mapSignalClass(ExitMausoleumSign, ExitMausoleumCmd);
18
19//----其他不便分类的信号
20signalCommandMap.mapSignalClass(NecessarySocketInitDataDoneSign, NecessarySocketInitDataDoneCmd);
21signalCommandMap.mapSignalClass(SetDefaultPreferenceSign, SetDefaultPreferenceCmd);
22signalCommandMap.mapSignalClass(ActiveSign, ActiveCmd);
23signalCommandMap.mapSignalClass(ChargeInWebSign, ChargeInWebCmd);
24
25//========================================
26// 所有的子滑动界面注册
27//========================================
28//LoginModuleContent和RegisterModuleContent与接入商相关,因此在子Context类中注册
29mediatorMap.mapView(FightDeployModuleContent, FightDeployModuleContentMediator, null, false, false);
30mediatorMap.mapView(FightSpyModuleContent, FightSpyModuleContentMediator, null, false, false);
31mediatorMap.mapView(ChooseServerModuleContent, ChooseServerModuleContentMediator, null, false, false);
32mediatorMap.mapView(AccelerateCoolingModule,AccelerateCoolingModuleMediator,null,false,false);
33........

运行流程

1 这段代码是项目的界面入口

1(_context.injector.getInstance(IEventDispatcher) as IEventDispatcher).dispatchEvent(new TEvent(TEvent.CHANGE_STATE, TimerSetView));

IEventDispatcher 发布了一个事件 TEvent.CHANGE_STATE,同时传递了一个视图类 TimerSetView。由于我们前面已经将该事件映射到了 ChangeStateCmd,因此,ChangeStateCmd 中的 execute() 方法被执行。

2 ChangeStateCmd做了什么?

让我们看看 ChangeStateCmd 的全部内容:

 1public class ChangeStateCmd extends Command 
 2{
 3	[Inject]
 4	public var evt:TEvent;
 5	
 6	[Inject]
 7	public var logger:ILogger;
 8	
 9	[Inject]
10	public var contextView:ContextView;
11	
12	[Inject]
13	public var viewModel:ViewModel;
14	
15	override public function execute():void
16	{
17		logger.debug(evt.info);
18		logger.debug(contextView.view.numChildren);
19		if(contextView.view.numChildren > 0)
20		{
21			var __currentView:ITimerView = contextView.view.getChildAt(0) as ITimerView;
22			if(__currentView)
23			{
24				contextView.view.removeChild(__currentView as DisplayObject);
25			}
26		}
27		var __newView:DisplayObject = viewModel.getView(evt.info) as DisplayObject;
28		center(__newView);
29		contextView.view.addChild(__newView);
30		logger.debug("get view:{0}", [__newView]);
31	}
32	
33	private function center($view:DisplayObject):void
34	{
35		$view.x = (contextView.view.stage.stageWidth - $view.width) / 2;
36		$view.y = (contextView.view.stage.stageHeight - $view.height) / 2;
37	}
38}

ChangeStateCmd 中,我们需要得到 TEvent 传来的视图类,使用其创建视图实例,然后将实例添加到舞台上。

注入的 evt 实例,也就是前面 TEvent.CHANGE_STATE 事件传来的事件实例。在这次的调用这,evt.info 的值,就是 TimerSetView

execute() 方法先移除已存在的视图实例,然后根据 evt.info 的值获取一个新的实例,并将其添加到舞台。

所有的视图类,都实现了 ITimerView 接口。因此在这里可以通过判断 ITimerView 的实现情况,来了解实例的创建是否成功。

创建实例的工作在 ViewModel 中完成,可查看源码了解,此处不深入讲解。

TimerSetView 视图实例被添加到舞台的时候,TimerSetViewMediator 会自动创建。

3 TimerSetViewMediator 做了什么?

让我们来看看 TimerSetViewMediator 的全部内容:

 1public class TimerSetMediator extends Mediator 
 2{
 3	[Inject]
 4	public var logger:ILogger;
 5	
 6	[Inject]
 7	public var v:TimerSetView;
 8	
 9	public function TimerSetMediator() 
10	{
11	}
12	
13	override public function initialize():void
14	{
15		super.initialize();
16		logger.info("initialize");
17		v.startBtn.addEventListener(MouseEvent.CLICK, handler_start);
18	}
19	
20	override public function destroy():void
21	{
22		v.startBtn.removeEventListener(MouseEvent.CLICK, handler_start);
23		super.destroy();
24		logger.info("destory");
25	}
26	
27	private function handler_start(e:Event):void 
28	{
29		logger.debug("click");
30		eventDispatcher.dispatchEvent(new TEvent(TEvent.TIMER_START, {minute:v.minute, second:v.second}));
31		eventDispatcher.dispatchEvent(new TEvent(TEvent.CHANGE_STATE, TimerActionView));
32	}
33}

v 被自动注入,它就是刚才我们创建的 TimerSetView 视图实例。

initialize() 方法在注入完成之后自动执行。因此,该方法适合进行 v 的视图侦听器。在本类中,加入了用户按下“Start”按钮时的鼠标事件侦听。

TimerSetView 的实例从舞台移除的时候, TimerSetViewMediator 被销毁。 destroy() 方法在此时执行。我们在这里移除“Start”按钮的鼠标事件侦听。

在单击“Start”按钮的时候,我们发布了两个系统事件 —— 1. TEvent.TIMER_START,并传递当前选择的分、秒;2. TEvent.CHANGE_STATE,切换到 TimerActionView 视图。

由于前面我们已经将 TEVent.TIMER_START 映射到了 TimerStartCmd ,因此 TimerStartCmd 中的 execute() 方法将被执行。

4 启动计时器

让我们来看看 TimerStartCmd 的全部内容:

 1public class TimerStartCmd extends Command 
 2{
 3	[Inject]
 4	public var timerModel:ITimerModel;
 5	
 6	[Inject]
 7	public var evt:TEvent;
 8	
 9	override public function execute():void
10	{
11		timerModel.minute = evt.info.minute;
12		timerModel.second = evt.info.second;
13		timerModel.start();
14	}
15	
16}

在这里,我们更新了 TimerModel 中保存的分、秒,然后启动了计时器。

5 请继续

到此,我已经把主要流程讲完。至于后面的计时器时间到,或者中断计时器等操作,和前面的流程类似。大家可以继续在 TimerModelTimerActionMediator 中找到它们的具体实现。

小结

本章使用一个简单的计时器例子,讲解了Robotlegs2中的MVCBundle的使用。拥有Robotlegs1使用经验的同学,现在应该已经可以转移到Robotlegs2上来了。

即使你从没有用过Robotlegs,那么也可以从本文开始,从头学习Robotlegs2。

下一章,我们将深入MVCBundle中,分析其中的Extension组成,并试图抛弃MVCBundle,使用更底层的方式来重新构架我们的计时器范例。