Well, the extra RAM for the MacBook Pro got here, so it's time to switch to using it for Flex development. Woot! What better to kick it off than talking about the Model Locator pattern?
Introduction
In the last part of this series, we installed Flex Builder and the Cairngorm framework, creating a new Flex Builder project that links to the Cairngorm .SWC file to make sure things are working properly.
In this edition, we're going to have a bit more fun, solving a major problem a lot of new Flex developers run into, learning how the Cairngorm microarchitecture differs a bit from ColdFusion frameworks as well as a bit about design patterns.
Our goals are as follows:
- Identify the problems with storing data in view components
- Learn about the Model Locator design pattern
- Implement a simple Model Locator and bind two view components to it
The Data Problem
When I first got Flex, I was stoked. I followed an example for connecting to a Web Service, wrote a
Then, I wanted to have a few different views on the data. I wanted a chart that broke down errors by type. I wanted the datagrid to show me everything in the log. I wanted another chart that broke down errors by day.
No big deal, though. I simply put stored the query results (don't cringe, Flex veterans, we'll move to arrays of VOs in a few days) in a variable in my main MXML called "logQuery". Then, I created a few different MXML components, which work a lot like ColdFusion custom tags:
LogGrid.mxml showed all of my log entires.
ErrorTypeChart.mxml showed all of my log entries, grouped by error type.
ErrorDateChart.mxml showed all of my log entries, grouped by error date.
To be wonderfully parameterized, I created a property of each MXML component, where I passed it the query. This is just like passing it as an attribute of a ColdFusion custom tag - not rocket science:
Oops.
While it worked, it's a brittle architecture. When I need to re-arrange views, or create further nested layouts, or dynamically create and remove views, I've got to remember to carry around this logQuery variable. And when I forget, I'm stuck going through a tag hierarchy to figure out where something became null. That's a lot like the old days of building ColdFusion sites out of many little nested ColdFusion tags - not a fun way to develop!
Instead, wouldn't it be better if we could just re-arrange these view components at will, maybe even putting both of the chart components inside a ErrorChartStack.mxml without requiring any changes?
We could do this if they had a way to find the logQuery data without passing it via attributes.
That logQuery represents data that is part of the "Model" of my application. The "Model" is that piece of Model View Controller that contains all of the data and business logic of your application.
In this case, we need a tool that allows any View component to locate a piece of the Model's data.
Introducing Model Locator
Model Locator is one those design patterns that has a long name for a simple concept. In short, it simply states that you'll create some "thing," called the Model Locator, that any component can ask for model data, regardless of its location.
Ok, simplest explanation I can give: it's like a waiter for your data. You add data, like logQuery, to the menu, and when your view components order the logQuery, they get the logQuery.
We'll see why it's a bad idea to run a restaurant using static instances of meals in a minute, but for now, that metaphor will have to do.
Flex has this really great feature called Data Binding. In short, when you bind something like the logQuery to a chart or a grid, the chart or grid will be automatically updated when the data changes. It's absolutely fantastic.
When we use the ModelLocator, tags like the
This binds the data in the grid to the instance of logQuery in the ModelLocator object. Remember, the ModelLocator acts like a waiter: the datagrid doesn't know where the data comes from, and doesn't care. It orders it, and gets what it asks for.
However, wouldn't it stink if you accidently created two instances of the ModelLocator object, and you bound the dataprovider for the datagrid to one, the charts to another, and only remembered to change one?
Yeah, it'd be a rough day of debugging. And that's why ModelLocators use static variables.
If you've only done ColdFusion work, static variables are likely to be a new concept. They're not a rough idea to learn though:
For a given static value, all instances of a given type of object share the same value.
If ColdFusion had static variables, and Widget.Foo was a static "this" scope variable in the Widget CFC, this code:
<cfset widgetTwo = createObject("component", "widget") />
<cfset widgetOne.Foo = "Static value set on WidgetOne, but output through WidgetTwo!" />
<cfoutput>#widgetTwo.Foo#</cfoutput>
Would produce these results (notice that we set Foo on WidgetOne, but we displayed it on WidgetTwo!):
By making sure all of the variables in our ModelLocator are static variables, we make sure that everything using something like my logQuery is looking at the same data.
So, in essence, ModelLocator is like a waiter serving everyone the same dish. I said it wasn't a good metaphor, but it served its purpose. (Part of me wishes that the example had involved dolphins, so I could make a "served its porpoise" pun.)
In fact, it gets even worse. ModelLocator is intended to be used as a singleton. A singleton is a class where there's only ever one instance created.
In other words, the metaphor is even worse: the Cairngorm diner only has one waiter, and anytime someone orders a given dish, they get the same plate as everyone else.
Side note: yes, it's possible, and probably a good idea, for large applications to have separate ModelLocators. That's getting beyond what we're doing here.
Exactly why we have to do it is beyond scope, but whenever you get your ModelLocator, you'll get it by saying something like this:
Doing this ensures that everyone is getting the same waiter, and that he serves the same dishes, even if they're half-eaten.
Ok, so to conclude Model Locator: instead of maintaining brittle ways of passing data around our applications through methods like tag attributes, we use a Model Locator as a dumbwaiter, letting our components order data by name.
That's probably enough lousy humor, and I can understand if you're clamoring for code. Model Locator is a simple concept, but it warrants a good explanation.
Implementing a Model Locator
We're going to implement a sample Model Locator. We'll fill it with some simple data, and then we'll create and bind a few views to it.
To get started, open up the CG4CF project we created yesterday. If you don't have it set up, you'll want to go do yesterday's tutorial real quick. It doesn't take long.
Next, we're going to create a series of folders under the root that give us a nicely named place to store data. Click on the name of the project (CG4CF, not CG4CF.mxml), and do File -> New -> Folder. For "Folder Name:", enter "com/firemoss/cg4cf/model" and then click Finish.
Now, drill down into the com/firemoss/cg4cf/model directory. Do File -> New -> ActionScript Class. In the "Package" setting, it should show com.firemoss.cg4cf.model. If it doesn't, back out and make sure you've got the "model" folder highlighted.
For "Name:" enter "ContactModelLocator". Don't click Finish yet. We're going to, you guessed it, maintain a little list of Contacts. Original example, no?
Next, click "Add.." beside the "Interfaces:" setting. Scroll down a ways, and you should see ModelLocator. If you don't, revisit yesterday's tutorial to make sure you've linked to the Cairngorm SWC as part of your build path. Highlight ModelLocator, and click OK.
Now, you can click finish. If the ModelLocator interface required us to create any methods, it'd stub them out for is. It doesn't though - I just thought it was a nice feature of Flex Builder 2 to mention.
To get our ModelLocator ready to be a singleton, we need to add a bit of boilerplate code that works around a quirk in AS3. It's not mandatory that you know exactly how this works - all you need to do is copy/paste the following code in place of everything you've currently got in ContactModelLocator.as:
{
import com.adobe.cairngorm.model.ModelLocator;
public class ContactModelLocator implements ModelLocator
{
private static var modelLocator : ContactModelLocator;
public static function getInstance() : ContactModelLocator
{
if ( modelLocator == null )
modelLocator = new ContactModelLocator();
return modelLocator;
}
}
}
Ok. Now, it's time to add a list of contact to the "menu of dishes" our ModelLocator will serve.
Normally, this list would come from something like a backend ColdFusion Component. We'll get there, but for now, we'll build it manually in the ContactModelLocator's initialize() method. You can ignore that code - what's important is noticing the new public static variable called "contacts":
Here's the complete code for the ContactModelLocator. An explanation follows.
{
import com.adobe.cairngorm.model.ModelLocator;
import mx.collections.ArrayCollection;
public class ContactModelLocator implements ModelLocator
{
// Private Variables
private static var modelLocator : ContactModelLocator;
// Public Variables
public static var contacts : ArrayCollection = new ArrayCollection();
// Singleton Implementation
public static function getInstance() : ContactModelLocator
{
if ( modelLocator == null )
modelLocator = new ContactModelLocator();
return modelLocator;
}
// Intialize State
public static function initialize() {
var contact:Object = new Object();
contact.firstname = "John";
contact.lastname = "Doe";
contacts.addItem(contact);
var contact:Object = new Object();
contact.firstname = "Jimmy";
contact.lastname = "Hoffa";
contacts.addItem(contact);
}
}
}
Ok, so I added a bit of code.
First, a new import line was added to allow use to use an ArrayCollection, a type of array that binds nicely.
Then, we added a new public, static variable named contacts, initializing it to an empty ArrayCollection.
Last, we added a static initialize() method (methods can be static too!) that sets up initial state of our application's model, adding a few contacts. Hey, look, it's Jimmy Hoffa! Our ModelLocator is good!
Now, we need to set up our application to initialize the ContactModelLocator as soon as the application is started.
We do this by changing a our CG4CF.mxml file. We're going to remove the import statement we added yesterday, and replace it with an import statement that brings in our ContactModelLocator.
Then (code follows in a moment), we add a private function "onCreationComplete" that calls the static initialize() function on the ContactModelLocator.
Last, we instruct our application to run this function as soon as it's done creating itself by adding telling the <mx:Application> tag to run the onCreationComplete function when creation is, well, complete.
Here's the code:
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
layout="absolute"
creationComplete="onCreationComplete()"
>
<mx:Script>
<![CDATA[
import com.firemoss.cg4cf.model.ContactModelLocator;
private function onCreationComplete():void
{
ContactModelLocator.initialize();
}
]]>
</mx:Script>
</mx:Application>
Ok - we've got a container for our data (ContactModelLocator), and it's initialized when the application starts. Now, we need to create some views to look at the data.
Adding a View
First, we'll create a simple MXML component that just shows how many contacts are in the list.
To do this, create a new folder under /com/firemoss/cg4cf named "view". Click this folder, and do File -> New -> MXML Component. Name it "ContactCount" and click Finish.
The follow code can be pasted in. Explanation follows, but try giving it a read for yourself first:
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300">
<mx:Script>
<![CDATA[
import com.firemoss.cg4cf.model.ContactModelLocator;
]]>
</mx:Script>
<mx:Text id="contactCount" text="{ContactModelLocator.contacts.length}" />
</mx:Canvas>
First, the <mx:Canvas tag acts as a "root" node in the XML.
Then, we declare a script block, and import our ContactModelLocator.
Last, a simple <mx:Text> tag binds its "text" attribute (what text to display) to the length of the contacts ArrayCollection in the ModelLocator.
See, no need to pass in data through attributes! It just looks up the data by name ("contacts"), and data binding does the rest.
Now, we need to add this component to our CG4CF.mxml file. To bring in components in another directory, we need to add a (scary phrase) XML namespace. It's simply the addition of a new "xmlns:" attribute to the <mx:Application> tag, where the value uses dots instead of slashes to describe from what directory to find components, and the * on the end states that all files should be imported.
Ok, it's easier to illustrate. Here's the code:
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
xmlns:view="com.firemoss.cg4cf.view.*"
layout="absolute"
creationComplete="onCreationComplete()"
>
<mx:Script>
<![CDATA[
import com.firemoss.cg4cf.model.ContactModelLocator;
private function onCreationComplete():void
{
ContactModelLocator.initialize();
}
]]>
</mx:Script>
<view:ContactCount />
</mx:Application>
Ok, paste in the code, and click save. If you don't get any compiler errors, you should be able to click the run button and get a gray screen with the number "2" displaying in the upper-left. That's our Text tag, automagically showing us the current length of the contact list. If we added a new contact (which we'll do in another edition of this series), we'd see the number increment automatically. That's the beauty of binding.
Adding Another View
Now, we'll add a quick DataGrid to show contacts. Copy/paste this code into ContactGrid.mxml in the "view" directory:
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="400" height="300">
<mx:Script>
<![CDATA[
import com.firemoss.cg4cf.model.ContactModelLocator;
]]>
</mx:Script>
<mx:DataGrid width="100%" height="100%" dataProvider="{ContactModelLocator.contacts}" />
</mx:Canvas>
Quiz Time
At this point, you should be able to read this code and know what it'll do and how it'll do it. If not, re-examine creating the ContactCount view.
Ok, last bit for today: adding the grid to the CG4CF.mxml file. It's a breeze: no data to pass, nothing fancy to do. Just this line, probably right below ContactCount tag:
Now, if you compile and run your application, you should have a grid of contacts. Nice, eh?
Conclusion
Because the interface of a Flex (and Flash) application is, at heart, a hierarchy of UI controls nested within one another, it's tempting and seems easy to pass data amongst components via tag attributes. However, this leads to brittle architecture: views are hard to rearrage. It's also painful to work with: you have to know, at every level, what data needs to get passed further "down the pipe."
By using the Model Locator pattern, we remove the location of our application's data from this hierarchy, giving all components an external resource, the ModelLocator, that can be used to locate and bind to data.
Next Time
Tomorrow's edition will be a lot shorter. We're going to learn the Value Object pattern, and see how combining it, along with the Model Locator pattern and data binding, make it very, very easy to manipulate the data within our application while controls using that data automatically update.
Comment 1 written by Dave Carabetta on 7 November 2006, at 10:24 PM
Comment 2 written by Joe Rinehart on 8 November 2006, at 6:43 AM
Comment 3 written by Rob on 8 November 2006, at 1:40 PM
I'm begining to understand how Cairngorm works. Looking forward for the next part of the series.
Note:
When copying the ContactModelLocator code, the public static funtion needs to be un-commented, otherwise you will get an error.
Other then that, everything is very clear.
Change this:
// Intialize State public static function
to this:
// Intialize State
public static function initialize() {
~Cheers
Comment 4 written by Thomas on 9 November 2006, at 3:36 AM
1008: return value for function 'initialize' has no type declaration.
3596: Duplicate variable definition.
For the following:
public static function initialize() {
var contact:Object = new Object();
contact.firstname = "John";
contact.lastname = "Doe";
contacts.addItem(contact);
var contact:Object = new Object();
contact.firstname = "Jimmy";
contact.lastname = "Hoffa";
contacts.addItem(contact);
}
Comment 5 written by Thomas on 9 November 2006, at 3:42 AM
Comment 6 written by Joe Rinehart on 9 November 2006, at 7:00 AM
Still, thanks for pointing them out. I'll try to avoid code that causes them in the future.
Comment 7 written by Kevin Hoyt on 9 November 2006, at 11:03 AM
FYI, another warning that's presented is in binding to ContactModelLocator.contacts and ContactModelLocator.contacts.length. The compiler says that binding will not be able to detect assignments to contacts. Contacts as a static member can't be marked [Bindable]. This leaves me wondering if CG developers simply ignore those types of warnings, or if something is coming in a future article that addresses this problem?
Another unrelated warning I got was that ContactModelLocator.initialize() doesn't have a return type. I added "void" and it seems to work just fine. FYI.
Thanks for the write-up - it's amazing work!
Kevin
Comment 8 written by Steve House on 4 December 2006, at 3:36 PM
a) fire off a separate command to look up the data from each table (directly call 6 commands)
b) fire off a series of 6 separate events to tell 6 other commands to lookup the data from each table
c) call all the services (BusinessDelegates actually) to load all the data, and if so, how do I call more than one BusinessDelegate in a single command and handle multiple responses
Any help would be appreciated. Thanks
Steve House
[Add Comment] [Subscribe to Comments]