Why change in ArrayCollection's length doesn't invoke Setter on component using it as data source? - apache-flex

I have a component where I expose the property 'questions' with the following code:
private var _questions:ArrayCollection;
private var questionsChanged:Boolean;
[Bindable("questionsChanged")]
public function get questions():ArrayCollection {
return _questions;
}
public function set questions(value:ArrayCollection):void {
if (_questions != value) {
_questions = value;
questionsChanged = true;
invalidateProperties();
dispatchEvent(new Event("questionsChanged"));
}
}
In this component, I use commitProperties() to implement my logic.
I use Cairngorm and the 'questions' is in the model and hence it's defined as a source for data binding.
When the 'questions' ArrayCollection's size changes elsewhere in the application, it is not invoking the setter method in the component that is destination for the data binding.
Could someone help me understand why this is the case?

You'll have to show the code where you are changing the array collection. But, this will fire the setter:
questions = somethingArrayCollection();
This will not:
questions.addItem(newQestion)
The questions variable is, basically, a pointer. Changing the thing that the variable points to does not need the set event.
I suggest you look at the CollectionChangeEvent, which the ArrayCollection fires when items are added to and from that. Listen to the event and perform your 'change' actions in the event handler. ( or tie into the lifecycle and invalidate some flag and perform your changes in commitProperties() )

Related

Cleaning up bindings and change listeners on nested properties when parent properties change in javafx

I have a model class with SimpleXXXXProperty properties. Javafx GUI elements are updated using either bindings or change listeners, e.g.
textField.textProperty().bind(myModel.myModelStatus());
or
myModel.myModelStatus().addListener((obj,oldv.newv) -> { update here });
When the instance of the model class changes, I rebind the controls and add the listeners again. However, I can see by the memory use that the old model still persists in memory.
What do I have to do to remove all references to the model so it can be cleaned up?
Unbind before binding again
Remove the listeners
Both
Is there are more automatic way of updating bindings and listeners on nested properties when the parent property changes?
Points to consider when you want to undo bindings (including listeners) to your model:
Undirectional bindings (p1.bind(p2)) are automatically unbound when binding the same property again (e.g. p1.bind(p3)), but it does not hurt to do it explicitely (p1.unbind()).
Bidirectional bindings (p1.bindBidirectional(p2) or Bindings.bindBidirectional(p1, p2)) have to be unbound explicitely (p1.unbindBidirectional(p2) or Bindings.unbindBidirectional(p1, p2)).
Listeners must be unregistered (prop.removeListener(l)).
The third is the tricky part, as listeners are often implemented as lambda expressions or method references. Unfortunately, lambda expressions as well as method references(!) are not constant:
// lambdas are not constant
InvalidationListener l1 = obs -> {};
InvalidationListener l2 = obs -> {};
assert l1 != l2; // they are NOT identical!
Well, this might be obvious for lambdas, but the same is also true for method references, which is really annoying:
// method references are not constant
Runnable runnable1 = this::anyMethod;
Runnable runnable2 = this::anyMethod;
assert runnable1 != runnable2; // they are NOT identical!
That means, you cannot register a lambda expression or a simple method reference as listener if you want to be able to unregister it:
// if you register listeners on a property like that...
label.textProperty().addListener(obs -> System.out.println(obs));
label.textProperty().addListener(this::handleLabelInvalid);
// ...these calls WON'T remove them due to the inequality shown above!
label.textProperty().removeListener(obs -> System.out.println(obs));
label.textProperty().removeListener(this::handleLabelInvalid);
Solution
You have to store a reference to the lambda expression or method referency by yourself. I use to use final fields for that:
public class MyClass {
// store references for adding/removal
private final InvalidationListener l1 = this::handleLabelInvalid;
private final InvalidationListener l2 = obs -> System.out.println(obs);
...
public void bind() {
label.textProperty().addListener(l1);
label.textProperty().addListener(l2);
}
public void unbind() {
label.textProperty().removeListener(l1);
label.textProperty().removeListener(l2);
}
private void handleLabelInvalid(Observable observable) { ... }
}

Flex ItemRenderers

I know an ItemRenderer is a ClassFactory and that you can use the newInstance method of ClassFactory to get an instance of the ItemRenderer. My question, however, is is it possible to use methods of the ItemRenderer without using ClassFactory.newInstance()?
In my case I can't use this newInstance method because it doesn't keep the state.
Is there any way I can do this? Thanks!
An ItemRenderer is a component, like any other. The itemRenderer property of a list based class has a value of a ClassFactory. If you have a reference to an instance of the itemRenderer component, you can call methods on it.
You cannot call a method on any component if an instance if that component instance has not been created yet. So to call a method on an itemRenderer without using ClassFactory.newInstance() you must manually create your own instance using the new keyword.
You might want to implement the ItemRenderer as smart as it is needed to recreate the state depending in the data being set. On the other hand, make sure that the data contains everything needed. You barely want to interact with the renderers in a different scope then the renderer itself.
If it should necessary, a DataGroup dispatches a RendererExistence event when a renderer is added.
private function newList():List {
const list:List = new List();
list.addEventListener(FlexEvent.INITIALIZE, list_initializeHandler);
return list;
}
private function list_initializeHandler(event:FlexEvent):void {
const listBase:ListBase = ListBase(event.target),
dataGroup:DataGroup = listBase.dataGroup;
dataGroup.addEventListener(RendererExistenceEvent.RENDERER_ADD, dataGroup_rendererAddHandler);
dataGroup.addEventListener(RendererExistenceEvent.RENDERER_REMOVE, dataGroup_rendererRemoveHandler);
}
private function dataGroup_rendererAddHandler(event:RendererExistenceEvent):void {
// renderer added
}
private function dataGroup_rendererRemoveHandler(event:RendererExistenceEvent):void {
// renderer removed
}
This is the way to go if you need to reference single item renderer instances.
Do you mean static functions and variables?
If you define a function (or variable, or const) as static, it is accessible via the class name, so you could define
class MyClass {
public static const className:String="MyClass.className (const)";
public static function getClassName():String {
return "MyClass.getClassName (function)";
}
}
trace(MyClass.className); //prints "MyClass.className (const)"
trace(MyClass.getClassName()); //prints MyClass.getClassName (function)

Flex detect CollectionEvent change

On one PC, I have a datagrid with dataprovider being an arraycollection populated by a collection of records retrieved from MySQL DB. On another PC, another component retrieve the same collection of records and update them. Instanly, the datagrid in PC one reflected the changes that were made in PC two which is nice because it pushes the changes to all affected controls online. However, I want to detect the the arraycollection changes to do some other things but such CollectionEvent.COLLECTION_CHANGE is not detected. Can you help please? Here is the code:
protected function doInit():void
{
acLeave.addEventListener(CollectionEvent.COLLECTION_CHANGE, onAcLeaveChange);
}
protected function onAcLeaveChange(event:CollectionEvent):void
{
do something
}
I am using lcds and the data management service handled the data synchronization already. That is why the first pc with the data grid data provider acLeave changed automatically. Somehow it is because lcds knows there is a client (pc one) online, then it pushes the changes to it. My question is that the datagrid data changed, I want to detect there is a data change occur so that I can do some other updates. Normally, to detect datagrid change, I can use datagrid datachange or simply addlistener to the data provider for collectionEvent.COLLECTION_CHANGE but in this case even though I can see the ac acLeave changed, the event did not fire. Please help!
Hi, me again and thanks for your advise. I have added the setter to acLeave but still unable to listen the collectionEvent change. Here is the modified code:
private var _acLeave:ArrayCollection = new ArrayCollection();
[Bindable]
public function get acLeave():ArrayCollection
{
return _acLeave;
}
public function set acLeave(value:ArrayCollection):void
{
_acLeave = value;
}
protected function doInit():void
{
acLeave.addEventListener(CollectionEvent.COLLECTION_CHANGE, onAcChange);
}
protected function dataGrid_creationCompleteHandler(event:FlexEvent):void
{
getAllResult.token = leaverequestService.getAll();
getAllResult.addEventListener(ResultEvent.RESULT, onGotResult);
}
protected function onGotResult(event:ResultEvent):void
{
acLeave = getAllResult.lastResult;
}
protected function onAcChange(event:CollectionEvent):void
{
// this never executed because unable to detect a change on acLeave
Alert.show("acLeave Changed !");
}
If your collection change handler is not firing, I am guessing something like this is happening:
pc1 adds collection change listener
pc2 changes the collection
LCDS sends pc1 a brand new ArrayCollection object (not just the element that changed) to use
The original ArrayCollection you are listening for the collectionChange is not necessarily getting changed, it is getting replaced. So no collection change event ocurrs.
If you add a setter method for this ArrayCollection (acLeave in your example), then you will know if this ever happens. Technically, you should use both the collection change event and this setter to be able to detect all the cases when the array could be changed.

Data binding across multiple objects in Flex 3

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.

Binding to a read-only getter in AS3

Consider the following code:
[Bindable(event="ReportHeaderVO_effectiveFromDateJulian_updated")]
public function set effectiveFromDateJulian ( value:Number ) : void
{
_effectiveFromDateJulian = value;
dispatchEvent( new FlexEvent("ReportHeaderVO_effectiveFromDateJulian_updated") );
}
public function get effectiveFromDateJulian () : Number
{
return _effectiveFromDateJulian;
}
public function get effectiveFromDate () : Date
{
return DateUtil.convertJDEJulianToDate(_effectiveFromDateJulian);
}
There is a setter and a getter for the effectiveFromDateJulian which is a number representation of the date. I have provided a seperate getter which retrieves the same value, only converted to a proper date. It is a getter only though and relies on the setter for the numeric property to get its data from; so the effectiveFromDate property is effectively read-only.
Data binding works on the effectiveFromDateJulian property; any updates work fine and notify everything properly. But when binding to the effectiveFromDate (getter only) property, I get a warning from the compiler:
warning: unable to bind to property 'effectiveToDate' on class 'com.vo::ReportHeaderVO'
Is there a way to make it possible to bind to this read-only property? I would assume I would have to dispatch an event on the setter that effects the read-only property, but I don't know what that would look like.
This is a simple example, you could imagine a read-only property that depends on several setters to function and when any of those setters are updated the read-only property would need to fire a propertyChanged event as well. Any ideas? Please let me know if I need to clarify anything.
Update:
From the Adobe documentation here:
http://livedocs.adobe.com/flex/3/html/help.html?content=databinding_8.html
Using read-only properties as the
source for data binding
You can automatically use a read-only
property defined by a getter method,
which means no setter method, as the
source for a data-binding expression.
Flex performs the data binding once
when the application starts.
Because the data binding from a
read-only property occurs only once at
application start up, you omit the
[Bindable] metadata tag for the
read-only property.
And this makes sense for constant values, but in this case the value does change, it just doesn't get set directly.
Make the readonly getter Bindable and dispatch the corresponding event from the original setter method.
[Bindable(event="ReportHeaderVO_effectiveFromDateJulian_updated")]
public function set effectiveFromDateJulian ( value:Number ) : void
{
_effectiveFromDateJulian = value;
dispatchEvent( new FlexEvent("ReportHeaderVO_effectiveFromDateJulian_updated") );
dispatchEvent( new FlexEvent("ReportHeaderVO_effectiveFromDate_updated") );
}
[Bindable(event="ReportHeaderVO_effectiveFromDate_updated")]
public function get effectiveFromDate (date:Date) : Date
{
return DateUtil.convertJDEJulianToDate(_effectiveFromDateJulian);
}

Resources