Unable to bind to property of custom component - apache-flex

Can't seem to bind to data from within a custom component. I've tried BindUtilis and {} but can't seem to fathom it out. Here's what I've got:
I have a class DataModel which has been made bindable
Within Mainn.mxml I have two components: DataGrid (used for testing) & CustomComponent (which extends Canvas)
When the data within DataModel.somelist is updated the DataGrid reflects the changes but the CustomComponent doesn't appear to.
I was expecting to see the trace (CustomComponent.dataProvider) fired whenever this._dataModel.itemList is changed. What am I doing wrong?
Main.mxml looks something like this:
<mx:Script>
<![CDATA[
import model.DataModel;
[Bindable]
private var _dataModel:DataModel = DataModel.getInstance();
]]>
</mx:Script>
<mx:VBox width="100%" height="100%">
<views:ItemDisplayList width="100%" height="300" id="idl" >
<views:dataProvider>
{this._dataModel.itemList}
</views:dataProvider>
</views:ItemDisplayList>
<mx:DataGrid id="dg" width="100%" height="300" >
<mx:dataProvider>
{this._dataModel.itemList}
</mx:dataProvider>
</mx:DataGrid>
</mx:VBox>
The CustomComponent has this AS class:
package code{
import model.DataModel;
import mx.containers.Canvas;
public class CustomComponent extends Canvas{
[Bindable]
private var _dataModel:DataModel = DataModel.getInstance();
private var _dataProvider:ArrayCollection ;
public function CustomComponent(){
super();
_dataProvider = new ArrayCollection();
trace("CustomComponent start");
}
public function get dataProvider() : ArrayCollection {
trace("get dataProvider");
return _dataProvider;
}
public function set dataProvider(value: ArrayCollection) : void {
trace("set dataProvider");
this._dataProvider = value;
invalidateProperties();
invalidateSize();
invalidateDisplayList();
dispatchEvent(new FlexEvent(FlexEvent.DATA_CHANGE));
}
...
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number ) : void{
trace("updateDisplayList");
super.updateDisplayList(unscaledWidth, unscaledHeight);
}
}
}

Are you resetting the dataProvider in your custom component, or by updating you mean adding/removing an item from dataModel.itemList?
If you're adding an item to dataModel.itemList, then your custom component wont update but the DataGrid will. This is because in the DataGrid (maybe it's in the core ListBase component in Flex?), when you set dataProvider, it adds an event listener for CollectionEvent.COLLECTION_CHANGE. When it hears that (when something in your ArrayCollection/IList changes, from remove/add/refresh/etc), it runs a few methods to update the DataGrid's itemRenderers. You'll have to code that manually if you create your own dataProvider property.
If you set the dataProvider explicitly on your custom component (custom.dataProvider = myArrayCollection), it will update. But that's not the most efficient thing to do. I recommend extending one of the List classes that has already implemented a dataProvider property, it's a lot to tackle.
Check out the source for the mx ListBase's dataProvider method to see what they did.
Hope that helps,
Lance

Related

How can you programmatically make the "mouseover" item into the selected item in a tree/list?

I would like to programmatically change a selected item, in a tree or list, to the item currently "marked/focused" under the mouse pointer .
I'm working with an Flex Air standalone application.
I was thinking in the lines of:
myTree.selectedItem = EVENT.TARGET (where EVENT could be a mouseover/rightclick/rollOver event, and TARGET should be the node/item currently under the mouse pointer).
Is there a way of doing this (or in any other way)?
Ahh, and i want to do it without left clicking ;-)
Thank you in advance,
Sebastian
I found this interesting enough so I am asking if this is the easiest way to achieve this. First off, instead of the list, you need to add the rollOver-listener to the ItemRenderer, not to the list itself (as the event.target and event.currentTarget will just show your list).
So lets create a custom ItemRenderer and add a rollOver listener
<xml version="1.0" encoding="utf-8"?>
<s:ItemRenderer xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
autoDrawBackground="true" height="20" rollOver="itemrenderer1_rollOverHandler(event)">
<fx:Script>
<![CDATA[
protected function itemrenderer1_rollOverHandler(event:MouseEvent):void
{
this.dispatchEvent(new CustomEvent(CustomEvent.SELECT_ITEM, data, true));
}
]]>
<s:Label id="label1" text="{data.label}"/>
</s:ItemRenderer>
You need to somehow get the value of the selected item (which is the data on the itemRenderer) so I created a CustomEvent-class just to do so.
package
{
import flash.events.Event;
public class CustomEvent extends Event
{
public var selectedItem:Object;
public static const SELECT_ITEM:String = "selectItem";
public function CustomEvent(type:String, selectedItem:Object, bubbles:Boolean=false, cancelable:Boolean=false)
{
super(type, bubbles, cancelable);
this.selectedItem = selectedItem;
}
}
}
then I added a eventListener to the main class and set the list.selectedItem property accordingly:
//for the main MXML initializer:
this.addEventListener(CustomEvent.SELECT_ITEM, rollOverChangeSelected);
//and the function:
protected function rollOverChangeSelected(ce:CustomEvent):void{
list.selectedItem = ce.selectedItem;
}
Another way: bindable variable
The list:
s:List id="list" allowMultipleSelection="true" selectionColor="red" rollOverColor="red" itemRenderer="customItemRenderer" selectedItem="{_rollOverSelectedItem}">
The variable and set / get methods:
[Bindable] public var _rollOverSelectedItem:Object;
public function get rollOverSelectedItem():Object
{
return _rollOverSelectedItem;
}
public function set rollOverSelectedItem(value:Object):void
{
_rollOverSelectedItem = value;
}
and the ItemRenderer's rollOver-method:
protected function itemrenderer1_rollOverHandler(event:MouseEvent):void
{
this.parentApplication.rollOverSelectedItem = data;
}
What is the best/proper way?

Access navigator from within custom IconItemRenderer in Flex

I have a class in Flex that crates a custom IconItemRenderer by extending the IconItemRenderer base class. I'm using this custom item within a list and listen to mouse press. Depending on the location of the mouse press, I have different options. One of which is to navigate to a different view. I know how to use the change listener of the list to push to a new view but don't want to implement it. The idea for the mouse click is that depending on the location, I can remove elements from the list or open up the current element.
For the life of me I cannot find a method to navigate to a new view from within the IconItemRenderer. This is the code I'm using, both the class and the list where I implement it.
package components
{
import spark.components.Button;
import spark.components.IconItemRenderer;
import spark.utils.MultiDPIBitmapSource;
public class DeleteItemRenderer extends IconItemRenderer
{
private var btn:Button;
private var xIcon:MultiDPIBitmapSource;
public function DeleteItemRenderer()
{
super();
super.iconWidth = super.iconHeight = 40;
super.labelField = 'title';
super.decorator = "assets/delete.png";
}
override public function set data(value:Object):void{
super.data = value;
}
override protected function layoutContents(unscaledWidth:Number, unscaledHeight:Number):void{
setElementPosition(decoratorDisplay, unscaledWidth-40, 5);
setElementSize(decoratorDisplay, 40, 40);
}
override protected function measure():void{
measuredHeight = 50;
}
override protected function drawBackground(unscaledWidth:Number, unscaledHeight:Number):void{
graphics.beginFill(0xffffff);
graphics.drawRect(0, 0, unscaledWidth, unscaledHeight);
graphics.endFill();
decoratorDisplay.smooth = true;
graphics.lineStyle(1,0xcccccc,1);
graphics.drawRect(0, 0, unscaledWidth, unscaledHeight);
graphics.endFill();
}
}
}
List
<s:List id="survey_list" visible="true" width="98%" height="70%" contentBackgroundColor="#FFFFFF" horizontalCenter="0">
<s:itemRenderer>
<fx:Component>
<components:DeleteItemRenderer width="99.9%" height="98%" verticalAlign="top" click="detectActionPress();">
<fx:Script>
<![CDATA[
import spark.components.ViewNavigator;
import spark.components.View;
import mx.events.FlexEvent;
import mx.core.FlexGlobals;
import mx.core.UIComponent;
import spark.components.List;
private var application:UIComponent = FlexGlobals.topLevelApplication as UIComponent;
private var pressOpen:Number = application.width - 40;
//private var _navigator:ViewNavigator = FlexGlobals.topLevelApplication.navigation; //navigation is not defined uhhhh why????
private function detectActionPress():void{
var localX:Number = this.mouseX;
if(localX <= pressOpen){
engangeElement();
}
else{
deleteElement();
}
}
private function deleteElement():void{
var parentList:List = owner as List;
parentList.dataProvider.removeItemAt(parentList.dataProvider.getItemIndex(data));
trace('element removed');
}
private function engangeElement():void{
var parentList:List = owner as List;
var _test:ViewNavigator = this.parentDocument as ViewNavigator;
//this.parentApplication.navigator.pushView(views.UnfinishedSurvey, parentList.selectedItem.shortcode)
_test.pushView(views.UnfinishedSurvey, parentList.selectedItem.shortcode);
}
]]>
</fx:Script>
</components:DeleteItemRenderer>
</fx:Component>
</s:itemRenderer>
</s:List>
Any ideas how I could push a new view from engageElement();
Why don't I have access to the navigator?
Thanks
Generally; I would recommend dispatching an event from inside the itemRenderer. Be sure that the event bubbles.
You can listen to the event on a class which is a hierarchical parent of the itemRenderer / List.
In the event handler you can access the navigator.
I wrote about the approaches here; which may provide more details.
There are alternate ways to do this. You could use a class that has an instance to the navigator and inject that into your renderer using some type of dependency injenction (DI) framework. Robotlegs and Swiz are two ActionScript based frameworks that support this.

Capture Event Generated By Value Object in Component

Let me try to explain this the best I can.
I have a component containing a data grid with the following data grid column
<mx:DataGridColumn dataField="email" headerText="Email Address">
<mx:itemRenderer>
<mx:Component>
<mx:VBox width="100%" horizontalScrollPolicy="off" verticalScrollPolicy="off" horizontalAlign="center">
<mx:TextInput id="tiEmailAddress"
width="95%"
text="{data.email}"
invalid="{data.isNotValidEmail(event);}"
valid="{data.isValidEmail(event);}"/>
<mx:EmailValidator id="validatorEmailAddress"
source="{tiEmailAddress}"
property="text"
required="true"
requiredFieldError="Email address is required."/>
</mx:VBox>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
Here's the guts of my value object:
[Bindable(event="contactChange")]
public class ContactVO extends EventDispatcher
{
private var _email:String = "default email";
public var selected:Boolean = false;
public var edited:Boolean = false;
public var isNew:Boolean = false;
public var isValid:Boolean = false;
public function ContactVO()
{ }
public function isNotValidEmail(e:Event):void
{
isValid = false;
email = e.target.text;
}
public function isValidEmail(e:Event):void
{
isValid = true;
email = e.target.text;
}
public function get email():String
{
return _email;
}
public function set email(value:String) : void
{
if (value != _email) {
_email = value;
edited = true;
}
dispatchEvent(new Event("contactChange", true));
}
}
Then back in the component, I have this which gets called at creationComplete
addEventListener("contactChange", processContactChange);
Through the Flex debugger, I can see the addEventListener statement called when the component is created, I can see the event fired from the value object when the validation is performed and the value changes, but processContactChange is never called, so I assume the event is never making it to my component.
Any idea what I've gotten wrong here? Thanks!
[SOLUTION]
The conversation with #Flextras below helped to deepen my understanding of this process and figure out where the disconnect was in my understanding. Basically, I changed the innards of my component's data column entry to the following:
<mx:TextInput id="tiEmailAddress"
width="95%"
text="{data.email}"
invalid="{data.isNotValidEmail(event);}"
valid="{data.isValidEmail(event);}"
creationComplete="{addListener(data)}"/>
<mx:Script>
<![CDATA[
private function addListener(data:Object):void
{
var eventDispatcher:EventDispatcher = data as EventDispatcher;
eventDispatcher.addEventListener("contactChange", outerDocument.processContactChange);
}
]]>
</mx:Script>
and removed the event listener from my creationComplete method
When you add the event listener you have to add it to the class that fires the event. In your code, you're adding it to the component that contains the DataGrid. Neither the itemRenderer, DataGrid, nor component containing the DataGrid fire the event.
IF the component containing the DataGrid has access to the ContactVO event, you can listen directly on that.
Otherwise, you can add an event listener in your itemRenderer.
If you absolutely need to execute code in the component containing the DataGrid, then the itemRenderer should listen on the Value Object for the event, then fire an event of it's own. Make sure that the 'itemRenderer' event bubbles; and it will move up the display hierarchy.

Flex Binding : Unexpected behaviour

I have noticed an unexpected behaviour with binding in Flex, my code is as follow :
Application code
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" horizontalAlign="center" xmlns:Model="Model.*">
<mx:Script>
<![CDATA[
import Model.DataDummy;
]]>
</mx:Script>
<mx:HBox width="100%" horizontalGap="30">
<mx:Button id="buttonChange1" label="Change property value" click="myDummy._resetMyProperty();" />
<mx:Label id="labelRaw" text="{'My Property=' + myDummy.MyProperty}" opaqueBackground="#DDDDDD" />
<mx:Label id="labelFormatted" text="{'My Property formatted=' + myDummy.MyPropertyFormatted}" opaqueBackground="#DDDDDD" />
</mx:HBox>
<Model:MyDummy id="myDummy" />
<mx:DataGrid id="dataGrid"
width="100%" dataProvider="{DataDummy.Dummies}">
<mx:columns>
<mx:DataGridColumn dataField="MyProperty" headerText="My property" />
<mx:DataGridColumn dataField="MyPropertyFormatted" headerText="My property Formatted" />
</mx:columns>
</mx:DataGrid>
<mx:Button id="buttonChange2" click="{for each ( var d:MyDummy in DataDummy.Dummies ){d._resetMyProperty();}}" label="Change property value in DataGrid" />
</mx:Application>
Model.MyDummy class code
package Model
{
import flash.events.EventDispatcher;
import mx.formatters.NumberFormatter;
import mx.utils.StringUtil;
[Bindable]
public class MyDummy extends EventDispatcher
{
/*** Constructor ***/
public function MyDummy()
{
this._resetMyProperty();
}
/*** Properties ***/
private var _myProperty:Number;
public function get MyProperty():Number
{
return _myProperty;
}
public function set MyProperty(value:Number):void
{
if ( value !== _myProperty )
{
_myProperty = value;
//var event:Event = new Event("ID_Changed");
//this.dispatchEvent(event);
}
}
//[Bindable (event="ID_Changed", type="flash.events.Event")]
public function get MyPropertyFormatted():String
{
var idFormatted:String = "";
if ( ! isNaN(this.MyProperty) )
{
var formatter:NumberFormatter = new NumberFormatter();
formatter.precision = 2;
idFormatted = formatter.format(this.MyProperty);
}
else
idFormatted = MyProperty.toString();
return StringUtil.substitute( "{0} (My property has been formatted)", idFormatted );
}
/*** Methods ***/
public function _resetMyProperty():void
{
this.MyProperty = Math.round(Math.random() * 1000000000);
}
}
}
Model.DataDummy class code
package Model
{
import mx.collections.ArrayCollection;
public class DataDummy
{
private static var _dummies:ArrayCollection;
public static function get Dummies():ArrayCollection
{
if ( _dummies == null )
{
_dummies = new ArrayCollection();
_dummies.addItem(new MyDummy());
_dummies.addItem(new MyDummy());
}
return _dummies;
}
}
}
The behaviour is as follow :
When I click on buttonChange1, _resetMyProperty is called on the instance myDummy.
The result is that the label "labelRaw" has its text changed and the label "labelFormatted" does not have its text changed. This does happen because MyPropertyFormatted is a readonly property and that readonly properties are binded only at the initialisation of the application and not afterwards, according to Flex documentation. With this, I agree.
When I click on buttonChange2, resetMyProperty method is called on every MyDummy element of the ArrayCollection Model.DataDummy.Dummies (this static property is binded to the DataGrid).
The result is that both columns of the DataGrid have their values changed, despite the fact that the DataGrid's second column is linked to the same readonly property MyPropertyFormatted of the MyDummy objects. I find this inconsistent with the previous behaviour I described.
My point is that :
1. On one hand, because I'm binding my controls to a single instance of an certain object, binding won't trigger on his readonly properties.
2. On the other hand, when I'm binding a control on a collection of the same certain objects, binding will trigger on every properties (readonly or not).
If I want binding to be triggered on readonly properties in point 1, I have to dispatch an event and precise on the readonly properties' MetaTag that their binding will be triggered according to this event (as show the commentaries in the code of the class Model.MyDummy class).
Why is this behaviour different ? I would like to precisely understand what an ArrayCollection instance's binding does that a single instance's binding does not.
Thank you for your help.
I suppose the right code is something like the following.
First, our model.MyDummy class:
package model
{
import flash.events.EventDispatcher;
import mx.events.PropertyChangeEvent;
import mx.formatters.NumberFormatter;
import mx.utils.StringUtil;
public class MyDummy extends EventDispatcher
{
//------------------------------------------------------------------------------
//
// Constructor
//
//------------------------------------------------------------------------------
public function MyDummy()
{
resetMyProperty();
}
//------------------------------------------------------------------------------
//
// Properties
//
//------------------------------------------------------------------------------
//--------------------------------------
// myProperty
//--------------------------------------
private var _myProperty:Number;
[Bindable(event="propertyChange")]
public function get myProperty():Number
{
return _myProperty;
}
public function set myProperty(value:Number):void
{
if (_myProperty == value)
return;
var oldPropertyValue:Number = _myProperty;
var oldFormatted:String = myPropertyFormatted;
_myProperty = value;
dispatchEvent(PropertyChangeEvent.createUpdateEvent(this, "myProperty", oldPropertyValue, value));
dispatchEvent(PropertyChangeEvent.
createUpdateEvent(this, "myPropertyFormatted", oldFormatted, myPropertyFormatted));
}
[Bindable(event="propertyChange")]
public function get myPropertyFormatted():String
{
var idFormatted:String = "";
if (!isNaN(myProperty))
{
var formatter:NumberFormatter = new NumberFormatter();
formatter.precision = 2;
idFormatted = formatter.format(myProperty);
}
else
idFormatted = myProperty.toString();
return StringUtil.substitute("{0} (My property has been formatted)", idFormatted);
}
//------------------------------------------------------------------------------
//
// Methods
//
//------------------------------------------------------------------------------
public function resetMyProperty():void
{
myProperty = Math.round(Math.random() * 1000000000);
}
}
}
We're firing propertyChange event to have possibility to fire collectionChangeEvent from our ArrayCollection (it listens propertyChange event automatically).
Then our model.DataDummy class:
package model
{
import mx.collections.ArrayCollection;
import mx.events.CollectionEvent;
public class DataDummy
{
//------------------------------------------------------------------------------
//
// Constructor
//
//------------------------------------------------------------------------------
public function DataDummy()
{
dummies = new ArrayCollection();
dummies.addItem(new MyDummy());
dummies.addItem(new MyDummy());
}
//------------------------------------------------------------------------------
//
// Variables
//
//------------------------------------------------------------------------------
[Bindable]
public var dummies:ArrayCollection;
}
}
We don't use statics to have advantage of data binding with [Bindable] metatag.
And finally our main class with minimal changes:
<mx:Application horizontalAlign="center" layout="vertical" xmlns:model="model.*" xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
//------------------------------------------------------------------------------
//
// Event Handlers
//
//------------------------------------------------------------------------------
protected function buttonChange2_clickHandler(event:MouseEvent):void
{
for each (var d:MyDummy in dataProvider.dummies)
{
d.resetMyProperty();
}
}
]]>
</mx:Script>
<mx:HBox horizontalGap="30" width="100%">
<mx:Button click="myDummy.resetMyProperty();" id="buttonChange1" label="Change property value" />
<mx:Label id="labelRaw" opaqueBackground="#DDDDDD" text="{'My Property=' + myDummy.myProperty}" />
<mx:Label id="labelFormatted" opaqueBackground="#DDDDDD"
text="{'My Property formatted=' + myDummy.myPropertyFormatted}" />
</mx:HBox>
<model:MyDummy id="myDummy" />
<model:DataDummy id="dataProvider" />
<mx:DataGrid dataProvider="{dataProvider.dummies}" id="dataGrid" width="100%">
<mx:columns>
<mx:DataGridColumn dataField="myProperty" headerText="My property" />
<mx:DataGridColumn dataField="myPropertyFormatted" headerText="My property Formatted" />
</mx:columns>
</mx:DataGrid>
<mx:Button click="buttonChange2_clickHandler(event)" id="buttonChange2" label="Change property value in DataGrid" />
</mx:Application>
As you can see all the bindings works as expected.
P.S. [Bindable(event="propertyChange")] is an equivalent of simple [Bindable] but this way you can avoid compiler warnings on myPropertyFormatted getter. Actually, using simple [Bindable] form causes mxmlc compiler to generate dispatchEvent code by itself. And you can use pointing a particular event in [Bindable] tag to have more control. For example in our case we can fire event for myPropertyFormatted.
P.P.S. I've changed your C#-like naming conventions to reflect actual ActionScript/MXML ones.

Flex List ItemRenderer with image loses BitmapData when scrolling

Hi i have a mx:List with a DataProvider. This data Provider is a ArrayCollection if FotoItems
public class FotoItem extends EventDispatcher
{
[Bindable]
public var data:Bitmap;
[Bindable]
public var id:int;
[Bindable]
public var duration:Number;
public function FotoItem(data:Bitmap, id:int, duration:Number, target:IEventDispatcher=null)
{
super(target);
this.data = data;
this.id = id;
this.duration = duration;
}
}
my itemRenderer looks like this:
<?xml version="1.0" encoding="utf-8"?>
<mx:VBox xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" >
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
]]>
</fx:Script>
<s:Label text="index"/>
<mx:Image source="{data.data}" maxHeight="100" maxWidth="100"/>
<s:Label text="Duration: {data.duration}ms"/>
<s:Label text="ID: {data.id}"/>
</mx:VBox>
Now when i am scrolling then all images that leave the screen disappear :(
When i take a look at the arrayCollection every item's BitmapData is null.
Why is this the case?
I changed Datatype of data in Class FotoItem from Bitmap to BitmapData
in the ItemRenderer i do the following:
override public function set data( value:Object ) : void {
super.data = value;
pic.source = new Bitmap(value.image);
}
this works now. No idea why it is not working with bitmaps
I think it might be something with your use of data.data - I believe data is a reserved keyword in Actionscript, and it might be best to name your image property something else, such as data.imageData.
I'm also not sure why you're importing ArrayCollection into your item renderer as you don't appear to be using it in your itemRenderer.
You may also be running into problems with itemRenderer recyling. You may want to override public function set data() and handle setting the individual item properties there instead of relying on binding.
Where are you looking at the arrayCollection to see that the bitmapData is null?

Resources