datagrid multiple components in one column - apache-flex

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.
}

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.

Datagrid and Inline Item renderer problem

I have a datgrid with two inline item renderers. The dataprovider for my DG is a nested object (objects within objects within objects i.e 3-layered).
Main Object - 1st Level
|
2nd Level Object 1
|
3rd level object '1' => ('name'=>somename,'id'=>someid)
3rd level object '2'
.
.
.
.
3rd level object 'n'
2nd Level Object 2
|
3rd level object '1' => ('name'=>somename,'id'=>someid)
3rd level object '2'
.
.
.
.
3rd level object 'n'
I use 2 item renderers (one for each datagrid column) which loops thro the 2nd level object1 and 2 respectively (the 2nd level object is a dynamic array of objects, in that the number of objects within keep changing).
Within the item renderer I loop thro the 2nd level object using a foreach and then display the data. The data is a linkbutton, which when clicked , calls a remote object function to delete the data from the database
now on the result event of the remote object function call, i call the function to repopulate the DG, so that the updated data is displayed.
When i click on the linkbutton in the first row, the backend works perfectly fine (the data gets deleted from the database and the refreshed data is sent back), but for some reason, the deleted data suddenly appears in the 2nd row.
When i delete it from the second row, it appears on the 3rd row (nothing happens in the backend since the data is already deleted).. and so on, till it appears on the last row and then the DG looks exactly the way it shld have looked after the first delete.
This is just the beginning. The second item renderer also displays a linkbutton, which when clicked, displays that data in the previous column (the one where this data can be deleted). When i click on 1st row, the data gets added in the previous column of the second row .. and so on..
Basically, my DG is acting really weird. I overrided the set data function in the item renderer to refrsh the data and called its invalidateDisplayList. I also call the Datagrid's invalidateDisplayList function after each refresh. The behavior remains the same.
Please help me on this ...
Here's my DB code :
<mx:DataGrid id="privilegesDG" width="100%" variableRowHeight="true" minHeight="500">
<mx:columns>
<mx:DataGridColumn headerText="Roles Assigned">
<mx:itemRenderer>
<fx:Component>
<mx:VBox creationComplete="box1_creationCompleteHandler()">
<fx:Script>
<![CDATA[
import com.pm.modules.events.UpdateDBEvent;
import mx.containers.HBox;
import mx.controls.Alert;
import mx.controls.Label;
import mx.controls.LinkButton;
import mx.events.FlexEvent;
override public function set data(value:Object):void{
super.data = value;
super.invalidateDisplayList();
}
protected function box1_creationCompleteHandler():void
{
for each(var temp:Object in data.roles){
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.id = temp.rid.toString();
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,event.target.id,0,true));
}
]]>
</fx:Script>
</mx:VBox>
</fx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
<mx:DataGridColumn headerText="Roles Not Assigned">
<mx:itemRenderer>
<fx:Component>
<mx:VBox creationComplete="box1_creationCompleteHandler()">
<fx:Script>
<![CDATA[
import com.pm.modules.events.UpdateDBEvent;
import mx.containers.HBox;
import mx.controls.Alert;
import mx.controls.Label;
import mx.controls.LinkButton;
import mx.events.FlexEvent;
override public function set data(value:Object):void{
super.data = value;
super.invalidateDisplayList();
}
protected function box1_creationCompleteHandler():void
{
for each(var temp:Object in data.notroles){
var lb:LinkButton = new LinkButton();
lb.label = temp.rname;
lb.id = temp.rid.toString();
lb.addEventListener(MouseEvent.CLICK,handleClick);
this.addElement(lb);
}
}
protected function handleClick(event:MouseEvent):void{
dispatchEvent(new UpdateDBEvent(UpdateDBEvent.ON_ASSIGN_ROLE_TO_PRIVILEGE,data.ID,event.target.id,0,0,0,true));
}
]]>
</fx:Script>
</mx:VBox>
</fx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
</mx:columns>
</mx:DataGrid>
The updateDBEvent is responsible for calling the remote function that updates the DB.
Not really sure if this will help, would need to look at your code to be able to say for sure. But if you want the DataGrid to redraw, dont call the
invalidateDisplayList()
instead, call
invalidateList()

How to get the value of a ComboBox within a DataGrid

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;
}
}
}

Flex DataGrid Add New row via single click event

I am trying to implement the following functionality:
Flex data grid has 1 default row created. When user clicks on the second row, a new row needs to be created and made editable.
Here is what works already - user tabs over the columns and when the user tabs while in the last column, a new row is created with default values.
Here is also what already works - user click a button outside the grid, which adds a new row.
(itemEditBegin and itemEditEnd have been implemented)
Here is what does NOT work: When I "single click" on the second row (no data yet - row is null), how do I detect that the currently clicked row is the second row and make it editable? Can I figure out the rowIndex from MouseEvent and use this to add a new row?
Find code below:
<mx:DataGrid id="myGrid" editable="true" click="clickEvent(event)"
itemEditEnd="endEdit(event)" itemEditBegin="beginEdit(event)" variableRowHeight="true" >
private function clickEvent(ev:Event):void
{
var i:Object = MouseEvent(ev).currentTarget;
// is this the right event?
}
If there is no data, there won't be any row or itemRenderer and hence technically there is no row index. The e.target would contain the ListBaseContentHolder and e.currentTarget would contain the DataGrid itself. However you can use the mouse-y position to calculate what row would have been present at the clicked location. Here is a dirty little trick to do it - it's not thoroughly tested and hence might fail for edge cases.
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
xmlns:local="*" creationComplete="create();">
<mx:DataGrid id="myGrid" editable="true" click="clickEvent(event)"
variableRowHeight="true" >
<mx:columns>
<mx:DataGridColumn dataField="d"/>
<mx:DataGridColumn/>
</mx:columns>
</mx:DataGrid>
<mx:Script>
<![CDATA[
import mx.controls.listClasses.ListRowInfo;
import mx.controls.listClasses.ListBaseContentHolder;
private function clickEvent(e:MouseEvent):void
{
if(!(e.target is ListBaseContentHolder))
return;
var holder:ListBaseContentHolder =
ListBaseContentHolder(e.target);
var rowIndex:Number = -1;
var length:Number = holder.rowInfo.length;
var rowInfo:ListRowInfo;
for(var i:Number = 0; i < length; i++)
{
rowInfo = holder.rowInfo[i];
if(e.localY > rowInfo.y &&
e.localY < rowInfo.y + rowInfo.height)
{
rowIndex = i;
break;
}
}
trace("Clicked on " + rowIndex);
}
private function create():void
{
myGrid.dataProvider = [{d:"A"},{d:"B"}];
}
]]>
</mx:Script>
</mx:Application>
Great question, DataGrid supports selection of objects in the DataGrid's dataProvider. But it does not allow easy selection/mouseOver of rows which do not have items in them (null rows).
1) I would either put a dummy row at the end of the DataGrid, so that way it can be easily selected by the DataGrid.
2) Otherwise, I would check to see if mouseX/mouseY are within the parameters of the null-row you just clicked on (using the null-row's global X,Y,width,height properties).
Let me know what works.

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"/>

Resources