Cairngorm Sample – How Business Logic Can Manage Views Part III

The goal of this series is to demonstrate how you can manage views with investing in your client side business logic.
The previous posts (part 1, part 2) were primarily about the infrastructure of a very simple single and multiple view dashboard application. This and the following posts will add further functionality to the application with the indent to demonstrate how your model objects become a focus point of your application.
In particular, we’ll add validation logic, formatting logic and an additional use case.


Iteration 4 – Adding Functionality
Let’s go back to our single view dashboard and let’s first make this application a bit more realistic with simulating a delay for the dummy remote service that is invoked after the user presses the “Get Quote” button. I’ll just add a flash.utils.setTimeout method in our iteration 2 delegate (StockMarketDelegate.as) to simulate a delay.
Usually, while a remote service is being processed, the UI is supposed to react to it with i.e. displaying a progress or disabling certain UI components to let the user know that further requests cannot be dispatched. In our use case we’d like to disable the “Get Quote” button while a remote service call is being processed. We can achieve this easily with adding a property to our representing model object from iteration 2 StockQuote. Let’s call it isPending and make it a Boolean where our view can bind to.

[Bindable]
public var isPending : Boolean;

Adding Validation Logic
Let’s add a validation of our stock quote with inspecting the actual stock quote value. We can easily use the mx.validators.StringValidator of the Flex Framework for this type of work.
In our application we need both parameters of validations (isPending and the result of StringValidator) to pass in order to let the UI allow a remote service request. Our model object could have an isValid property, which combines these two parameters of validation. I compute isValid with the following method.

private function validate() : void
{
isValid = ( isSymbolValid && !isPending );
}

isSymbolValid is a property, which will be the result of our StringValidator.
We could add the mx:StringValidator tag into the StockMarketPod.mxml and have it calling the StockQuote model object on its “valid” and “invalid” events.

<mx:StringValidator
minLength="2" triggerEvent="change"
source="{ symbolTextInput }" property="text"
valid="stockQuote.validateSymbol( true );"
invalid="stockQuote.validateSymbol( false );"/>

To call a model object directly from the view isn’t the clearest form of MVC since views should only dispatch events, which are handled by your controller. But for this example I find it good enough to have a method be called directly from the view. In my humble opinion this use case doesn’t justify a “round trip” to the model object just yet. Think: “The simplest thing that could possibly work”.
Adding Formatting Logic
This is easy. We want to have our stock quote result being formatted with a currency formatter. You could define the formatter in StockMarketPod.mxml with

<mx:CurrencyFormatter
id="standardEuroFormatter"
currencySymbol="€" precision="2"/>

and have it formatting with a method binding

<mx:Label text="{ standardEuroFormatter.format( stockQuote.lastStockQuote ) }"/>

Now, let’s have a complete look into our StockQuote model object.

package com.adobe.cairngorm.samples.dashboard.model
{
public class StockQuote
{
[Bindable]
public var lastStockQuote : Number;
[Bindable]
public var isValid : Boolean;
[Bindable]
public var statusMessage : String;
private var _isPending : Boolean;
private var isSymbolValid : Boolean;
[Bindable]
public function get isPending() : Boolean
{
return _isPending;
}
public function set isPending( value : Boolean ) : void
{
_isPending = value;
validate();
}
public function validateSymbol( isValid : Boolean ) : void
{
isSymbolValid = isValid;
validate();
}
private function validate() : void
{
isValid = ( isSymbolValid && !isPending );
}
}
}

The new isPending state is being manipulated by the GetStockQuoteCommand.

private var model : ModelLocator = ModelLocator.getInstance();
private var stockQuote : StockQuote = model.stockQuote;
public function execute( event : CairngormEvent ) : void
{
stockQuote.isPending = true;
var stockQuoteEvent : GetStockQuoteEvent = GetStockQuoteEvent( event );
var symbol : String = stockQuoteEvent.symbol;
var delegate : StockMarketDelegate = new StockMarketDelegate( this );
delegate.getQuoteForSymbol( symbol );
}
public function onResult( event : * = null ) : void
{
//for demo purpose: event would normally be an event object of remote service result.
stockQuote.lastStockQuote = event as Number;
stockQuote.isPending = false;
stockQuote.statusMessage = "";
}
public function onFault( event : * = null ) : void
{
stockQuote.lastStockQuote = NaN;
stockQuote.statusMessage = "Quote retrieval error.";
stockQuote.isPending = false;
}

And let’s finally look into our view StockMarketPod.mxml

<?xml version="1.0" encoding="utf-8"?>
<mx:Panel
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:util="com.adobe.cairngorm.samples.dashboard.util.*">
<mx:Script>
<![CDATA[
import com.adobe.cairngorm.control.CairngormEventDispatcher;
import com.adobe.cairngorm.samples.dashboard.model.StockQuote;
import com.adobe.cairngorm.samples.dashboard.events.GetStockQuoteEvent;
[Bindable]
public var stockQuote : StockQuote;
private function getQuoteForSymbol() : void
{
var event : GetStockQuoteEvent = new GetStockQuoteEvent( symbolTextInput.text );
CairngormEventDispatcher.getInstance().dispatchEvent( event );
}
]]>
</mx:Script>
<mx:CurrencyFormatter
id="standardEuroFormatter"
currencySymbol="€" precision="2"/>
<mx:StringValidator
minLength="2" triggerEvent="change"
source="{ symbolTextInput }" property="text"
valid="stockQuote.validateSymbol( true );"
invalid="stockQuote.validateSymbol( false );"/>
<mx:Form>
<mx:FormItem label="Symbol">
<mx:TextInput
id="symbolTextInput"/>
<mx:Button
label="Get Quote"
enabled="{ stockQuote.isValid }"
click="getQuoteForSymbol();"/>
</mx:FormItem>
<mx:FormItem label="Price Quote">
<mx:Label text="{ standardEuroFormatter.format( stockQuote.lastStockQuote ) }"/>
<mx:Label text="{ stockQuote.statusMessage }"/>
</mx:FormItem>
</mx:Form>
</mx:Panel>

Note, that this version of StockMarketPod.mxml doesn’t have a reference to ModelLocator anymore. Instead, the model objects, which are needed, are passed into StockMarketPod.mxml.

<mx:Script>
<![CDATA[
import com.adobe.cairngorm.samples.dashboard.model.ModelLocator;
import com.adobe.cairngorm.samples.dashboard.model.StockQuote;
[Bindable]
private var model : ModelLocator = ModelLocator.getInstance();
[Bindable]
private var stockQuote : StockQuote = model.stockQuote;
]]>
</mx:Script>
<view:StockMarketPod
stockQuote="{ stockQuote }"
title="Stockmarket Pod"/>

I find it beneficial to give view components only the information they need instead of global access to all your business logic through a reference of your ModelLocator. Furthermore, this makes view components more reusable across projects.
Optionally; to further increase view reusability, we could also replace CairngormEventDispatcher calls inside StockMarketPod.mxml with view events dispatched via EventDispatcher. We would handle these events outside the view component that we want to make reusable across applications and dispatch the Cairngorm event using CairngormEventDispatcher in order to reach application specific Cairngorm commands.
Download and “view source” the complete iteration 4 example.
Iteration 5 – Adding a Use Case
Let’s pretend our customer now wants us to also purchase stocks after a stock quote has been retrieved. We are supposed to add another text input field with a quantity field that defines how many stocks the user would want to buy. Furthermore, we would need a purchase button. The purchase button must only be enabled when the user is allowed purchase a stock quote. This is the case when:

  • the quantity field contains a valid value.
  • another stock purchase request is not pending.
  • a stock quote has been successfully retrieved.

We could use another model object responsible for managing all this. Let’s call it StockPurchase. StockPurchase can have a very similar structure than StockQuote. But the third bullet point requires StockPurchase to know about StockQuote’s state. Only if StockQuote has been completed, StockPurchase’s validation can pass completely. There are various ways to let StockPurchase know about StockQuote’s state. For this example I just pass a reference of StockQuote to the constructor of StockPurchase. I’ll do that inside an initialize method (here in ModelLocator). This is being called at start up.

private function initialize() : void
{
stockQuote = new StockQuote();
stockPurchase = new StockPurchase( stockQuote );
}

Here is the full source code of StockPurchase.
And here are the additions to StockMarketPod.mxml including the additional validator and formatter.
A new PurchaseStockCommand finally sends the remote purchase request, handles the response and manipulates the client side model objects accordingly. Our views listen to model object events through bindings and translate the model data according to view related rules. This translation process could be done using view helpers like the mx:CurrencyFormatter used in this sample.
Download and “view source” the complete iteration 5 example.
I hope you’ve enjoyed my dashboard iterations so far. The most important point I wanted to bring across is how valuable I think it is to think about our model objects and try to extract much functionality out of our views. Going forward this way, I find it’s much easier to build unit-testable, flexible and scalable Rich Internet Applications.
In the next iteration I’ll cover some additional refactorings on the view and model objects and how state changes in the model can cause view related methods to be executed.