This is part 6.
In Part 1, I explained the reasons as to why we chose to use Parsley in our projects.
In Part 2, I explained what Parsley is and some basic concepts and how they relate to Parsley.
In Part 3, I showed how to set up Parsley and showed a basic injection example.
In Part 4, I showed how to send messages using Parsley that do not extend the Event class.
In Part 5, I showed how to send messages using Flex Events.
This article builds upon what was covered in Part's 2, 3, 4 and 5 so if you haven't seen them, I encourage you to start with at the beginning of this series.
This should be a more interesting example than the previous three. In this example we cover two important features of Parsley: Nested Contexts and Parsley Object lifecycle methods.
Nested Contexts
In Parsley you are not limited to one global context. You can nest contexts, place one inside of the other, to create context scopes. This is similar to how you can nest scopes using curly brackets { } in your code.
When you create a context in Parsley only that view and its view descendants can have access to that context. You can have as many contexts as you would like, and the same rule applies. Now, it’s important to note that an object that is part of a nested child context can still have access to objects of ancestor objects, just like it works with regular code scopes. The concept is very similar to what you are actually used to with scopes.
If you look at diagram 1 on the right side, Objects that belong to the “Main Context” can’t see objects that belong to any of the sub contexts. Objects in “Sub Context A” can see objects of itself and objects of the “Main Context”, but can’t see objects of “Sub Context B” nor “Sub Context A-1” or “Sub Context A-2”. The same applies throughout.
Another great feature is that nested contexts not only apply to injection, it also can be applied to messaging. By default, all messaging in Parsley is global, but you can specify on the receiving end if you wish to only receive messages from its local context, sending is always global. You specify this with the [MessageHandler (scope="local")] tag.
In regards to injection, you don’t have to specify if it’s global or local, Parsley figures it out by itself, but it is important to note that it does this by the class type. So you should not use the same class name in multiple nested contexts. Technically speaking you can, but then you must use ids to help Parsley figure out which one you want, the local or a different one. Ex. [Inject(id="defaultMyModel")], more on this in the docs.
Parsley Object Lifecycle methods
In Flex, Flash, and just about every Object Oriented development environment, objects have a lifecycle. That is; they are created, used, and destroyed. Parsley objects are no different, regardless of whether Parsley manages them or not. In Parsley the lifecycle is somewhat tied to the view display list, when the object that holds a context is removed from the display list, the context of it, and its children is destroyed and all the objects that where managed within those contexts are also destroyed. Also, when a view is added to the context with the <Configure /> tag, it is added to the context when the view is added to the stage, and it is removed from the context when it is removed from the stage. That said, Flex has a quirky way of re-parenting components when scrollbars are added, which means that they are removed and added from stage while this happens. Parsley is aware of this, and objects are not destroyed during this process.
Now Parsley can inject into properties and into methods; that means that those properties will be null during the object creation. So, how do you know when those properties have been injected? Well, Parsley has a great feature to help you with that. You can tag a function within the managed object with [Init] and that function will be called when all injections are done and the object has been successfully added to the context and it is ready to participate in all of Parsley features. This is somewhat like CreationComplete, but in regards to Parsley. If anything depends on the objects being injected, this is where you can trigger the execution of those dependencies.
Parsley also offers a [Destroy] tag, which you can place in a function of a managed object. That function will get called right before the object is destroyed. This is a handy feature if you wish to remove event listeners, recall that AS3 does not include destructors, and you avoid the quirkiness of the removedFromStage event firing when your object gets re-parented.
Now remember that this is tied to the component that holds the context and it last as long as that object is not removed from the display list. This can be overridden with the <ViewSettings autoremoveComponents="false" /> tag, but that is beyond the scope of this example, and something you probably won’t need much.
There are more features regarding object lifecycle, such as factories, Asynchronous Object Initialization, and controlling the Object Initialization Order which you can read in more detail in the docs.
Enough talk, let’s get to the example.
The Example
In this particular case, I’m going to start of showing the example running and then we will dig a bit into the code. View Source is enabled.
Here is the structure of project.
This project is obviously larger than the previous examples. It has two levels of context, the global and the local, but as you probably saw in the sample you can create as many local contexts as you like.
The LoggerMessage class is a basically the same class we used on the previous messaging example.
The GlobalModels is the model used globally and the LocalModel is the model used in the subcontext. As usual, these are quite simple for demonstration purposes.
The GlobalContext defines the global context and it contains the GlobalModel.
The LocalContext is used as a subcontext and it contains the LocalModel.
There are a total of 3 views. The GlobalView which holds the GlobalContext and is a child of ParsleyNestedContextExample, the main app. The LocalView which holds the LocalContext and it will be a child of GlobalView. The SharedComponents which holds no context and is a child of LocalView.
Here is a diagram overview:
Diagram 2 - Parsley View Context Relation |
So basically any object that is a view child of “Global View”, can have access to the Global Context, that includes “Local View 0” and “Local View 1”. View Childs within “Local View 0” can have access to the local context of “Local View 0” and the global context, but they don’t have access to the local context of “Local View 1”.
And here is the code. If you are looking for the new stuff, jump down to the LocalModel.as.
Let's start with LoggerMessage.as:
package messages { /*************************************************************** * This is a simple class that will hold a message so it can be * delivered thought our application. It only has one property * that is the message itself. Note that it does not * extend Flexs' Event. * * For more Parsley examples visit: artinflex.blogspot.com * */ public class LoggerMessage { /******************** * This is the private var that holds our message * */ private var _message:String; /******************** * Constructor that takes the parameter as message * @param message is a String that contains the text that will be set * */ public function LoggerMessage( message:String) { _message = message; } /********************************* * read only property that delivers the message. * */ public function get message():String { return _message } } }
Well, there is nothing new and special here, it just holds a string as a message. Note that I'm not planning on using Flex Events to send messages as it doesn't extend the Event class.
Next let's look at the Global and Local Context config files, for more info on these please refer to example in part 3.
Here is GlobalContext.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. In this particular case this is the Global context which is meant to be shared throughout the whole app. For more Parsley Examples visit: artinflex.blogspot.com --> <mx:Object xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:model="model.*"> <!-- Withing the object tags you declare de objects. In this case I'm just including the GlobalModel Class --> <model:GlobalModel /> </mx:Object>
And here is the LocalContext.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. In this particular case this is the Local context which is NOT meant to be shared throughout the whole app, it is intended to be used in a more private context. For more Parsley Examples visit: artinflex.blogspot.com --> <mx:Object xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:model="model.*"> <!-- Withing the object tags you declare de objects. In this case I'm just including the LocalModel Class --> <model:LocalModel /> </mx:Object>
Both don't have anything new, each hold Model class, GlobalModel and LocalModel respectively. The new stuff is in the LocalModel. So what do these models look like?
Here is GlobalModel.as:
package model { import messages.LoggerMessage; /********************************************************************* * GlobalModel is a simple class meant to demonstrate the Shared Model * concept. This class is intended to be shared through the whole app. * * For more examples visit: artinflex.blogspot.com **/ public class GlobalModel { [Bindable] public var sharedString:String; // The string to be shared } }
And here is the LocalModel.as:
package model { import messages.LoggerMessage; /********************************************************************* * LocalModel is intended to be used in a more private context, and * not shared among the whole app. * * Also LocalModel demonstrates another Parsley Feature are the * Object lifecycle Methods: [Init] [Destroy] * * For more Parsley examples visit: artinflex.blogspot.com **/ public class LocalModel { [Bindable] public var sharedString:String; // The string to be shared /****************************** * The MessageDispatcher is a special Parsley Flag * that instructs Parsley to Inject Parsley’s message * dispatcher into the function variable declared below. * This allows this class to send messages anywhere without * having to use Flex/Flash events. * */ [MessageDispatcher] public var dispatcher:Function; /**************************** * viewNumber is used to identify the view that this model * belongs to. This is the private value. * */ private var _viewNumber:int = 0; /**************************** * viewNumber is used to identify the view that this model * belongs to. It dispatches a message when the view number is set. * */ public function set viewNumber(value:int):void { _viewNumber = value; var message:String = "LocalModel assigned view number: " + value.toString(); dispatcher(new LoggerMessage(message)); } public function get viewNumber():int { return _viewNumber; } /******************************************************* * In Parsley, a function taged with Init will be called * when this object has been created and successfully added * to the context. In this case it sends a message. * ****************************************************/ [Init] public function handleInit():void { var message:String = "The LocalModel has been created and is ready."; dispatcher(new LoggerMessage(message)); } /******************************************************* * In Parsley, a function taged with Destroy will be called * when this object is about to be removed from the context * and disposed. This object will get Garbage Collected. * ****************************************************/ [Destroy] public function handleDispose():void { var message:String = "LocalModel about to be destroyed for View - " + viewNumber.toString(); dispatcher(new LoggerMessage(message)); } } }
Now here we can see a couple of new things. First, well you can notice that we can Inject into any model object that is managed by Parsley, Injecting is not limited to Views. For example we are injecting the MessageDispatcher here. Also you will note the [Init] and they [Destroy] tags in front of two separate functions. As indicated in the comments, Init is called when the object has been set and is managed by Parsley, so you can use any of Parsley's features. You can use Init and Destroy in view and non view components that are managed, regardless if they were added with <Configure /> or <ContextBuilder />.
Ok, now let's look at the views.
Start of with GlobalView.mxml:
<?xml version="1.0" encoding="utf-8"?> <!-- ************************************************************* This is the view that is "Global" and will hold the Global Context. This view will also hold all the local child windows that will have their own context. For more Parsley examples visit: artinflex.blogspot.com ************************************************************* --> <mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:parsley="http://www.spicefactory.org/parsley" width="390" height="570" verticalGap="5" > <mx:Label text="Global Message Log" fontWeight="bold"/> <mx:TextArea editable="false" id="messageLogDisplay" width="100%" height="80" /> <mx:HBox width="100%" horizontalAlign="center" horizontalGap="40"> <!-- These buttons are to create and remove LocalView Children. --> <mx:Button label="Create Local View" click="handleCreateLocalView(event)"/> <mx:Button label="Remove Local View" click="handleRemoveLocalView(event)"/> </mx:HBox> <!--This is the container that will hold the local views created.--> <mx:VBox height="430" width="390" id="localViewsContainer" verticalScrollPolicy="auto"> </mx:VBox> <!-- The date formatter is to show times in the logger --> <mx:DateFormatter formatString="LL:NN:SS" id="dateFormatter" /> <!-- ContextBuilder is how you tell parsley to manage objects that are not Views. In this case I’m loading the GlobalContext configuration file which is intended to be shared in the whole app. Notice that you can also specify a complete function which gets called once it finishes creating the context and its children (Managed Objects). --> <parsley:ContextBuilder config="{GlobalContext}" complete="handleGlobalContextCreated()" /> <!-- Configure is how you tell Parsley that you want this view to become part of the Context and participate in all of Parsley’s features. <Configure /> looks for the closest Context in the view hierarchy and attaches the view to that particular context. In this case, this view becomes part of the context within this particular file. --> <parsley:Configure /> <mx:Script> <![CDATA[ import messages.LoggerMessage; import mx.formatters.DateFormatter; /************************************************* * This function is being called when the ContextBuilder has finished * creating the Context and its children specified. The context gets * created before the view is configured, so Injections will be performed * after this function gets called. The objects are ready to be injected. * */ public function handleGlobalContextCreated():void { addToLogDisplay("Global Context has been created."); } /**************************************************** * [Init] can be used in any Parsley managed object, including * Views, and it’s not limited to objects declared in the Config * file of the context Builder. The functions tagged with [Init] * will get called after Parsley finishes adding this view to the * Context, all Injections have been performed and it is ready to * send and receive messages. This is a very handy tool. * */ [Init] public function handleInit():void { addToLogDisplay("GlobalView has been added to context and is ready."); } /*************************************************** * This simple function takes a string and adds the current time to the * front. It then adds the new concatenated string to the Global * Log Viewer and sets the log viewer to scroll to the bottom on the * next frame. * @param message is the text that will be added to the log */ private function addToLogDisplay(message:String):void { var currentDate:Date = new Date(); message = dateFormatter.format(currentDate) + " - " + message; if (messageLogDisplay.text.length > 0) messageLogDisplay.text = messageLogDisplay.text + "\n" + message; else messageLogDisplay.text = message; // This is so that the logviewer scrolls to the bottom automatically. // It is set on a callLater so it is called on the next frame // so that the messageLogDisplay has done all its measuring after the new // text is added and it really scrolls to the bottom. callLater(moveScrollToBottom); } /****************************************** * This is the global messages handler that receives any messages * of the type LoggerMessage from anywhere and it appends them to * the log viewer. * @param the message object that contains the message * */ [MessageHandler] public function receiveGlobalMessage(logMessage:LoggerMessage):void { addToLogDisplay(logMessage.message); } /*********************************************** * This is the function that gets called when the create local * view button is clicked. It creates a new instance of LocalView * and adds that instace to the container set to hold these views. * * @param event is the mouse event from the Click of the button. * */ private function handleCreateLocalView(event:MouseEvent):void { var newLocalView:LocalView = new LocalView(); newLocalView.viewNumber = localViewsContainer.numChildren; localViewsContainer.addChild(newLocalView); } /************************************************* * This function gets called when the “remove local view” button * is click and removes the last view added to the container. * It also checks to make sure that there are views to be removed. * * @param event is the mouse event fromt the click of the button. * */ private function handleRemoveLocalView(event:MouseEvent):void { if (localViewsContainer.numChildren > 0) { localViewsContainer.removeChildAt(localViewsContainer.numChildren -1); } } /**************************************************** * This is the function that is set to be called later so that * the text area container that has the texts is scrolled to * the bottom. * */ private function moveScrollToBottom():void { messageLogDisplay.verticalScrollPosition = messageLogDisplay.maxVerticalScrollPosition; } ]]> </mx:Script> </mx:VBox>
This particular view has several interesting sections. It has buttons to add and remove LocalView children. It also has a TextArea that will act as a real time log display. It also uses [Init], which is invoked after then context is fully created and this view is added to that context and is ready to participate in Parsley. Notice the [MessageHandler] tag. It doesn't specify a scope, which will be different in SharedComponents.mxml. By default message handlers are global, that is they will receive messages sent from any context, that can be changed as it will be seen in the SharedComponents view.
Now let's take a look at the LocalView.mxml:
<?xml version="1.0" encoding="utf-8"?> <!-- ************************************************************* This is the view that is "Local" and will hold the LocalContext. It is intended to be created dynamically by the GlobalView, and have several siblings. Each one has its own context. This view will hold a child with the components that bind to the context objects. For more Parsley examples visit: artinflex.blogspot.com ************************************************************* --> <mx:VBox xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:parsley="http://www.spicefactory.org/parsley" width="100%" xmlns:view="view.*" borderStyle="solid" cornerRadius="5" backgroundColor="#60FA03" backgroundAlpha="0.15" height="206"> <!-- ContextBuilder is how you tell parsley to manage objects that are not Views. In this case I’m loading the LocalContext configuration file which is intended to be shared privately only by this view and its children. Notice that you can also specify a complete function which gets called once it finishes creating the context and its children (Managed Objects). --> <parsley:ContextBuilder config="{LocalContext}" complete="handleLocalContextCreated()" /> <!-- Configure is how you tell Parsley that you want this view to become part of the Context and participate in all of Parsley’s features. <Configure /> looks for the closest Context in the view hierarchy and attaches the view to that particular context. In this case, this view becomes part of the context within this particular file, but it also can participate in the context of its ancestors. --> <parsley:Configure /> <!-- SharedComponents is a view that contains the components that will bind to properties of the models. This view has two Shared components to demonstrate the scope of the context.--> <view:SharedComponents> </view:SharedComponents> <view:SharedComponents> </view:SharedComponents> <!--The text input and button are here to demonstrate custom messages and how they can also be scoped.--> <mx:HBox width="100%" height="25"> <mx:TextInput x="10" y="88" width="267" id="messageTextBox"/> <mx:Button x="255" y="88" label="Send Message" id="sendMessageButton" enabled="false" click="handleSendMessageClick(event)"/> </mx:HBox> <mx:Script> <![CDATA[ import model.LocalModel; import messages.LoggerMessage; /****************************** * The [MessageDispatcher] is a special Parsely tag that injects * a Parsley messaging function into the specified function. * Messages sent through this function do not need to extend * the Event class and can be received either globally or locally. * */ [MessageDispatcher] public var dispatcher:Function; /******************************* * This is the viewNumber assigned by the Global view, intended * to indentify views and the context associated with it. It also * collaborates to illustrate part of the lifecycle of Parsley objects. * */ public var viewNumber:int; /********************************* * [Inject] is a special Parsley tag that asks Parsley to inject * the object specified below. Parsley will look in all of the * contexts of this view and its ancestors (not its siblings, * nor cousins, etc…) for a managed object of the class type * specified, and it will inject that object into this view. * */ [Inject] public var localModel:LocalModel; /********************************** * This function gets called when the context creation is complete, * notice that the dispatcher will be null, it is here primarily to * demonstrate that specifically. Dispatcher gets its value after * the context has been created and configuration is complete. * Dispatcher can be safely called in the [Init] method. * */ public function handleLocalContextCreated():void { // If you debug you will note that dispatcher will be null // and that's why this message doesn't get sent. dispatcher // is injected afterwards, in the configure event. if (dispatcher != null) dispatcher(new LoggerMessage("Local Context has been created.")); } /************************************** * Methods tagged with [Init] get called when Parsley finishes * processing this object or view and incorporates it into the * context. After this method is called, all injections have * been performed, so it is safe to use them. * */ [Init] public function handleInit():void { localModel.viewNumber = viewNumber; sendMessageButton.enabled = true; dispatcher(new LoggerMessage("LocalView is configured and ready")); } /**************************************** * Methods tagged with [Destroy] get called when Parsley is about * to remove this object/view from the context, and it will become * available for garbage collection. You can use this method to * remove event listeners that could prevent this object/view to * be garbage collected. * */ [Destroy] public function handleDestory():void { dispatcher(new LoggerMessage("LocalView-" + viewNumber.toString() + " is about to be destroyed")); } /**************************************** * Simple method that dispatches the custom message entered in the * text box. Notice that dispatching messages is always Global, * what selects if the message is local or not is the message handler. * */ private function handleSendMessageClick(event:MouseEvent):void { dispatcher(new LoggerMessage(messageTextBox.text)); } ]]> </mx:Script> </mx:VBox>
LocalView has two shared components, and creates it's own LocalContext and configures itself to participate in that context. I would like you to notice that in the creation complete handler of the local context there is a call for the dispatcher. The dispatcher will basically always be null. The dispatcher will be injected after the view is configured which happens after the context has been created. I added this to demonstrate exactly that.
LocalView also allows you to send custom log messages. Notice that dispatching messages is always global. What chooses if it's local or not is the receiving message handler.
Ok, let's look at the SharedComponents.mxml:
<?xml version="1.0" encoding="utf-8"?> <!-- This component hosts two textboxes, one bound to a more private model, and one to the global model. It also has a textbox that display messages received only within the closest context. --> <mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml" width="380" height="82" borderStyle="solid" cornerRadius="5" horizontalScrollPolicy="off" verticalScrollPolicy="off" backgroundColor="#0538F6" backgroundAlpha="0.15" xmlns:parsley="http://www.spicefactory.org/parsley"> <!-- Configure is how you tell Parsley that you want this view to become part of the Context and participate in all of Parsley’s features. <Configure /> looks for the closest Context in the view hierarchy and attaches the view to that particular context. In this case, this view becomes part of the context of its ancestors, no new context were created in this file. --> <parsley:Configure /> <!-- The text box bound to the more local model --> <mx:Canvas x="0" y="0" width="190" height="50" cornerRadius="5" borderStyle="solid"> <mx:Label x="10" y="2" text="Local Context" fontWeight="bold"/> <mx:TextInput x="10" y="20" id="localTextBox"/> </mx:Canvas> <!-- The text box bound to the global model --> <mx:Canvas x="190" y="0" width="190" height="50" borderStyle="solid" cornerRadius="5"> <mx:Label x="10" y="2" text="Global Context" fontWeight="bold"/> <mx:TextInput x="10" y="20" id="globalTextBox"/> </mx:Canvas> <!-- Binding is set with the special Flex Binding Tag to allow bidirectional binding. --> <mx:Binding destination="localTextBox.text" source="localModel.sharedString" /> <mx:Binding destination="localModel.sharedString" source="localTextBox.text" /> <mx:Binding destination="globalTextBox.text" source="globalModel.sharedString" /> <mx:Binding destination="globalModel.sharedString" source="globalTextBox.text" /> <!-- This is the text box that displays local messages --> <mx:Label x="0" y="58" text="Received Local Message:"/> <mx:TextInput x="148" y="54" width="220" editable="false" id="receiveMessageTextB" enabled="true" color="#000000" backgroundColor="#BCD0D6"/> <mx:Script> <![CDATA[ import model.GlobalModel; import model.LocalModel; import messages.LoggerMessage; /*********************************************** * LocalModel is declared in the Local Context it will be unique * only within that specific view * */ [Inject] [Bindable] public var localModel:LocalModel; /*********************************************** * You can still access objects from a parent context. GlobalModel * is from the context the grandparent of this component. You can * access objects of any context of any ancestor of this view * */ [Inject] [Bindable] public var globalModel:GlobalModel; /*********************************************** * MessageHandler declares that this function should be called * when any message of type LoggerMessages is dispatch. Notice * the special scope tag. This indicates that this particular * function will only be called when the message is dispatched from * the same context this view is attached to. * */ [MessageHandler (scope="local")] public function receiveLocalMessage(logMessage:LoggerMessage):void { receiveMessageTextB.text = logMessage.message; } ]]> </mx:Script> </mx:Canvas>
SharedComponents has a TextBox bound to the LocalContext and one to the GlobalContext. Changes to the Local box will only be within the parent's Context, while the global view affects all SharedComponents. Finally take a look a the [MessageHandler (scope="local")]. This handler will only be invoked when the message was dispatched from withing it's local context. In this case from the context of it's parent LocalView.
Well, that's it. Hope you find this post useful. I apologize for the long delay in posting it. If you have any other particular question leave a comment. There will be more examples in the near future.
Next is the Decoupled Bindings Example.
you have the best Parsley primer on the web
ReplyDeleteperhaps you could do one listing pros and cons of doing the same thing in different ways?
for example, cases to use [Publish] [Subscribe] instead of [Inject] or what's the advantage/disadvantage of [ManagedEvents]
thanks and keep up the great work!
Thank you. I just posted a Decoupled Bindings example ([Publish][Subscribe]) and it has some comments on the difference of it in regards to regular Injection. Personally, I have mixed feelings regarding Decoupled Bindings. There are features of it I really like, and some that I don’t like, one of them is mentioned right at the end of that note.
ReplyDeleteI am planning on doing a conclusions post towards the end of the series, and I will certainly take your suggestion and do some comparisons in it.
"The SharedComponents which holds no context and is a child of LocalView." Since you define SharedComponents with tag, you should have saif, the view is a part of the LocalView's context... Right?
ReplyDeleteActually, it doesn't depend on if it defined with a MXML tag or not. You could add it with action script and it would be the same. What really matters is who it is a VIEW child of.
ReplyDeleteNow, SharedComponents will have access to the context of any of it's VIEW ancestors. That includes LocalView's context and the Global Context also, as shown in the example.
But yes, the SharedComponents view itself is wired into the closest context it can find in it's view hierarchy, which in this case is it's LocalView's context.
Hello, nice article, things are now getting a bit more complex :)
ReplyDeleteThe thing which is still not obvious for me is why we end up with having the _viewNumber property inside the model, it seems strange the model has to know anything about the view.
But if I'm not wrong, it's never used for anything except logging, I mean this property is not used for the "mechanics", so I guess it's more for illustration purpose
Actually there is a "special" model called the "Presentation Model" which knows ALOT about the view. There is an article about this later in the series.
ReplyDeleteYou can model anything. A model is "supposed" to hold state, behavior, and data. Generally speaking, in an MVC application the model is tailored more towards modeling the "Domain" problem and not so "Presentation" problem. What that means is that there is a tendency to have your models represent database views or tables, but you can also have models that hold the state, behavior, and data of your views, and you can mix and match.
You are right about that the viewNumber being used for illustration purposes in this example, but I suggest you take a look at the presentation model.
http://artinflex.blogspot.com/2010/11/presentation-model-implemented-in.html
Ok I see, indeed you're right as Java/J2EE developer, "model" is more "domain model" for me. But that's part of the fun to try new things, so I'm definitely looking forward to the presentation model!
ReplyDeleteNice series of tutorials, thanks!
ReplyDeleteI've translated this one into Flex 4. You can download it on my blog:
http://flexperiential.com/2011/02/05/flex-4-version-of-arts-flex-notes-project/
Art, your examples have been a great resource and help, thank you for sharing. With the spice lib forums being down (at least for me) I'm hoping maybe this finds someone who can provide some insight to a lifecycle issue I am facing.
ReplyDelete(Example of a injected presentation model dependency being destroyed in a sub context.)
An Air application with a single window containing the global context. The window component has two states. view1 is included in state1 and view2 is included in state2. Each of these flex views defines their own context which declare a PM, and Controller. (i.e. viewOnePM, viewOneController, viewTwoPM, viewTwoController) Each of these two views inject their respective pmodel and controller, and each of their subviews inject their respective pmodel.
The issue is when the application view state changes the methods marked [Destroy] in the leaving view's controller and PM are being called (i have a trace statement in there for debugging.) And the new incoming view's controller and PM are [Init]ed. Great! Right until I switch states back to the first view. The controller and PM are gone, and not re-injected because the view is not actually gone. The subviews properties are still held at what their last binding update was from the PM, but the PM is now gone, and since the controller is as well any method calls to the controller hit a null object and crashes. What am I missing about how objects in sub contexts are injected into views. I understand why they are being destroyed I think, but why are they not being re-inited?
@Pelted
ReplyDeleteIn regards to a View, [Init] and [Destroyed] go along the way of addedToStage, and removedFromStage. The problem is that Flex may or not dispatch the addedToStage event multiple times, depending if there is a scroller or not, and some other things. Parsley is smart about this and only calls [Init] once if it detects those cases when addedToStage get's fired multiple times.
That said, I don't think that Parsley is mishandling your stage events. However, do put a trace on the addedToStage event to see if it's being fired at all, and check whether your context was removed or not.
Flash does not have a Destructor or any event that get's dispatched when an object is about to get garbage collected so the added/removed from stage events are the best bet.
You can probably change the itemDestructionPolicy and that will fix your issue but I have had unexpected results with itemDestructionPolicy in the past.
I setup a very simplistic test app and I am able to get the multiple contexts working as expected. What is very odd is shown here bellow.
ReplyDeleteThis is the console output from the trace statements. init() and destroy() are just methods tagged with [Init] and [Destroy] for parsley.
What does not make sense is marked with the asterisk. The method marked [Init] on the model and the controller are NOT the constructors, but according to the docs these methods are safely used after injection. Anyway, when the state changes View1 is removed from the stage and View2 is added. View2's model and controller are injected into the view as expected and the view works. View1's controller and model have the method marked [Destroy] called by parsley. When I change the state back to state 1 and View1 is re-added to the stage it's model and controller do not get their methods marked with [Init] called by parsley again yet the view (in this case) still works. This is counter intuitive.
According to the docs:
"The methods marked with [Destroy] get invoked after the Context instance they belong to has been destroyed with Context.destroy() or when the object was removed from the Context."
I realize there is no destructor in Flash, but according to the docs, I don't see how the methods marked [Destroy] get called thus implying the context is destroyed yet the bindings and actions from the view to the controller still work when the view is re-added to the stage yet no indication of the context being recreated by the [Init] methods being called ever happens.
In my actually application that is much more complex in layout the actions from the view to the controller that was [Destroy]ed result in exceptions when view is re-added later because of this. On a larger application my "guess" is that the objects managed non-view objects once removed from the context are made available for garbage collection. My thought is that this is what is happening to me on the MUCH larger application. In this small test case it works, but does not make since based on my understanding of the object lifecycle.
--- Example from traces ---
INFO: Initialize Flex Support
Application State Changed: state1
View1_PM - init():
View1_Controller - init():
View1 - AddedToStage
View1_Controller - onButtonClick():
View1_Controller - onButtonClick():
View1 - RemovedFromStage
View2 - AddedToStage
View2_PM - init():
View2_Controller - init():
Application State Changed: state2
*View1_Controller - destroy():
*View1_PM - destroy():
View2_Controller - onButtonClick():
View2_Controller - onButtonClick():
View2 - RemovedFromStage
View1 - AddedToStage
Application State Changed: state1
View2_PM - destroy():
View2_Controller - destroy():
View1_Controller - onButtonClick():
View1_Controller - onButtonClick():
I can't set up a test project like you have today, but I'll get around it and let you know what I find out.
ReplyDeletegreat feature,great tutorial and great explanation. Will the number of contexts affect performance of the application ?
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteHi, I 've a problem!. I have s:application .. that loads a module with mx:ModuleLoader id="Login" url="Login.swf" , Login is an s:module with parsley:ContextBuilder config="{LoginConfig}" in its fx:declaration section, and LoginConfig is and mxml parsley:objects . Next line to contextbuilder tag in login module is configure/ , well .. i'have an mx:button click=onClick() .. / and in same module, in fx:script section, i 've :
ReplyDelete[MessageDispatcher]
public var dispatcher:Function;
private function _onClick():void {
dispatcher(new LoginRequestMessage(LoginRequestMessage.LOGIN_REQUEST_MESSAGE));
}
ok: my loginConfig parsley:object defines the command wich is configured for this message with selector attribute, and a service is configured too..
AND THIS IS THE POINT:
ALL IS WORKING OK !! BUT .. it's working ok if a change login s:module to s:panel or anything else but not a module!, even using the global s:app context wich is an s:app with mx:moduleLoader.. login.swf .. (yes it has its own global context, and the module converted to another thing different to a s:module (ie s:panel etc) works with his conextBuilder attrib param - his own context - loginContext , where message, cmd, etc are confired ok, even if a create a new project and use that module as an s:app ,converting his loginModel to an unique model with all this conf, all works ok.
SO, IN CONCLUSIÓN:
ALL works ok if i'have an s:app with his contxt and a view loaded from main app , with another conext (viewContext) , and all procces (btn- onClic- [messageDispacther]- parsley- "commandAsigned".execute(..).. Works!
But IT NEVER WORKS IF A Change Login component to a s:module. !!!
When btn is displayed and user click btn, onCLick executes , [messageDispatcher] function is not null, onClick calls that function for parsley to work with conf, passing an instance of the message class neededand configured, correctly ,with is all configured in it's s:object ..
BUT THE WHEN I EXECUTE THAT LINE CODE:
dispatcher(new LoginRequestMessage(LoginRequestMessage.LOGIN_REQUEST_MESSAGE));
NOTHING IS HAPPENING!.. THERE IS NO DISPATCHING OF EVENT !!!
My problem is then, How to dispatch a message well configured in a Module wich has a moduleContext ( param of its contextBuilder tag ) and its conf there, and not in the s:app context (s:app loads the module with mx:moduleLoader tag)
Observation: If a put all configuration, message-command , service, etc, ( that is in loginConfig context), in s:app context (global context) and I use modules ( login as s:module) it WORKS!! So.. the problem seems to be that , with modules, you need to do something else ( some extra configuration ) because standard configuration that works when using s:app contextBuilder=globalCOntext and viewComp:panel contextBuilder=compContext , when the comp is a module, it is aparentely still attached to the global context .. but when debugging i see loginCong ( local context in the watch debugging window in my flashbuilder) but i'don't know then what is happening ! , message is not disptaching when a use the module but dispatchs ok when i change from comp: mx:module to any uicomp , and it works too if a leave the login comp as an mx:module but i put all conf in the s:app context ( the global context used in contextBuilder tag in s:app )
sorry my english and the long explanation, but it's my first post, really i am lost about this porblem, and I really Need to solve it as soon as possible
Thanks for reading and responses!