« April 2006 | Main | July 2006 »
June 18, 2006
Cairngorm Sample – How Business Logic Can Manage Views Part II
In the last post I’ve introduced you to the stock market dashboard sample application and how you can create business logic to manage your view. In this post and example, I’ll apply another use case to the sample application. With adding multiple pods to the stock market dashboard, the user can request price quotes independently. Each pod has its own stock market data. I’ll cover how your business logic can be adjusted to manage each view. I’ll also touch on how stateless Commands can help making this application easier.
Iteration 3 – Creating business logic that manages multiple views
Often, I find use cases where many views of a same type share common functionality. Each view can be represented by a model object. The view can request the appropriate model object from a manager object at start up. For example the StockMarketDashboard.mxml would look like this with additional pods:
<view:StockMarketPod quoteId="quote1" title="Stockmarket first pod"/> <view:StockMarketPod quoteId="quote2" title="Stockmarket second pod"/> <view:StockMarketPod quoteId="quote3" title="Stockmarket third pod"/> <view:StockMarketPod quoteId="quote4" title="Stockmarket fourth pod"/>
Notice that the view needs to send some form of unique identifier to the manager. We add this unique identifier to our StockQuote class.
package org.nevis.cairngorm.samples.dashboard.model
{
public class StockQuote
{
[Bindable]
public var lastStockQuote : Number;
[Bindable]
public var stockQuoteError : String;
}
}
The manager object StockQuoteManager, is declared at initialization on the ModelLocator instance. But how does this manager object look like?
Usually, I implement it with a hash map that is keyed by the view’s unique identifier. In the constructor we can initialize the hash map. In Flex 1.x we would have used a generic Object to create a hash map. In Flex 2, I like to use flash.utils.Dictionary, which more clearly shows the indent of the code.
public function StockQuoteManager()
{
stockQuotes = new Dictionary();
}
The getStockQuote method either creates or returns a specific StockQuote object based on the view’s unique identifier quoteId.
public function getStockQuote( quoteId : String ) : StockQuote
{
var key : String = quoteId;
if( stockQuotes[ key ] == null )
{
var stockQuote : StockQuote = new StockQuote();
stockQuotes[ key ] = stockQuote;
}
return StockQuote( stockQuotes[ key ] );
}
The StockMarketPod.mxml almost looks identical to the old version of iteration 2, which refactored ModelLocator properties to the StockQuote model object. We just need to send the unique identifier to the Command, since the Command itself needs to retrieve a StockQuote object from the manager object. So the GetStockQuoteEvent class gets another quoteId property.
In GetStockQuoteCommand we need to store the unique identifier as an instance property since the command needs to retrieve and update the correct StockQuote object in an onResult or onFault handler.
private var model : ModelLocator = ModelLocator.getInstance();
private var quoteId : String;
public function execute( event : CairngormEvent ) : void
{
var stockQuoteEvent : GetStockQuoteEvent = GetStockQuoteEvent( event );
var symbol : String = stockQuoteEvent.symbol;
quoteId = stockQuoteEvent.quoteId;
var delegate : StockMarketDelegate = new StockMarketDelegate( this );
delegate.getQuoteForSymbol( symbol );
}
public function onResult( event : ResultEvent = null ) : void
{
//simulate a result from service
var lastStockQuote : Number = Math.random() * 50 + 5;
var stockQuote : StockQuote = model.stockQuoteManager.getStockQuote( quoteId );
stockQuote.lastStockQuote = lastStockQuote;
stockQuote.stockQuoteError = "";
}
public function onFault( event : FaultEvent = null ) : void
{
var stockQuote : StockQuote = model.stockQuoteManager.getStockQuote( quoteId );
stockQuote.lastStockQuote = NaN;
stockQuote.stockQuoteError = "An error occured.";
}
Also, notice that in the example above we make use of stateless commands. Since the introduction of stateless commands in Cairngorm, new command instances are being created each time a Cairngorm event is dispatched (see Alistair McLeod’s blog entry). You can now securely save instance properties in your commands without fearing that the instance property will be modified by another Cairngorm event. Would your command not be stateless, our quoteId could be overwritten by a quoteId from another stock pod. When the command's onResult is triggered by the incoming response from the first remote service request, the response would map to a wrong StockQuote object. But thankfully, we don't need to think about that anymore. ;)
Spinning it a wee bit further.
Notice, that your manager object could contain other functionality that applies to all pods. Think about a domain model that suits your use case best.
Furthermore, in a slightly more elaborate use case it might be significant that the onResult handler in GetStockQuoteCommand requests the currently up-to-date StockQuote object from the manager object as it does in the example shown. While the command is waiting for a response for a request to a remote service, the state of the RIA could change. When the response comes in, the manager object might want to return a different StockQuote object or not a StockQuote object at all.
For example, the end user might be able to close certain stock quote pods. In this case, its representing StockQuote model object may also have been deleted. On subsequent requests to the StockQuote object, the manager object might want to throw an exception. This exception might be handled differently by the command.
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).
Posted by auhlmann at 04:21 PM | Comments (12)
June 01, 2006
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).
Posted by auhlmann at 12:20 PM | Comments (9)
