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"/>
Related
OK you Flex experts, I need some help. I have a datagrid in my main application with an itemrenderer (mxml). When you press the image in the ir, a custom component (mxml) opens. The cc has a button that is supposed to call a function in the main application which updates the arraycollection (dataprovider) and therefore the datagrid updates. I've tried several variations of parentDocument, outerDocument, and custom events, but I cannot get that function to work from the button. I think it's b/c I have the cc nested in the ir. Anything that I call directly from within the ir works.
Does anyone have any suggestions or even better a working example I could take a look at?
Here's what I tried:
//in main application
public function creationComplete_handler(event:FlexEvent):void{
dgList.addEventListener("ceRD", fnt_ceRD);
}
public function fnt_ceRD():void {
Alert.show("called");
}
<mx:AdvancedDataGrid id="dgList" dataProvider="{acLists}" designViewDataType="flat">
<mx:columns>
<mx:AdvancedDataGridColumn headerText="Roster" sortable="false" itemRenderer="c_CO.AppLocal.ListManager.iRenderers.irADVStudents" /> </mx:columns>
</mx:AdvancedDataGrid>
In itemrenderer, used the popupmanager so that I could center on top of application as opposed to button in datagrid
public function btnRoster(event:MouseEvent):void{
var rShow:rosterDetails = new rosterDetails();
PopUpManager.addPopUp(rShow, FlexGlobals.topLevelApplication.mainContent, true);
PopUpManager.centerPopUp(rShow);
}
In custom component:
<fx:Metadata>
[Event("ceRD", true, false)]
</fx:Metadata>
<fx:Script>
<![CDATA[
import flash.events.Event;
protected function btnSave(event:MouseEvent):void {
var i_ceRD:Event = new Event("ceRD");
dispatchEvent(i_ceRD);
PopUpManager.removePopUp(this);
}
]]>
</fx:Script>
You are experiencing trouble because the listener for "ceRD" is added to the DataGrid while the event is being dispatched from the Roster Details component. There is no relationship between the DataGrid and RosterDetails.
Consider moving the logic to create and display the RosterDetails out of the ItemRenderer. I would suggest having the irADVStudents renderer dispatch an 'imageClick' event which can be handled in your main application.
Inline renderers are helpful for adding simple event handling:
<mx:AdvancedDataGrid dataProvider="{acLists}">
<mx:columns>
<mx:AdvancedDataGridColumn headerText="Roster" sortable="false">
<mx:itemRenderer>
<mx:Component>
<local:irADVStudents imageClick="outerDocument.onRoster(event)"/>
</mx:Component>
</mx:itemRenderer>
</mx:AdvancedDataGridColumn>
</mx:columns>
</mx:AdvancedDataGrid>
Then you can simply add a listener for your 'save' event. Ensure to remove the listener as this is a potential memory leak. (A weak listener is also an option).
public function onRoster(event : Event) : void
{
var rosterDetails : RosterDetails = new RosterDetails();
rosterDetails.addEventListener("save", onSave);
PopUpManager.addPopUp(rosterDetails, this, true);
PopUpManager.centerPopUp(rosterDetails);
}
protected function onSave(event:Event):void
{
Alert.show("SAVED");
}
Custom events can be used to pass data around the application, such as which item was clicked on from the renderer.
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.
While this may be a simple problem, I'm having a heck of a time coming up with a solution.
I have a DataGrid with a ComboBox as an ItemRenderer for one of my columns. When the user selects a row, I want to get the ComboBox's selected value for the selected row.
EDIT: I should have mentioned that the dataField2_Array property in myData is actually an Array is the dataProvider for the ComboBox. Each object in myData could have completely different values in that Array so the ComboBox in each row of the DataGrid could have completely different options to pick from.
Any suggestions?
Some sample code:
<mx:DataGrid id="myGrid"
dataProvider="{myData}">
<mx:columns>
<mx:DataGridColumn headerText="Column 1" dataField="dataField1" />
<mx:DataGridColumn headerText="Column 2" dataField="dataField2_Array">
<mx:itemRenderer>
<mx:Component>
<mx:HBox paddingLeft="5">
<mx:ComboBox id="myComboBox" dataProvider="{data.dataField2_Array}" />
</mx:HBox>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
</mx:columns>
</mx:DataGrid>
<mx:DataGrid ="MyDataGrid">
<mx:columns>
<mx:DataGridColumn headerText="Resource" width="200" itemRenderer="com.myClasses.myGridDropdownRenderer"/>
</mx:columns>
</mx:DataGrid>
Here is your itemRenderer for your datagrid.
<?xml version="1.0" encoding="utf-8"?>
<mx:ComboBox prompt="Please select a Rating" change="stuffChanged()" dataProvider="{data.dataField2_Array}"
xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
import flash.events.Event;
import mx.controls.Alert;
import mx.core.Application;
import mx.collections.ArrayCollection;
override public function set data( value:Object ) : void {
super.data = value;
}
public function stuffChanged():void{
var myListData:DataGridListData = DataGridListData(listData);
var r:int=myListData.rowIndex;
var c:int=myListData.columnIndex;
//Application.application.whateverStuff[r+1][c]=this.value;
Application.application.whateverStuff[r+1][c]=
this.selectedItem.data;
}
]]>
</mx:Script>
</mx:ComboBox>
This will be in your main Application which will be holding this value.
[Bindable] public var whateverStuff:ArrayCollection;
Now when your data is changed, it holds the data. You click on the button store this value in a rows object.
[Bindable] public var rows:Object = new Object();
rows=Application.application.whateverStuff;
When your sending the value back to database, send all along with this rows object.
Update:
After i read your comment on the previous riposte, i came to know that each of your combo box has a different options. You should have mentioned it earlier.
When you click on the selected rows, you should able to collect the ID of the row, and this would ensure that only the ID of that row is getting updated in the database regardless even if you update your combo box of other rows.
Once your select a row, click and verify on which row ID you have selected using Alert or trace, then send that rows value alone through an event dispatcher.
Add a function called myGrid_click to the click event of your DataGrid:
<mx:DataGrid id="myGrid" dataProvider="{myData}" click="myGrid_click(event)" >
In this function, store the grid's selectedIndex and use that to get the object out of its dataProvider (let's say it's an array of MyObjects, and we're interested in the dataField2 property of these MyObjects):
public function myGrid_click(event:MouseEvent):void {
var index:int = myGrid.selectedIndex;
var obj:MyObject = myData[index];
var value:String = obj.dataField2;
}
If, as is often the case, the object isn't storing the real value, and is just storing an index to a lookup table (dataField2_Array?), write a for loop to iterate over dataField2_Array looking for that value (actualValue) and assign it to a previously declared variable of greater scope (selectedRowComboBoxValue):
public function myGrid_click(event:MouseEvent):void {
var index:int = myGrid.selectedIndex;
var obj:MyObject = myData[index];
var value:int = obj.dataField2;
for (var i:int = 0; i < dataField2_Array.length; i++) {
if (value == dataField2_Array[index].id) {
selectedRowComboBoxValue = dataField2_Array.actualValue;
break;
}
}
}
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.
Hi I wanted to ask if there is a possibility to have both strings and radiobuttons in one column depending on the value of another column perhaps
|column1 | column 2 |
|r | radiobutton|
|s | string |
If there is an r in column 1 in column2 should appear a radiobutton, otherwise column 2 just shows a string.
Thanks for your answers
Sebastian
You do need to write an item renderer to do this. However, you want to update the state of the render whenever the "data" property is set. This is important since item renderers are recycled. Basically the data property gets set whenever the data for that renderer changes and this happens as you scroll the DataGrid.
Here's a simple Application with a DataGrid:
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="onCreationComplete()">
<mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
[Bindable] private var collection:ArrayCollection;
private function onCreationComplete():void
{
collection = new ArrayCollection();
for (var i:uint = 0; i < 20; i++)
collection.addItem({name:'Person #'+i});
}
]]>
</mx:Script>
<mx:DataGrid width="600" dataProvider="{collection}" rowCount="5">
<mx:columns>
<mx:DataGridColumn itemRenderer="com.foo.ItemRenderer"/>
</mx:columns>
</mx:DataGrid>
</mx:Application>
And a simple MXML-based renderer:
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Script>
<![CDATA[
override public function set data(value:Object):void
{
super.data = value;
// only show radio buttons if the "name" property of the data contains "5"; otherwise show a label
radioS.visible = radioS.includeInLayout = radioM.visible = radioM.includeInLayout = radioL.visible = radioL.includeInLayout = data.name.indexOf(5) > -1;
labelName.visible = labelName.includeInLayout = data.name.indexOf(5) < 0;
}
]]>
</mx:Script>
<mx:Label id="labelName" text="{data.name}"/>
<mx:RadioButton id="radioS" label="Small" groupName="radioGroup"/>
<mx:RadioButton id="radioM" label="Medium" groupName="radioGroup"/>
<mx:RadioButton id="radioL" label="Large" groupName="radioGroup"/>
</mx:HBox>
You need to write your own itemRenderer.
From a high-level, what you need to do is tell the column that you will render the column on a per row basis. Then, per row - you check the conditional that you need (like looking at a different column or whatever) and take the action that you want (like adding a radio button vs. some other component).
In the data column do something like this:
<mx:DataGridColumn id="yourColumn"
headerText="Cool Column" editable="false" itemRenderer="SpecialCanvas"/>
Then in a component called 'SpecialCanvas' (let's say he extends a canvas), you can can events or override methods to render as needed... For example:
override protected function initializationComplete():void
{
// check for the conditional that you want and add the component that
// you need to this canvas or what not.
}