Friday, April 24, 2009

Building Flex Web Services client with Cairngorm and Spring Actionscript


I’ve been using Spring for the past few years for my Java applications, and that is why I was extremely happy to hear about the new Spring Actionscript Framework (formerly known as Prana framework) the framework is simply an AS implementation of spring’s core feature - IoC (Inversion of Control). I was even happier to find out that the creators of Spring AS included an extension, which covers Cairngorm integration.

In the following example I’ve wired my sample GlobalWeather application using Spring AS. The project is available for download, below I listed the steps I had to take in order to port it to Spring AS.

1. Registering the Commands.

I started my application-context.xml with the Commands registration, a task I always felt should not be done using code, now Spring AS makes it very simple. Finally Caingorm users can separate the command registration tasks from they're code:

<?xml version="1.0" encoding="utf-8"?>
<objects xmlns="http://www.pranaframework.org/objects" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.pranaframework.org/objects
http://www.pranaframework.org/schema/objects/prana-objects-0.7.xsd">

<object id="controller" class="org.springextensions.actionscript.cairngorm.control.CairngormFrontController">
<constructor-arg>
<object>
<property name="getCitiesByCountry" value="GetCitiesByCountryCommand"/>
<property name="GetWeatherEvent" value="GetWeatherCommand"/>
</object>
</constructor-arg>
<constructor-arg value="sample.command"/>
<method-invocation name="addCommandFactory">
<arg><ref>globalWeatherCommandFactory</ref></arg>
</method-invocation>
</object>
</objects>

2. Adding custom command factory, Model and Service

In the GlobalWeather application, the Commands inheritance chain plays an important role. The BaseGlobalWeather command contains references to two GlobalWeather application resources, the Service and ModelLocator. The child commands inherent the references for those dependencies, this is a pattern I use in many of my applications.

Spring AS supports this functionality by providing the option of using custom command factories. Using Custom command factories you can make sure that commands are not only created but also injected with they're right dependencies. I've created the GlobalWeatherCommandFactory a factory that injects the created class with both model and service:

package sample.springextensions
{
import as3reflect.ClassUtils;
import com.adobe.cairngorm.commands.ICommand;
import net.webservicex.BaseGlobalWeather;
import org.springextensions.actionscript.cairngorm.commands.ICommandFactory;
import sample.command.BaseGlobalWeatherCommand;
import sample.model.GlobalWeatherModelLocator;

public class GlobalWeatherCommandFactory implements ICommandFactory
{
public var service:BaseGlobalWeather;
public var model:GlobalWeatherModelLocator;

public function canCreate(clazz:Class):Boolean
{
return (ClassUtils.isSubclassOf(clazz, BaseGlobalWeatherCommand));
}

public function createCommand(clazz:Class):ICommand
{
var result:BaseGlobalWeatherCommand = new clazz();

result.service = this.service;
result.model = this.model;

return result;
}
}
}

The command Factory can now be declared in the application-context file along with it's dependencies:

<object id="globalWeatherModelLocator" class="sample.model.GlobalWeatherModelLocator" factory-method="getInstance"/>

<object id="globalWeatherService" class="net.webservicex.BaseGlobalWeather"/>

<object id="globalWeatherCommandFactory" class="sample.springextensions.GlobalWeatherCommandFactory">
<property name="model" ref="globalWeatherModelLocator"/>
<property name="service" ref="globalWeatherService"/>
</object>

My application-context.xml is done and now looks like this:

<?xml version="1.0" encoding="utf-8"?>
<objects xmlns="http://www.pranaframework.org/objects" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.pranaframework.org/objects
http://www.pranaframework.org/schema/objects/prana-objects-0.7.xsd">

<object id="globalWeatherModelLocator" class="sample.model.GlobalWeatherModelLocator" factory-method="getInstance"/>

<object id="globalWeatherService" class="net.webservicex.BaseGlobalWeather"/>

<object id="globalWeatherCommandFactory" class="sample.springextensions.GlobalWeatherCommandFactory">
<property name="model" ref="globalWeatherModelLocator"/>
<property name="service" ref="globalWeatherService"/>
</object>

<object id="controller" class="org.springextensions.actionscript.cairngorm.control.CairngormFrontController">
<constructor-arg>
<object>
<property name="getCitiesByCountry" value="GetCitiesByCountryCommand"/>
<property name="GetWeatherEvent" value="GetWeatherCommand"/>
</object>
</constructor-arg>
<constructor-arg value="sample.command"/>
<method-invocation name="addCommandFactory">
<arg><ref>globalWeatherCommandFactory</ref></arg>
</method-invocation>
</object>
</objects>

Now that the application context is ready, it's time to move on to the main class of my sample project, the main.mxml. it's interesting to see how much wiring and plumbing code is no longer needed thanks to Spring AS.

First I need to initiate the application context once the application starts:

private var _applicationContext:XMLApplicationContext;

private function onCreationComplete() : void
{
//load the xml
_applicationContext = new XMLApplicationContext("application-context.xml");
_applicationContext.addEventListener(Event.COMPLETE, init);
_applicationContext.load();
}

Once the context is loaded, the application can initiate. Again, note that many of the lines that were used before when the wiring was hardcoded, are no longer needed:

private function init(event:Event):void
{
//disptach the first event
var e:GetCitiesByCountryEvent = new GetCitiesByCountryEvent("israel");
CairngormEventDispatcher.getInstance().dispatchEvent(e);
//the following lines are no longer needed
//var f:FrontController = new FrontController();
//f.addCommand(GetWeatherEvent.GET_WEATHER,GetWeatherCommand);
//f.addCommand(GetCitiesByCountryEvent.GET_CITIES_BY_COUNTRY,GetCitiesByCountryCommand);
}

A very important thing to note here is that Flex has a limitation when it comes to IoC. In Flex, in order for a class to compile and be available on runtime you must hardcode it somehow into the application code. The meaning of this is that all the classes that are currently only declared on my application-context.xml should also be mentioned in the actionscript code. Hopefully this is a limitation that the Adobe Flex team will some how solve in the near future, until then you have to add the following to to the code:

//hack: references for classes that are injected using Spring AS
private static var compiledClasses:Array = [Services,BaseGlobalWeatherCommand,GetCitiesByCountryCommand,
GetWeatherCommand,BaseGlobalWeather,GlobalWeatherModelLocator,CustomCairngormFrontController,GlobalWeatherCommandFactory]

The following is the entire code of my main.mxml:

<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="onCreationComplete()"
xmlns:business="sample.business.*">
<business:Services id="services"/>
<mx:Script>
<![CDATA[
import sample.springextensions.GlobalWeatherCommandFactory;
import sample.springextensions.CustomCairngormFrontController;
import org.springextensions.actionscript.cairngorm.control.CairngormFrontController;
import net.webservicex.BaseGlobalWeather;
import sample.command.BaseGlobalWeatherCommand;
import org.springextensions.actionscript.context.support.XMLApplicationContext;
import com.adobe.cairngorm.control.CairngormEventDispatcher;
import sample.command.GetCitiesByCountryCommand;
import sample.event.GetCitiesByCountryEvent;
import sample.command.GetWeatherCommand;
import sample.event.GetWeatherEvent;
import sample.model.GlobalWeatherModelLocator;

//hack: references for classes that are injected using Spring AS
private static var compiledClasses:Array = [Services,BaseGlobalWeatherCommand,GetCitiesByCountryCommand,
GetWeatherCommand,BaseGlobalWeather,GlobalWeatherModelLocator,CustomCairngormFrontController,GlobalWeatherCommandFactory]

private var _applicationContext:XMLApplicationContext;

private function onCreationComplete() : void
{
//load the xml
_applicationContext = new XMLApplicationContext("application-context.xml");
_applicationContext.addEventListener(Event.COMPLETE, init);
_applicationContext.load();
}

private function init(event:Event):void
{
//disptach the first event
var e:GetCitiesByCountryEvent = new GetCitiesByCountryEvent("israel");
CairngormEventDispatcher.getInstance().dispatchEvent(e);
//the following lines are no longer needed
//var f:FrontController = new FrontController();
//f.addCommand(GetWeatherEvent.GET_WEATHER,GetWeatherCommand);
//f.addCommand(GetCitiesByCountryEvent.GET_CITIES_BY_COUNTRY,GetCitiesByCountryCommand);
}
]]>
</mx:Script>
<mx:VBox>
<mx:Label text="Waiting for a respond" color="red"
visible="{Boolean(GlobalWeatherModelLocator.getInstance().workflowstate == GlobalWeatherModelLocator.WAITING_WAITING_FOR_SERVICE_RESPONSE)}"/>
<mx:TextArea width="400" height="400" editable="false" text="{GlobalWeatherModelLocator.getInstance().cities}"/>
</mx:VBox>
</mx:Application>



The application is now ready to use.

4. Summary

Using Spring AS can certainly improve your code and your coding process, as it eliminates the need to wire or configure classes inside actionscript, instead offering an XML standard that many Java developers are familiar with. Spring AS in conjunction with Cairngorm enables developers to concentrate on business logic while not having to worry so much about the plumbing or wiring of the application elements. It provides a clear separation between logic and configuration and enables the writing flexible and modular applications that are easier to maintain and test.

5. Downloads

The entire sample project may be downloaded from here.
The 3 required SWC maybe downloaded from here
  1. Cairngorm.swc
  2. spring-actionscript.swc
  3. spring-actionscript-cairngorm.swc

2 comments:

Anonymous said...

Great post! thanks. I'm considering using Spring Actionscript for a new project. I wonder if there's a workaround for the the class references issue. Having to specify all loaded classes names in your source code is very "Unspringish".

Lior said...

Hi, thanks for the feedback. I don't think there's a solution for this issue yet, there are a number of workarounds for this problem but none of them dismisses the need to specify the loaded classes names in your code.

http://forum.springsource.org/showpost.php?p=238090&postcount=3