Back in 2004, Martin Fowler published an article about the presentation model. That article is pretty much the reference that most people use when discussing the Presentation Model.
In 2007 Paul Williams published a port of Martin Fowler’s example in Flex. Now, I took Paul Williams’ example and ported it into Parsley.
If you are completely new to Parsley I suggest you take a look at a series of articles I wrote to get the basic concepts. http://artinflex.blogspot.com/2010/09/quick-dive-into-parsley-intro-why.html
What is the Presentation Model?
The Presentation Model is a pattern in which you separate your application logic and state from the user interface components. Basically, you take the logic and state of your application out of your view components.
I have seen some confusion on the net about how the Presentation Model (PM) pattern relates with the MVC (Model View Controller) pattern. The most obvious difference is that MVC includes a Controller, which the plain Presentation Model doesn’t. The second difference is in the concept of what a Model is. In both, the Model holds business logic and state, but the difference is that usually the MVC Model is more data oriented, while the PM model is more view oriented. Now there is absolutely nothing that says that you can’t combine the PM pattern and the MVC pattern, you most definitely can combine them. Just because usually the models in an MVC implementation model the data, it doesn’t mean that you can’t also have models that model the view.
So what are the benefits of implementing a presentation model? The first benefit is that it allows you to run tests on your logic without a view, you can much easier run unit test on the presentation model. It also makes it easier to port your code to other devices or other frameworks, or completely change your UI elements without affecting the logic. As the application grows in functionality, having the functionality broken into discrete classes also helps keep things organized and easier to update. It can also help reducing copying and pasting code, as you can have the presentation model broken into several classes and have common things to your views in one class that is injected in multiple views
What is the drawback? A little more work at the beginning. Also, the more portable you create the presentation model, the less likely that you will take advantage of some of Flex’s cool tools, like Validators for example.
Is it worth using the Presentation Model? Hmmm… that’s a complex answer. As most of everything, it depends. In the simplest of projects it is just not worth the extra work. As the application logic gets more complicated, then it’s worth it. Flex 4 uses somewhat of this PM concept with its new Skinning model. In Flex 4, you are separating the view components from the logic, except that Flex 4 promotes a bi-directional relation between the skin and the skin container, while in the classical PM pattern only one needs to know of the other (UI and Model), but it doesn’t have to be bidirectional.
The truth is that it is quite possible that you don’t reuse a whole lot of your views, but you probably copy and paste a lot of code between your views, for validation, formatting, data acquisition and saving, etc… Well, that’s when the presentation model begins to be useful. Also, it eventually becomes easier to add more business/application logic.
Martin’s and Paul’s examples implement the Presentation Model as a singleton, where there is ONE presentation model for the view. While this is ok for that example, you might not always want to keep the state of your views persistent. In my example I use Dynamic Objects so that each view can have its own PM.
If you really want to get a deeper knowledge of what the presentation model is, you should read Martin Fowler’s article at: http://martinfowler.com/eaaDev/PresentationModel.html
And check out Paul’s at: http://blogs.adobe.com/paulw/archives/2007/10/presentation_pa_3.html
Standard Flex Presentation Model example by Paul Williams
My example is based entirely on Paul’s example, so it is a good idea to take a look at Paul’s example.
You can find a running version of Paul’s example at:
http://examples.pmwilliams.co.uk/adobeblog/presentationpatterns/presentationmodel/PresentationModel.html
And you can view the source code at:
http://examples.pmwilliams.co.uk/adobeblog/presentationpatterns/presentationmodel/srcview/index.html
I also suggest you take a look at the notes, especially since the code isn’t commented. It will help you figure things faster. Here are the notes:
http://examples.pmwilliams.co.uk/adobeblog/presentationpatterns/presentationmodel/srcview/source/notes.html
Briefly explained, there are five main classes involved.
Extracted from Paul’s notes:
PresentationModel.mxml - this is the root of the application and is only responsible for 'bootstrapping' activities such as instantiating the root view component and the root presentation model object.
AlbumBrowser.as - this is the presentation model for the root view. It encapsulates the behaviour and state of the album browser view. This component instantiates and collaborates with an instance of the AlbumForm class.
AlbumForm.as - this is the presentation model for the album form view. It encapsulates the behaviour and state of the album form view.
AlbumBrowserView.mxml - this is the root view component. It is injected with an instance the AlbumBrowser presentation model. The view renders the AlbumBrowser instance to the screen, and observes it for changes.
AlbumFormView.mxml - this is the view for the album form. It is injected with an instance of the AlbumForm presentation model. The view renders the AlbumForm instance to the screen, and observes it for changes.
And here is the class diagram provided by Paul with the note:
Presentation Model Class Diagram in Flex by Paul Williams |
Flex Presentation Model using Parsley example
Now Paul actually uses dependency injection, but not framework managed. To replace Paul’s version with a Parsley dependency injection, it’s really simple. The only change would be to use one <FastInject>, declare the context and not assign the AlbumBrowser to the AlbumBrowserView’s albumBrowser property in the PresentationModel main view.
But Parsley is so much more than a simple dependency injection framework. There is a lot more we can gain from a few more changes.
First off, in Paul’s version as you can see in his diagram, there is a one to one relation between the AlbumBrowser model and the AlbumForm model. Additionally they are tightly coupled together, which means that the AlbumFormView is tightly coupled to the AlbumBrowserView through their presentation model, which complicates testing components individually. You also could not have two simultaneous AlbumFormViews, so you could not easily turn this into a Multi-Document-Interface app.
To decouple both PM, and create the possibility of turning this app easily into and MDI app, you can easily accomplish that with Parsley Messaging, and that’s exactly what I’ve done.
So I kept the basic functionality as it is, but added a whole lot more of potential, by changing a couple of things and taking advantage of Parsley’s features. A particular Parsley Feature not discussed previously in my series of articles of Parsley is the [MessageInterceptor] tag, which I take advantage in this example.
I took the liberty of renaming some classes and properties to make it easier to follow the code. For example Paul’s AlbumForm which is a model I renamed it as AlbumFormModel. I also changed the layout to make it fit the blog layout better.
Here is my class diagram after my changes:
Presentation Model Class Diagram using Parsley and Flex |
So let’s take a look at the running app (View source is enabled):
It behaves pretty much like Paul’s, same functionality, more potential.
Let’s look at the project structure itself:
As mentioned before, the model classes where renamed to make it easier to follow the code.
Note that the Observer utilities have been removed as they are no longer needed, that was a pretty clever class from Paul.
There is a new message class called: SelectedAlbumChangeMessage which is dispatched whenever a new album is selected from the list.
Also new is the Context config file which holds both Model classes.
We still have:
PresentationModelMain - this is the root of the application and is only responsible for 'bootstrapping' activities such as instantiating the root view component and the root presentation model object.
AlbumBrowserModel - this is the presentation model for the root view. It encapsulates the behaviour and state of the album browser view. It no longer instantiates and collaborates with an instance of the AlbumFormModel class directly. It is now done through the SelectedAlbumChangeMessage being dispatched.
AlbumFormModel - this is the presentation model for the album form view. It encapsulates the behaviour and state of the album form view.
AlbumBrowserView - this is the root view component. It is injected with an instance the AlbumBrowserModel presentation model. The view renders the AlbumBrowser instance to the screen, and observes it for changes.
AlbumFormView - this is the view for the album form. It is injected with an instance of the AlbumForm presentation model. The view renders the AlbumForm instance to the screen, and observes it for changes.
So now let’s look at the code:
I'll start with the views.
First let's take a look at the root, the AlbumBrowserView.mxml:
<?xml version="1.0" encoding="utf-8"?> <!--- This is the AlbumBrowser root view. It contains little logic. The login is implemented in the Presentation model that is injected into this view. This is comes from Martin Fowler's post about Presentation Model, which was then ported to Flex by Paul Williams. I adapted it to show the same example implemented usign the Parsley Framework Martin Fowler http://martinfowler.com/eaaDev/PresentationModel.html Paul Williams http://blogs.adobe.com/paulw/archives/2007/10/presentation_pa_3.html Arturo Alvarado http://artinflex.blogspot.com/ --> <mx:Panel xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:view="com.adobe.ac.view.*" xmlns:util="com.adobe.ac.util.*" title="{ albumBrowserModel.windowTitle }" width="410" height="460" layout="vertical" verticalScrollPolicy="off" horizontalScrollPolicy="off" styleName="albumBrowser" xmlns:parsley="http://www.spicefactory.org/parsley" > <mx:Script> <![CDATA[ import com.adobe.ac.model.AlbumBrowserModel; import com.adobe.ac.vo.AlbumVO; import mx.controls.Alert; import mx.events.CloseEvent; import mx.managers.PopUpManager; [Bindable] public var albumBrowserModel : AlbumBrowserModel; ]]> </mx:Script> <!-- Use Fast inject so that this view isn't wired into the context --> <parsley:FastInject property="albumBrowserModel" type="{AlbumBrowserModel}" /> <mx:List id="browser" width="381" height="227" borderStyle="solid" dataProvider="{ albumBrowserModel.albums }" selectedItem="{ albumBrowserModel.selectedAlbum }" labelField="title" change="albumBrowserModel.changeSelectedAlbum( AlbumVO( browser.selectedItem ) )"/> <view:AlbumFormView id="form" width="390" /> </mx:Panel>
As you would expect from an application using the presentation model, there isn't anything too fancy here. I <FastInject> the AlbumBrowserModel into a local var called albumBrowserModel. Then through binding, it updates itself from it's albumBrowserModel. It notifies the PM (albumBrowserModel) through the change event calling specific methods of the PM so that the PM knows that the view has changed. This view hosts an AlbumFormView, which is the other view.
Ok, so let's look at it's Presentation Model, here is AlbumBrowserModel.as:
package com.adobe.ac.model { import com.adobe.ac.delegate.AlbumDelegate; import com.adobe.ac.messages.SelectedAlbumChangeMessage; import com.adobe.ac.vo.AlbumVO; import flash.errors.IOError; import mx.collections.ArrayCollection; import mx.collections.Sort; import mx.collections.SortField; import org.spicefactory.parsley.core.messaging.MessageProcessor; /************************** * This is the presentation Model for the AlbumBrowser root view. * It encapsulates the behaviour and state of the album browser view. * * This is comes from Martin Fowler's post about Presentation Model, which * was then ported to Flex by Paul Williams. I adapted it to show the * same example implemented usign the Parsley Framework * * Martin Fowler http://martinfowler.com/eaaDev/PresentationModel.html * Paul Williams http://blogs.adobe.com/paulw/archives/2007/10/presentation_pa_3.html * Arturo Alvarado http://artinflex.blogspot.com/ */ [Bindable] public class AlbumBrowserModel { /**************** * The current list of albums that are selected */ public var albums : ArrayCollection; /***************** * The title displayed on the window */ public var windowTitle : String = "My Albums"; /****************** * The currently selected album of the list */ public var selectedAlbum : AlbumVO; /****************** * Message dispatcher used to notify that the current album * selected has changed */ [MessageDispatcher] public var messageDispatcher:Function; /************************** * Constructor * It calls the delegate to fill the list af albums and * then proceeds to sort them by title name */ public function AlbumBrowserModel() { var delegate : AlbumDelegate = new AlbumDelegate(); albums = delegate.getAlbums(); albums.sort = new Sort(); albums.sort.fields = [ new SortField( "title", true ) ]; albums.refresh(); } /********************** * Called whenever the user selects a new album from the list. * It proceeds to dispatch a message that indicates the current/previous * album and the newly selected album. It updates the selectedAlbum * property to reflect the change. */ public function changeSelectedAlbum( album : AlbumVO ) : void { if ( album != null ) { var selectedAlbumChangeMessage:SelectedAlbumChangeMessage = new SelectedAlbumChangeMessage(selectedAlbum, album); selectedAlbum = album; if (messageDispatcher != null) { messageDispatcher(selectedAlbumChangeMessage); } } else { windowTitle = "My Albums"; } } /************************ * Receives the selectedAlbumChangeMessage dispatched by changeSelectedAlbum. * The message can be intercepted and have the value of cancelled changed before * it returns to this view. * Currently the AlbumFormModel has an interceptor that assigns that value. * If the change is canceled, it returns the selected item to the previously * selected item. */ [MessageHandler] public function handleAlbumChangeMessage(selectedAlbumChangeMessage:SelectedAlbumChangeMessage):void { if (selectedAlbumChangeMessage.cancelled) { selectedAlbum = selectedAlbumChangeMessage.previousAlbum; windowTitle = selectedAlbumChangeMessage.previousAlbum.title; } else { windowTitle = selectedAlbumChangeMessage.selectedAlbum.title; selectedAlbum = selectedAlbumChangeMessage.selectedAlbum; } } } }
So the Presentation Model holds the state and logic. In this case it's holding state by keeping the currently selected album, also the title that the window should display, and the entire list of albums. It holds the logic because it decides on what to do when the user clicks on a new item in the list.
When the item is clicked in the list the changeSelectedAlbum() method is called. It then creates a SelectedAlbumChangeMessage and dispatches it reporting the user intent to change the selected album.
Now right under that method, you see the handler that handles that same message. You might think I'm crazy, why would I want to do something as silly as that? Why not as well just call the method directly? Well, when I dispatch that message other classes can actually also listen to that message also. In this case the model for the detail view also can listen and react accordingly. But even better, Parsley has this wonderful feature called Message Interceptors, with the [MessageInterceptor] tag. What message interceptors do is that they are called before any registered handler for a dispatched message, and they can change the message content and decide whether to let the message continue or not.
So in this case note that the handler checks to see if a property of the dispatched message has been set, the canceled property. The canceled property indicates if the user wishes to cancel the album change, if he/she had changes pending. If the user decides to cancel the change, the flag will be set and the selected item will be put back to what it was before the user selected the new album.
Now that decision is not made here, it is made in the detail view (AlbumForm). This differs on Paul's original implementation, where this model would hold a reference to the AlbumFormModel and check directly if it had changes pending. The benefit of my implementation is that it decouples both models and it allows to have multiple AlbumFormModels.
Great, now let's look at the AlbumFormView.mxml:
<?xml version="1.0" encoding="utf-8"?> <!--- This is the AlbumBrowser AlbumFormView. It contains little logic. The login is implemented in the Presentation model that is injected into this view. This is comes from Martin Fowler's post about Presentation Model, which was then ported to Flex by Paul Williams. I adapted it to show the same example implemented usign the Parsley Framework Martin Fowler http://martinfowler.com/eaaDev/PresentationModel.html Paul Williams http://blogs.adobe.com/paulw/archives/2007/10/presentation_pa_3.html Arturo Alvarado http://artinflex.blogspot.com/ --> <mx:Form xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:view="com.adobe.ac.view.*" horizontalScrollPolicy="off" verticalScrollPolicy="off" xmlns:parsley="http://www.spicefactory.org/parsley"> <mx:Script> <![CDATA[ import com.adobe.ac.model.AlbumFormModel; [Bindable] public var albumFormModel : AlbumFormModel; ]]> </mx:Script> <!-- Use Fast inject so that this view isn't wired into the context --> <parsley:FastInject property="albumFormModel" type="{AlbumFormModel}" /> <mx:FormItem label="Artist" required="true"> <mx:TextInput id="artist" width="290" text="{ albumFormModel.album.artist }" errorString="{ albumFormModel.artistError }" change="albumFormModel.updateArtist( artist.text )" enabled="{ albumFormModel.canEdit }"/> </mx:FormItem> <mx:FormItem label="Title" required="true"> <mx:TextInput id="title" width="290" text="{ albumFormModel.album.title }" errorString="{ albumFormModel.titleError }" change="albumFormModel.updateTitle( title.text )" enabled="{ albumFormModel.canEdit }"/> </mx:FormItem> <mx:FormItem> <mx:CheckBox id="classical" label="Classical" width="290" selected="{ albumFormModel.album.isClassical }" click="albumFormModel.updateIsClassical( classical.selected )" enabled="{ albumFormModel.canEdit }"/> </mx:FormItem> <mx:FormItem label="Composer" required="{ albumFormModel.canEditComposer }"> <mx:TextInput id="composer" width="290" text="{ albumFormModel.album.composer }" errorString="{ albumFormModel.composerError }" change="albumFormModel.updateComposer( composer.text )" enabled="{ albumFormModel.canEditComposer }"/> </mx:FormItem> <mx:FormItem> <mx:HBox> <mx:Button id="apply" label="Apply" enabled="{ albumFormModel.canApply }" click="albumFormModel.applyChanges()"/> <mx:Button id="cancel" label="Cancel" enabled="{ albumFormModel.canCancel }" click="albumFormModel.cancelChanges()"/> </mx:HBox> </mx:FormItem> </mx:Form>
Again, little logic here. The AlbumFormModel is injected with <FastInject> so that the view doesn't go through reflection. The components are bound to a local copy of the album, using one way data binding. When the user updates the content in the TextInput, though the change event a method in the model is invoked that registers that there was a change. It also get's the text for the errors from the model.
Let's look at the presentation model, here is AlbumFromModel.as:
package com.adobe.ac.model { import com.adobe.ac.messages.SelectedAlbumChangeMessage; import com.adobe.ac.vo.AlbumVO; import mx.controls.Alert; import mx.events.CloseEvent; import org.spicefactory.parsley.core.messaging.MessageProcessor; /************************** * This is the presentation Model for the AlbumBrowser's AlbumFormView. * It encapsulates the behaviour and state of the AlbumFormView. * This component instantiates and collaborates with an instance of the * AlbumForm class. * * This is comes from Martin Fowler's post about Presentation Model, which * was then ported to Flex by Paul Williams. I adapted it to show the * same example implemented usign the Parsley Framework * * Martin Fowler http://martinfowler.com/eaaDev/PresentationModel.html * Paul Williams http://blogs.adobe.com/paulw/archives/2007/10/presentation_pa_3.html * Arturo Alvarado http://artinflex.blogspot.com/ */ [Bindable] public class AlbumFormModel { private static const ARTIST_ERROR : String = "Please enter an artist"; private static const TITLE_ERROR : String = "Please enter an album title"; private static const COMPOSER_ERROR : String = "Please enter a composer"; /**************** * Indicates if the album can be edited. Mostly depends on if there * is an album assigned or not */ public var canEdit : Boolean = false; /***************** * Indicates if the composer can be edited. Mostly depends on if it's * set to classical or not. */ public var canEditComposer : Boolean = false; public var artistError : String = ""; public var titleError : String = ""; public var composerError : String = ""; /****************** * Indicates if the Cancel button should be enabled or not. Mostly depends * on if there has been changes to the fields of the album or not. */ public var canCancel : Boolean = false; /****************** * Indicates if the Apply button should be enabled or not. Mostly depends * on if there has been changes to the values or not, and if the values * entered are valid. */ public var canApply : Boolean = false; private var artistValid : Boolean = false; private var titleValid : Boolean = false; private var composerValid : Boolean = false; /*************** * Current values of the album. It's a copy of the original so that * the changes can be reverted. Undo/Cancel */ private var _album : AlbumVO; /**************** * A reference to the original Album data to be able to updated it when * the apply button is pressed. */ private var _albumToUpdate : AlbumVO; /***************** * The current value of the album being worked on. */ public function get album() : AlbumVO { return _album; } public function set album( album : AlbumVO ) : void { artistError = ""; titleError = ""; composerError = ""; if ( album != null ) { _album = album.clone(); _albumToUpdate = album; canEdit = true; canEditComposer = _album.isClassical; albumUpdated(); } else { _album = null; _albumToUpdate = null; canEdit = false; canEditComposer = false; } } /******************************** * This is the message interceptor of the AlbumChangedMessage that is dispatched * from the AlbumBrowserModel when the user clicks on a different album from the * list. * It does several things. It first checks to see if the current album being * worked on has any changes pending. If it doesn't it updates the current album * to the new album and authorizes the message to continue. * If the current album has changes pending to be applied, it prompts the user * if he/she is willing to loose the changes. Depending on the answer provided * it updates the message accordingly and proceeds to authorize the message to * continue, and updates the current album if the user selects to discard the changes. */ [MessageInterceptor(type="com.adobe.ac.messages.SelectedAlbumChangeMessage")] public function interceptSelectAlbumChange (processor:MessageProcessor) : void { var listener:Function = function (event:CloseEvent) : void { var message:SelectedAlbumChangeMessage = processor.message as SelectedAlbumChangeMessage; if ( event == null || event.detail == Alert.YES ) { message.cancelled = false; album = message.selectedAlbum; } else message.cancelled = true; processor.proceed(); }; var message:SelectedAlbumChangeMessage = processor.message as SelectedAlbumChangeMessage; if (changesToSave()) { Alert.show("Do you want to abandon your changes?", "Album Browser", Alert.YES | Alert.NO, null, listener); } else { album = message.selectedAlbum; processor.proceed(); } } /***************************************** * Called when the artist field is updated an proceeds to validate the data. */ public function updateArtist( artist : String ) : void { album.artist = artist; albumUpdated(); artistError = setErrorString( artistValid, ARTIST_ERROR ); } /***************************************** * Called when the title field is updated an proceeds to validate the data. */ public function updateTitle( title : String ) : void { album.title = title; albumUpdated(); titleError = setErrorString( titleValid, TITLE_ERROR ); } /***************************************** * Called when the isClasical checkbox is updated an proceeds to validate the data. */ public function updateIsClassical( isClassical : Boolean ) : void { album.isClassical = isClassical; canEditComposer = album.isClassical; if ( isClassical == false ) { album.composer = ""; composerValid = true; composerError = ""; } albumUpdated(); } /***************************************** * Called when the composer field is updated an proceeds to validate the data. */ public function updateComposer( composer : String ) : void { album.composer = composer; albumUpdated(); composerError = setErrorString( composerValid, COMPOSER_ERROR ); } /************************ * Called when any field is updated, and proceeds to validate the data */ private function albumUpdated() : void { validateAlbum(); } /************************* * Performes validation of the data, and sets the flags accordingly. */ private function validateAlbum() : void { artistValid = album.artist != ""; titleValid = album.title != ""; composerValid = ( album.isClassical == false ) || ( album.isClassical && album.composer != "" ); canCancel = !album.equals( _albumToUpdate ); canApply = canCancel && artistValid && titleValid && composerValid; } /**************************** * Sets the error string for the hint if the field has invalid data */ private function setErrorString( valid : Boolean, errorString : String ) : String { if ( valid == false ) return errorString; else return null; } /****************************** * Updates the original album data to save the changes. */ public function applyChanges() : void { _albumToUpdate.artist = _album.artist; _albumToUpdate.title = _album.title; _albumToUpdate.composer = _album.composer; _albumToUpdate.isClassical = _album.isClassical; albumUpdated(); } /******************************** * Reverts the changes. Copies the original data back again. */ public function cancelChanges() : void { album = _albumToUpdate.clone(); } /********************************* * Indicates if there are changes pending to be applied. */ public function changesToSave() : Boolean { return canCancel; } } }
Well, the majority of this class has to do with validation, I didn't change that part, and I feel like it can be simplified, but that was not the goal of this example. In regards to validation Paul wrote: "In a presentation model application the validation logic belongs in the model class. I've often felt Flex Validators are a little cumbersome to use from ActionScript code, so I haven't used them in this example."
Personaly I think two directional binding would have saved some code, but you can see that you can have pretty cool logic in a fairly simple way. For example. If nothing has changed, neither the Apply nor the Cancel buttons are enabled, and it's based on real changes, not some cheap flag that the user has typed something. If the user returns things as they were, the buttons are disabled again. I personally really like when applications do that. Also note that the Apply button is enabled based on more than just changes pending, it also dependent on the form being valid.
Now what I want you to pay attention to is the [MessageInterceptor] tag in-front of interceptSelectAlbumChange() method. Message interceptors intercept messages before the handlers actually get a chance to receive the message. Message interceptors are a great feature of Parsley.
Recall that in the the AlbumBrowserModel, a SelectedAlbumChangeMessage is dispatched and also handled. Now in this model, the interceptSelectAlbumChange() will be called before the handler on the AlbumBrowserModel. What the interceptSelectAlbumChange does is check to see if there are any changes pending. If there are no changes pending, it sets the current album to the new album in the message, and let's the SelectedAlbumChangeMessage proceed unchanged, which is received by the handler in the AlbumBrowserModel and it does it's thing. The interesting part is that if there are changes pending, it brings out a popup asking the user if he/she is willing to loose the changes. If the user is not willing to loose the changes it, proceeds to change the value of a property in the message (in this case the cancelled property, forgive the typo), and then let's the message proceed. The message is "suspended" for a short period of time. Message interceptors can also decide to not let the message continue. But in my particular case I wanted the message to proceed regardless, and just update a flag in the message when necessary.
Author Note: Szabolcs made an observation in the comments. I'm using the MessageInterceptor to intercept the message, change the flag and handle the message. While in this case it works just fine, it's probably not the best idea in a larger project or one with potential for growth. As Szabolcs notes, MessageInterceptors always get called before the handlers, but I don't have much control of which MessageInterceptor gets called first. What if I wrote some other Interceptor that decided to cancel the message, but this one got called first? It would be a better practice to have a MessageHandler in the AlbumFromModel to handle the message independent from the MessageInterceptor. Although for this case, it would be overkill, it set's things up better for future growth.
Ok, let's look at the message, here is SelectedAlbumChangeMessage.as:
package com.adobe.ac.messages { import com.adobe.ac.vo.AlbumVO; /********************** * This is the message that is dispatched when the user selects a new * album from the list. * * This class is not part of the original Paul Williams example. The * purpose of this class is to decouple the AlbumBrowserModel From the * AlbumFormModel which are tighly coupled in PaulWilliams example. * * @author Arturo Arturo Alvarado * */ public class SelectedAlbumChangeMessage { private var _previousAlbum:AlbumVO; private var _selectedAlbum:AlbumVO; private var _cancelled:Boolean = false; /************************ * Constructor * @param previousAlbum The album that was previously selected * @param selectedAlbum The album that has been selected and is the new target */ public function SelectedAlbumChangeMessage(previousAlbum:AlbumVO,selectedAlbum:AlbumVO) { _previousAlbum = previousAlbum; _selectedAlbum = selectedAlbum; } /************************* * The previously selected Album, current before the selection occours. */ public function get previousAlbum():AlbumVO { return _previousAlbum; } /************************* * The album that has been selected to become the current album. */ public function get selectedAlbum():AlbumVO { return _selectedAlbum; } /************************* * Should the change event be cancelled or not. */ public function set cancelled(value:Boolean):void { _cancelled = value; } public function get cancelled():Boolean { return _cancelled; } } }
Nothing fancy, forgive the typo in the cancelled property named. Noticed it when I was deep into writing the article.
And here is the AlbumVO.as:
package com.adobe.ac.vo { /************************** * The is the value object of one Album. It contains the properties * that describe the albums such as title, artist and Composer. * It also contains to helper methods to copy the object (clone) and * to compare the contests of it. * * This is comes from Martin Fowler's post about Presentation Model, which * was then ported to Flex by Paul Williams. I adapted it to show the * same example implemented usign the Parsley Framework * * Martin Fowler http://martinfowler.com/eaaDev/PresentationModel.html * Paul Williams http://blogs.adobe.com/paulw/archives/2007/10/presentation_pa_3.html * Arturo Alvarado http://artinflex.blogspot.com/ */ [Bindable] public class AlbumVO { /*********************** * The tittle name of the album */ public var title : String = ""; /*********************** * The Artist name of the album */ public var artist : String = ""; /********************** * Flag that indicates */ public var isClassical : Boolean = false; /********************** * The name of the composer, in case this is a classical album */ public var composer : String = ""; /******************************* * This function returns a new copy of this object * @return a complete new copy of this object */ public function clone() : AlbumVO { var clone : AlbumVO = new AlbumVO(); clone.title = title; clone.artist = artist; clone.composer = composer; clone.isClassical = isClassical; return clone; } /********************************* * Determines if the Album passed contains the same values as * this album * @param source The album which you wish to compare to this one. * @return true if albums are the same. */ public function equals( source : AlbumVO ) : Boolean { var result : Boolean = source.title == title; result = result && source.artist == artist; result = result && source.composer == composer; result = result && source.isClassical == isClassical; return result; } } }
A somewhat standard Value Object. Note that it has two "unusual" methods. The clone() and equals(). Both are used in the AlbumFormModel. The clone is used to keep a copy of what the data looked like when it was received, and the equals is used to check to see if any values have changed from when it was received.
And then there is the AlbumDelegate.as:
package com.adobe.ac.delegate { import com.adobe.ac.vo.AlbumVO; import mx.collections.ArrayCollection; /************************** * This is a delegate in charged of obtaining the list of albums. * Usually delegates contact services to obtain the info. In this * case it simply has the data itself and creates and returns an * ArrayCollection of the list of albums with artists and composers. * * This is comes from Martin Fowler's post about Presentation Model, which * was then ported to Flex by Paul Williams. I adapted it to show the * same example implemented usign the Parsley Framework * * Martin Fowler http://martinfowler.com/eaaDev/PresentationModel.html * Paul Williams http://blogs.adobe.com/paulw/archives/2007/10/presentation_pa_3.html * Arturo Alvarado http://artinflex.blogspot.com/ */ public class AlbumDelegate { /************************ * Gets a list of albums * @return an ArrayCollection of albums of type AlbumVO */ public function getAlbums() : ArrayCollection { var albums : ArrayCollection = new ArrayCollection(); var album : AlbumVO = null; album = new AlbumVO(); album.title = "OK Computer"; album.artist = "Radiohead"; albums.addItem( album ); album = new AlbumVO(); album.title = "The Joshua Tree"; album.artist = "U2"; albums.addItem( album ); album = new AlbumVO(); album.title = "Nevermind"; album.artist = "Nirvana"; albums.addItem( album ); album = new AlbumVO(); album.title = "Thriller"; album.artist = "Michael Jackson"; albums.addItem( album ); album = new AlbumVO(); album.title = "Dark Side of the Moon"; album.artist = "Pink Floyd"; albums.addItem( album ); album = new AlbumVO(); album.title = "Definitely Maybe"; album.artist = "Oasis"; albums.addItem( album ); album = new AlbumVO(); album.title = "Sgt. Pepper's Lonely Hearts Club Band"; album.artist = "The Beatles"; albums.addItem( album ); album = new AlbumVO(); album.title = "Like a Prayer"; album.artist = "Madonna"; albums.addItem( album ); album = new AlbumVO(); album.title = "Appetite For Destruction"; album.artist = "Guns N' Roses"; albums.addItem( album ); album = new AlbumVO(); album.title = "Revolver"; album.artist = "The Beatles"; albums.addItem( album ); album = new AlbumVO(); album.title = "Piano Quintet In A Major Opus 81"; album.artist = "Andreas Haefliger and Takacs Quartet"; album.composer = "Antonin Dvorak"; album.isClassical = true; albums.addItem( album ); album = new AlbumVO(); album.title = "Lieutenant Kije (suite) Opus 60"; album.artist = "Lille National Orchestra"; album.composer = "Sergei Prokofiev"; album.isClassical = true; albums.addItem( album ); album = new AlbumVO(); album.title = "Alessandro Severo - Overture"; album.artist = "Academy Of Ancient Music"; album.composer = "George Frideric Handel"; album.isClassical = true; albums.addItem( album ); album = new AlbumVO(); album.title = "Sinfonia In C Minor"; album.artist = "Czech Chamber Philharmonic Orchestra"; album.composer = "Josef Barta"; album.isClassical = true; albums.addItem( album ); album = new AlbumVO(); album.title = "Sleeping Beauty - Waltz"; album.artist = "Kirov Orchestra"; album.composer = "Peter Ilich Tchaikovsky"; album.isClassical = true; albums.addItem( album ); return albums; } } }
Usually you would be retrieving data from somewhere, this is a "placeholder" for the real class.
Let's not forget the magic glue, the context config file. Here is PresentationModelContextConfig.mxml:
<?xml version="1.0" encoding="utf-8"?> <!--- This is the file that declares which objects you wish to manage through Parsley. Any object declared within the Object Tag will be allowed to participate in any of Parsley's features. This is a Simple MXML implementation, but this can also be declared in and XML file. --> <mx:Object xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:spicefactory="http://www.spicefactory.org/parsley" xmlns:model="com.adobe.ac.model.*"> <!--- This is the presentation model for the root AlbumBrowserView --> <model:AlbumBrowserModel /> <!--- This is the presentation model for the AlbumFormView. It is set a a dynamic object so everytime a new AlbumFormView is instatiated, it will have it's OWN presentation model. --> <spicefactory:DynamicObject type="{AlbumFormModel}" /> <mx:Script> <![CDATA[ import com.adobe.ac.model.AlbumFormModel; ]]> </mx:Script> </mx:Object>
As mentioned earlier, the AlbumFormModel is set as a Dynamic Object so that if there are multiple AlbumFormModels, simultaneously or not, state is not preserved among them. In most cases you will set your Presentation Models as DynamicObjects, regardless if it is a MDI app or not, unless you really don't ever remove the view corresponding to that PM.
Last, the main application file, where it all starts. Here is PresentationModelMain.mxml:
<?xml version="1.0" encoding="utf-8"?> <!-- This is an example application that demonstrates the Presentation model implemented using Parsley. This is comes from Martin Fowler's post about Presentation Model, which was then ported to Flex by Paul Williams. I adapted it to show the same example implemented usign the Parsley Framework. If you are new to Parsley I suggest you start with some basic Parsley concepts to make it easier to follow this example. You can get a quick start at: http://artinflex.blogspot.com/2010/09/quick-dive-into-parsley-intro-why.html Martin Fowler http://martinfowler.com/eaaDev/PresentationModel.html Paul Williams http://blogs.adobe.com/paulw/archives/2007/10/presentation_pa_3.html Arturo Alvarado http://artinflex.blogspot.com/ --> <mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:view="com.adobe.ac.view.*" layout="vertical" styleName="application" width="420" height="470" horizontalScrollPolicy="off" verticalScrollPolicy="off" xmlns:parsley="http://www.spicefactory.org/parsley" viewSourceURL="srcview/index.html"> <mx:Style source="styles.css"/> <!--- ContextBuilder is how you tell Parsley what objects you want it to manage, and how you want it to do it. --> <parsley:ContextBuilder config="{PresentationModelContextConfig}" /> <!--- The AlbumBrowser view--> <view:AlbumBrowserView /> </mx:Application>
Well this was a fairly long post. It puts into practice a lot of what is discussed through out the series. I really do hope you find this post useful.
Next is the presentation model implemented with multiple AlbumForms. Click here for the next article.
Feel free to leave comments, questions and suggestions.
Great Work !!!!!!!!!1
ReplyDeleteHi, this is great work and really helpful series of articles.... thank you!
ReplyDeleteI have a note and a question if you don't mind:)
Note: You decided to use the [MessageInterceptor] to not just intercept but also to really handle the message (change the selected album). This means if there were other interceptors (somewhere in other classes) which decide to cancel the message.... this interceptor would still make the changes to this PM.
I think it would be cleaner and safer to have the interceptor only for deciding about the cancel-flag.... and a [MessageHandler] to make any changes only after ALL interceptors processed the message. What do you think?
The other question is related also to a previous article. When using PM pattern and also services to get the data.... there is a way to pass the requestor-PM to the command or service so eventually the PM would handle the response. What do you think of this approach?
Keep up the good work!
Thanks
Szabolcs
I thank you for putting together this series of tutorials as I've found them to be of great assistance in learning Parsley. You have enough material here that you should consider publishing it as a book.
ReplyDeleteNorman Klein
Author: Laszlo in Action
Thanks,
ReplyDeleteSzabolcs: You are absolutely right about the double use of the Interceptor, and although in this particular case it works fine, it's probably not the best idea. I will note that in the post.
Regarding the other question. In general I believe it would be wise to dispatch the message that invokes the command from the PM, and of course the PM would handle the response. The PM would act as a Mediator between the Model-Controller and the Views.
Is it always wise and good practice to use PM as Mediators? Hmmmm... PM's add another layer, which adds more code which takes longer to create which makes things a bit harder to follow. It really depends on the complexity of the projects and views. The more complex the project and views are, the more they will benefit from these "extra" layers. It also depends on the planned growth in the future. If you plan to add more and more in the future it will pay off.
If you ever take a look at the Wordpress code for example... (it's in php) at first look you go "wow, where do I start?", it's all broken in layers, but once you start figuring things out you really appreciate those layers when you build on top of it.
Thanks Norman, didn't think about it. Do you think I have potential? Know any publishers?
ReplyDeleteI don't know off-hand. The Parsley framework is specialized enough that it wouldn't appeal to a general audience, so it might be difficult to get a mainstream publisher interested. But I regularly attend some general meetings at Adobe and I'll ask some of the Adobe employees for recommendations for the most appropriate avenue to publish your work.
ReplyDeleteNorman
Great series of articles and very well written!!
ReplyDelete@Anonymous. Thank you.
ReplyDeleteThanks Art for this wonderful series. You have got me started on my quest with Parsley.
ReplyDeleteHats off to your explanation which is crystal clear all through the series. I have gone through almost every article in the series.
ReplyDeleteThanks a ton.
how applyChanges() affect the arrayCollect in the list,what is the logical?
ReplyDeleteapplyChanges() in AlbumFormModel, albums is in AlbumBrowserModel , how applyChanges() can change albums? I do not know
ReplyDelete@Anonymous Thanks, glad to help
ReplyDelete@Mike & Jack. It's black magic. Ok, maybe not. In some programming languages (such a C) you can choose pass variables by value or by reference. In ActionScript you don't have much of a choice. Strings, bool, Numbers & ints are passed by value, pretty much everything else is passed by reference.
So what in the world does that mean? When you pass objects by value, a copy of the value is made, when you pass by reference a reference (pointer) to the original object is passed.
So in general, when you pass any custom class around in ActionScript, the values are not being copied, it's just a reference to the original object, (that's why you need a clone function, for when you really wish to copy something).
So when the message is dispatched, a reference to the object contained in the ArrayCollection is being passed. When you update the values of the object, the values of the object in the ArrayCollection are also updated because it IS the same object.
That's why for the UNDO, a copy of the AlbumVO needs to be made so we can set the values back.
_album = album.clone();
Check out the comments, it sort of mentions this.
This is Great.. You are genius man..
ReplyDelete