Updating itemrenderer dataprovider - apache-flex

I am creating an opensource application for managing environmental resources in my county.
I have a problem on a dataprovider for an itemrenderer (combobox) i am using on a datagrid.
The application works, but I get a warning saying the itemrenderer dataprovider will not be reassigned data on its update with a setter. Even if I do not need to reassign the combobox itemrenderer dataprovider, for a matter of best practice I would like to solve this warning.
This is the usual code I use for getting dataprovider data as an array collection populated from the result of a web service in the parentDocument of the itemrenderer:
//set farmers arrayCollection values for combobox itemrenderer
[Bindable]
private var _acFarmers:ArrayCollection=new ArrayCollection;
public function set acFarmers(acFarmers:ArrayCollection):void{
_acFarmers=acFarmers;
}
//get machines ArrayCollection values
public function get acFarmers():ArrayCollection{
return _acFarmers;
}
This is the code for the datagrid itemrenderer (showing only the interested column of the datagrid):
<mx:DataGridColumn headerText="Agricoltore" dataField="farmerId" width="200" rendererIsEditor="true" editable="false">
<mx:itemRenderer>
<fx:Component id="cmpCmbFarmers">
<mx:HBox>
<s:ComboBox width="200"
id="cmbFarmers"
dataProvider="{outerDocument.acFarmers}"
labelField="companyName"
change="onSelectionChange(event)"
>
<fx:Script>
<![CDATA[
import mx.controls.dataGridClasses.DataGridListData;
import mx.controls.listClasses.BaseListData;
import mx.events.ListEvent;
private var _ownerData:Object;
private function setSelected():void {
}
override public function set data(value:Object):void{
if(value != null) {
super.data=value;
_ownerData=value;
if(value.collectingMachineId!==null){
for each(var dp:Object in cmbFarmers.dataProvider){
var dpFarmerId:String=dp.farmerId
var dataFarmerId:String=value.farmerId;
if(dpFarmerId==dataFarmerId){
cmbFarmers.selectedItem=dp;
}
}
} else {
cmbFarmers.selectedIndex=0;
data.farmerId=cmbFarmers.selectedItem.farmerId;
}
}
}
import spark.events.IndexChangeEvent;
protected function onSelectionChange(event:IndexChangeEvent):void
{
data.farmerId=cmbFarmers.selectedItem.farmerId;
}
]]>
</fx:Script>
</s:ComboBox>
</mx:HBox>
</fx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
This code works if I call the itemrenderer service to get combobox data BEFORE calling the datagrid data service and setting the datagrid arraycollection at re response of the service.
BUT a warning is displayed because the combobox dataprovider will not get changes after a set function on its dataprovider (_acFarmers).
This is the only warning I have on an entire project but i did not manage how to solve it.
I would really appreciate any help.
Thanks
Paolo

You have to put [Bindable] metatag above your setter instead of above _acFarmers. Because right now your acFarmers property is not the source for data binding.
Or better use just public variable instead of getter and setter since you didn't implement other actions in them.
Link about databinding

Try to use dataChangeEvent
you can use the dataChange event with an item renderer or item editor. Flex dispatches the dataChange event every time the data property changes.
<mx:ItemRenderer dataChange="someFunction()">
or
dataToDisplay.addEventListener( FlexEvent.DATA_CHANGE, dataChange );

Related

Re-rendering a DataGrid

I have a datagrid within a custom component. This DG accepts a nested object as its dataprovider, and so my datagrid is rendered like this :
<mx:DataGrid id="privilegesDG" dataProvider="{privArray}" width="100%" variableRowHeight="true">
<mx:columns>
<mx:DataGridColumn dataField="Name" />
<mx:DataGridColumn dataField="Alias" />
<mx:DataGridColumn headerText="Roles Assigned" dataField="roles">
<mx:itemRenderer>
<fx:Component>
<mx:VBox creationComplete="box1_creationCompleteHandler(event)">
<fx:Script>
<![CDATA[
import com.pm.modules.events.UpdateDBEvent;
import mx.containers.HBox;
import mx.controls.Label;
import mx.controls.LinkButton;
import mx.events.FlexEvent;
[Bindable]private var prID:int;
protected function box1_creationCompleteHandler(event:FlexEvent):void
{
for each(var temp:Object in data.roles){
prID = temp.rid;
var hgrp:HBox = new HBox();
hgrp.autoLayout = false;
var lbl:Label = new Label();
lbl.text = temp.rname;
var lb:LinkButton = new LinkButton();
lb.label = 'X';
lb.focusEnabled = true;
lb.addEventListener(MouseEvent.CLICK,handleClick);
hgrp.addElement(lbl);
hgrp.addElement(lb);
this.addElement(hgrp);
}
}
protected function handleClick(event:MouseEvent):void{
dispatchEvent(new UpdateDBEvent(UpdateDBEvent.ON_DELETE_PRIVILEGE_ROLE_MAP,0,0,0,prID,0,true));
}
]]>
</fx:Script>
</mx:VBox>
</fx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
</mx:columns>
</mx:DataGrid>
So i have an inline itemrenderer that displays the nested data. Now eveytime an update operation occurs, i call a function to re-populate this datagrid. But the column which gets populated with the nested data dsnt display anything...
I searched the net and found that mayb i shld call the updateDisplayList function for this component ?
So i tried smthing like this :
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void{
super.updateDisplayList(unscaledWidth,unscaledHeight);
}
protected function handleResult(event:ResultEvent):void{
if(event.result.toString() == 'false')
Alert.show("Could not perform operation");
else{
RO.getPrivilegesAndRoles(); //re-populates DG
invalidateDisplayList();
}
}
I really dont know what I shld put inside that function. Should i call the re-populating function inside updateDisplayList. I tried that but it dsnt work ...
The creationComplete event of your item renderer occurs only one time on initialization. As far as item renderers are recycling the new data won't populate in your item renderer.
You can solve it two ways:
Create MXML item renderer. As far as its base class is VBox it is the best option here. So you can get rid of data binding to populate data changes automatically. To populate all the roles use Repeater component instead of your loop using {data.roles} as data provider.
The second option is to use ActionScript and to have UIComponent as the base class. In this case you should override override public function set data(value:Object):void and call invalidateDisplayList() from there as far as subscribing on data.roles's collectionChange event and call invalidateDisplayList() from change handler too. Then in updateDisplayList() you should loop your data and create/populate all the controls. The second option can give you better performance but it is not a good way for novices.
So I recommend you to use MXML version with binding.

ItemRender data change

I have a List with an ItemRenderer. When I click a button, then a property of the ArrayCollection I bound to the list is changed.
When I click the button, then it does change the property, but the list doesn't change.
How do I solve this.
Here's my code
<fx:Script>
<![CDATA[
[Bindable]
public var controllers:ControllerCollection = new ControllerCollection();
private function hideTheFirstItem(evt:MouseEvent):void
{
(controllers[0] as Controller).meetsRequirements = false;
//these methods don't work unfortunatly
controllers.itemUpdated(controllers[0]);
controllers.refresh();
}
]]>
</fx:Script>
<mx:List id="listControllers" dataProvider="{controllers}">
<mx:itemRenderer>
<fx:Component>
<solutionItems:displaySolutionItem visible="{data.meetsRequirements}" />
</fx:Component>
</mx:itemRenderer>
</mx:List>
<mx:Button label="test" click="hideTheFirstItem(event)" />
(ControllerCollection extends ArrayCollection)
Thanks!
Vincent
Two ways:
collection.refresh()
collection.itemUpdated()
Of course, ControllerCollection is not a standard Flex Collection class; so I am just assuming that it implements the ICollectionView interface.
Update:
I do notice that your code is set to modify the first element of the ArrayCollection
private function hideTheFirstItem(evt:MouseEvent):void
{
(controllers[0] as Controller).meetsRequirements = false;
//these methods don't work unfortunatly
controllers.itemUpdated(controllers[0]);
controllers.refresh();
}
I wanted to be sure to specify that the first element of the collection may not be the first element currently visible in the view. I wonder if that is causing you issues.
Without seeing your item renderer, I need to make some assumptions.
First, I will assume that your item renderer is using data binding to the meetsRequirements property. If that is the case, then the meetsRequirements property needs to notify when that property changes. If you add the [Bindable] tag to that property or the Controller class, then the meetsRequirements property will notify the itemRenderer to update that field based on your data binding.
If my assumptions are wrong, we need to see the code to give you any further thoughts.
First, don't try to create new collections if you don't need to.
I believe your problem lies with this statement: (controllers[0] as Controller).meetsRequirements = false; which should fail on compile because a collection item cannot be retrieved using the square bracket annotation. You need to use getItemAt(index:int) function.
Furthermore, you wouldn't want to set visible to false to an item renderer if you want to 'remove' it because then you'd have an empty spot. What you want to do is filter it out:
<s:Application creationComplete="onCreationComplete()">
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
[Bindable] public var data:ArrayCollection = new ArrayCollection();
private function onCreationComplete():void
{
// TODO: need to add data here
// Adding filter function
data.filterFunction = filterItems;
}
private function filterItems(item:Object):Boolean
{
return item.meetsRequirements;
}
private function hideFirstItem():void
{
if(data.length > 0)
{
Controller(data.getItemAt(0)).meetsRequirements = false;
}
data.refresh();
}
]]>
</fx:Script>
<mx:List id="listControllers" dataProvider="{data}" />
<mx:Button label="test" click="hideFirstItem()" />
</s:Application>
This should do it. Untested though.
Try this:
<fx:Script>
<![CDATA[
[Bindable(Event="refreshMyList")]
public var controllers:ControllerCollection = new ControllerCollection();
private function hideTheFirstItem(evt:MouseEvent):void
{
(controllers[0] as Controller).meetsRequirements = false;
dispatchEvent(new Event("refreshMyList"));
}
]]>
</fx:Script>

Flex 4 . Passing data from Main App to custom component

I havent found quite what I want elsewhere or at least am not accomplished enough to adapt
I'm trying to create a project that shows radio stations with logo, listen button etc
The datasource is an xml file with child tags like 'name','image' and 'listen' for each parent, 'station'.
I access this data via the main application with creationComplete="radioService.send()"
I can then use it on the main app via dataProvider="{radioService.lastResult.stations.station}
However, I want to use the results to populate variables in the custom component in both the components themselves
hard coded examples
mx:Image source="Images/BBC5.gif" id="bbc5Logo" "/>
mx:LinkButton id="bbc5Listen" click="bbc5Listen_clickHandler(event)" />
and in the clickhandler
protected function bbc5Listen_clickHandler(event:MouseEvent):void
{
var url:String = "http://www.bbc.co.uk/iplayer/console/bbc_radio_five_live";
var request:URLRequest = new URLRequest(url);
}
Any help, much appreciated
I done something similar before though I am not sure of what look and feel you wish your app to have but it would be possible to create a Datagrid and bind the datasource to that grid.
Then you can add fields to the datasource like ( img field and use the link in the XML by setting the xpath towards that field) , it would also be possible to create a button and place that in the row of the datagrid for listen.
Obviously this is a slightly different implementation but the same principle applys if you have created a custom component then make sure it makes use of DataSource and DataSource fields which will aquire the data from the right part of the XML. Then it would be possible to map that DataSource field to a String for example.
Hope this helps
You can actually use Custom Events, where you can pass values from your main app to your custom component.
http://www.anujgakhar.com/2009/03/04/flex-passing-data-with-custom-events/
Another approach could be to create Value Objects from your XML and bind your components to these objects, this way you would decouple your components from the XML , which can be handy if at some point in the future, you don't want to use XML, but JSON for instance.
Each Value Object would have the properties defined in your question , i.e , imageSource , id , url etc... and each component would be bound to one Value Object
Edit
Have a look at this video training, it covers data retrieval from an XML , ArrayCollections of ValueObjets etc...
http://www.adobe.com/devnet/flex/videotraining.html
there are two ways to accomplish this:
you should wrap your data structure
from xml source to a [Bindable] actionscript
object (not mandatory, but you can
handle it easier), and bind the array of these data instances (let's say RadioChannel.class) as the dataProvider of your grid. Inside your itemRenderer implementation you can access the current row's RadioChannel instance via the data getter/setter:
RadioChannel.as:
[Bindable]
public class RadioChannel {
private var _name: String;
private var _imageUrl: String;
private var _listenUrl: String;
public function RadioChannel()
{
}
public function get name():String {return _name;}
public function set name(value:String):void {_name = value;}
public function get imageUrl():String {return _imageUrl;}
public function set imageUrl(value:String):void {_imageUrl = value;}
public function get listenUrl():String {return _listenUrl;}
public function set listenUrl(value:String):void {_listenUrl = value;}
public function toString():String
{
return 'RadioChannel ' + name + ' - image: ' + imageUrl +
' | listen at ' + listenUrl;
}
}
RadioList.mxml:
<?xml version="1.0" encoding="utf-8"?>
<mx:DataGrid xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:mx="library://ns.adobe.com/flex/mx" dataProvider="{radioChannels}">
<fx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
private var _radioChannels:ArrayCollection = new ArrayCollection();
[Bindable]
private function get radioChannels():ArrayCollection {return _radioChannels;}
private function set radioChannels(value:ArrayCollection):void {_radioChannels = value;}
]]>
</fx:Script>
<mx:columns>
<mx:DataGridColumn>
<mx:itemRenderer>
<fx:Component>
<mx:Image source="{data.imageUrl}">
<fx:Script>
<![CDATA[
public override function set data(value:Object):void
{
super.data = value;
trace('current instance: ' + RadioChannel(data).toString());
}
]]>
</fx:Script>
</mx:Image>
</fx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
<mx:DataGridColumn dataField="name" />
<mx:DataGridColumn>
<mx:itemRenderer>
<fx:Component>
<mx:HBox>
<fx:Script>
<![CDATA[
protected function onLinkClicked(event:MouseEvent):void
{
var currentChannel:RadioChannel = RadioChannel(data);
if (data)
{
var request:URLRequest = new URLRequest(currentChannel.listenUrl);
[...]
}
}
]]>
</fx:Script>
<mx:Button click="onLinkClicked(event)" label="Listen" />
</mx:HBox>
</fx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
</mx:columns>
</mx:DataGrid>
The other way is to build up custom
events that deal with an extra
parameter: the url of the channel.
These custom events should cascade back to the list component, where they should be handled.
Since for the second solution you would still need the first's binding as well, i'd choose the first one, and handle the click event locally.
hope this helped.
I think this may be useful, to pass the value from mainApp to custom component you can follow this method.Since you have the value in mainApp,in the custom component you can access the value by using parentDocument object.parentDocument.rootAppVar

Flex DataGrid with ComboBox itemRenderer

I'm going spare trying to figure out the "correct" way to embed a ComboBox inside a Flex (3.4) DataGrid. By Rights (e.g. according to this page http://blog.flexmonkeypatches.com/2008/02/18/simple-datagrid-combobox-as-item-editor-example/) it should be easy, but I can't for the life of me make this work.
The difference I have to the example linked above is that my display value (what the user sees) is different to the id value I want to select on and store in my data provider.
So what I have is:
<mx:DataGridColumn headerText="Type" width="200" dataField="TransactionTypeID" editorDataField="value" textAlign="center" editable="true" rendererIsEditor="true">
<mx:itemRenderer>
<mx:Component>
<mx:ComboBox dataProvider="{parentDocument.transactionTypesData}"/>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
Where transactionTypesData has both 'data' and 'label' fields (as per what the ComboBox - why on earth it doesn't provide both a labelField and idField I'll never know).
Anyway, the above MXML code doesn't work in two ways:
The combo box does not show up with any selected item.
After selecting an item, it does not store back that selected item to the datastore.
So, has anyone got a similar situation working?
While Jeff's answer is a partial answer for one approach for this (see http://flex.gunua.com/?p=119 for a complete example of this being used to good effect), it isn't as general as I wanted.
Thankfully, I finally found some great help on Experts Exchange (the answers by hobbit72) describes how to create a custom component that works in a grid as a ItemRenderer.
I've extended that code to also support using the combo box as an ItemEditor as well. The full component is as follows:
<?xml version="1.0" encoding="utf-8"?>
<mx:ComboBox
xmlns:mx="http://www.adobe.com/2006/mxml"
dataChange="setSelected()"
change="onSelectionChange(event)"
focusEnabled="true">
<mx:Script>
<![CDATA[
import mx.events.DataGridEvent;
import mx.events.ListEvent;
import mx.controls.dataGridClasses.DataGridListData;
private var _ownerData:Object;
private var _lookupField:String = "value";
// When using this component as an itemEditor rather than an itemRenderer
// then set ' editorDataField="selectedItemKey"' on the column to
// ensure that changes to the ComboBox are propogated.
[Bindable] public var selectedItemKey:Object;
public function set lookupField (value:String) : void {
if(value) {
_lookupField = value;
setSelected();
}
}
override public function set data (value:Object) : void {
if(value) {
_ownerData = value;
setSelected();
}
}
override public function get data() : Object {
return _ownerData;
}
private function setSelected() : void {
if (dataProvider && _ownerData) {
var col:DataGridListData = DataGridListData(listData);
for each (var dp:Object in dataProvider) {
if (dp[_lookupField] == _ownerData[col.dataField]) {
selectedItem = dp;
selectedItemKey = _ownerData[col.dataField];
return;
}
}
}
selectedItem = null;
}
private function onSelectionChange (e:ListEvent) : void {
if (selectedItem && _ownerData) {
var col:DataGridListData = DataGridListData(listData);
_ownerData[col.dataField] = selectedItem[_lookupField];
selectedItemKey = selectedItem[_lookupField];
}
}
]]>
</mx:Script>
</mx:ComboBox>
Using this component is straight forward. As an ItemRenderer:
<mx:DataGridColumn headerText="Child" dataField="PersonID" editable="false" textAlign="center">
<mx:itemRenderer>
<mx:Component>
<fx:GridComboBox dataProvider="{parentDocument.childrenData}" labelField="Name" lookupField="PersonID" change="dispatchEvent(new mx.events.DataGridEvent(mx.events.DataGridEvent.ITEM_FOCUS_OUT, true, true))"/>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
Using this component is straight forward. And as an ItemEditor:
<mx:DataGridColumn labelFunction="lookupChildName" headerText="Child" dataField="PersonID" editable="true" editorDataField="selectedItemKey">
<mx:itemEditor>
<mx:Component>
<fx:GridComboBox dataProvider="{parentDocument.childrenData}" labelField="Name" lookupField="PersonID" change="dispatchEvent(new mx.events.DataGridEvent(mx.events.DataGridEvent.ITEM_FOCUS_OUT, true, true))"/>
</mx:Component>
</mx:itemEditor>
</mx:DataGridColumn>
Note that when using it as an ItemEditor, a custom labelFunction (that looks up the Name from the PersonID in my case) must be used, otherwise you only see the key in the grid when the field isn't being edited (not a problem if your keys/values are the same).
Note that in my case, I wanted the item focus out event to propogate up to provide immediate feedback to the user (my DataGrid has itemFocusOut="handleChange()"), hence the change event creating an ITEM_FOCUS_OUT event.
Note that there are probably simpler ways to have a ComboBox as an ItemEditor when you don't mind the ComboBox only shown when the user clicks on the cell to edit. The approach I wanted was a generic way to show a combo box in a DataGrid for all rows, and being editable and with decent event propogation.
The easiest way to add itemRenderers to DataGrids is to make a custom MXML component. In your case make a canvas, HBox, or VBox as the custom component and add the combobox as a child.Set the dataProvider on the dataGrid itself and assign the itemRenderer to the column, and then override the set data function of the itemRenderer to access all data from the given data provider for that instance as seen below:
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
override public function set data(value:Object):void{
trace(value.data);
trace(value.name);
}
]]>
</mx:Script>
<mx:ComboBox width="100%" height="100%" id="myComboBox"/>
</mx:HBox>
This method will be called for each instance of the itemRenderer
In my case I used a spark datagrid where one of the columns has an ItemRenderer that utilises a DropDownListBox. My problem was that when my item list change, the DropDownLists doesn't get updated with the new dataProvider. To solve this, I had to pass the dataProvider for the DropDownListBox as part of the data (of the ItemRenderer), and then by overriding the setter of the data to just assign the DropDownlListBox's dataProvider. Probably a bit of overhead, but if someone have a better solution, please let me know:
<s:GridItemRenderer 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[
override public function set data(v : Object) : void {
super.data = v;
if (v == null)
return;
dropDown.dataProvider = data.dataProvider;
}
]]>
</fx:Script>
<s:DropDownList id="dropDown" width="100%" height="100%" dataProvider="{data.dataProvider}" labelField="name"/>

DataGrid reference is NULL after event was dispatched

I'm a total Flex newbie and I struggle with event model. I have the following scenario:
having DataGrid with dataProvider set to ArrayCollection
the data grid is a simple to-do list, first column contains check box as item renderer
other columns are plain strings
What I need to achieve is that after data grid has been created or initialised, I need to update the colour style of item renderers values conditionally. The condition says, if value of property Done (stored in data provider) is true, then set colour of text to grey.
The problem is that item renderers are initialised before data grid is created, therefore data grid reference which I can obtain in item renderer is NULL. So I decided to notify item renderers after data grid is completed. Question is how to do it using Flex event model.
It looks like event dispatched by data grid is not listened by item renderer. Please have a look at my code:
<!-- Data grid inside root panel main.mxml -->
<mx:DataGrid id="taskGrid" dataProvider="{tasks}" creationComplete="dispatchEvent(new Event('update',true));">
<mx:columns>
<mx:DataGridColumn dataField="done" headerText="!">
<mx:itemRenderer>
<mx:Component>
<c:StatusCheckBox change="this.onChange(event);"/>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
<mx:DataGridColumn dataField="status" headerText="Status" editable="false" itemRenderer="components.CustomLabel"/>
</mx:columns>
</mx:DataGrid>
<!-- components.CustomLabel.mxml -->
<mx:Label xmlns:mx="http://www.adobe.com/2006/mxml" creationComplete="init();">
<mx:Script >
<![CDATA[
import (...)
private var dg:DataGrid;
private var tasks:ArrayCollection;
private function init():void {
dg = this.listData.owner as DataGrid;
addEventListener("update",updateStyle);
if (dg) Alert.show("dg is not null!"); // data grid is not null when init() finish
}
private function updateStyle(e:Event = null):void {
if (dg) {
if (listData.rowIndex < dg.dataProvider.length) {
var task:Task = dg.dataProvider[listData.rowIndex] as Task;
if (task.done) this.setStyle("color","Blue");
else this.setStyle("color","Black");
}
}
}
]]>
</mx:Script>
</mx:Label>
When I start-up my application and data grid is created the 'update' event is triggered. However the data grid instance (dg) used in updateStyle function in CustomLabel component is null. Why it's now null? As you can see in init() method dg variable is not null (the Alert pops up for every instance of item renderer)
Could you help me? Many thanks guys.
If you listen to dataChange event, it can trigger the handler.

Resources