-
Notifications
You must be signed in to change notification settings - Fork 0
ManagedCommands
#Managed Commands
The Command support in Parsley 3 is a complete rewrite of the DynamicCommand facility in Parsley 2. The way you implement a simple command is still mostly the same and you can still map commands to messages like in previous versions. But the implementation has changed completely and also added a lot of new functionality.
Command Support in Parsley 3 builds on top of the standalone Spicelib Commands library. That library already allows for convenient ways to implement a command, to group them for sequential or parallel execution, or to create dynamic flows with decision points based on results of individual commands. It also allows to pass results from one command to subsequent commands in a sequence. Therefore it is recommended to start reading the documentation on The Command Framework first. This chapter will only provide brief summaries of the content covered in the Spicelib manual and instead focus on the features that Parsley adds to that library.
The features that Parsley offers on top of Spicelib Commands are:
-
Dynamically add a command to a Context only for the time it executes. This way a command can get injections, send and receive messages while it executes and take advantage of any built-in or custom container feature, while getting automatically disposed and removed from the Context when it finishes executing. See Command Lifecycle for details.
-
In addition to the fluent API offered by Spicelib to build, chain and execute commands, Parsley offers a declarative way to configure Commands (or sequences or flows) in MXML or XML. See Command Groups and Command Flows for details.
-
Like with DynamicCommands in Parsley 2 it allows you to map a Command (or sequence or flow) to a message, so that the container automatically instantiates and executes a new command whenever a matching message is heard. See Mapping Commands to Messages for details.
If you do not need any of the features mentioned above, you can as well use Spicelib Commands in their standalone mode. Running commands or a sequence of commands in unmanaged mode is a perfectly valid use case if the commands do not need injections, messaging or other container features. This is quite a common use case when a set of commands deal with a separate, isolated task. Remember that the standalone Commands project already offers a lot of container-like convenience features like the ability to transparently pass data from commands to subsequent commands. You should always start with the most lightweight option, if you discover later that you do need Parsley features in your commands you can usually easily "upgrade" them through adding a few lines of code while leaving everything else untouched.
##Implementing a Command
A slightly more detailed documentation on how to implement a Command can be found in Implementing a Command in the Spicelib section of the manual. This chapter will provide a quick overview with a few examples and describe the additional options you have when the command is managed by Parsley.
###Synchronous Commands
public class SimpleCommand {
public function execute (): void {
trace("I execute, Therefore I am");
}
}
The method to execute is determined by naming convention, there is no interface to implement or base class to extend. This allows some flexibility in the signature of the execute method, which may receive result callback functions, results from previous commands or the message that triggered the command.
###Synchronous Commands with Result
public class SimpleCommand {
public function execute (): String {
return "This is the result";
}
}
A synchronous execute method may declare a different return type than void. In this case the returned value is interpreted as the result of the command. The result may get passed to execute methods of subsequent command (in a sequence or flow) or to decoupled result handlers. For details see Handling Results and Observing Commands.
###Asynchronous Commands with Result
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);
}
}
A command is seen as asnchronous by the framework as soon as it either has a public var of type Function named callback or a method parameter of type Function in the execute method. In these cases the framework will inject a function that can be used to signal command completion or an error condition. For both regular results and errors the result is simply passed to the callback function, the framework can distinguish between results and errors based on the type of the object.
###Asynchronous Commands with AsyncToken
public class GetUserListCommand {
[Inject("myService")]
public var service: RemoteObject;
public function execute (): AsyncToken {
return service.getUserList();
}
}
When all you do is invoking a RemoteObject and passing the results or faults back to the framework for further processing, there is a shortcut like shown above. Instead of doing all the plumbing around the Responder yourself, you leave this task to the framework. Technically, the command above produces a synchronous command, but of a type (in this case AsyncToken) that is known by the framework as not immediately available. The framework will then wait for either result or fault and treat that as the outcome of the command. The support for AsyncToken is built into Parsley, but you can create custom Implementing a Result Processor for other types if required.
###Result Handlers in Commands
public class GetUserListCommand {
[Inject("myService")]
public var service: RemoteObject;
public function execute (): AsyncToken {
return service.getUserList();
}
public function result (users: Array): void {
// process users
}
}
If you use the shortcut for AsyncTokens you may still want to process the result inside the command before it gets passed to other parts of the application. The method name must be result for the result handler and error for the error handler. It must accept one parameter, the type can be the result type you expect from the remote call.
###Changing the Result
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);
}
}
You can also overwrite the result in case you want to modify or transform it before it gets passed to other parts of the application. In the example you receive XML from the server, but return the resulting UserProfile instance after parsing the XML. The UserProfile will then be treated as the command result.
###Other Options
Since Managed Commands are built on top of Spicelib Commands, all the features of that library are also available when using Command in Parsley. More example can be found in the Spicelib Manual:
-
An example for a command that either executes synchronously or asynchronously depending on whether a matching result is already available in a client-side cache is shown in Asynchronous Commands.
-
A command can get cancelled, both from the outside and from within the command as shown in Command Cancellation.
-
For more options on how to produce an error outcome see Error Handling.
##Mapping Commands to Messages
If you used DynamicCommands in Parsley 2, you are already familiar with the concept of mapping commands to messages. In Parsley 3 this feature has been generalized, allowing to map any command, including sequences and flows, to a Parsley message, as long as it is implemented in a way that could also be executed by the new Spicelib Commands library (with a few feature additions only available in Parsley).
###Mapping Commands in MXML
A mapping declaration can be as simple as this:
<parsley:MapCommand type="{LoadUserCommand}"/>
Here the only thing that gets specified is the type of command to execute for matching messages. The type of the message the command should map to is determined from the method signature of the execute method, which can receive the trigger message as a method parameter:
public class GetUserProfileCommand {
[Inject("myService")]
public var service: RemoteObject;
public function execute (#hlt msg: GetUserProfileMessage #hlt): AsyncToken {
return service.getUserProfile(msg.userId);
}
}
Like always when something gets configured per-type in Parsley, this is interpreted polymorphically,
therefore the command will also execute for subclasses of GetUserProfileMessage
.
For every matching message the container will create a new instance of the command, add it to the Context, so that it can act as a fully managed object, e.g. get injections, send messages, etc., process the result of the command, e.g. passing it to matching methods in other objects that are configured as result handlers, and finally remove it from the Context after execution.
#####Explicit Message Type Configuration
If you do not want to pass the message to the execute method (for example to avoid unecessary dependencies), you can also specify the message type explicitly in MXML:
<parsley: MapCommand type="{LoadUserCommand}" messageType="{LoadUserMessage}"/>
#####Full Command Configuration
If you want to specify more than just the type of the command, like setting property values or constructor arguments like for regular object configuration in MXML, you can use a nested Command tag:
<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
The tag also supports most of the attributes you know from the regular [MessageHandler]
tags:
<parsley:MapCommand
type="{ShoppingCartCommand}"
#hlt selector="save"
scope="local"
order="1" #hlt
/>
###Mapping Commands in XML
The syntax for mapping a command in XML is the same as for MXML, only using dash-notation instead of camel case:
<map-command
type="com.company.shop.ShoppingCartCommand"
selector="save"
scope="local"
order="1"
/>
###Mapping Commands Programmatically
Finally you can also map a command programmatically:
var context: Context = ...
MappedCommands
.create(GetUserProfileCommand)
.scope(ScopeName.LOCAL)
.register(context);
In the example above the Context will create a new instance of GetUserProfileCommand
whenever a matching message
(deduced from the signature of the execute method) is heard in local scope.
If you need more setup than just specifying the class, you need to pass a factory function. You cannot pass existing command instances, as the container must create new instances for each message:
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);
##Command Groups
The Spicelib Commands library offers the option to configure a group of commands for sequential or parallel execution. If you want to configure such a group programmatically and execute it in unmanaged mode, you can read Command Groups in the Spicelib Manual. This chapter discusses the options that Parsley adds on top.
###Declaring Groups in MXML
To map a sequence of commands to a message, you can use the following syntax:
<parsley:MapCommand messageType="{LoginMessage}">
<parsley:CommandSequence>
<parsley:Command type="{LoginCommand}"/>
<parsley:Command type="{LoadUserProfileCommand}"/>
</parsley:CommandSequence>
</parsley:MapCommand>
Most examples in this chapter show how to map a command group to a message.
Alternatively you can also declare command groups as factories (wrapped inside a tag) and then execute
them programmatically. But you cannot add a
tag to the top level of an MXML configuration class.
For parallel execution you'd just use the tag instead of
:
<parsley:MapCommand messageType="{LoadDashboardMessage}">
<parsley:ParallelCommands>
<parsley:Command type="{LoadUserProfileCommand}"/>
<parsley:Command type="{LoadPrivateMailboxCommand}"/>
</parsley:ParallelCommands>
</parsley:MapCommand>
###Declaring Groups in XML
Like always the syntax is the same, you only have to switch to dash notation:
<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>
###Using Results from Preceding Commands
Results can get passed to subsequent commands in a sequence in a decoupled way.
If your LoginCommand
produces an instance of User
, the next command
can accept it as a parameter in the execute method, together with other parameters like
the callback function or the message that triggered the sequence:
public class GetUserProfileCommand {
public function execute (#hlt user: User #hlt, callback: Function): void {
[...]
}
}
The order of parameters does not matter. In case of multiple preceding commands that all produced a result, the result will simply be matched by type. If more than one command produced the same type, the last of them will be injected.
##Command Flows
Command Flows add the concept of decision points to define a dynamic sequence of commands where the next command to execute is determined by a link that inspect the result of the preceding command.
Parsley offers MXML and XML tags for defining a flow, including all default link types offered by the Spicelib Commands library:
<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>
In the example above the LoadAdminConsoleCommand
is only executed when the LoginCommand
produced a result of type AdminUser
.
For mapping the flow to a message the same options like for sequences are available, so we won't repeat them all here.
Parsley offers MXML and XML tags for all built-in link types Spicelib Commands offer:
#####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
A custom link class encapsulating more complex logic can be added, too. Anything that implements
the LinkTag
interface can be inserted as a child tag inside the tag:
<Command type="{LoginCommand}">
<links:MyCustomLinkType/>
</Command>
##Starting a Command Programmatically
When you don't map a Command to a message you can still trigger the Command execution programmatically. To do this you basically have two options:
- Configure the Command(s) in MXML or XML and inject a
CommandFactory
into any managed object and then use the factory to create command instances and execute them. - Create the Command programmatically (using the regular Spicelib APIs) and then pass the Command to Parsley for managed execution.
This section gives examples for both variants.
###Injecting a Command Factory
First you need to declare a command factory in MXML or XML. The factory can produce a single command, a sequence or a flow:
<parsley:CommandFactory id="loginCommand">
<parsley:CommandSequence>
<parsley:Command type="{LoginCommand}"/>
<parsley:Command type="{LoadUserProfileCommand}"/>
</parsley:CommandSequence>
</parsley:CommandFactory>
You can then inject this factory into any managed object in the same Context or a child Context:
[Inject("loginCommand")]
public var loginCommand: ManagedCommandFactory;
The type always needs to be ManagedCommandFactory
. The id is optional if you only
declare a single factory per Context.
You can then create any number of new command instances from this factory and execute them:
loginCommand.newInstance().execute();
This command will get added to the Context for the time it executes like any other command variant offered by Parsley.
###Initiating Managed Execution Manually
This option allows you to configure a command with the plain Spicelib API and then pass it to Parsley for managed execution:
var loginSequence: Command = Commands
.asSequence()
.add(new LoginCommand())
.add(new LoadUserProfileCommand())
.complete(completeHandler)
.error(errorHandler)
.build();
var context: Context = ...;
ManagedCommands
.wrap(loginSequence)
.execute(context);
The setup for the sequence is performed using the Spicelib API, but instead of calling execute
direclty like you would when using just Spicelib Commands, you call build
and then pass the fully
configured command to Parsley for managed execution.
When you only need to specify the command type of a single command without further configuration, you can alternatively skip the Spicelib setup step:
var context:Context = ...;
ManagedCommands
.create(MyCommand)
.execute(context);
##Handling Results and Observing Commands
Parsley offers multiple ways of dealing with results or observing command execution in general. They will be described in the following sections.
###Result Handlers in Commands
When you base a command on an AsyncToken returned by the execute method, you can have result handlers inside the command itself as described in Result Handlers in Commands. These internal result handlers will always get invoked before any decoupled result handlers in other objects. They can also potentially alter the result before the external result handlers get invoked.
###Decoupled Result Handlers and Command Observers
Apart from handling results inside the command itself, other objects can get notified, too. There is a set of tags for declaring these on any managed object (like most tags available as metadata, MXML and XML tags).
#####CommandResult
This tag can be used to obtained the result produced by a command in a different object. (It is not needed and should not be used to define a result handler inside the command itself).
[CommandResult]
public function handleResult (user:User, message:LoginMessage) : void {
In this case the User instance returned by the remote call will be passed to the result handler alongside the original message that triggered the action. Like with normal message handlers the parameter type for the message is used to determine which handlers to invoke. It is always a combination of message type (polymorphically) and an optional selector value which serves as a secondary selection key. The type of the result also has to match for this method to get invoked.
If the command that produced the User instance is part of a sequence that was triggered by the specified message type, then by default the result handler will only get invoked after the entire sequence has been completed. This way you do not need to bother processing the result in case a subsequent command causes the sequence to abort with an error for example.
In case you do need to process the result as early as possible, you can use the immediate attribute:
[CommandResult(immediate="true")]
public function handleResult (user:User, message:LoginMessage) : void {
This attribute does not have any effect if the command is not part of a sequence or flow.
#####CommandComplete
If you are not interested in the actual result, but instead only want to execute some logic triggered by the completion of the command, you can use another tag:
[CommandComplete]
public function userSaved (message:SaveUserMessage) : void {
This means that this method would get invoked whenever any command triggered by an SaveUserMessage
has completed.
In case of sequences this means the method gets invoked after all commands in that sequence completed successfully.
#####CommandError
This tag can be used for receiving the eventual fault events or other errors.
[CommandError]
public function handleResult (fault:FaultEvent, trigger:SaveUserMessage) : void {
The parameters are again both optional and the rules for matching are the same as
for [CommandResult]
.
#####Overriding the Result
Like with a result handler inside a command, a decoupled result handler can also overwrite
the original result. To be able to do this the method needs an additional parameter of type
CommandObserverProcessor
, so the exact mechanics are a bit different than for an
internal result handler:
[CommandResult]
public function handleResult
(result:XML, msg:LoginMessage, processor:CommandObserverProcessor) : void {
var parsedResult:Object = parseResultSomehow(result);
processor.changeResult(parsedResult);
}
The CommandObserverProcessor
interface is a sub-interface of the MessageProcessor
interface that provides access to the Command
that was executed on top of the common MessageProcessor
functionality.
#####Local Result Handlers
Parsley allows for local result handlers for a command that was executed in global scope. This solves a common problem in modular applications where a single tab or window dispatches a message that is supposed to trigger a command in a shared service in the root application. The Command in the shared service has to listen to the global scope, as it does not belong to the same Context as the sending object in the module loaded into that tab or window. But for the result handling it is very often only this particular tab or window which wants to handle it. For this reason command results and errors are always re-routed to the Context where the message that triggered the command originated from.
This allows to use a local handler like this:
[CommandResult(scope="local")]
public function handleResult (result:Object, msg:LoginMessage) : void {
[...]
}
This even works when the command executed in a parent Context, as long as the trigger message originated from the same Context as this result handler.
Apart from that option any part of the application can still handle the result when listening in the global scope.
###Command Status Flags
Finally you can also observe the status of executing commands:
[CommandStatus(type="com.foo.messages.SaveUserMessage")]
public var isSaving:Boolean;
This boolean flag will always be true if one or more asynchronous commands matching the specified type and selector are currently executing. It will be false otherwise. This is very convenient for tasks like disabling buttons during command execution for example.
Unfortunately, when using this tag as metadata the trigger message has to be specified as a String. For this reason you may sometime prefer to use MXML configuration in such a case:
<Object type="{SomeType}">
<CommandStatus property="isSaving" type="{SaveUserMessage}"/>
</Object>
For this configuration style there is no risk that refactoring leads to runtime errors due to stale fully qualified class names in buried somewhere in metadata tags. But of course, now the property name is only a String, albeit a shorter one.
##Command Lifecycle
Like already mentioned multiple times in preceding sections, a managed command is added to the Context for the time it executes. This section provides a bit more detail so that you can avoid common errors like trying to access Parsley features from your command after it already finished executing.
A command is not kept in the Context longer than necessary as it is intended to be short-lived. As long as it is part of the Context, it cannot get garbage-collected for example as a Context holds references to all managed objects. Therefore commands are removed from the Context as soon as the container sees them as completed. This section explains what that exactly means.
###Synchronous Commands
Take the following simple (albeit very contrived) example of a synchronous command with one injected collaborator:
class MultiplyCommand {
[Inject]
public var multiplier: Multiplier;
public function execute (msg: MultiplyMessage): int {
return multiplier.multiply(msg.valueA, msg.valueB);
}
}
The command performs a simple multiplication of two integers passed in through the message that triggered the command. Unfortunately it is not the smartest command, so it needs a collaborator to do the actual calculation.
Since the container adds the command to the Context before it executes it, any kind of injection or other setup has been performed before the execute method is invoked.
Likewise, since this is a synchronous command, we also know that the command is very short-lived.
Right after the execute method returns its result, the command will be removed from the Context.
Since a command supports the full
set of container services you could also add a second method marked with [Destroy]
.
You would then see that it would be invoked right after the execute method.
###Asynchronous Commands
Let's now imagine that the injected multiplier is not the smartest either and it needs some time to perform the calculation. This means we have to turn the command into an asynchronous command and ask for a callback function to get injected, too:
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");
}
}
If you execute the command above the traces would be:
before invoking callback
destroy method called
after invoking callback
Passing the result to the callback also marks the completion of the command so that it is immediately removed from the Context. If you need to perform any kind of task that requires container features (like sending a message) you must do that before passing out the result.
#####Asynchronous Command based on AsyncTokens and Result Handlers
In the other variant of an asynchronous command as described in the mechanism is slightly different:
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
}
}
Here the framework waits until the AsyncToken produced either a result or a fault and then invokes the result handler. Only after invocation of the result handler the command will be removed from the Context.
- Features List
- What's New in Parsley 3.0
- Migrating from Parsley 2 to Parsley 3
- Building the Framework from Source
- Dependencies
- Hello World Sample Application
- Adding the Framework SWCs
- Defining Object Dependencies
- Sending and Receiving Messages
- Assembling the Objects
- Initializing the Framework
- Adding more Services
Configuration and Initialization
- Configuration with AS3 Metadata
- MXML Configuration
- XML Configuration Files
- Runtime Configuration
- Configuration DSL
- ActionScript Configuration
- Combining multiple Configuration mechanisms
- Configuration Properties
- Constructor Injection
- Method Injection
- Property Injection by Type
- Property Injection by Id
- Declaring Dependencies in MXML or XML
- Overriding Dependencies in Child Contexts
- Comparing Dependency Injection and Decoupled Bindings
- Basic Usage
- Avoiding Conflicts
- Using Scopes
- Publishing Managed Objects
- Persistent Properties
- Bindings in Flash
- Dispatching Messages
- Receiving Messages
- Managed Events
- Injected MessageDispatchers
- MessageHandlers
- MessageBindings
- Intercepting Messages
- Error Handlers
- Using Selectors
- Using Scopes
- Using the Messaging API
- Implementing a Command
- Mapping Commands to Messages
- Command Groups
- Command Flows
- Starting a Command Programmatically
- Handling Results and Observing Commands
- Command Lifecycle
- About Managed Objects
- Using Factories
- Asynchronous Object Initialization
- Object Initialization Order
- Object Lifecycle Methods
- Lifecycle Observer Methods
- Dynamic Objects
- Initializing View Wiring Support
- Explicit Component Wiring
- Component Wiring without Reflection
- Automatic Component Wiring
- Metadata Configuration
- MXML and XML Configuration
- Component Lifecycle
- Flex Popups and Native AIR Windows
- View Wiring in Modular Applications
- Available Extension Points
- Custom Metadata Tags
- Custom MXML and XML Tags
- Working with ObjectDefinitions
- Working with Scopes
- Custom Configuration Mechanisms
- Replacing IOC Kernel Services
- Initializing Extension Modules