Welcome to the quickstart! We're going to build a small application, and we'll be doing it with Model-Glue. No backend server is required: we're going to "pretend" our service layer exists by using classes called Business Delegates.
When this tutorial starts a path with "/", such as "/examples/quickstart," the "/" refers to the directory containing the Model-Glue distribution (the one that contains README).
A completed and runnable version of what we're about to build is located in /examples/quickstart.
Let's go.
To create a Model-Glue application, there's a little bit of plumbing to do. Model-Glue comes with an "application template" that you can use to get up and running quickly. It's meant for use with Flex Builder (as is this quickstart), but it shouldn't be hard to use with a text editor and the command line compiler.
First, create a Flex project as you normally would. I created one called "ModelGlueQuickstart."
Next, copy the contents of the /applicationtemplate directory into your new project. It's OK to overwrite /bin.
Third, copy and paste the contents of ApplicationTemplate.mxml into your applications main mxml file (for me, that's ModelGlueQuickstart.mxml). Once that's done, you can delete ApplicationTemplate.mxml and save your main mxml file. It's OK to get a compiler error at this point.
Last, we need to link your application to the Model-Glue library. Right click your project and select "Properties." Drill into Flex Build Path (in the left nav) -> Library Path (top tab in the right pane). Choose "Add SWC" and navigate to the ModelGlue.swc file that's in /modelglue/bin/ModelGlueFlex.swc. Once you've selected the SWC, choose OK to exit the properties editor. Your application should now compile successfully. If you run it, you'll see that there's a clickable button that informs you Model-Glue is "Up and Running" when it's clicked.
We're going to want people to log in and out of our application, so we need a login form. Instead of copying / pasting all the code for this from this quickstart, go ahead and copy the /examples/quickstart/base/model, /examples/quickstart/base/view, and /examples/quickstart/base/business directories into your application.
Once you've copied them, open up your main mxml file. In place of the Panel tag that's in there by default, paste in the following code:
<view:Login />
Ok, we've got a login form. As it is, it runs its "doLogin" function when the form is submitted.
Now, we need the function do something. We'll add a private function to the /view/Login.mxml file that states that a user is requesting to login: this is an architectural event, not a UI event, so we create a ModelGlueEvent instance with the name of "user.requestsLogin" and dispatch it. We set this function as the click handler for the login button. Our script block should now read:
<mx:Script>
<![CDATA[
import com.firemoss.modelglue.event.ModelGlueEvent;
private function doLogin():void {
new ModelGlueEvent("user.requestsLogin").dispatch();
}
]]>
</mx:Script>
We can now run our app. If we click the button, we'll get an exception stating that no event named "user.requestsLogin" exists. That makes sense: we haven't yet defined it.
Model-Glue works via a system of architectural Event Handlers. You define events in a central file (/config/ModelGlueConfiguration.mxml by default). When an event is dispatched, a series of "messages" are "broadcast." Controllers then "listen" for these messages and run functions that have been associated with the message in the ModelGlueConfiguration.mxml file. These functions are known as "listener functions."
Ok, it's probably easier to demonstrate. It's not a complicated system at all.
First, we'll create a controller to handle security. This should go into a file name SecurityController.as in your /control directory:
package control
{
import com.firemoss.modelglue.controller.ModelGlueController;
import com.firemoss.modelglue.event.ModelGlueEvent;
import mx.controls.Alert;
public class SecurityController extends ModelGlueController
{
public function authenticateUser(e:ModelGlueEvent):void {
Alert.show("Trying to log in.", "Logging in!");
}
}
}
We've created one function that's eligible to be a listener function: it takes a single argument of type ModelGlueEvent and returns nothing.
Now, let's tell ModelGlue that this controller should be loaded when the application starts and that it should associated the authenticateUser listener function with the userAuthenticationNeeded message. Add the following XML to the ModelGlueConfiguration directory, replacing the existing <control:Controller /> tag. Your <controllers /> block should now look like this:
<controllers> <control:SecurityController id="securityController" /> </controllers>
To register the authenticateUser function to listen for the userAuthenticationNeeded message, we populate the messageListeners property (an array) of the SecurityController with MessageListener instances:
<controllers>
<control:SecurityController id="securityController">
<control:messageListeners>
<MessageListener message="userAuthenticationNeeded" method="{securityController.authenticateUser}" />
</control:messageListeners>
</control:SecurityController>
</controllers>
Ok, we're on the home stretch! Now, we create an Event Handler named "user.requestsLogin" and tell it to broadcast the "userAuthenticationNeeded" message. We'll do this by replacing the existing <event:Handler> tag in ModelGlueConfiguration.mxml with a new event handler. Our <eventHandlers /> block now looks like this:
<eventHandlers> <event:Handler name="user.requestsLogin"> <event:broadcasts> <event:Message name="userAuthenticationNeeded" /> </event:broadcasts> </event:Handler> </eventHandlers>
Once the application compiles, try running it: when you click the login button, the event is created and dispatched. The "userAuthenticationNeeded" message is broadcast, and the authenticateUser function in SecurityController runs as a result.
We now need to model a user and attempt to perform the user's login. The model directory already contains a value object that can represent a user (UserVO). We'll start by modifying doLogin() in the Login form (Login.mxml) to populate a UserVO and add it to the event's data. Code for the new doLogin() function is listed below:
private function doLogin():void {
var user:UserVO = new UserVO();
user.username = username.text;
user.password = password.text;
var event:ModelGlueEvent = new ModelGlueEvent("user.requestsLogin");
event.setValue("user", user);
event.dispatch();
}
In our listener function, we can now ask for a value named "user" and show a bit about it:
public function authenticateUser(e:ModelGlueEvent):void {
var user:UserVO = e.getValue("user");
Alert.show("Trying to log in user: " + user.username, "Logging in!");
}
The strictly typed / compiled language crowd is probably now cringing over the user of string literals ("user") instead of properties. We'll clean this up later.
The controller itself has no business performing the actual authentication. That's something that'd better be handled by a server-side operation, whether it's over Flash Remoting, XML, SOAP, or whatever RPC format is in vogue next week. To hide the implementation of server-side operations from our application, we use a design pattern known as Business Delegate. All the delegate does is wrap all of the functions the target service provides. Unlike other Flex frameworks, Model-Glue's BusinessDelegate implementation is also set up, by default, to handle responses and faults sent back from the server side.
OO ColdFusion developers might call the delegates a Service or Service Facade. It's sort of like that, but delegate is more appropriate in Flex.
Because delegates serve to hide an implementation, they're best defined as an interface that's then implemented specifically for your server technology. In our case, we define an interface called IAuthentictationDelegate in the business package, and we implement it with a MockAuthenticationDelegate that'll "pretend" to act like a server. It has a single method, "authenticate" to which we can pass a user and a callback function to call when a result is returned.
We won't cover the implementation of MockAuthenticationDelegate in detail: it's not important. All we need to know is that we pass authenticate a user and a callback function, and that it'll call a callback function, passing a UserVO back as a single argument. (One other thing: ValidUser / Password is the only user / password combination it'll consider valid!.)
Let's hook our controller up to the delegate and ask for authentications to be performed. We modifiy the authenticateUser() listener function to ask the delegate to perform authentication, using the new userAuthenticationResult function as a callback for when the delegate's done doing its business (pun intended).
public function authenticateUser(e:ModelGlueEvent):void {
var user:UserVO = e.getValue("user");
var del:IAuthenticationDelegate = new MockAuthenticationDelegate();
del.authenticate(user, userAuthenticationResult);
}
public function userAuthenticationResult(user:UserVO):void {
Alert.show("Logged in: " + user.authenticated);
}
We'll now show the username when someone has logged in (blank if you don't log in with "ValidUser" and "Password", case-sensitive!!).
When you build simple Flex applications, your UI components generally hold all of your data. As your app grows and UI components need to refer to the same data, it's brittle to pass data as attributes of tags through a tree. A better option is to use a controller-layer class to store data. (Many Flex / Flash developers call this "model location" and implement it with ModelLocators.)
In Model-Glue, each of your controllers can act as a container of data to which you can bind from any UI component. All you have to do is ask for the controller and perform your binding to its properties.
We'll do this now by modifying our app to show a message showing the current username.
First, let's store the current user in our SecurityController:
[Bindable] public var currentUser:UserVO = new UserVO();
Second, we'll modify the userAuthenticationResult() function to update the current user:
public function userAuthenticationResult(user:UserVO):void {
this.currentUser = user;
}
Last, we'll add the instance of the SecurityController contained inside Model-Glue as a property of our overall application. Open up ModelGlueQuickstart.mxml and replace it script block with the following:
<mx:Script>
<![CDATA[
import com.firemoss.modelglue.event.ModelGlueEvent;
import com.firemoss.modelglue.ModelGlueFramework;
import control.SecurityController;
[Bindable]
private var securityController:SecurityController;
private function creationComplete():void {
this.securityController = ModelGlueFramework.getController("securityController") as SecurityController;
}
]]>
</mx:Script>
Add creationComplete() as a handler for the creationComplete event on the application tag:
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:config="config.*" xmlns:view="view.*" creationComplete="creationComplete()" >
Add a Label tag beneath the Login tag, and we'll be able to see who the current user is:
<mx:Label text="Current User: {securityController.currentUser.username}" />
package control
{
import com.firemoss.modelglue.controller.ModelGlueController;
import com.firemoss.modelglue.event.ModelGlueEvent;
public class EnvironmentController extends ModelGlueController
{
[Bindable]
public var applicationStatus:String;
public function updateApplicationStatus(e:ModelGlueEvent):void {
this.applicationStatus = e.getArgument("status", "");
}
}
}
Next, we'll set up ModelGlueQuickstart.mxml to show this in the same way we set it up to show the current user:
<mx:Script>
<![CDATA[
import com.firemoss.modelglue.event.ModelGlueEvent;
import com.firemoss.modelglue.ModelGlueFramework;
import control.SecurityController;
import control.EnvironmentController;
[Bindable]
private var securityController:SecurityController;
[Bindable]
private var environmentController:EnvironmentController;
private function creationComplete():void {
this.securityController = ModelGlueFramework.getController("securityController") as SecurityController;
this.environmentController = ModelGlueFramework.getController("environmentController") as EnvironmentController;
}
]]>
</mx:Script>
And below our current user label:
<mx:Label text="Status: {environmentController.applicationStatus}" />
Next, we'll set up our EnvironmentController in ModelGlueConfiguration.mxml, registering its listener function with the "applicationStatusChanged" message:
<controllers>
<control:SecurityController id="securityController">
<control:messageListeners>
<MessageListener message="userAuthenticationNeeded" method="{securityController.authenticateUser}" />
</control:messageListeners>
</control:SecurityController>
<control:EnvironmentController id="environmentController">
<control:messageListeners>
<MessageListener message="applicationStatusChanged" method="{environmentController.updateApplicationStatus}" />
</control:messageListeners>
</control:EnvironmentController>
</controllers>
Now, whenever we want to update the status message, we can send it as an Argument of the appropriate message broadcast. Here's a modified user.requestsLogin Event Handler that does just that:
<event:Handler name="user.requestsLogin"> <event:broadcasts> <event:Message name="applicationStatusChanged"> <event:arguments> <event:Argument name="status" value="Logging In" /> </event:arguments> </event:Message> <event:Message name="userAuthenticationNeeded" /> </event:broadcasts> </event:Handler>
You'll quickly notice a problem when this runs: the status updates to "Logging In," but is never set back. We'll address this in the next section.
When we created our login functionality, we used a callback function to receive a result from a server. This created a few dependencies, one of which was on the architecture of asynchronous operations and callbacks.
As an alternative, we can choose to view a Model-Glue application as a Hub-and-Spoke messaging architecture. In such an architecture, the Model-Glue framework sits in the "center". Each view, business delegate, or LiveCycle Data Service consumer (or class wrapping the consumer) acts as a "spoke," knowing only about the Hub and no other spokes. While your Model-Glue controllers may communicate directly with the spokes (example: calling authenticate() on a business delegate), a clean Hub-and-Spoke architecture only allows the spokes to communicate with the hub through messages. In Model-Glue, these messages are ModelGlueEvent instances (or subclasses of ModelGlueEvent).
Currently, we're breaking the Hub-and-Spoke rule: our IAuthenticationDelegate (a Spoke) relies on using a callback function to communicate the results of a login request back to the Controller (the Hub). It's a simplified example, but this has made clearing our "Logging In" status message a bit of an issue.
Now, we'll switch to using pure Hub-and-Spoke. We'll create a base AuthenticationDelegate implementation that uses ModelGlueEvents to communicate a response from the server back into the hub via a Model-Glue Event. Once that's done, we'll show how this allows for extension or modification of behavior without altering existing code.
First, we'll change the signature of IAuthenticationDelegate's authenticate() method, removing the callback:
function authenticate(user:UserVO):void;
Next, we'll create AbstractAuthenticationDelegate in /business. We'll use a shortcut parameter of ModelGlueEvent's constructor to pass values in to the event without calling setValue() for each:
package business
{
import model.UserVO;
import com.firemoss.modelglue.event.ModelGlueEvent;
public class AbstractAuthenticationDelegate
{
protected function authenticationSucceeded(user:UserVO):void {
new ModelGlueEvent("user.loginSucceeded", {user:user}).dispatch();
}
protected function authenticationFailed(user:UserVO):void {
new ModelGlueEvent("user.loginFailed", {user:user}).dispatch();
}
}
}
Third, we'll have our MockAuthenticationDelegate extend AbstractAuthenticationDelegate, getting rid of the callback bits:
package business
{
import model.UserVO;
public class MockAuthenticationDelegate extends AbstractAuthenticationDelegate implements IAuthenticationDelegate
{
public function authenticate(user:UserVO):void
{
if (user.username == "ValidUser" && user.password == "Password") {
user.userId = 1;
user.authenticated = true;
this.authenticationSucceeded(user);
} else {
user = new UserVO();
this.authenticationFailed(user);
}
}
}
}
Last, we'll add handlers for the user.loginSucceeded and user.loginFailed events:
<event:Handler name="user.loginSucceeded"> <event:broadcasts> <event:Message name="userAuthenticationSucceeded" /> <event:Message name="applicationStatusChanged"> <event:arguments> <event:Argument name="status" value="Login successful!" /> </event:arguments> </event:Message> </event:broadcasts> </event:Handler> <event:Handler name="user.loginFailed"> <event:broadcasts> <event:Message name="applicationStatusChanged"> <event:arguments> <event:Argument name="status" value="Login failed!" /> </event:arguments> </event:Message> </event:broadcasts> </event:Handler>
In a nutshell, the advantage of Hub-and-Spoke versus callbacks and responders is that it allows unrelated parts of the application to be informed of architectural events. You'll notice that we added a message named "userAuthenticationSucceeded" to the user.loginSucceeded Event Handler. While we're not using this now, it allows any controller, view, or just about any other interested class to subscribe to this message.
Let's have a little fun and make our login form disappear whenever a login is successful. We could do this by observing the state of the model or binding controls like viewstack to a workflow constant (which I think is really messy). In this case, it's good to have an example of how to create listener function in views as well as Controllers. First, we'll wrap our login form in a viewstack:
<mx:ViewStack id="mainView" width="700" height="400"> <view:Login /> <mx:Panel id="homeView" title="Home"> <mx:Label text="You've logged in! It's up to you to write events to log out!" /> </mx:Panel> </mx:ViewStack>Next, we'll write a function called loginSucceeded() in our main mxml file and subscribe it to the desired message:
private function creationComplete():void {
this.securityController = ModelGlueFramework.getController("securityController") as SecurityController;
this.environmentController = ModelGlueFramework.getController("environmentController") as EnvironmentController;
ModelGlueFramework.subscribe("userAuthenticationSucceeded", this, this.loginSucceeded);
}
private function loginSucceeded(e:ModelGlueEvent):void {
mainView.selectedChild = this.homeView;
}
That's all there is to it!
Right now, we've got "magic words" like "userAuthenticationSucceeded" floating all over our application. They're ripe for mistyping. With Flex, we can add compile-time checking for typos.
Fortunately, it's easy to clean up with some static constants in ModelGlueConfiguration.mxml. Ordinarily, they'd be best defined as you wrote the app. In this case, we're implementing them after the fact, so we'll only do a few for example.
Let's define a few examples, showing how we can use string constants to replace event and message names by adding a script block to ModelGlueConfiguration.mxml:
<mx:Script> <![CDATA[ public static const EVENT_USER_REQUESTS_LOGIN:String = "EVENT_USER_REQUESTS_LOGIN"; public static const MESSAGE_USER_AUTH_NEEDED:String = "MESSAGE_USER_AUTH_NEEDED"; ]]> </mx:Script>
Now that we've got constants, we can replace references to string literals in ModelGlueConfiguration.mxml:
<control:SecurityController id="securityController">
<control:messageListeners>
<MessageListener message="{ModelGlueConfiguration.MESSAGE_USER_AUTH_NEEDED}" method="{securityController.authenticateUser}" />
</control:messageListeners>
</control:SecurityController>
And:
<event:Handler name="{ModelGlueConfiguration.EVENT_USER_REQUESTS_LOGIN}">
<event:broadcasts>
<event:Message name="applicationStatusChanged">
<event:arguments>
<event:Argument name="status" value="Logging In" />
</event:arguments>
</event:Message>
<event:Message name="{ModelGlueConfiguration.MESSAGE_USER_AUTH_NEEDED}" />
</event:broadcasts>
</event:Handler>
We can do the same anywhere else we use a string literal, such as the Login form:
var event:ModelGlueEvent = new ModelGlueEvent(ModelGlueConfiguration.EVENT_USER_REQUESTS_LOGIN);
As it is, our listener functions rely on values of certain names, like "user", being in the event's data.
This is probably OK for many applications, but there are situations where we may want strict, compile-time checking of such things.
For instance, we may want an AuthenticationEvent that always contains a user. Also, listener functions interested in authentication events can then require that the event they received is of this type.
We can do this very easily by creating a subclass of ModelGlueEvent named AuthenticationEvent in the /control/event directory:
package control.event
{
import com.firemoss.modelglue.event.ModelGlueEvent;
import model.UserVO;
import config.ModelGlueConfiguration;
public class AuthenticationEvent extends ModelGlueEvent
{
public var user:UserVO;
public function AuthenticationEvent(name:String, user:UserVO) {
this.user = user;
}
}
}
Now, we create and dispatch an AuthenticationEvent in our Login form instead of ModelGlueEvent:
private function doLogin():void {
var user:UserVO = new UserVO();
user.username = username.text;
user.password = password.text;
new AuthenticationEvent(ModelGlueConfiguration.EVENT_USER_REQUESTS_LOGIN, user).dispatch();
}
Last, we can update all listener functions interested in authentication events to require the typed event and use its user property, such as authenticateUser() in SecurityController:
public function authenticateUser(e:AuthenticationEvent):void {
var user:UserVO = e.user;
var del:IAuthenticationDelegate = new MockAuthenticationDelegate();
del.authenticate(user);
}