Parsley 3中的命令支持完全重写了在Parsley 2中的DynamicCommand 设施。你实现一个简单的命令的方式基本上仍然是相同的,你仍然可以像在以前的版本中把命令映射到消息。但是实现已经完全变了,还添加了很多新功能。
Parsley 3的命令支持是在独立的Spicelib Commands库上构建的。库已经有方便的方式来实现一个命令,将它们分组为顺序或并行执行,或基于个人的命令结果用decision points创建动态flows。它还允许在序列中从一个命令传递结果到后续命令。因此,建议首先开始阅读文档 21命令框架。本章将只提供覆盖在Spicelib手册中简短摘要的内容,转而关注Parsley在库上增加的特性。
Parsley在Spicelib Commands之上提供的特性是:
l 只在它执行的时候动态地添加一个命令到上下文中。这样一个命令执行的时候可以被注入,发送和接收消息,利用任何内置或自定义容器特性。而在执行完成时会从上下文中得到自动处理和移除。详情查看 7.7命令生命周期.
l 除了Spicelib提供的Flow API来构建,链和执行命令,Parsley在MXML或XML中提供了一种声明的方式来配置命令(or sequences or flows) 。详情查看 7.3命令组 和7.4命令流.
l 像 Parsley 2中的DynamicCommands,它允许你映射一个命令(或序列或流)到一个消息,因此无论什么时候一个匹配消息被接受到,容器自动实例化和执行一个新的命令。详情查看7.2 映射消息到命令。
如果你不需要上面所提到的任何特性,你可以使用Spicelib Commands的单独模式。如果命令不需要注入,消息传递或其他容器功能,运行命令或命令序列在非托管模式是一个完全有效的用例。一组命令处理一个单独的、孤立的任务,这是一个相当普遍的用例。请记住,独立的命令项目已经提供了大量的容器般的便利功能,比如能够透明地从命令传递数据到后续的命令。你应该总是先从最轻量的选择,如果你稍后发现,你需要Parsley的特性命令,你通常通过添加几行代码就可以轻松地“升级”,同时保留一切不变。
关于如何实现一个命令的一个稍微详细文档可以在21.1实现一个命令 可以找到,在Spicelib手册的部分。本章将提供一个快速概述与几个例子和描述当命令被Parsley管理的时候你有的额外选项。
public class SimpleCommand {
public function execute (): void {
trace("I execute, Therefore I am");
}
}
该方法执行是由命名约定,没有接口来实现或基类来扩展。这允许一些灵活的execute方法的签名,这可能会收到结果回调函数,之前的命令或消息的结果,引发了这个命令。
public class SimpleCommand {
public function execute (): String {
return "This is the result";
}
}
一个同步的执行方法可能声明一个不同的返回类型的情况比返回void更多。在这种情况下,将返回值解释为该命令的结果。结果可能会传递给方法的执行后续命令(在一个序列或流)或解耦的结果处理程序。详细信息请参阅 7.6处理结果和观察命令。
public class GetUserListCommand {
[Inject("myService")]
public var service: RemoteObject;
public var callback: Function;
public function execute (): void {
service.getUserList().addResponder(new Responder(result, error));
}
private function result (result: ResultEvent): void {
callback(result.result);
}
private function error (event: FaultEvent): void {
callback(event.fault);
}
}
一个命令被框架是看做异步的,因为它要么就有一个public var的Function类型的callback ,要么execute 方法中接受一个Function类型的方法参数,在这些情况下,框架将注入一个函数,可用于标记命令完成或错误。对于常规的结果和错误,最后仅仅是传递给回调函数,框架可以基于对象的类型区分结果和错误。
public class GetUserListCommand {
[Inject("myService")]
public var service: RemoteObject;
public function execute (): AsyncToken {
return service.getUserList();
}
}
当你所有要做的就是调用RemoteObject,并把结果或错误返回到框架中进行进一步的加工的话,那么有一个快捷方式就像上面所示。它代替你做关于Responder的事,你把这个任务留给了框架。从技术上讲,上面的命令生成一个同步的命令,但是那是框架知道的一个类型(在本例中AsyncToken),不是我们立即可用的。框架将等待result或fault 并且把它当做命令的结果。对AsyncToken 的支持构建到了Parsley中,但是你可以为其他类型创建自定义的 21.5.2实现结果处理器,如果需要的话。
public class GetUserListCommand {
[Inject("myService")]
public var service: RemoteObject;
public function execute (): AsyncToken {
return service.getUserList();
}
public function result (users: Array): void {
// process users
}
}
如果你使用AsyncTokens的快捷方式,你可能仍然需要在传递给应用程序的其他部分之前就在命令中处理结果。结果处理器的方法名必须是result,错误处理器的方法名必须是error。它必须接受一个参数,参数类型可以是你远程调用的返回类型。
public class GetUserProfileCommand {
[Inject("myService")]
public var service: RemoteObject;
public function execute (msg: GetUserProfileMessage): AsyncToken {
return service.getUserProfile(msg.userId);
}
public function result (profile: XML): UserProfile {
return parse(profile);
}
}
你也可以覆盖result 如果你想修改或在它被传递到应用程序的其他部分之前改变它。在该示例中,你从服务器接收XML,但是在解析XML之后返回UserProfile实例的结果。UserProfile将被视为命令的结果。
因为管理命令是在SpicelibCommands基础上构建的,当在Parsley中使用命令的时候,所有该库的特性都可以使用。更多的例子可以在Spicelib手册中找到:
l 一个命令,执行同步或异步取决于一个匹配的结果是否已经缓存在客户端的例子在 21.1.2 异步命令.
l 一个命令可以取消,在外面和在命令里面都是,查看 21.1.4命令取消。
l 更多选项关于如何产生一个错误的结果,查看 21.1.3错误处理。
如果你在Parsley 2中使用过DynamicCommands,你已经很熟悉映射命令消息的概念。在Parsley 3中这个特性已经被泛化,允许映射任何命令,包括序列和流到Parsley 消息中。只要他的实现可以通过新的Spicelib Commands library执行(有些特性只能在Parsley中使用)
一个映射声明可以很简单,比如这个:
<parsley:MapCommand type="{LoadUserCommand}"/>
这是唯一的能够让指定类型的命令来执行匹配的消息。命令需要映射的消息的类型是由execute方法的方法签名来决定的。它可以接收触发器消息作为方法参数:
public class GetUserProfileCommand {
[Inject("myService")]
public var service: RemoteObject;
public function execute (msg: GetUserProfileMessage): AsyncToken {
return service.getUserProfile(msg.userId);
}
}
跟往常一样当有东西被按照类型配置在Parsley中的时候,这是多态地解译,因此命令还将执行GetUserProfileMessage的子类。
对于每一个匹配的消息,容器将创建一个新的命令的实例,将它添加到上下文,以便它可以作为一个完全托管对象,例如注入,发送消息等等,处理命令的结果,例如传递它到其他对象中被配置为结果处理器对应的方法中,最后在执行后把它从上下文中移除。
Explicit Message Type Configuration
如果你不想把消息传给execute方法(例如,以避免不必要的依赖项),还可以显式地在MXML中指定消息类型:
<parsley: MapCommand type="{LoadUserCommand}" messageType="{LoadUserMessage}"/>
Full Command Configuration
如果你想指定的不仅仅是命令的类型,比如设置属性值或构造函数参数,像配置在MXML中的常规对象一样,你可以使用一个嵌套的命令标签:
<parsley:MapCommand>
<parsley:Command type="{LoadUserProfileCommand}">
<parsley:Property name="type" value="{UserProfile.EXTENDED}"/>
<parsley:Property name="cache" value="true"/>
</parsley:Command>
</parsley:MapCommand>
Other Options
标签还支持[MessageHandler] 的大多数属性:
<parsley:MapCommand
type="{ShoppingCartCommand}"
selector="save"
scope="local"
order="1"
/>
在XML中映射命令的语法跟在MXML是一样的,只是使用破折号代替驼峰式大小写:
<map-command
type="com.company.shop.ShoppingCartCommand"
selector="save"
scope="local"
order="1"
/>
最后你可以编程来映射一个命令:
var context: Context = ...
MappedCommands
.create(GetUserProfileCommand)
.scope(ScopeName.LOCAL)
.register(context);
在上述例子中,上下文会创建一个GetUserProfileCommand 的新实例,每当一个匹配的消息(execute 方法的签名)在local scope 被听到的时候。
如果你需要比仅指定类更多的设置,你需要传递一个工厂函数。你不能传递现有的命令实例,容器必须为每个消息创建一个新的实例:
private function createCommand (): Object {
var com:GetUserProfileCommand = new GetUserProfileCommand(someParam);
com.profileType = ProfileType.EXTENDED;
return com;
}
[...]
var context: Context = ...
MappedCommands
.factoryFunction(createCommand, GetUserProfileCommand)
.scope(ScopeName.LOCAL)
.register(context);
Spicelib Commands库提供了选项来配置一组命令按照序列或并行执行。如果你想要以编程方式配置这样一个组,并且在非托管模式下执行它,你可以在Spicelib手册中阅读 21.2命令组。这一章讨论Parsley添加的选项。
映射一系列的命令到消息,你可以使用以下语法:
<parsley:MapCommand messageType="{LoginMessage}">
<parsley:CommandSequence>
<parsley:Command type="{LoginCommand}"/>
<parsley:Command type="{LoadUserProfileCommand}"/>
</parsley:CommandSequence>
</parsley:MapCommand>
在这一章的大多数示例会展示如何映射一组命令到消息。此外,你还可以声明命令组作为工厂(包在<CommandFactory> 标签里),然后以编程方式执行它们。但是你不能在MXML配置类的最外层添加一个<CommandSequence>标签。
对于并行执行你只需要用 <ParallelCommands>标签替换 <CommandSequence>:
<parsley:MapCommand messageType="{LoadDashboardMessage}">
<parsley:ParallelCommands>
<parsley:Command type="{LoadUserProfileCommand}"/>
<parsley:Command type="{LoadPrivateMailboxCommand}"/>
</parsley:ParallelCommands>
</parsley:MapCommand>
跟之前一样语法是相同的,你只需要切换破折号:
<map-command message-type="com.bluebeard.auth.LoginMessage">
<command-sequence>
<command type="com.bluebeard.auth.LoginCommand"/>
<command type="com.bluebeard.user.LoadUserProfileCommand"/>
</command-sequence>
</map-command>
结果可以用解耦方式传递到序列中的后续命令中。如果你的LoginCommand处理一个User的实例,下一个命令可以在execute方法中接受它作为一个参数,连同其他参数,如回调函数或引发了序列的消息:
public class GetUserProfileCommand {
public function execute (user: User, callback: Function): void {
[...]
}
}
参数的顺序无关紧要。如果前面有多个命令,都产生了一个结果,那么只会按类型匹配。如果不止一个命令产生了相同的类型,那么最后一个将被注入。
命令流添加了决策点的概念,来定义一个动态的命令序列,下一个命令的执行是由检查前一个命令的结果后的链接(link)决定的。
Parsley提供了MXML和XML标签来定义一个流(flow),包括所有默认的Spicelib Commands 库提供的链接类型。
<parsley:MapCommand messageType="{LoginMessage}">
<parsley:CommandFlow>
<parsley:Command type="{LoginCommand}">
<parsley:LinkResultType type="{AdminUser}" to="{loadAdminConsole}"/>
<parsley:LinkResultType type="{User}" to="{loadProfile}"/>
</parsley:Command>
<parsley:Command id="loadAdminConsole" type="{LoadAdminConsoleCommand}">
<parsley:LinkAllResults to="{loadProfile}"/>
</parsley:Command>
<parsley:Command id="loadProfile" type="{LoadProfileCommand}"/>
</parsley:CommandFlow>
</parsley:MapCommand>
在上述例子中,LoadAdminConsoleCommand 只会在LoginCommand 产生一个AdminUser类型的结果的时候才会执行。
映射一个流到消息中跟序列一样是可用的,所有我们不会在这里重复它们。
Parsley提供了所有Spicelib Commands提供的内置链接(link)类型的XML和MXML标签。
Linking by Result Type
<parsley:Command type="{LoginCommand}">
<parsley:LinkResultType type="{AdminUser}" to="{loadAdminConsole}"/>
<parsley:LinkResultType type="{User}" to="{loadProfile}"/>
</parsley:Command>
Linking by Result Value
<parsley:Command type="{LoginCommand}">
<parsley:LinkResultValue value="{Login.ADMIN}" to="{loadAdminConsole}"/>
<parsley:LinkResultValue value="{Login.USER}" to="{loadProfile}"/>
</parsley:Command>
Linking by Property Value
<parsley:Command type="{LoginCommand}">
<parsley:LinkResultProperty name="isAdmin" value="{true}" to="{loadAdminConsole}"/>
<parsley:LinkResultProperty name="isAdmin" value="{false}" to="{loadProfile}"/>
</parsley:Command>
Linking all Results of a Command
<parsley:Command type="{LoginCommand}">
<parsley:LinkAllResults to="{loadAdminConsole}"/>
</parsley:Command>
Custom Links
一个自定义链接类封装可以添加更复杂的逻辑。任何实现了LinkTag 接口都可以作为<Command> 的子标签。
<Command type="{LoginCommand}">
<links:MyCustomLinkType/>
</Command>
当你不映射一个命令到消息你仍然可以以编程方式触发命令执行。要做到这一点,你基本上有两种选择:
l 在MXML或者XML中配置命令,注入一个CommandFactory 到任意托管对象中,,然后使用工厂创建命令实例并且执行它们。
l 以编程方式创建命令(使用常规Spicelib API),然后传递命令到Parsley托管执行。
本节对于两种情况都给出了示例。
首先,你需要在MXML或XML中声明一个命令工厂。工厂可以产生一个单独的命令,一个序列或流:
<parsley:CommandFactory id="loginCommand">
<parsley:CommandSequence>
<parsley:Command type="{LoginCommand}"/>
<parsley:Command type="{LoadUserProfileCommand}"/>
</parsley:CommandSequence>
</parsley:CommandFactory>
然后你可以在同一个上下文或子上下文中将这个工厂注入到任何托管对象。
[Inject("loginCommand")]
public var loginCommand: ManagedCommandFactory;
类型必须是ManagedCommandFactory,id是可选的如果你只声明一个单独的工厂在每个上下文中。
然后你可以用这个工厂创建任意数量的新命令实例并且执行它们:
loginCommand.newInstance().execute();
这个命令会在它执行的时候被添加到上下文中,就像其他有Parsley提供的命令变体一样。
这个选项允许你用Spicelib API配置命令然后传递给Parsley托管执行。
var loginSequence: Command = Commands
.asSequence()
.add(new LoginCommand())
.add(new LoadUserProfileCommand())
.complete(completeHandler)
.error(errorHandler)
.build();
var context: Context = ...;
ManagedCommands
.wrap(loginSequence)
.execute(context);
序列的设置是用的Spicelib API,但是代替了就像你使用Spicelib Commands一样直接调用execute ,你调用build 然后传递这个配置好的命令给Parsley托管执行。
如果你只需要指定的命令类型是不需要进一步的配置的单个命令,你可以选择跳过Spicelib设置步骤:
var context:Context = ...;
ManagedCommands
.create(MyCommand)
.execute(context);
Parsley提供多种方式处理结果或观察命令执行。他们将在以下部分中描述。
当你使用一个execute方法返回AsyncToken 的命令的时候,你可以在命令内部使用结果处理器 7.1.5 命令结果处理器。这些内部结果处理程序将总是在其他对象中的解耦处理器之前调用。它们还可能在外部结果处理程序得到调用之前修改结果。
除了命令本身的结果处理器,其他的对象也可以得到通知。有一组标签在托管对象中声明这些(像大多数标签一样有元数据,MXML和XML标签)。
CommandResult
这个标签可以用来获得其他的对象的命令产生的结果(这不需要也不应该被用来在命令内部定义一个结果处理程序)。
[CommandResult]
public function handleResult (user:User, message:LoginMessage) : void {
在本例中,远程调用返回的User实例会和触发这个动作的原始消息一起被传递到结果处理器。像正常的消息处理器一样,消息的参数类型是用于确定哪些处理程序来调用。它总是一个组合的消息类型(多态)和一个可选的选择器值,作为二次选择的键。结果的类型还必须匹配这个方法来得到调用。
如果产生User 实例的命令是序列的一部分,是由特定的消息类型触发的,那么在默认情况下结果处理程序只会在整个序列已经完成之后调用。
如果你需要尽早处理结果,你可以使用immediate属性:
[CommandResult(immediate="true")]
public function handleResult (user:User, message:LoginMessage) : void {
如果命令不属于一个序列或流,这个属性没有任何影响。
CommandComplete
如果你对实际结果不感兴趣,而是只想在命令完成后执行某些逻辑,你可以使用另一个标签:
[CommandComplete]
public function userSaved (message:SaveUserMessage) : void {
这意味着每当SaveUserMessage 触发的命令完成的时候这个方法都会得到调用。如果是序列这意味着方法会在序列中的所有命令成功完成的时候调用。
CommandError
This tag can be used for receiving the eventual fault events or other errors.
[CommandError]
public function handleResult (fault:FaultEvent, trigger:SaveUserMessage) : void {
参数又都是可选的,并且匹配规则跟 [CommandResult]是一样的.’
Overriding the Result
像一个命令里的结果处理器,解耦的结果处理程序也可以覆盖原有的结果。要这样做方法需要一个额外的CommandObserverProcessor类型的参数,所以实际的技术跟内部结果处理程序有一点不同。
[CommandResult]
public function handleResult
(result:XML, msg:LoginMessage, processor:CommandObserverProcessor) : void {
var parsedResult:Object = parseResultSomehow(result);
processor.changeResult(parsedResult);
}
CommandObserverProcessor 接口是MessageProcessor 的子接口,它提供访问在共同的MessageProcessor 功能上执行的Command 。
Local Result Handlers
Parsley有在全局作用域中执行的命令的local结果处理器。这解决了在模块化应用程序一个常见的问题,一个选项卡或窗口发出一个信息,需要在根应用程序的共享服务中触发一个命令。共享服务中的命令必须监听全局作用域,因为它不属于加载到那个标签或窗口的模块中发送对象的上下文。但对于结果处理,经常只有这个特殊的标签或窗口想要处理它。因为这个原因,命令的结果和错误总是会重新从引发命令的消息的起源处路由到上下文。
这允许使用一个本地处理程序如下:
[CommandResult(scope="local")]
public function handleResult (result:Object, msg:LoginMessage) : void {
[...]
}
这甚至当命令在父上下文中执行的时候也起作用,只要触发消息的源跟这个处理器是同一个上下文。除了这种选择,当应用程序的任何部分监听全局作用域的时候,仍然能够处理结果。
最后你也可以观察命令执行的状态:
[CommandStatus(type="com.foo.messages.SaveUserMessage")]
public var isSaving:Boolean;
如果匹配指定类型和选择器的一个或多个异步命令正在执行,这个布尔标志将永远是true。否则将是false。这对任务来说非常方便,比如在命令执行的时候禁用按钮。
不幸的是,当使用这个标签作为元数据的时候,消息触发器必须指定为一个字符串。因为这个原因,你可能在这种情况下有时喜欢使用MXML配置:
<Object type="{SomeType}">
<CommandStatus property="isSaving" type="{SaveUserMessage}"/>
</Object>
这种配置风格没有任何危险,重构后导致运行时错误,是由于元数据标签某处埋藏了陈旧的限定类名。
就像前面的部分已经多次提到,托管命令在执行的时候被添加到上下文中。本节提供了一些更多的细节,这样你就可以避免常见的错误比如在命令执行完成后试图访问Parsley的特性。
一个命令保存在上下文的时间不会太久,因为他本来就是短生命周期的。只要它是上下文的一部分,它就不能得到垃圾收回收,因为上下文持有所有托管对象的引用。因此命令从上下文一旦删除,容器将其视为已经完成。本节解释确切含义。
采取以下简单(尽管很做作)的例子,一个用一个注入合作者同步命令:
class MultiplyCommand {
[Inject]
public var multiplier: Multiplier;
public function execute (msg: MultiplyMessage): int {
return multiplier.multiply(msg.valueA, msg.valueB);
}
}
命令用引发了这个命令的消息传递的两个整数执行一个简单的乘法。不幸的是它不是最聪明的命令,所以它需要一个合作者(collaborator )来做实际的计算。
因为容器在命令执行之前添加到上下文,任何注入或其他设置在execute方法调用的时候已经完成。同样,由于这是一个同步命令,我们也知道命令是短生命周期的。execute方法返回它的结果后,该命令将被从上下文删除。因为一个命令支持全套容器服务,你也可以用第二种方式,标签为[Destroy],然后你将看到,它将execute方法执行之后立即被调用。
现在让我们想象注入的乘数不是最聪明的,它需要一些时间来执行计算。这意味着我们必须把命令转换为异步命令,要求一个回调函数来获得注入。
class MultiplyCommand {
[Inject]
public var multiplier: Multiplier;
public var callback: Function;
public function execute (msg: MultiplyMessage): int {
return multiplier
.multiply(msg.valueA, msg.valueB)
.addResultHandler(resultHandler);
}
private function resultHandler (result: int): void {
trace("before invoking callback");
callback(result);
trace("after invoking callback");
}
[Destroy]
public function destroy (): void {
trace("destroy method called");
}
}
如果你执行上述命令的traces将是:
before invoking callback
destroy method called
after invoking callback
传递结果到回调方法也标志着完成命令,这样就可以从上下文删除。如果你需要执行任何类型的需要用到容器特性(如发送消息)任务,你必须在传递结果之前那样做。
Asynchronous Command based on AsyncTokens and Result Handlers
在其他的变体异步命令中所描述的机制略有不同:
public class GetUserListCommand {
[Inject("myService")]
public var service: RemoteObject;
public function execute (): AsyncToken {
return service.getUserList();
}
public function result (users: Array): void {
// command still managed here
}
}
这里框架将一直等待,直到AsyncToken产生一个结果或错误,然后调用结果处理程序。只有调用命令结果处理程序后,该命令才会从上下文中删除。