Cairngorm Sample – How Business Logic Can Manage Views

There are many ways to update your views when your server- or client side business logic changes. The strategy you find me usually recommending is the ModelLocator strategy, which leverages the Flex binding feature. See Steven Webster’s article for more information.
Basically, your views bind to properties that are retrieved from your ModelLocator. These properties can be changed from your Commands, other business logic or other views and once changed; all listening views are updated seamlessly.
Since many of the Cairngorm examples out there are meant to be easy to understand, they often just show these properties exposed as single properties on the ModelLocator. Once your RIA grows in size, this indeed can be quite limiting. I’ll showcase another Cairngorm sample application that focuses on how you can improve your architecture with investing in slightly more advanced business logic. The sample application is a stock market dashboard that allows users to retrieve price quotes on a company stock. From time to time I’ll add features to this application, to showcase some possible ways to architect a Cairngorm application. This version of it will use Cairngorm for Flex 2 Beta 3.


Iteration 1 – The simplest thing that could possibly work
But let’s first look at the very simplest possible solution with still leveraging MVC principles advocated by Cairngorm. The stock market dashboard in its first iteration just contains a single pod with minimal UI controls. See example.
With pressing the “Get Quote” button the user dispatches a Cairngorm event that is handled by a Command instance to request a new quote.
StockMarketPod.mxml dispatches the Cairngorm event on behalf of a Button’s click event:

<mx:Button label="Get Quote" click="getQuoteForSymbol();"/>

And the corresponding mx:Script tag:

import org.nevis.cairngorm.samples.dashboard.events.GetStockQuoteEvent;
private function getQuoteForSymbol() : void
{
var event : GetStockQuoteEvent = new GetStockQuoteEvent( symbolTextInput.text );
dispatchEvent( event );
}

GetStockQuoteCommand handles the Cairngorm event and asks a business delegate class (StockMarketDelegate) for a quote.

public function execute( event : CairngormEvent ) : void
{
var symbol : String = GetStockQuoteEvent( event ).symbol;
var delegate : StockMarketDelegate = new StockMarketDelegate( this );
delegate.getQuoteForSymbol( symbol );
}

In a real world scenario this would be most likely a server side call, but for the sake of simplicity in this demo, I’ve commented the code that would be needed to call a remote service in the business delegate.
StockMarketDelegate.as just calls back to the Command immediately. Notice that if the user enters a symbol “fail”, the Command’s onFault handler will be invoked.

public function StockMarketDelegate( responder : Responder )
{
//disabled for demo
//this.service = ServiceLocator.getInstance().getService( "stockMarketDelegate" );
this.responder = responder;
}
public function getQuoteForSymbol( symbol : String ) : void
{
//disabled for demo
//var call : AsyncToken = service.getQuoteForSymbol( symbol );
//call.resultHandler = responder.onResult;
//call.faultHandler = responder.onFault;
if( symbol == "fail" )
{
responder.onFault();
}
else
{
responder.onResult();
}
}

Our StockMarketPod view just needs two information from the application’s business logic.

  • The answer to the quote (the stock price)
  • An error message, that communicates to the user if a request fails.

Because this is so simple, I have just created two properties directly on the ModelLocator.

public var lastStockQuote : Number;
public var stockQuoteError : String;

GetStockQuoteCommand can then just change these values in the onResult or onFault handler. For this demo purpose, it performs some dummy calculations to simulate the remote service result.

public function onResult( event : ResultEvent = null ) : void
{
//simulate a result from service
var stockQuote : Number = Math.random() * 50 + 5;
model.lastStockQuote = stockQuote;
model.stockQuoteError = "";
}
public function onFault( event : FaultEvent = null ) : void
{
model.lastStockQuote = NaN;
model.stockQuoteError = "An error occured.";
}

The StockMarketPod view can bind to those properties and further format the result based on its presentation needs:

<mx:FormItem label="Symbol">
<mx:Label text="{ formatQuote( model.lastStockQuote ) }"/>
</mx:FormItem>
<mx:FormItem>
<mx:Label text="{ model.stockQuoteError }"/>
</mx:FormItem>

And the formatting logic of a mx:Script block.

private function formatQuote( quote : Number ) : String
{
return ( isNaN( quote ) ) ? "" : String( quote );
}

Note that this type of code could be refactored and extracted out from the view into a unit-testable utility class.
Iteration 2 – Creating business logic that fit your needs
Now, the above example is the simplest thing that could possibly work. As your RIA grows in size this can be limiting. You may find that your ModelLocator instance is “overcrowded” with properties that you might even forget what they are needed for. You might also run into naming conflicts with other properties designed for different use cases.
A common refactoring is to create business objects that encapsulate the properties of your use case. These business objects can represent the information that your views need in a business context. You can design them at a granularity that fits your use case best. For example in a larger RIA you might want to think about a number of classes to represent your use case. Your views may then bind to these business objects or to properties of it. Your views might bind to other application objects that translate your business objects to a form that’s more useful to your views.
This way, your ModelLocator might better communicate its intent as it’s much easier for developers to grasp what business logic your application contains.
In our stock market dashboard, we could e.g. encapsulate the lastStockQuote and stockQuoteError properties in a simple business object:

package org.nevis.cairngorm.samples.dashboard.model
{
public class StockQuote
{
[Bindable]
public var lastStockQuote : Number;
[Bindable]
public var stockQuoteError : String;
}
}

Note, that in a true business object you would probably not even have a property that directly holds a String with an error message. Instead, the business object would just hold an error type and other application objects would translate that error type to e.g. a String representation, which could then be used by views.
Our ModelLocator would just define a single property representing a stock market pod view in a business context. E.g. with

public var stockQuote : StockQuote = new StockQuote ();

And our view would change to this:

<mx:FormItem label="Symbol">
<mx:Label text="{ formatQuote( model.stockQuote.lastStockQuote ) }"/>
</mx:FormItem>
<mx:FormItem>
<mx:Label text="{ model.stockQuote.stockQuoteError }"/>
</mx:FormItem>

You might want to check out the complete source (right click on the sample app and choose “View Source” or download it as a ZIP file).