All previous chapters demonstrated the Parsley feature set from the perspective of an application developer. But since Parsley provides such basic IOC capabilites like Dependency Injection or fully decoupled Messaging it is equally important that the framework is easy to extend. If you are going to build your own framwork, whether inhouse or Open Source, you wouldn't want to reinvent the wheel for these basic features and prefer to integrate with existing solutions.
So extensibility is an area a lot of thought has been put into. And hopefully when starting
to build your own extensions you will enjoy the huge number of available extensions points,
the flexibility and the ease of use. One extension point where Parsley particularly shines
is the ObjectDefinitionDecorator
interface which allows you to write a single
extension class and use it as a custom Metadata, MXML or XML configuration tag.
When browsing the Parsley code base you'll notice that almost everything in the internal API lives behind interfaces. This makes it easy to create your own implementations for a particular aspect of the framework and switch it while still using the default implementations for other internal services.
Let's start with a quick overview over the extensible parts of the framework.
[Inject]
or [MessageHandler]
. <Object>
or <MapCommand>
. ViewManager
, the MessageRouter
and the core
Context
interface. Other sections in this chapter deal with information about internal APIs that are useful for more than just one of the extension points listed above:
Finally there are further features of the framework that do not even require to directly talk to the framework API or implement framework interfaces, but nevertheless may be often used in a way similar to other types of extensions. These are:
The two links above both point to the lifecycle chapter, these features are not discussed in this chapter.
Being one of the most widely used extension points it allows to create custom metadata you can add
to any object managed by Parsley. Like with most other extension points all the builtin tags of the core framework
like [Inject]
or [MessageHandler]
are implemented as extensions themselves.
Every custom metadata tag has to implements a simple interface with just one method:
public interface ObjectDefinitionDecorator {
function decorate (builder:ObjectDefinitionBuilder) : void;
}
The method will be invoked by the container for each configuration tag it encounters for an object that was added to the container. It doesn't matter whether it is a builtin configuration tag or a custom extension tag, or whether it is a metadata tag, an MXML or XML tag. As long as the tag is mapped to a class that implements this interface the container will invoke it for each tag on each object.
The builder
parameter it passes to the decorator can be used to specify configuration options for the
object definition it currently processes.
A custom metadata tag usually performs one or both of the following two tasks:
ObjectDefinitionBuilder
instance passed to the decorator gives convenient access to core features,
like specifying injections to be performed or registering some of the built-in object processors like for a
message handler. See 13.4 Working with ObjectDefinitions for more details. ObjectProcessor
that configures each instance created from a definition.
See 13.2.4 The ObjectProcessor Interface for details. For your own applications you usually implement two types of custom metadata tags. One type is a fully generic, reusable extension. In this case you are of course invited to blog about it, post on our forum or contribute the tag to the framework, if you think that it is generally useful. The other type of tag would be specific to your application. Even if it is just reused in a few areas of a single application, a tag may very well help removing a lot of unnecessary plumbing.
As the builtin tags all use this extension point themselves, you can examine these implementations
as a starting point for you own tags. Most of the tag implementations reside in the parsley-config
source folder in the package org.spicefactory.parsley.tag
. As you'll see most of them are fairly
simply, only delegating to the configuration DSL. You should always strive to reduce the logic in your tag
implementations, making it easier to apply the feature programmatically in cases where using metadata is not appropiate.
To give a simple example, lets assume that you have an application that dispatches NavigationEvents managed by
Parsley whenever the user navigates to a new tab or window. Instead of using the generic MessageHandler
tag for getting notified you could create a custom tag that applies only for these navigation events:
[NavigateTo("addUserForm")]
public function handleNavigation () : void
The implementation for this tag would look like this:
[Metadata(types="method", multiple="true")]
public class NavigateTo implements ObjectDefinitionDecorator {
[DefaultProperty]
public var target:String;
[Target]
public var method:String;
public var scope:String = ScopeName.GLOBAL;
public function decorate (builder:ObjectDefinitionBuilder) : void {
MessageHandler
.forMethod(method)
.scope(scope)
.type(NavigationEvent)
.selector(target)
.apply(builder);
}
}
The [Metadata]
tag is required to configure the metadata tag, it is explained in 13.2.5 Registering Metadata Tags.
The [DefaultProperty]
tag marks a property that can be used without attribute name. Without that tag you'd need to always
explicitly specify the attribute name, so the example from above would read: [NavigateTo(target="addUserForm")]
.
The [Target]
attribute tells the framework that this is the property that holds the name of the member of the class
(property or method). For metadata to be placed on the class level this is not required of course.
The
scope
property allows to define handlers only interested in navigation events for a particular scope.
Finally we use these property values in building a regular MessageHandler
and applying it to
the ObjectDefinitionBuilder
.
We are passing the scope value, using the global scope as the default in case the attribute is omitted. The name of the navigation
target is used as the selector for the handler, while the message type (NavigationEvent
) is hard-coded in this case.
In the simple example in the preceding section we merely applied an existing feature using the configuration DSL.
When you want to add custom functionality not provided out of the box, some more work is required. In many cases this
involves performing some configuration task on the target object once it gets instantiated, but before it is passed
to the application. For this purpose an ObjectProcessor
can be used. This is the interface that needs
to be implemented:
public interface ObjectProcessor {
function init (target: ManagedObject) : void;
function destroy (target: ManagedObject) : void;
}
Again, fairly simple. The init
method will be invoked after the object was instantiated, but before its
[Init]
method will be invoked (this is the default order which can be changed in configuration).
Likewise the destroy
method will be invoked
when the object was removed from the Context.
The 1:N Relationship between Decorators and Processors
Understanding this concept is important before implementing your first ObjectProcessor
.
There is a 1:N relationship between a decorator and a processor. When you implement
the ObjectDefinitionDecorator
interface like shown in preceding sections, it will be invoked once for a particular
definition in your Context. If it is a definition created by an <Object>
tag or any other tag which represents a
singleton, then the decorator will also map to only one processor. But for other types of objects, like those declared
with <DynamicObject>
or <Command>
multiple instances may be created from a single definition.
In this case one decorator can map to multiple processors, as a new processor might get created for each new instance.
Whether you need a new processor instance for each new target instance solely depends on whether you keep state inside
your processor that only applies to one target. In this case you should implement the StatefulProcessor
subinterface which adds a clone
method to create new instances for each new target.
With this mechanism you have the option to keep state that only
applies to a single target instance inside the processor. This greatly simplifies a lot of common tasks performed by processors
as most processors need to unregister some configuration artifact in the destroy
method which they
created and applied in the init
method, like adding and removing a message handler which always points
to one concrete target only.
Example: The MessageDispatcherProcessor
All that may sound more complicated than it actually is, so let's look at an example for a fairly simple stateful processor,
the implementation for the builtin [MessageDispatcher] tag
tag.
public class MessageDispatcherProcessor implements PropertyProcessor, StatefulProcessor {
private var scope:String;
private var property: Property;
private var dispatcher:MessageDispatcher;
function MessageDispatcherProcessor (scope:String = null) {
this.scope = scope;
}
/* implementing PropertyProcessor */
public function targetProperty (property: Property): void {
this.property = property;
}
/* implementing ObjectProcessor */
public function init (target: ManagedObject) : void {
this.dispatcher = new MessageDispatcher(target.context.scopeManager, scope);
property.setValue(target.instance, dispatcher.dispatchMessage);
}
public function destroy (target: ManagedObject) : void {
dispatcher.disable();
}
/* implementing StatefulProcessor */
public function clone () : StatefulProcessor {
return new MessageDispatcherProcessor(scope);
}
}
The init
methods simply creates the dispatcher function (using a utility class) and injects
it into the target property. Later the dispatcher gets disabled once the object gets removed from the Context
to prevent it from getting used after the target instance became unmanaged. Since this is tied to the lifecycle
of one particular instance the processor is stateful. Therefore it implements StatefulProcessor
in
addition to PropertyProcessor
, both subinterfaces of ObjectProcessor
. The clone
method only passes the scope name to the constructor of the new processor instance, as that does not change
from target to target. The dispatcher is not getting passed, like the property instance which will get passed
into the new instance by the framework anyway.
Finally, in the corresponding decorator, which represents the actual tag you can use in metadata, MXML and XML, you need to add the processor to the definition:
[Metadata(name="MessageDispatcher", types="property")]
[XmlMapping(elementName="message-dispatcher")]
public class MessageDispatcherDecorator implements ObjectDefinitionDecorator {
[Target]
public var property:String;
public var scope:String;
public function decorate (builder:ObjectDefinitionBuilder) : void {
builder
.property(property)
.process(new MessageDispatcherProcessor(scope))
.mustWrite()
.expectType(Function);
}
Here we simply create a new instance of our processor and apply it to the ObjectDefinitionBuilder
.
You can also see that it is convenient to implement PropertyProcessor
. You could do the reflection yourself,
but the syntax above is a handy shortcut for just passing the name of the property as a String, and then relying
on the framework to check whether the property actually exists on the target class and whether the validation rules
you specified are met by that property. In this case we know the property must be writable and of type Function
.
If everything is correct, the framework will then pass the Property
instance (from Spicelib Reflect API)
to the targetProperty
method of your PropertyProcessor
.
Finally in rare cases you may also want to override the default phase the processor gets invoked in.
By default all processor get invoked in the preInit
phase except for the processor for the [Init]
method itseld, which executes in the init
phase. If you need your processor to be applied as a final step
eben after the init method had been called you can specify a phase explicitly:
public function decorate (builder:ObjectDefinitionBuilder) : void {
builder
.property(property)
.process(new MyCustomProcessor(comeParam))
.mustWrite()
.initIn(InitPhase.postInit());
}
If in very rare cases you even need ordering within a particular phase, you can pass an additional order attribute (the default is 0):
public function decorate (builder:ObjectDefinitionBuilder) : void {
builder
.property(property)
.process(new MyCustomProcessor(comeParam))
.mustWrite()
.initIn(InitPhase.postInit(10));
}
Finally you have to tell the framework and the Flex compiler about your custom metadata.
The Parsley support for metadata configuration tags is built on top of the Spicelib Reflection API which offers the capability to map attributes of metadata tags to properties of classes, bringing AS3 metadata a bit closer to the type-safe nature of Java Annotations for example. See 19.8 Mapping classes to metadata tags in the Spicelib Manual for details.
Making the custom tag available for metadata configuration requires three steps:
1. Add the [Metadata]
tag to the class declaration:
[Metadata(types="method")]
public class NavigatorTo implements ObjectDefinitionDecorator {
With the types attribute we specify on which declarations we want the tag to be processed. In this case we only allow it on method declarations. Other tags may be placed on properties or on the class level.
2. Add the class to the Spicelib Metadata Support:
Metadata.registerMetadataClass(NavigatorTo);
This must happen before you create your first Parsley Context
.
3. Add the metadata tag to mxmlc or compc compiler arguments:
-keep-as3-metadata+=NavigatorTo
If you create a reusable library containing Parsley tag extensions it is recommended to compile the library into an SWC. In this case you no longer have to explicitly add the compiler flag for applications that use this SWC as they will be automatically included for all applications that use this SWC. In this case you only have to provide the compiler argument to compc when creating the SWC.
For some general explanations on metadata configuration, like metadata inheritance or inconsistencies of the Flex compiler in dealing with metadata, see 3.1 Configuration with AS3 Metadata.
In many applications you may be able to stick to the builtin tags like <Object>
or <Command>
for your MXML or XML configuration files. But in case you want to add custom options the format is easy to extend.
Like with the metadata extension point, all the builtin tags like <Object>
or <Command>
are implemented
as extensions themselves.
Although technically there are a lot of differences between MXML and XML configuration, they are very similar from the framework's perspective. This means that you usually only have to create one implementation of one of the available interfaces and will then be able to use the same tag in MXML and XML configuration.
The following example shows all the existing types of MXML and XML tags in a single configuration artefact.
This example shows an MXML configuration class, but apart from minor syntactical differences the structure for XML configuration files is exactly the same, and the same set of interfaces is supported.
As you see, you'll have two choices for tags on the root level: tags that implement RootConfigurationElement
or literal values. In case of literal values they are interpreted as an instance that should be added to the Context as is.
The class may have metadata tags for configuration which the framework will process, but the tag cannot have those
framework specific MXML child tags. In case of tags implementing RootConfigurationElement
the framework will
invoke the process
method. For details see 13.3.2 The RootConfigurationElement Interface and 13.3.4 Example: The MessageConfirmation Tag.
The NestedConfigurationElement
shares some similarities with the interface for root tags. It can be used wherever
tags allow child tags that represent values, like the <Property>
, <ConstructorArgs>
or <Array>
tags.
Like with root tags in any of these places literal values are allowed and are used as is, while for the tags implementing the
framework interface their resolve
method will be invoked first to determine the actual value represented by that tag.
For details see 13.3.3 The NestedConfigurationElement Interface.
Finally there are places where tags implementing ObjectDefinitionDecorator
can be used. Amongst other places they can
be used as immediate children of the root tags <Object>
or <DynamicObject>
.
This is the same interface that custom metadata tags have to implement. Thus a single implementation of that interface can be used
as metadata, MXML or XML, allowing for a great level of flexibility.
The reason that they are allowed immediately below tags like <Object>
is that they contain logic to configure
an object, in the case of MXML or XML usage always the object defined by its parent tag. Custom metadata tags can support nested
decorator tags, too. For this they must have a DefaultProperty of type Array that expects elements of type ObjectDefinitionDecorator
.
And of course the tag must contain logic that knows how to apply the decorators to the corresponding definition.
Most of the builtin metadata tags support MXML or XML configuration, too. Consider the following metadata tag:
[MessageHandler(scope="local")]
public function handleMessage (msg:MyMessage) : void
The equivalent XML configuration would be:
<object type="com.foo.MyClass">
<message-handler method="handleMessage" scope="local"/>
</object>
As you see the major difference is that you have to specify the method name in XML. This is not needed with metadata as the name will be deduced from the property the tag is placed upon.
This interface can be implemented by tags that are intended for use on the top level of MXML or XML configuration files.
public interface RootConfigurationElement {
function process (registry: ObjectDefinitionRegistry) : void;
}
In many cases the implementation of the process
method creates, configures and registers a single definition
for an object. Many of the builtin tags like <Object>
behave like that. But it is not a requirement. The tag
can alternatively register multiple object definitions or even none at all and instead do any other kind of processing.
The ObjectDefinitionRegistry
instance passed to the process
method gives access to the object definition builders.
For instruction on how to use the DSL see 13.4 Working with ObjectDefinitions.
This interface is similar to RootConfigurationElement
, but comes with a subtle difference: In contrast to
the root tag which basically can perform any kind of processing, this tag has to represent exactly one value. The reason
is that such a tag is only used in places where a value is expected. For child tags of the <Array>
tag for example
it is expected that each child tag represents an element of that Array. Therefor the interface is slightly different:
public interface NestedConfigurationElement {
function resolve (registry: ObjectDefinitionRegistry) : Object;
}
The resolve
method must return a value. This is interpreted as the actual value the tag represents.
But there can be one more level of indirection: The returned value can represent something which needs to be resolved
at the time the value is applied to the instance it belongs to. This applies for the <ObjectRef>
tag for
example. That tag has to resolve an id value or an object type at the time the target object is configured.
Such a return value must implement the ResolvableValue
interface. For examples you may examine
the NestedObjectTag
or ObjectReferenceTag
classes.
One of the simplest tag implementations is the <MessageConfirmation>
utility tag, so it serves
well as a quick example. The tag allows a message type and optional selector to be defined that should trigger
a confirmation dialog before proceeding with message processing. It can be used like this in MXML configuration
classes:
<MessageConfirmation
type="{DeleteUserMessage}"
scope="local"
title="Confirmation"
text="Do you really want to delete this user?"
/>
So whenever a message of type DeleteUserMessage
is dispatched this utility will kick in first
and show the dialog using the provided text. When the user clicks cancel, message processing will be cancelled.
When she clicks Ok, processing of the message resumes, invoking any [MessageHandler]
that was defined
for that message type.
This is how the implementation looks like:
public class MessageConfirmationTag implements RootConfigurationElement {
public var text:String;
public var title:String;
public var scope:String = ScopeName.GLOBAL;
public var type:Class;
public var selector:*;
public function process (registry: ObjectDefinitionRegistry) : void {
var builder:ObjectDefinitionBuilder
= registry.builders.forClass(MessageConfirmation);
builder
.constructorArgs(title, text);
MessageHandler
.forMethod("showAlert")
.type(type)
.selector(selector)
.scope(scope)
.apply(builder);
builder
.asSingleton()
.register();
}
}
It should be almost self-explanatory. It is a good example for how clean and simple the use of the configuration DSL
can make the implementation of such a tag. It merely uses the values of the properties set by the user to talk to the API
to define constructor arguments and a message receiver for the actual utility class. Note that the tag as such does not
contain the actual processing logic. This has been moved into the MessageConfirmation
class that is configured
and registered by this tag. It is always good to keep configuration and runtime logic separate.
Creating a namespace for your custom MXML tags is not required. You can use them like any other class in MXML: map a tag prefix to the package of your tag and use it as is. But if you create a larger collection of reusable tags your users will probably be happy if you provide a single namespace that holds all the tags of the extension.
Creating such a namespace is a Flex SDK feature. Nothing is specific to Parsley in this respect, so you can simply look up the Flex documentation on that feature. But for the sake of completeness, we'll give a short summary of the necessary steps here.
First you need to create a manifest file, that list all the tags that should be included in the namespace.
If you checkout the Parsley project from SVN you can find the manifest files for all Parsley namespaces in the
manifest
folder. This is an example for how such a file might look like:
<componentPackage>
<component id="Objects" class="com.foo.ObjectsTag"/>
<component id="Object" class="com.foo.ObjectTag"/>
<component id="View" class="com.foo.ViewTag"/>
<component id="DynamicObject" class="com.foo.DynamicObjectTag"/>
<component id="NestedObject" class="com.foo.NestedObjectTag"/>
</componentPackage>
It is a simple mapping of a tag name to the fully qualified class name. As you see the tag name does not have to be the same as the class name.
To compile such a namespace into your SWC the following compiler options need to be specified for compc
:
-namespace http://www.myNamespaceUrl.com ${project.dir}/manifest/myNamespace-manifest.xml
-include-namespaces=http://www.myNamespaceUrl.com
In contrast to MXML creating a namespace for custom tags is required for XML. It's the only way to expand the basic feature set of the Parsley XML tags. The XML support is built on top of the Spicelib XML-Object-Mapper. For full documentation see 20 XML to Object Mapper. But often the full feature set is not required. For basic tags that can also be used in MXML they often only utilize attributes for configuration and avoid any complex structure. Since mapping to attributes is the default behaviour of the XML-Object-Mapper, nothing has to be configured explicitly then. You'd only need to create the namespace and list the classes that should belong to that namespace:
XmlConfigurationNamespaceRegistry
.getNamespace("http://www.myNamespaceUrl.com")
.mappedClasses(SomeTag, SomeOtherTag, YetAnotherTag);
You can then use the new namespace alongside the builtin Parsley namespace:
<objects xmlns="http://www.spicefactory.org/parsley"
xmlns:myNS="http://www.myNamespaceUrl.com">
<object type="com.foo.NormalObject"/>
<myNS:some-tag />
<myNS:yet-another-tag />
</objects>
For the mechanism and supported interfaces see 13.3.1 The Structure of Configuration Files as it is the same as for MXML configuration classes.
In the proceeding sections you were introduced to the mechanisms of creating custom tags, either as metadata, MXML or XML.
With all of these types of tags you often need to create an ObjectDefinition
or modify and existing one.
This can be done with the help of the configuration DSL. This section lists the most important options. For further
details you may want to browse the ASDoc for the DSL.
This is usually only required in MXML or XML tags as metadata tags is most often used to configure existing definitions.
If you implement RootConfigurationElement
or NestedConfigurationElement
for a custom MXML or XML tag,
then the provided ObjectDefinitionRegistry
instance can be used to obtain a new builder for an ObjectDefinition
:
public function process (registry: ObjectDefinitionRegistry) : void {
var builder:ObjectDefinitionBuilder = registry.builders.forClass(MessageConfirmation);
[...]
}
Things you can do with such a builder will be explained in the following sections.
When implementing ObjectDefinitionDecorator
for a custom metadata tag, a builder has already
been created and will be passed to the decorator:
public function decorate (builder:ObjectDefinitionBuilder) : void {
You can define literal values or injections.
Literal Values
builder
.property("firstName")
.value("Ann");
Injection by Type
If the type can be deduced from reflecting on the property type:
builder
.property("service")
.value(Inject.byType());
If the type cannot be determined through reflection, it can be specified explicitly:
builder
.property("service")
.value(Inject.byType(LoginService));
Injection by Id
builder
.property("service")
.value(Inject.byId("loginService"));
This is equivalent to using [Inject(id="loginService")]
on the property declaration.
Like for properties literal values and injections can be specified.
Literal Values
builder.constructorArgs("Alison", "Goldfrapp");
Injection by Type
If the type for all parameters can be deduced from reflecting on the parameters of the constructor yo can simply omit all parameters:
builder.constructorArgs();
Note that there is a bug in current Flash Players that reports '*'
as the type for constructor arguments
instead of the actual declared type if you reflect on a class before any instances of that class have been created.
If the types cannot be determined through reflection, it can be specified explicitly:
builder
.constructorArgs(
Inject.byType(LoginService),
Inject.byType(CartService),
Inject.byType(ProductService)
);
Injection by Id
builder.constructorArgs(Inject.byId("loginService"));
Of course any mechanisms can be mixed, like an injection by type with a literal value:
builder
.constructorArgs(
Inject.byId("loginService")
"http://www.someDomain.com/"
);
If you want to apply one of the builtin message receivers like [MessageHandler]
or [CommandResult]
programmatically, they are all available through their corresponding API:
var builder: ObjectDefinitionBuilder = ...;
MessageHandler
.forMethod("intercept")
.type(LoginMessage)
.scope("local")
.apply(builder);
CommandResult
.forMethod("result")
.type(SaveUserMessage)
.scope("window")
.apply(builder);
CommandStatus
.forProperty("status")
.type(LoadConfigMessage)
.selector("loadServices")
.apply(builder);
In cases where you created a custom message receiver by directly implementing one of the core
receiver interfaces like MessageTarget
or CommandObserver
or extending one of the builtin
receivers, you can pass a custom factory function for these receivers:
var builder: ObjectDefinitionBuilder = ...;
var factory:Function = function (): Object {
return new MyCustomMessageReceiver(someParam, anotherParam);
};
builder.method("handleMessage").process(new MethodReceiverProcessor(factory, ScopeName.LOCAL));
The factory function will be invoked for each instance that gets created from the target definition.
The framework will do the rest for you. Like registering that receiver when the object gets created and removing it again when the object gets removed from the Context. It also takes care of the complex proxy handling for singleton definitions. In those cases a receiver proxy will be registered even before the target instance will be created, to avoid scenarios where an instance "misses" a message just because it was created after the sender at container startup.
This is a hook that allows you to define the way how the target instance gets instantiated. In this case any constructor arguments that were already specified will be ignored since it is this instantiator who is responsible for creating the object then. This is how an instantiator can be specified:
var builder: ObjectDefinitionBuilder = ...;
builder.instantiate(new MyInstantiator());
And this is the interface that must be implemented:
public interface ObjectInstantiator {
function instantiate (target:ManagedObject) : Object;
}
It simply has to return the new instance that should be added to the Context. The specified target
parameter gives access to the ObjectDefinition and the associated Context. The property target.instance
is null still at this point. Obviously, because this will become the instance that this method returns.
This is a hook for performing configuration logic when the object gets created and when it gets removed. This is explained in detail in section 13.2.4 The ObjectProcessor Interface.
Do not use such a processor to register and unregister message receivers. It would work in many cases, but would not be as robust as using a factory for a message receiver as demonstrated in 13.4.5 Custom Message Receivers.
Finally after all configuration options have been specified like shown in preceding sections,
the object has to be registered. Registering a definition has the same effect as adding
an <Object>
tag to an MXML configuration class: the object represented by the definition becomes
available for dependency injection or may be fetched from the Context directly, e.g. with Context.getObject()
.
There are two types of objects you can register: Singletons, which will be created on container startup and will remain managed until the Context gets destroyed. In this case only one instance will be created from the definition. The second option is a DynamicObject. This can be created and removed from the Context at any point in time. And more than just one instance can be created from such a definition. Depending on what type of definition you create there are some final options for the registration available:
Singletons
builder
.asSingleton()
.id("someId")
.lazy(true)
.order(1)
.register();
All the options above are optional, to simply apply the defaults (non-lazy singleton with no particular order) you can reduce the above to:
builder
.asSingleton()
.register();
Dynamic Objects
builder
.asDynamicObject()
.id("someId")
.register();
Again the id is optional and may be omitted. Lazy or order options do not apply to DynamicObjects.
Sometimes you want to create an object that is not registered and available as a root definition, but only gets used within another definition as a kind of private dependency. Something equivalent to the following MXML configuration:
<Object type="{SomeService}">
<ConstructorArgs>
<NestedObject type="{SomeCollaborator}"/>
</ConstructorArgss>
</Object>
In this case the instance of SomeCollaborator
is not available for injection or for being fetched directly
from the Context instance. Instead it only gets injected as a dependency into the constructor of the host object,
with the lifecycle of the dependency being tied to that of the host.
If you want to do the same in code, this is how it works:
var registry: ObjectDefinitionRegistry = ...;
var dependency:DynamicObjectDefinition = registry.builders
.forClass(SomeCollaborator)
.asDynamicObject()
.build();
var builder:ObjectDefinitionBuilder = registry.builders
.forClass(SomeService);
builder
.constructorArgs(Inject.fromDefinition(dependency));
builder
.asSingleton()
.register();
As you see, we do not call register
for the first definition we create. We just call build
instead,
which gives us a definition the container does not know about. We then pass it to Inject.fromDefinition
for the
constructor arguments of the other definition and only register the latter with the container.
When reflecting on a class where there is already an existing builder for, you can simply use the ClassInfo
instance avaibale through that builder:
var p:Property = builder.typeInfo.getProperty("service");
If for some reason you have to create a ClassInfo instance yourself, remember to always pass the ApplicationDomain that the Context your tag lives in belongs to:
var registry: ObjectDefinitionRegistry = ...;
var ci:ClassInfo = ClassInfo.forClass(SomeClass, registry.domain);
Without passing the domain the tag may not work properly in modules loaded into child ApplicationDomains.
In some cases you may want to create custom configuration tags that interact with some kind
of central manager in a similar way like the builtin message receiver tags interact with the MessageReceiverRegistry
for example. And often it would be beneficial if there would be a way to avoid just having one global manager instance.
If you want to allow the users of your custom extension tags to specify scopes the same way like for messaging,
so that the feature can be applied globally or just locally for one Context only or for a any custom scope,
this can be accomplished by registering a scope extension.
A core framework feature that is built on top of a ScopeExtension is the support for Decoupled Bindings with
the [Publish]
and [Subscribe]
tags. If you want to browse the code for inspiration on building
scope-wide services, you find the implementation in the parsley-extensions
source folder in the package
org.spicefactory.parsley.binding
.
Through the use of scopes a publisher may publish an object to a local scope only, which allows for multiple windows, popups or modules to have their own local publishing space without creating ambuigities through multiple publisher declarations.
For a custom scope-wide service it is recommended to use a service interface and program the tags against that interface. You can then register the implementation for the manager like this:
BootstrapDefaults.config.scopeExtensions
.forType(NavigationManager)
.setImplementation(DefaultNavigationManager);
This way a new instance of your scope-wide manager will be created for each scope, the global scope, each local scope of each Context and each custom scope. Someone else would then be able to specify alernative implementations without the need to change any of the configuration tags that might talk to the service.
A tag implementation may then fetch the manager instance that belongs to the specified target scope:
public class EnterScreenDecorator implements ObjectDefinitionDecorator {
public var scope:String = ScopeName.GLOBAL;
[...]
public function decorate (builder:ObjectDefinitionBuilder) : void {
var scope:Scope = builder.registry.context.scopeManager.getScope(scope);
var navigationManager:NavigationManager
= scope.extensions.byType(NavigationManager) as NavigationManager;
navigationManager.doSomethingWithDefinition(definition);
}
}
In the example above the default global scope could be overwritten by the user through the scope attribute.
Of course your manager does not necessarily have to deal with definitions directly. You can also use
the scope-wide manager in an ObjectProcessor
and work with the actual target instance instead of just
the definition.
Although Parsley is already quite flexible in how you configure objects with support for configuration
with Metadata, MXML, XML or ActionScript, you may require even more freedom in how you create object
definitions. You may want to process configuration loaded through Sockets or WebServices or simply programmatically
create some object definitions. For this purpose you can implement the ConfigurationProcessor
interface.
The interface contains a single method:
public interface ConfigurationProcessor {
function processConfiguration (registry:ObjectDefinitionRegistry) : void;
}
Your implementation of this interface may create any number of ObjectDefinition
instances and
add them to the registry provided by the framework. If you still want to process the metadata tags of the classes
you add to the registry (in addition to your own logic of constructing definitions) you should use the
mechanism provided by the framework:
var type:Class = AddToCartAction;
var id:String = "addToCartAction";
registry
.builders
.forClass(type)
.asSingleton()
.id(id)
.register();
The configuration DSL of the framework always processes metadata on the configured classes and combines the metadata configuration with the options specified programmatically through the DSL.
Your custom processor can then be combined with any existing builder:
MXML:
<parsley:ContextBuilder>
<parsley:FlexConfig type="{BookStoreConfig}"/>
<parsley:XmlConfig file="logging.xml"/>
<parsley:CustomConfig>
<mynamespace:MyCustomConfigurationProcessor/>
</parsley:CustomConfig>
</parsley:ContextBuilder>
ActionScript DSL:
var viewRoot:DisplayObject = ...;
ContextBuilder.newSetup()
.viewRoot(viewRoot)
.newBuilder()
.config(FlexConfig.forClass(BookStoreConfig))
.config(XmlConfig.forFile("logging.xml"))
.config(new MyCustomConfigurationProcessor())
.build();
After the code above executed the objects defined with MXML and XML happily coexist with objects added
through your custom builder in the same Context
.
Asynchronous Processors
If your processor operates asynchronously you have to implement the AsyncConfigurationProcessor
interface instead (which extends the ConfigurationProcessor
interface). This might be necessary
if you obtain the configuration with some remote service call for example. The interface looks like this:
[Event(name="complete", type="flash.events.Event")]
[Event(name="error", type="flash.events.ErrorEvent")]
public interface AsyncConfigurationProcessor
extends ConfigurationProcessor, IEventDispatcher {
function cancel () : void;
}
First you must be prepared that your asynchronous processor may get cancelled (for example if the application
destroys the associated Context
before it was fully configured). Finally you have to throw either
Event.COMPLETE
or ErrorEvent.ERROR
, depending on whether configuration succeeded or failed.
(Note: Prior to version 2.4 kernel services required a dedicated factory. This is no longer needed, you can now simply specify the implementation class like shown in the following examples.)
All central services of the IOC kernel live behind interfaces which can be replaced by custom implementations. The replacement can be done globally or just for a single Context, in case you build a modular application with a hierarchy of multiple Contexts.
In the following example the central ViewManager
will be replaced globally by specifying
a custom implementation:
BootstrapDefaults.config.services.viewManager.setImplementation(MyCustomViewManager);
A new instance of your custom ViewManager will then be created for each new Context.
If you want to replace the ViewManager
for a single Context (and its children) only,
you can specify the factory with the ContextBuilder
DSL:
var viewRoot:DisplayObject = ...;
ContextBuilder.newSetup()
.viewRoot(viewRoot)
.services().viewManager().setImplementation(MyCustomViewManager)
.newBuilder()
.config(FlexConfig.forClass(BookStoreConfig))
.build();
For reusable replacements to be used in Flex applications it is recommended to create a simple
custom tag like shown in 13.8 Initializing Extension Modules that can then be used as a child tag in a ContextBuilder
tag.
Decorating IOC Kernel Services
For most scenarios it is recommended not to create a replacement from scratch but instead decorate an existing service. There are two reasons why you should prefer a decorator. First, in most cases you want to add or modify a certain aspect of a core service and not implement it with a totally different behavior. Second, there might be 3rd party frameworks or extensions that in turn want to replace or decorate a core service, which would fail if those were full replacements, as one replacement would overwrite the other. With a decorated service there is no limitation on the number of decorators you wrap around the original service.
This is how you would write a very simple decorator that simply logs whenever a view root is added or removed from the Context. (This is a contrived example as Parsley does that anyway, it's just to demonstrate the principle.)
package {
// [imports...]
public class LoggingViewManager implements ViewManager {
private static const log:ILogger = Log.getLogger("com.foo.LoggingViewManager");
private var delegate:ViewManager;
function LoggingViewManager (delegate:ViewManager) {
this.delegate = delegate;
}
public function addViewRoot (view:DisplayObject) : void {
log.info("Adding view root " + view.name);
delegate.addViewRoot(view);
}
public function removeViewRoot (view:DisplayObject) : void {
log.info("Removing view root " + view.name);
delegate.removeViewRoot(view);
}
}
}
As you can see all you need to do is implement the kernel interface (ViewManager
in this example)
like when replacing kernel services and then adding a constructor that accepts the real ViewManager
as
a delegate. You can then apply this decorator globally like this:
BootstrapDefaults.config.services.viewManager.addDecorator(LoggingViewManager);
Knowing your Environment
In many cases you may need access to the configuration settings or collaborating services or just the Context
instance itself. This can be accomplished for service replacements and decorators through implementing the InitializingService
interface:
class SomeService implements ViewManager, InitializingService {
public function init (info:BootstrapInfo) : void {
}
public function addViewRoot (view:DisplayObject) : void {
}
public function removeViewRoot (view:DisplayObject) : void {
}
}
The BootstrapInfo
class gives you access to all the other kernel services and the configuration settings for
the Context your service instance belongs to. The init
method will be invoked once after your service has been
instantiated.
The next section gives you an overview over all services that can be replaced or decorated.
List of IOC Kernel Services
BootstrapManager | Responsible for processing the configuration
and then building and initializing the Context .
May be fed with different types of ConfigurationProcessor s, like the builtin
ones for ActionScript, MXML or XML configuration. |
Context | This is the core interface of the framework, putting all the other pieces together and delegating most of the work to the other parts of the kernel listed below. It allows you to pull objects out of the container or examine its contents. |
ObjectDefinitionRegistry | The registry for all ObjectDefinitions the Context
will manage. |
ObjectLifecycleManager | Responsible for processing ObjectDefinitions , instantiating,
configuring and disposing the actual instances described by those definitions. |
ScopeManager | Responsible for managing all scopes that are associated with a single Context. |
MessageRouter | The core interface of the Messaging Framework. |
ViewManager | Responsible for dynamically wiring views to the Context . |
All the services listed above can be replaced through the BootstrapDefaults
.
When you create a set of configuration tags and possibly some scope-wide managers that integrate with them, you may want to offer your users a convenient way to activate that extension:
<parsley:ContextBuilder config="{MyConfig}">
<app:NavigationSupport/>
</parsley:ContextBuilder>
Here a custom MXML tag as a child of the <ContextBuilder>
tag takes the role of doing all the necessary
setup to make the extension available for that Context.
Child tags of <ContextBuilder
have to implement the ContextBuilderProcessor
interface:
public class NavigationSupportTag implements BootstrapConfigProcessor {
private static var initialized:Boolean = false;
public static function initialize () : void {
if (initialized) return;
Metadata.registerMetadataClass(EnterScreenDecorator);
Metadata.registerMetadataClass(ExitScreenDecorator);
BootstrapDefaults.config.scopeExtensions
.forType(NavigationManager)
.setImplementation(DefaultNavigationManager);
[...]
initialized = true;
}
public function processConfig (config:BootstrapConfig) : void {
initialize();
}
}
The processBuilder
method of the implemented interface will simply delegate to a public static method.
This has the advantage that you can use the same class programmatically, in cases where you don't use the MXML tags
to declare the Context:
NavigationSupport.initialize();
Like shown in our example, most common tasks in such an initializer are registering custom metadata tags or scope-wide managers.
If you also offer your tags in an XML variant built on top of the custom configuration namespace support in Parsley you may want to prefer to offer them with a separate initializer class. This way you would make sure that users who do not use XML configuration do not end up with the entire Spicelib XML-Object-Mapper framework compiled into their SWF.
<parsley:ContextBuilder config="{MyConfig}">
<app:NavigationSupport/>
<app:NavigationXmlSupport/>
</parsley:ContextBuilder>
In the example above XML support is activated explicitly in addition to the core extension module. The XML initializer is responsible for configuring the custom XML configuration namespace. For details see 13.3.6 Creating Custom XML Namespaces.