Like all software, Flex applications are complex. This requires that developers building Flex applications are able to solve architectural and design problems rather than simply throwing together a bunch of off-the-shelf components. Using a microarchitecture as described in the preceding section can help in this regard. However, when using a microarchitecture you can solve only the problems that the microarchitecture is specifically designed to address. If a problem falls outside that scope, the problem remains unsolved. In this manner, it’s perhaps more useful to at least understand some of the common patterns that are used to solve problems. You can then utilize these patterns either as part of or apart from a microarchitecture.
Many papers, articles, and books have been written about design and architectural patterns used in software development. The patterns that you could use when building a Flex application are too numerous to list and detail in this chapter. However, a few patterns are common enough that we would be remiss not to describe them. Notably, nearly every Flex application could profit from the use of the Model-View-Controller pattern and the Business Delegate pattern, both of which we’ll discuss in more detail in the following sections. Additionally, we’ll start by looking at a pattern for dealing with browser integration, which is a problem that is unique to web-based applications.
Shortly we’ll look at much of the code in more detail. However,
before we do so, you’ll need to understand how the FlickrFlex
application handles browser integration (deep linking and the back and
forward buttons). The principle is rather simple: all significant state
change requests are routed through the BrowserManager
class by
updating the address fragment. For example, when the user clicks on a
Search button, the FlickrFlex application does not directly change the
state to the search results screen or immediately make the request to
the search method on the Flickr service. Instead, the application
updates the address fragment via BrowserManager
and waits for BrowserManager
to dispatch an event notifying
the application to update as necessary. We’ll see a few specific
examples of this later on. However, the basic way in which the
application manages this behavior depends on a simple infrastructure
provided by a few methods in a few of the .mxml files, which we’ll look at now.
The main application .mxml
file is Flickr.mxml. This file
contains little more than a Style
tag
and an instance of FlickrController
.
The FlickrController
class is where
we start to set up browser integration. Within the initializeHandler()
method, we tell the
application to listen for changes to the URL via the BrowserManager
:
_browserManager.addEventListener(BrowserChangeEvent.APPLICATION_URL_CHANGE, urlChangeHandler); _browserManager.addEventListener(BrowserChangeEvent.BROWSER_URL_CHANGE, urlChangeHandler);
When the URL changes, the first thing we do is parse the fragment into an array, using a forward slash as a delimiter:
var fragments:Array = _browserManager.fragment.split("/");
We then remove the first element from the array and use that to
determine what state to set on the view. If, for example, the fragment is search/nature
(which is what the fragment
would be if the user runs a search using the term
nature), the first element in the array will be
search
, which means we want to tell
the view to change to the search screen. In every case, we pass along
the remaining fragment to the view:
var topFragment:String = fragments.shift(); if(topFragment == "search") { view.setMode(FlickrView.MODE_SEARCH_SCREEN, fragments.join("/")); } else if(topFragment == "") { view.setMode(FlickrView.MODE_HOME, fragments.join("/")); } else if(topFragment == "details") { view.setMode(FlickrView.MODE_PHOTO_DETAILS, fragments.join("/")); }
Within FlickrView
’s setMode()
method, we change the state and then
assign the remaining fragment to the screenFragment
property of the corresponding
screen if appropriate:
public function setMode(mode:String, fragment:String = null):void { currentState = mode; if(fragment != null && fragment.length > 0) { if(mode == MODE_SEARCH_SCREEN) { searchScreen.screenFragment = fragment; } else if(mode == MODE_PHOTO_DETAILS) { photoDetailsScreen.screenFragment = fragment; } } }
If the entire fragment was search/nature
, the setMode()
method would change the state to the
search screen state and assign nature
to the screenFragment
property of
searchScreen
. Later in the chapter,
when we talk about views and controllers, we’ll see how the search screen handles the fragment.
However, at this point the specifics of how fragments are ultimately
utilized in specific views and controllers aren’t as important as the
overall pattern. To summarize and restate: for FlickrFlex we have a
hierarchical structure of objects (FlickrController
contains FlickrView
, FlickrView
contains HomeController
, etc.). FlickrController
listens for URL changes, and
when the fragment changes, FlickrController
starts to trickle down the
fragment updates by notifying FlickrView
. FlickrView
uses the fragment to decide how to
change state, and it then passes along the remainder of the fragment to
the controller corresponding to the new state. This pattern could
theoretically be continued for as many levels of hierarchy that
exist.
The majority of Flex applications utilize a service of one sort or another, whether the services are SOAP services, REST services, AMF (Remoting) services, or any other sort. Elements of the Flex application need to interact with these services. For example, a user may request from a service a list of states or provinces for a given country, or a user may fill out a form in Flex and then submit the form to a service method.
In a naïve approach, the actual implementation that a Flex
application uses to interact with a service may be exposed directly to
all elements within the Flex application. For example, if a part of the
application needs to query a SOAP service for a list of states or
provinces, it is possible to allow that part of the application to
create an instance of the WebService
component with the appropriate operation, and to bind the results of the
operation directly to a UI component. This may work, but it is a
shortsighted solution that creates fragility.
Instead of the aforementioned solution, you can use a common pattern known as the Business Delegate pattern. A business delegate is a class that serves as a proxy to a remote service, defining an API that the rest of the application can use without having to know anything about the implementation details. That way, the rest of the application only has to know about its contract with the business delegate. If the implementation details change, the rest of the application doesn’t have to change as long as the business delegate API remains the same. For example, an application may initially utilize a set of SOAP web services. Later the services may be migrated over to use Remoting (AMF) instead. If the application doesn’t use a business delegate, many parts of the application may need updating to work with the new services. However, if the application uses a business delegate, only the business delegate needs to change.
FlickrFlex uses the Business Delegate pattern. In this
application, we define a class called com.oreilly.pf3.flickr.services.FlickrService
.
This class allows us to define a simple API for the rest of the
FlickrFlex application to use without having to know anything about the
implementation details. The FlickrService
class defines only three
methods: searchPhotosByText()
,
getPhotoDetails()
, and getPhotoSizes()
. The rest of the application
can call these methods without having to know whether FlickrFlex is
actually making requests to local XML files or a remote service.
However, it turns out that in this example we are going to have the
application connect to a remote service.
In the FlickrFlex application, we are connecting to only one service: the Flickr service. We don’t want to get too bogged down in the details of the Flickr service since our primary focus in this chapter is to better understand common Flex application design and development problems and solutions. However, to make sense of the code for the sample application we’ll need to explain just a few things about working with the Flickr service.
For the purposes of the sample application, we’re necessarily using only a small subset of the Flickr API. We’re calling only three Flickr service methods: one to search photos, one to get information about a specific photo, and one to get the different sizes of a specific photo.
You can always read more about the Flickr API, including all the available methods and their signatures, at http://www.flickr.com/services/api.
The Flickr service uses what is known as
representational state transfer, more commonly known as REST. REST uses URLs to access resources. The Flickr service
allows us to call methods via resources. In the case of the sample
application, there are three methods that we relate to business delegate
methods: flickr.photos.search
,
flickr.photos.getInfo
, and flickr.photos.getSizes
. Next we need to look
at how to access these methods.
You must use the same pattern to access any Flickr resources. You
must make an HTTP request to the REST resource along with a query string
indicating the method and parameters. The REST resource is http://api.flickr.com/services/rest/. Therefore, you can
see that in FlickrService
we define a
constant called REST_URL
as
follows:
static private const REST_URL:String = "http://api.flickr.com/services/rest/?";
The trailing ?
is there to
precede the requisite query string that we will look at next.
The query string for Flickr service requests must always include
the method name along with the required parameters for that method. The
Flickr API documentation lists the required parameters for each method.
All methods require the API key. You can see that in FlickrService
we define three methods that
call REST service methods, each method calling a different service
method with different parameters, but each following the same basic
pattern. We’ll look at searchPhotosByText()
. This method calls the
flickr.photos.search
method, which
requires a text
parameter, which is a
comma-delimited list of words for which to search.
public function searchPhotosByText(text:String):PendingOperation { text = text.split(" ").join(","); var method:String = "flickr.photos.search"; var parameters:Array = new Array(); parameters.push(new NameValue("api_key", _apiKey)); parameters.push(new NameValue("method", method)); parameters.push(new NameValue("text", text)); var url:String = REST_URL + createQueryString(parameters); var pendingOperation:PendingOperation = new PendingOperation(url); return pendingOperation; }
You can see that this method creates an array of NameValue
objects, which are simply objects
with name and value properties, and then passes that array to createQueryString()
to create the query
string, which it appends to the REST URL.
If we look next at createQueryString()
, we can see that it simply
takes all the names and values and strings them together with =
and &
delimiters:
private function createQueryString(parameters:Array):String { var queryString:String = ""; var i:int; for(i = 0; i < parameters.length; i++) { queryString += parameters[i].name + "=" + parameters[i].value + "&"; } return queryString; }
The searchPhotosByText()
method
then returns a new PendingOperation
object, passing it the newly constructed URL. The PendingOperation
class simply makes an HTTP
request to the specified URL, and it proxies the response, dispatching
an event when the response is returned.
package com.oreilly.pf3.flickr.services { import flash.events.Event; import flash.events.EventDispatcher; import flash.events.HTTPStatusEvent; import flash.net.URLLoader; import flash.net.URLRequest; import mx.rpc.events.FaultEvent; import mx.rpc.events.ResultEvent; public class PendingOperation extends EventDispatcher { static public const RESULT:String = "result"; static public const ERROR:String = "error"; private var _loader:URLLoader; private var _vo:*; public function PendingOperation(url:String, vo:* = null) { _loader = new URLLoader(); _loader.addEventListener("complete", resultHandler); _loader.addEventListener(HTTPStatusEvent.HTTP_STATUS, errorHandler); _loader.load(new URLRequest(url)); _vo = vo; } private function resultHandler(event:Event):void { dispatchEvent(new ResultEvent(RESULT, false, true, ParsingUtility.parse(new XML(_loader.data), _vo))); } private function errorHandler(event:HTTPStatusEvent):void { dispatchEvent(new FaultEvent(ERROR)); } } }
You can see that the PendingOperation
class relies on a method of
the ParsingUtility
class. We’re using
this custom ParsingUtility
class to
determine what type of response the service has returned and convert it
to the correct type. For example, when the PendingOperation
object is handling the
response from a flickr.photos.search
method, the ParsingUtility
class
detects the response as such and converts the XML into an ArrayCollection
of Photo
objects, which is exactly what the rest
of the FlickrFlex application expects.
We’ve designed the FlickrService
and PendingOperation
classes in this way so that
the rest of the application needs to know nothing about the
implementation details. As far as the controllers and views and the rest
of the application are concerned, it makes no difference whether the
Flickr service is a REST service. If Flickr decided to change to an AMF
service at some point, we could adapt the application with little to no
changes to any of the code outside the services
package.
The Model-View-Controller (MVC) architectural pattern is utilized across many languages and platforms. If you have experience building software of any sort, chances are MVC is familiar to you. Volumes have been written about this pattern, and we won’t try to repeat much of what has already been said. Instead, we’re primarily concerned with the usefulness of the pattern in Flex applications and the nuances of how to implement it practically in Flex. However, if you aren’t already familiar with this pattern, we’ll outline it briefly.
The principle concern of MVC is to effectively separate the business logic and the user interface code such that they interact with one another only through well-defined programming interfaces. The idea is that this approach reduces fragility because you can make changes to one without having to necessarily make changes to the other.
As the name implies, this pattern typically uses three parts—model, view, and controller:
The model is the way in which data is represented programmatically.
The view is the user interface. Typically, a view does not directly modify the model it represents. Instead, it allows the controller to modify the model, and the view updates itself when the model changes. Additionally, usually a view does not call methods of its controller, nor does it even have a reference to a controller. Instead, a view usually merely dispatches events that a controller can handle.
The controller is the business logic. A controller typically has a reference to a model and a view, and it uses these references to modify the model and call methods of the view directly.
Although it’s possible to write models, views, and controllers in a 1:1:1 ratio, it’s also possible that a controller could be used with many different views and that a model could likewise be used by many different views and controllers.
You can implement the MVC pattern in many different ways at many different levels. At one level, it is possible to create one model and one controller for an entire application (which uses many different views). It is also possible to implement the pattern at a more granular level, creating controllers, models, and views for all components. How you implement the pattern will depend on the complexity of the application and your preferences as a developer. In the FlickrFlex application, we have opted for a more granular approach.
Next we’ll look at creating models, views, and controllers in Flex applications.
Although there are variations on how to implement a model in a
Flex application, those variations are relatively narrow in scope, and
for the most part Flex developers tend to build models in a very similar fashion. At the core of the
model implementation is something called a business
object (or sometimes more specifically a value
object). A business object is a type that represents entities
(logical groupings of data) within the business domain (application).
A value object is a more specific type of business object that has no
additional logic applied to it, and merely stores data. In the case of
FlickrFlex, we have only one
business object, a class called com.oreilly.pf3.flickr.model.data.Photo
.
The Photo
class is a fairly
standard implementation. It models the same basic data as is returned
by the various photo-related service methods, defining private
properties for all of them:
private var _id:String; private var _owner:String; private var _secret:String; private var _server:String; private var _farm:String; private var _title:String; private var _thumbnailUrl:String; private var _description:String; private var _tags:ArrayCollection; private var _imageMediumUrl:String; private var _imageLargeUrl:String;
The class then defines accessor methods for most of the properties, as in the following:
public function get id():String { return _id; }
Also, a few properties can be updated after the initial creation
of the object. Therefore, we create mutator methods as well, and
because we want user interface elements (views) to be able to update
themselves based on the changes, we set the properties as bindable.
The description
property is an
example:
[Bindable(event="descriptionChanged")] public function set description(value:String):void { _description = value; dispatchEvent(new Event("descriptionChanged")); } public function get description():String { return _description; }
Various developers have different perspectives on how a business
object should be created. Some developers are of the opinion that
business objects should not know anything about the manner in which
they are created. Other developers prefer to make business objects
responsible for creating themselves, given specific input. For
Photo
, we’ve opted for the latter
approach, creating a static parseFromXml()
method that accepts the
return XML from a Flickr photo search, and constructs and returns a
corresponding Photo
object:
static public function parseFromXml(xml:XML):Photo { var id:String = xml.@id; var owner:String = xml.@owner; var secret:String = xml.@secret; var server:String = xml.@server; var farm:String = xml.@farm; var title:String = xml.@title; return new Photo(id, owner, secret, server, farm, title); }
Frequently, applications require complex models, and it can be
useful to maintain an application-wide reference to the models used by
various parts of the application. For this purpose, a model locator
can be a valuable part of the model. The model locator is typically
implemented using the Singleton design pattern, and it stores
references to various models in use. For the FlickrFlex application,
we’ve defined a model locator called com.oreilly.pf3.flickr.model.ApplicationModel
.
The model locator for FlickrFlex is an application-wide repository for stateful model data, including the following:
The current search term
The current search results
The selected photo
The index of the selected photo in the search results
The implementation of controllers within Flex applications differs among developers much more widely than does the implementation of models. As we indicated earlier, it is possible to build an application that uses one application-wide controller. This is an approach advocated by some microarchitectures such as MVCS. This is certainly a workable approach, and it has its advantages. However, one drawback of this approach is that the one controller is generalized, and the larger and more complex the application becomes the larger and more complex the controller becomes. Therefore, for many applications it is advantageous to use more granular controllers that are specific to components of the application. This is the approach we use with the FlickrFlex application.
Implementing controllers in Flex can be challenging because of the way Flex is designed. Typically, views are user interface elements and controllers are not. As a result, it may initially seem to make sense to have components create and maintain a reference to their own controller. However, this approach has serious drawbacks; most notably it reverses the typical relationship between views and controllers. A controller should provide the programmatic interface to a component, and it should be able to theoretically control any view that implements the interface the controller expects. By having a view construct a controller, these roles and behaviors are no longer possible.
With the FlickrFlex application we’ve taken a slightly
unconventional approach. We’ve decided to define controllers using
MXML, making controllers extend UIComponent
. This enables controllers to be
added as children of other containers, and it allows controllers to
then add views to themselves. If you look in Flickr.mxml, you’ll notice that it is very
simple. In this document, we create an instance of FlickrController
:
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
xmlns:views="com.oreilly.pf3.flickr.views.*" xmlns:controllers="com.oreilly.
pf3.flickr.
controllers.*" backgroundColor="#4F5050">
<mx:Style source="assets_embed/css/FlickrFlex.css"/>
<controllers:FlickrController/>
</mx:Application>
In FlickrController
we create
only one component instance: an instance of FlickrView
. FlickrController
is the control for FlickrView
.
Inside FlickrView
you can see that in the various states we create
instances of different controllers: HomeController
, SearchController
, and PhotoDetailsController
. In each controller
document we create just one component instance: HomeScreen
, SearchScreen
, and PhotoDetailsScreen
, respectively. If you
look at the code for these views, you’ll notice that they don’t
reference the controllers. Instead, if any communication is necessary
with the controllers, it is achieved by dispatching events.
Next we’ll look specifically at the SearchScreen
and SearchController
to better understand how
these two classes are designed and how they work both together and in
the context of the application.
In the com.oreilly.pf3.flickr.views.screens
package
you’ll see SearchScreen.mxml, the
view for the search form/search results in the FlickrFlex application.
SearchScreen
is fairly
representative of the rest of the views in the application and how we
typically build views in general. The key points to notice about
SearchScreen
are as follows:
The view defines an API to grant access to anything related
to its own state, including changes to components within it.
You’ll notice that SearchScreen
defines getters and/or setters for the following: dataProvider
, searchTerm
, isSearching
, and selectedPhoto
. We’ll talk more about
each in just a moment.
The view does not have any reference to a controller. You
can see that SearchScreen
doesn’t know anything about SearchController
. However, there are a
few instances where SearchScreen
needs to communicate with
its controller. This is achieved through dispatching events. You
can see that when the user clicks on an item in the tile list the
view dispatches an event, and it dispatches a different event when
the user clicks on the Search button.
The SearchScreen
component,
as with all views, is primarily responsible for user interface layout
and for relaying user interaction to a controller via events. The
SearchScreen
consists of a search
form (a text input and a button) and a tile list, as shown in the
following snippet from the code:
<mx:HBox width="100%" height="45" backgroundColor="#212122" verticalAlign="middle"> <mx:HBox styleName="green" height="100%"> <mx:Label text="tags" /> <mx:TextInput id="searchTermInput" text="{_searchTerm}" /> <mx:Spacer width="10" /> <mx:Button id="searchButton" label="Search for Photos" styleName="green" height="100%" click="dispatchEvent(new PhotoSearchEvent(searchTermInput.text));" /> </mx:HBox> </mx:HBox> <mx:TileList id="photoList" width="100%" height="100%" itemRenderer="com.oreilly.pf3.flickr.views.renderers.PhotoRenderer" dataProvider="{_dataProvider}" change="dispatchEvent(new PhotoSelectEvent(photoList.selectedItem.id));" />
You can see that in the two cases where there is user interactivity (the user clicking on the Search button or selecting an item from the tile list), the view merely dispatches an event.
As mentioned earlier, the view defines an API for reading and
writing values and/or state on the view. One of the key
getters/setters is dataProvider
,
which is fairly standard for most views. The dataProvider
getter/setter merely gets/sets
the bindable _dataProvider
property, which is an ArrayCollection
in the case of SearchScreen
. The tile list is bound to the
_dataProvider
property such that
whenever the property changes, the tile list reflects those changes.
We’ll look at when and how this property changes when we look at the
controller code in just a minute.
The other two getters/setters are searchTerm
and isSearching
. The searchTerm
getter/setter is used to get and
set the keyword(s) displayed in the search form, when applicable. The
isSearching
getter/setter is used
to change the state to disable the view when a search is running or
when the result is returned from the service.
The selectedPhoto
getter is
designed to return a reference to the Photo
object corresponding to the selected
tile list item. This provides a well-defined API such that the
controller can access that information without having to know anything
about how the view is implemented.
The SearchController
class is
designed to handle all the business logic for searching, displaying
results, and navigating to photo details. The SearchController
has an instance of SearchScreen
with an ID of view
:
<screens:SearchScreen id="view" photoSearch="photoSearchHandler(event);" photoSelect="photoSelectHandler(event);" />
When the photoSearch
or
photoSelect
event occurs
(dispatched by the view), the controller handles the event. In both
cases, the controller handles the event by updating the browser
address fragment using BrowserManager
:
private function photoSearchHandler(event:PhotoSearchEvent):void { BrowserManager.getInstance().setFragment("search/" + event.searchTerm); } private function photoSelectHandler(event:PhotoSelectEvent):void { BrowserManager.getInstance().setFragment("details/" + event.photoId); }
You’ll recall from earlier in the chapter that state changes are
often managed by changes to the browser address (via BrowserManager
). You may recall from that
discussion that when changes are detected in the URL, FlickrController
tells FlickrView
to change its state, and FlickrView
may then pass along part of the
address fragment to the corresponding controller via a screenFragment
property. Therefore, when the
fragment is changed to search/
search
term,
the search term gets assigned to the screenFragment
property of SearchController
. Therefore, we define a
screenFragment
setter as
follows:
public function set screenFragment(value:String):void { value = unescape(value); ApplicationModel.getInstance().searchTerm = value; if(_searchTerm != value) { view.searchTerm = value; searchForPhotos(value); } }
You can see that if the search term is the same as the existing
search term, no further action is necessary. However, if the search
term is different, the controller updates the search term for the view
and runs a method (searchForPhotos()
) that uses the FlickrService
instance to query the Flickr
service for photos with that search term:
private function searchForPhotos(searchTerm:String):void { _searchTerm = searchTerm; view.isSearching = true; var pendingOperation:PendingOperation = _service.searchPhotosByText(_searchTerm); pendingOperation.addEventListener(PendingOperation.RESULT, searchForPhotosResultHandler); }
You can see that the controller sets isSearching
to true
. When the result is returned, not only
does the handler method update the dataProvider
of the view, but it also sets
isSearching
to false
:
private function searchForPhotosResultHandler(event:ResultEvent):void { var photos:ArrayCollection = event.result as ArrayCollection; ApplicationModel.getInstance().searchResults = photos; view.dataProvider = photos; view.isSearching = false; }