Data binding across multiple objects in Flex 3 - apache-flex

I am new to Flex (got assigned to maintain an old project at work) and am having some trouble getting data binding to work correctly. I have a popup form class, AddOffer.mxml which uses a model AddOfferModel.as. On my popup form, I have the following component:
<mx:FormItem label="{getResource('addOffer.form.OFFER_DATE')}:"
labelWidth="90">
<views:OfferWindowDatesFragment
id="offerWindowField"
start="{model.offerStartDate}"
stop="{model.offerStopDate}" />
</mx:FormItem>
My AddForm.mxml file also has some embedded actionscript where I define my 'model' variable:
[Bindable]
public var model:AddOfferModel;
The model variables I am trying to bind to are standard getters/setters and look like this inside AddOfferModel.as:
[Bindable]
public function set offerStartDate(val:EditableInstant):void
{
_offerStartDate = val;
}
public function get offerStartDate():EditableInstant
{
return _offerStartDate;
}
private var _offerStartDate:EditableInstant;
[Bindable]
public function set offerStopDate(val:EditableInstant):void
{
_offerStopDate = val;
}
public function get offerStopDate():EditableInstant
{
return _offerStopDate;
}
private var _offerStopDate:EditableInstant;
Inside the OfferWindowDatesFragment component class, the start and stop variables look like this:
[Bindable]
public function set start(val:EditableInstant):void
{
_start = val;
}
public function get start():EditableInstant
{
return _start;
}
private var _start:EditableInstant;
[Bindable]
public function set stop(val:EditableInstant):void
{
_stop = val;
}
public function get stop():EditableInstant
{
return _stop;
}
private var _stop:EditableInstant;
Basically, I just want to bind the start and stop variables in my OfferWindowDatesFragment class to the offerStartDate and offerStopDate variables in the AddOfferModel.as file. Whenever I access the start/stop variables in functions inside the OfferWindowDatesFragment class, they are null.
I have an event listener function that gets triggered in OfferWindowDatesFragment anytime a user selects a new date, it looks like this:
private function changeOfferDate():void
{
start.currentValue = offerDateEditor.start;
stop.currentValue = offerDateEditor.stop;
}
Every time I reach this function, it throws up an error because 'start' and 'stop' are both null ... but should have been initialized and bound already. If I look at the variables in the debugger, I can confirm that values on the right side of the assignment expression are valid, and not what is causing the error.
I am not real familiar with how initialization works in Flex, and I assumed as long as I instantiated the component as seen in the first code snippet at the top of my post, it would initialize all the class variables, and setup the bindings. Am I missing something? Perhaps I am not properly initializing the model or class data for AddForm.mxml or AddFormModel.as, thereby binding null references to the start/stop fields in my OfferWindowDatesFragment class?
Any help would be greatly appreciated. Thanks!
EDIT:
I looked into this further and tried using Mate to inject the 'model' variable inside AddOffer.mxml with a valid AddOfferModel object:
<Injectors target="{AddOffer}" debug="{debug}">
<ObjectBuilder generator="{AddOfferModel}" constructorArguments="{scope.dispatcher}" cache="local"/>
<PropertyInjector targetKey="model" source="{lastReturn}" />
</Injectors>
I load the AddOffer.mxml dialog as the result of a button click event on another form. The function that pops it up looks like this:
public function addOffer():void
{
var addOfferDialog:AddOffer = new AddOffer();
addOfferDialog.addEventListener("addOffer", addOfferFromDialog);
modalUtil.popup(addOfferDialog);
}
It doesn't seem to be assigning anything to the 'model' variable in AddOffer.mxml. Does loading a view/dialog this way not trigger an injection from Mate by chance? (I realize this last part might belong in the Mate forums, but I'm hoping somebody here might have some insight on all of this).

In AddOffer.mxml, you have this code:
[Bindable]
public var model:AddOfferModel;
Is there something outside AddOffer.mxml that is setting this to a valid AddOfferModel? There should be. The nature of how the Flex component life cycle means that you can expect that things may be null at times as a View builds. So you should build your components to be able to "right themselves" after receiving bad data, if the data eventually comes good.
Data binding is one way to do this, but it may not paper over everything depending on what else is going on.
Have you verified that the model value you're getting is not null at the point where the user selects the date and that its offerStartDate and offerEndDate properties have been populated with valid EditableInstants? If both of those are correct, I'd start looking for pieces of the Views that expect to have stuff at a given instant and then don't recover if it is provided later.

Related

Passing data between views and models in Flex MVC framework

I have never used Flex/Actionscript before so excuse me if I'm asking anything obvious but I have been working on this for 3 days (includes searching google and stackoverflow) before writing this post.
I have been asked to modify a game written in Flex 4 made with a mini-IoC-based MVC framework. Two mods I have been asked to make is a game over screen, which displays the final score and a difficulty selection on the introduction screen. I have made the game over screen, and successfully managed to get the controller to bring it up when the game timer runs out.
So for now, the game structure goes:
Intro View -> Game View -> Game Over View
^ |
|__________ (retry?) ________|
The first problem is getting the score to be passed from the mainGame ActionScript file to the game over screen.
Things I have tried:
Importing mainGame in the gameOverViewBase and calling the mainGame.score variable in the gameOverView mxml file.
-EDIT!!! = the above method works if I change the score variable in mainGame to a constant, but if it remains a variable, the controller won't load the gameOverView and the game sits at an empty mainGame view.
Making a function that adds to a new score variable in the gameOverViewBase whenever the player scores during the game.
Passing the score as a parameter to the GameOverView when the MainGame ends
controller.loadGameOver(score);
This seemed like the most logical way to go about it. So I followed the function through the other components of the game setting the loadGameOver to take an integer as a parameter until I got to the main game actionscript file:
public function loadGameOver(score:int) : void
{
loadView(GameOverView);
}
The loadView function (shown below) is where I get stuck because I can't see where to pass the 'score' parameter. It looks like this:
private function loadView(viewClass:Class, modelClass:Class = null) : void
{
var view:View = new viewClass();
if(!view) throw new Error("Could not load view");
viewContainer.removeAllElements();
viewContainer.addElement(view);
view.controller = this;
}
The second problem is the difficulty selection on the introduction screen. I have done this with 3 buttons (easy, normal, hard) in the mxml file and for every button in the ActionScript:
protected function onEasyButtonClick() : void
{
set = "easy"
controller.loadMainGame(set);
}
Once again, I end up at the above loadView function.
To sum up: I need to know how to pass the data between the views and models. If that's not the ideal method, I am open to any other methods that you think are better.
Thank You!
Ben
P.S. I can send my source code to anyone who would like to help :)
You don't specify which MVC framework you're using which would be helpful. However, score should definitely be a property of a model and the model data should be accessible to the view either directly, perhaps via binding (thanks weltraumpirat), or via some intermediary class.
I would suggest you have a look at some of the existing view classes and try to figure out how they are fed the model data. You can use this approach to get the data you need for your view.
[EDIT]:
The mainGame property is not being set on your GameOverView instance so you're unable to access its score property either through binding or through trace. The loadView method of your controller class accepts a Model class reference which it uses to construct a new Model instance to be used by the new View. Unfortunately this is no use to you as your GameOverView needs the instance of MainGame which was created for the MainGameView (and which contains the current score).
I don't know if the following fits into the philosophy of the framework you're using. However, I would change the loadView method to accept an instance of a Model rather than a class reference, and create and cache a reference to an instance of MainGame when your controller is instantiated. That way you can pass the same Model reference to both the MainGameView and GameOverView when these are created.
public class WhackAMoleBase extends Application implements IGameController
{
public var viewContainer:Group;
private var mainGame:MainGame
public function WhackAMoleBase() : void
{
super();
// Create and cache an instance of the main game Model
mainGame = new MainGame();
addEventListener(FlexEvent.CREATION_COMPLETE, onCreationComplete);
}
public function loadIntroduction() : void
{
loadView(IntroductionView);
}
public function loadMainGame() : void
{
loadView(MainGameView, mainGame);
}
public function loadGameOver() : void
{
// Use the same instance of MainGame which the MainGameView
// has been using as the Model for the GameOverView
loadView(GameOverView, mainGame);
}
// Accept a Model instance rather than a class Reference
private function loadView(viewClass:Class, model:Model = null) : void
{
//Create a new instance of the supplied view
var view:View = new viewClass();
if(!view) throw new Error("Could not load view");
//Clear any previous views in the container and add
viewContainer.removeAllElements();
viewContainer.addElement(view);
//Property based dependency injection
view.controller = this;
//There may or may not be a model required for the
//requested view; check and instantiate appropriately
if(model)
{
//Give model reference to the controller
//and link the view up to the model
model.controller = this;
view.model = model;
}
}
private function onCreationComplete(event:FlexEvent) : void
{
//When the application is first created, we want to show the introductory view
loadView(IntroductionView);
}
}

Listen to bindable property in bindable Arraycollection

I've got a bindable model class (lets call it myModel) with two properties, label and value. The value gets updated frequently, so it is marked as bindable.
Works fine so far, the data is updated and the standard property change event is dispatched.
Now I have to make an ArrayCollection from the object instances of this model to use it as a data provider in a data group. The data gets then passed to a custom itemRenderer in which I access the myModel properties via data.label and data.value.
The only problem I've got now is that the myModel value property doesn't change any more (I suppose because I stored the objects in the ArrayCollection).
The ArrayCollection is marked bindable as well btw, because new object instances of myModel can be added during runtime.
Is there any way to make this work? Any help regarding this would be much appreciated!
Edit: I almost forgot, the value object in the myModel class is updated by another bindable class. Yes, I know that's bonkers but that's why I'm here, to get some input on a simpler (and in fact working) way to solve this problem.
2nd edit: Allright guys, a little bit of code to illustrate the issue;
Lets start with the first bindable class;
[Bindable]
public class FirstClass
{
public var name:String;
public var firstValue:Number;
public var secondValue:Number;
public var thirdValue:Number;
public function FirstClass()
{ }
}
The values (first to third) get updated by a controller class. So far so good.
Now to the second model class (for matters of consistency, lets keep the MyClass name)
[Bindable]
public class MyClass
{
public var label:String;
public var value:Number;
public function FirstClass()
{ }
}
These are the two model classes. Background behind this is that I need a String value (a label) for each property of an instance of FirstClass. I'd like to make this simpler, so I'm really not settled on this "solution" cough ;).
Anyhow, we've got the two models, now to my .mxml class;
[Bindable] private var firstClassInstance:FirstClass;
I create a new ArrayCollection and add objects like this;
myArrayCollection.addItem(new MyClass("This is a label", firstClassInstance.firstValue));
And again, the DataGroup uses this ArrayCollection as a data provider.
As we already established (thank you #Windowns), the ArrayCollection looks only for objects being added or removed, not property changes of these objects.
Call itemUpdated on your ArrayCollection when you update a "piece" of an item stored in it.
There could be many issues with binding. Please post code to help us see what is happening. Here are some "high level" things to watch out for that might answer your question
When using an bindable arraycollection of objects, it's important to note that the binding for the arraycollection only looks at each object instance and if it's added or removed from the collection. It will not know about any property changes that occur to your object. Commonly when you use an itemrenderer, the properties are bound to display elements. Like maybe the "value" property bound to a label in the itemrenderer. Now when your object instance (myModel) changes it's "value" property the label should pick it up. Also note that you need to mark any properties you intend to bind to visual elements with the [Bindable] meta-tag.
public class myModel
{
[Bindable]
public var label:String;
[Bindable]
public var value:String;
public function myModel() {}
}
Answer after code post:
When you do the following:
myArrayCollection.addItem(new MyClass("This is a label", firstClassInstance.firstValue));
You are taking the value of firstClassInstance.firstValue and supplying it as a hard value (as in not passing value by reference). So if you do the following:
myArrayCollection.getItemAt(addedClassIndex).value = 5;
Will not cause any changes to be noted in the firstClassInstance.firstValue as there is no "referening information" stored. We are only working with the basic type of Number which is never passed by reference like all other objects are in Flex.
Maybe try this:
[Bindable]
public class MyClass
{
public var label:String;
[Bindable] //might be redundant due to global [Bindable] flag, been a while since i've used a global one
public function get value():Number{
return m_objRef.firstValue;
}
public function set value(value:Number):void{
m_objRef.firstValue = value;
}
private var m_objRef:FirstClass;
public function MyClass(_label:String, _valueObj:FirstClass) {
m_objRef = _valueObj;
label = _label;
}
}
Allright guys (and gals ;)) after two hours of messing around with BindingUtils, I finally found the solution to my problem.
The two model classes can remain the way they are, so passing the instance of FirstClass isn't necessary.
Simply binding the value properties of FirstClass to the value field of MyClass works as expected and the values in the ArrayCollection get updated as well.
So the solution;
myClassObject = new MyClass();
myClassObject.label = "This is a label";
BindingUtils.bindProperty(myClassObject, "value", firstClassObject, "firstValue");
And then simply add the myClassObject to the ArrayCollection.
Keep in mind that all the code here is pseudo code, so never mind any typos.
Still, #Windowns suggesting with passing the FirstClass object to the MyClass will be incorporated into my final solution as it makes switching between properties a lot easier (FirstClass has got lots of them, not just the 4 in my first post). Many thanks for that!
I took Amy's advice and researched a little further on the itemUpdated method. Turns out, the solution was right there.
See here: http://flex4examples.wordpress.com/2009/08/28/1st/
I applied this methodology (with little variations) to my code and it works quite good. Performance on the iPad2 is good and so is the memory usage of my component.
Let's hope that Amy is fine with this solution as well. Fingers crossed. ;)

Can somebody please explain this common binding pitfall to me? (using the wrong bindable event name)

I refer to this site link text
Using the wrong event name in the
[Bindable] tag can cause your
application to not bind your property,
and you will not even know why. When
you use the [Bindable] tag with a
custom name, the example below looks
like a good idea:
public static const EVENT_CHANGED_CONST:String = "eventChangedConst";
private var _number:Number = 0;
[Bindable(event=EVENT_CHANGED_CONST)]
public function get number():Number
{
return _number;
}
public function set number(value:Number) : void
{
_number = value;
dispatchEvent(new Event(EVENT_CHANGED_CONST));
}
The code above assigns a static
property to the event name, and then
uses the same assignment to dispatch
the event. However, when the value
changes, the binding does not appear
to work. The reason is that the event
name will be EVENT_CHANGED_CONST and
not the value of the variable.
The code should have been written as
follows:
public static const EVENT_CHANGED_CONST:String = "eventChangedConst";
private var _number:Number = 0;
[Bindable(event="eventChangedConst")]
public function get number():Number
{
return _number;
}
public function set number(value:Number) : void
{
_number = value;
dispatchEvent(new Event(EVENT_CHANGED_CONST));
}
I agree, the wrong example does look like a good idea and I would do it that way because I think it's the right way and avoids the possibility of a typing error. Why is the name of the constant used instead of it's value? Surely this can't be right?
I appreciate your insights
Because the standard Flex compiler isn't that clever at times... and I feel your pain! I've complained about this exact problem more than a few times.
If I remember correctly, it's because the compiler does multiple passes. One of the early passes changes the Metadata into AS code. At this point in the compiler it hasn't parsed the rest of the AS code, so its not capable of parsing Constants or references to static variables in other files.
The only thing I can suggest is sign up to the Adobe JIRA, vote for the bug, and hope that the compiler fixes in 4.5 bring some relief.

Questions about bindable in Flex

Since I found the webpages explaning the bindable propety quite confusing,so I would like to post my question here,which is quite simple,if I declare a variable to be bindable,does that mean whenever I changed the value of this variable in another class,all appearence of this variable will be synchronized to be the same value at the same time?
Say,if boolean variable "select" is declared to be bindable in Class A and default to be false,and we have an if statement in class A like if(select).
Then in another class,we changed the value of "select" to be true,will that if(select) statement pass the test ?
Also,how about the following setter method that is defined to be bindable:
[Bindable]
public function set isShowingAvg(b:Boolean):void
{
_isShowingAvg = b;
hasChanged();
}
Does this code imply that changing the value of _isShowingAvg is also going to be broadcasted?
Thanks in advance.
Thanks for your idea.
Declaring a property as Bindable means that when you change the value, an event will get broadcasted. This event enables data binding, but it's not necessarily automatic.
If the consuming class is MXML and you use brackets, like this:
<mx:Button enabled="{selected}" />
Then the MXML compiler will generate the appropriate binding code and anytime selected changes, enabled will also get changed.
If you're using it outside MXML then you'll either subscribe to the event to detect changes or use BindingUtils.
In your example I think you need to mark the getter [Bindable] and not the setter.
example:
public static const SHOWING_AVG_CHANGED:String = "showingAvgChangedEvent";
[Bindable(event="showingAvgChangedEvent")]
public function get isShowingAvg():Boolean
{
return _isShowingAvg;
}
public function set isShowingAvg(isShowing:Boolean):void
{
_isShowingAvg = isShowing;
dispatchEvent(new Event(SHOWING_AVG_CHANGED));
}

flex doesn't seem to bind with custom actionscript object

I have a custom actionscript object defined as bindable with a number of public properties.
[Bindable]
public class MyObject extends Object {
public var mobileNumber:String;
...
In my mxml I have:
<mx:Script><![CDATA[
import mx.binding.utils.BindingUtils;
import org.test.MyObject;
[Bindable]
private var obj: MyObject = new MyObject();
]]></mx:Script>
<mx:Label text="Mobile Number" id="mobileNumberLabel"/>
<mx:TextInput id="mobileNumberText" text="{obj.mobileNumber}" />
<mx:LinkButton label="Load" id="loadButton" enabled="true" click="obj = obj.load();"/>
<mx:LinkButton label="Save" id="saveButton" enabled="true" click="obj.write();"/>
My issue is that when I enter a new value in the field for mobile number and then click the save button, the value typed is not logged out... i.e.:
public function write():void {
var bytes:ByteArray = new ByteArray();
trace("write - mobile:" + this.mobileNumber);
bytes.writeObject(this);
EncryptedLocalStore.setItem(KEY, bytes);
}
I also tried adding in:
private function init():void {
BindingUtils.bindProperty(mobileNumberText, "text", obj, "mobileNumber");
}
but no luck with that either.
I'm probably missing something simple here, but not sure what it is. Hope you can help, thanks.
tst's answer is correct - bindings are one-way. I'm guessing you already knew that though, since you tried to setup the reverse binding in your init() method.
However, there's two problems with your init() method.
First, it's not clear where you put that init() method, or what calls it.
Second, you got the method parameters backwards.
What I typically do in situations like this is either use the mxml tag as the first responder suggested, or if I'm in AS3 code, I do something like this:
private function onCreationComplete(event:Event):void
{
BindingUtils.bindProperty(obj, "mobileNumber", mobileNumberText, ["text"]);
}
Note a couple of points here:
1/ BindingUtils.bindProperty() is "left hand side = right hand side". Thus, it's kinda like writing
obj.mobileNumber = mobileNumberText.text;
Or, closer to what is "actually going on" inside the binding classes:
obj["mobileNumber"] = mobileNumberText["text"];
2/ BindingUtils.bindProperty() actually wants an array as the last param. This is so that you can do "chained properties" logically like:
obj.mobileNumber = mobileNumbersGrid.selectedItem.text;
which would be
BindingUtils.bindProperty(obj, "mobileNumber", mobileNumbersGrid,
["selectedItem", "text"]);
3/ Best practice tip: if you're binding a property chain whose initial member is a property of yourself (this), then write it as:
BindingUtils.bindProperty(obj, "mobileNumber", this,
["mobileNumbersGrid", "selectedItem", "text"]);
This neatly handles the case where your "right hand side object" this.mobileNumbersGrid instance itself is replaced with a new instance object.
4/ If you ever reassign obj ("the left hand side"), you need to create a new binding to the new obj instance. Typically, you do this by turning the local obj property into a getter/setter pair like this:
public function get obj():MyObject
{
return _obj;
}
private var _obj:MyObject = new MyObject();
public function set obj(value:MyObject):void
{
_obj = value;
BindingUtils.bindProperty(_obj, "mobileNumber", mobileNumberText, ["text"]);
}
(Note: to be really careful, you'd stash the returned value from BindingUtils.bindProperty() which is a ChangeWatcher instance, and you'd unwatch() to prevent the old object from receiving property changes)
Hope this helps. Bindings are among the most powerful tools in the Flex framework, but can cause a lot of headaches when used poorly. In particular, be aware of memory leaks and "unexpected updates" when bindings are left "hanging around".
This code:
<mx:TextInput id="mobileNumberText" text="{obj.mobileNumber}" />
makes a one-directional binding between obj.mobileNumber and mobileNumberText.text. What you need is an additional reverse binding - add the following line to your code and it will work:
<mx:Binding source="mobileNumberText.text" destination="obj.mobileNumber"/>
Did you try implementing IEventDispatcher, or extending EventDispatcher?
Bindings work through dispatching of PropertyChangeEvents, so maybe this is the problem?
also, I am not sure, whether bindings work on variables, or whether they require properties (getters/setters).
You should try compiling with -keep-generated-actionscript and see if the resulting code makes any sense, i.e. dispatches any events, if the variable is accessed ...

Resources