I'm trying to create a datagrid which will resize vertically to ensure all the renderers are displayed in full. Additionally,
Renderers are of variable height
Renderers can resize themselves
Generally speaking, the flow of events is as follows :
One of the item renderers resizes itself (normally in response to a user click etc)
It dispatches a bubbling event which the parent datagrid picks up
The DataGrid attempts to resize to ensure that all renderers remain visible in full.
I'm currently using this code within the datagrid to calculate the height:
height = measureHeightOfItems(0, dataProvider.length ) + headerHeight;
This appears to get an incorrect height. I've tried a number of variations including callLater ( to ensure the resize has completed so measure can work correctly), and overriding meausre() and calling invalidateSize() / validateSize(), but neither works.
Below are 3 classes which will illustrate the problem. Clicking the button in the item renderers resizes the renderer. The grid should also expand so that all of the 3 renderers are shown in their entirety.
Any suggestions would be greatly appreciated.
Regards
Marty
DataGridProblem.mxml (Application file)
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
xmlns:view="view.*">
<mx:ArrayCollection id="dataProvider">
<mx:String>Item A</mx:String>
<mx:String>Item B</mx:String>
<mx:String>Item C</mx:String>
</mx:ArrayCollection>
<view:TestDataGrid
id="dg"
dataProvider="{ dataProvider }"
width="400">
<view:columns>
<mx:DataGridColumn dataField="text" />
<mx:DataGridColumn itemRenderer="view.RendererButton" />
</view:columns>
</view:TestDataGrid>
</mx:Application>
view.TestDataGrid.as
package view
{
import flash.events.Event;
import mx.controls.DataGrid;
import mx.core.ScrollPolicy;
public class TestDataGrid extends DataGrid
{
public function TestDataGrid()
{
this.verticalScrollPolicy = ScrollPolicy.OFF;
this.variableRowHeight = true;
this.addEventListener( RendererButton.RENDERER_RESIZE , onRendererResize );
}
private function onRendererResize( event : Event ) : void
{
resizeDatagrid();
}
private function resizeDatagrid():void
{
height = measureHeightOfItems(0, dataProvider.length ) + headerHeight;
}
}
}
view.RendererButton.mxml
<?xml version="1.0" encoding="utf-8"?>
<mx:HBox xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Button width="50" height="50"
click="onClick()" />
<mx:Script>
<![CDATA[
public static const RENDERER_RESIZE : String = "resizeRenderer";
private function onClick() : void
{
this.height += 20;
dispatchEvent( new Event( RENDERER_RESIZE , true ) );
}
]]>
</mx:Script>
</mx:HBox>
You can achieve this goal with the AdvancedDataGrid using the following code. If you remove the this.headerHeight, it works for List as well, which makes me believe it should work for the regular old DataGrid.
override protected function measure():void
{
super.measure();
if ( this.dataProvider )
{
var newHeight : int = measureHeightOfItems( 0, this.dataProvider.length ) + this.headerHeight;
this.minHeight = newHeight;
this.height = newHeight;
}
}
To re size the Data-grid at runtime....use rowcount property and bind it with the dataprovider length. As the dataprovider is updated so will be the rowcount.
You can see the example how to use rowcount in flex here
To do things such as variable width and height cells/rows/column, as well as other "advanced" features - try extending the AdvancedDataGrid rather than the older, more boring DataGrid
For what it's worth, I never managed to resolve this issue. Code quickly became obsessed with dealing with edge cases.
I ended up throwing out the dataGrid approach, and wrote a solution using VBox & HBox to facilitate resizing.
for what its worth; you were supposed to use the variableRowHeight="true" property of the datagrid / advanced datagrid.
if it makes you feel better i created the custom VBox,HBox solution then after that discovered that its already done!! (snap!!)
good luck!
There's an underlying problem to your approach. ItemRenderers are intended to render the data item in a consistent manner, and if you throw out the current renderer and create a new one and set the data property of the new renderer, it should look identical to the previous one. You're not supposed to make transient changes to the renderer outside of the data member, and the renderer is going to behave in odd ways when you do that.
I suspect that if you changed your data model objects to contain a count property and data bound your renderer's height to the expression "{50 + data.count * 20}", and then made the button's click handler increment data.count by 1, that this would work properly. (My experience is that the DataGrid will redraw itself with the proper size for each row as long as the changes get done as a result of the data property, before it calls makeRowsAndColumns(). ) I haven't tried that approach for your example, so I can't say for sure that it actually works, but certainly that's the proper mindset for working with item renderers.
I have a little bit Dirty solution for such problem Try this one
private function ResizeGrid():void{
if ( this.dg.maxVerticalScrollPosition > 0 ){
this.dg.height +=5;
setTimeout(this.ResizeGrid,100);
}
}
And Call your function with some delay like follows
setTimeout(this.ResizeGrid,100);
This is dirty approach but it works for me :)
The following code in gridItemRenderer helped me:
protected function resize():void
{
column.grid.validateSize();
column.grid.validateDisplayList();
}
I have a solution for this problem, renderer changes need to sync with your dataprovider.
Then only measureHeightOfItems method will give accurate results.
Related
I have a simple DataGrid with data. Of one of the columns, I want to use a ComboBox to edit the field, instead of the standard edit box.
How do I do that? I have tried all kind of things I found on the internet, but they all fail in simply updating the value. I'd say it shouldn't be too hard to do this.
I'm actually in the process of doing this myself, and with the spark:DataGrid it actually gets a bit easier than halo - but both follow the same setup / architecture.
Start with:
spark.components.gridClasses.ComboBoxGridItemEditor;
Depending on the nature of your data setup and/or how prolific this kind of editing will be for your application, you can write it inline as most documentation will suggest within a <fx:component>, or simply subclass this (although behind the scenes these are the same thing - the later being much easier to reuse). The data for the combo in my scenario is a sub selection of a bigger parent object, so I chose to make it easier on myself and add an additional property dataField to mimic other renderer / editors - in what actually shows in just the cell itself (when not in editing mode).
A basic setup looks something more or less like this (at least mine does):
public class AccountComboEditor extends ComboBoxGridItemEditor
{
private _dataField:String;
public function AccountComboEditor()
{
super();
//note - typically you wouldn't do "logic" in the view but it's simplified as an example
addEventListener(FlexEvent.CREATION_COMPLETE, onCreationComplete);
}
public function get dataField():String { return _dataField; }
public function set dataField(value:String):void
{
if (_dataField !=value) //dosomeadditionalvalidation();
_dataField = value;
}
override public function prepare():void
{
super.prepare();
if (data && dataField && comboBox) comboBox.labelField = data[dataField];
}
protected function onCreationComplete(event:FlexEvent):void
{
//now setup the dataProvider to your combo box -
//as a simple example mine comse out of a model
dataProvider = model.getCollection();
//this isn't done yet though - now you need a listener on the combo to know
//what item was selected, and then get that data_item (label) back onto this
//editor so it has something to show when the combo itself isn't in
//editor mode
}
}
So the real take away is to setup the labelField of the combobox, either internally in the subclass or externally if you need to expose it as an additional property.
The next part is to use this as part of the mx.core.ClassFactory for the actual data grid. A simple view would look like something similar:
<s:DataGrid>
<fx:Script>
private function getMyEditor(dataField:String):ClassFactory
{
var cf:ClassFactory = new ClassFactory(AccountComboEditor);
cf.properties = {dataField : dataField };
return cf;
}
</fx:Script>
<s:columns>
<mx:ArrayList>
<s:GridColumn itemEditor="{getMyEditor('some_data_property')}" />
</mx:ArrayList>
</s:columns>
</s:DataGrid>
This Creating item renderers... doc will give you more info.
I figured it out. I just wanted a simple drop down box, instead of a text-editing field.
The following code does want I want:
<mx:DataGridColumn dataField="type" headerText="Type" editorDataField="value">
<mx:itemEditor>
<fx:Component>
<mx:ComboBox>
<mx:dataProvider>
<fx:String>Gauge</fx:String>
<fx:String>Graph</fx:String>
<fx:String>Indicator</fx:String>
</mx:dataProvider>
</mx:ComboBox>
</fx:Component>
</mx:itemEditor>
</mx:DataGridColumn>
I have just implemented a dropdownlist of checkboxes taken from this ComboCheck example but made it extend DropDownList instead of ComboBox to provide better functionality that I required. I am attempting to create a DropDownList where some items are bold and non-checkboxes (or can be checkboxes) and others are not.
I have not been able to find anything online about doing this yet and have been trying to figure it out. I am currently using an ArrayCollection as a dataProvider but I think this could possibly be my issue and I should be trying to setup the labels in flex not AS3.
Does anyone know if this is possible? And if so do they have any links that could possibly help point me in the right direction?
Thanks.
EDIT: Code added for the itemRenderer, this worked I just need to specify each item that I want to be bold, though is there a better way to do this in the flex code as opposed to checking for a matching string in the renderer?
public class ComboCheckItemRenderer extends ItemRenderer{
public var item:CheckBox;
public function ComboCheckItemRenderer(){
super();
item = new CheckBox();
item.x = 5;
addElement(item);
item.addEventListener(MouseEvent.CLICK, onClick);
}
private var _data:Object;
[Bindable]override public function set data (value:Object):void {
if (value!=null) {
_data = value;
item.label = value.label;
if(item.label == "item1"){
item.setStyle("color","0x00ff00");
item.setStyle("fontWeight","bold");
}
item.selected = value.selected;
}
}
Edit 2: What I am ultimately trying to do is create a dropdown of checkboxes with data that I obtain from blazeDS that basically has a bunch of group titles and their corresponding sub-elements. I am trying to have the dropdown make the groups be in bold and to the left, and their sub-elements normal font and offset to the right. I also need to know when they are clicked whether it was a group header or sub-element, so that I can add them to an object that I will be sending back to my service to perform a sql query on.
ie.
[ ]**GROUP**
[ ] element
[ ] element
[ ]**GROUP**
[ ] element
What does your data look like? Why aren't you using MXML for this? Why are you overriding set data() as opposed to hooking the dataChange event? You are writing way more code than you need to here.
Lets look at it in a more "Flexy" way. Notice how I am using data binding for everything and conditionally setting the fontWeight based on the data that comes in. Anything more complicated should bust out to a function in the Script tag.
<?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">
<fx:Script>
<![CDATA[
protected function onClick(event:MouseEvent):void {
}
]]>
</fx:Script>
<s:CheckBox x="5" click="onClick(event)"
label="{data.label}" selected="#{data.selected}"
fontWeight="{data.label == 'item1' ? 'bold' : 'normal'}"/>
</s:ItemRenderer>
In light of your question you added in your edit, I would ask: What criteria are you using? You can put any function in your binding expression, so at a very minimum, you might do something like this:
<fx:Script>
<![CDATA[
private var itemsToBold:Array = ["label1", "label2"];
private function getFontWeight(label):String {
if(itemsToBold.indexOf(label) > 0)
return "bold";
return "normal";
}
]]>
</fx:Script>
<s:CheckBox fontWeight="{getFontWeight(data.label)}"/>
As I am somewhat new to Flex I may be missing something fundamental here. I have a Spark List container whose dataProvider is bound to a result set coming back from a RemoteObject call. Pretty standard stuff.
<s:List id="list" dataProvider="{model.stuff}" width="100%" height="100%"
selectedIndex="#{selectedSlider.value}"
itemRenderer="{stuffRenderer}">
</s:List>
The selectedIndex is associated with an HSlider, but that is not the problem. My issue is that I would like to automatically select a certain "preferred" element from the list (only initially...to guide the user).
I tried to do that in a creationComplete event but my data hadn't shown up yet...setting selectedIndex didn't work...it was too early.
What's the right way to do this?
private function findAllUsers_resultHandler(e:ResultEvent):void
{
list.dataProvider = new ArrayCollection(e.result as Array);
if(firstTry)
{
list.selectedIndex = 0;
firstTry = false;
}
}
spark.components.List has spark.components.SkinnableDataContainer in its class hierarchy which dispatches a dataProviderChanged event whenever the dataProvider changes. Unfortunatly there is no [Event] metadata in SkinnableDataContainer that allows using this event in MXML. So, you'll need to create your own custom component that extends List.
package
{
import spark.components.List;
[Event(name="dataProviderChanged", type="flash.events.Event")]
public class MyList extends List
{
public function MyList()
{
super();
}
}
}
By using your custom component you can add an event listener for dataProviderChanged and update your selectedIndex accordingly.
<ns1:MyList id="list" dataProvider="{model.stuff}" width="100%" height="100%"
dataProviderChanged="selectedIndex = selectedSlider.value"
selectedIndex="#{selectedSlider.value}"
itemRenderer="{stuffRenderer}">
</ns1:MyList>
BTW: This works with other List-based components (like DropDownList) too.
I believe it should work if you just set the initial value of the slider to the index you want to be selected at the beginning.
Something like this:
<s:List dataProvider="{yourData}" selectedIndex="{hSlider.value}" /> <s:HSlider id="hSlider" minimum="0" maximum="{yourData.length - 1}" stepSize="1" value="theIndexYouWantAsInitial" liveDragging="true" />
That should work.
HTH
FTQuest
I'm running into an odd issue with itemRenderers inside a TileList.
Here is a working example without an itemRenderer: 152.org/flex/
Here is the broken version with an itemRenderer: 152.org/brokenExample/
(I don't have the rep to make both of these a link)
Both examples have "View Source" enabled.
To see the problem use the broken example, select an album and scroll down one row. Scroll back up and the images will be switched. If you try this on the working example it's fine.
This seems to be a widely known bug, but I can't find a solution for it.
UPDATE
I started playing with this example again and found out something else. Turns out you don't have to override the data setter. You can create a new method in the itemRenderer that is set whenever the tile wants to refresh. So the trick is to not rely on the initialize or creationComplete methods.
This is what I have for the itemRenderer in the Application.
<itemRenderers:ImageTile img="{data}"/>
This is the code I have in the itemRenderer.
public function set img(value:String) : void {
trace("setting source: " + value);
this.source = value;
this.name = value.toString().split("/").pop().split(".").shift();
}
I updated my example to reflex this change.
I don't have your app handy, so I can't test end-to-end, but I've looked at your source. You probably need to override the data setter in your itemRenderer:
<?xml version="1.0" encoding="utf-8"?>
<mx:Image xmlns:mx="http://www.adobe.com/2006/mxml" initialize="init()">
<mx:Script>
<![CDATA[
override public function set data(value:Object):void
{
super.data = value;
this.source = data;
this.name = data.toString().split("/").pop().split(".").shift();
}
private function init() : void {
// Removed from your source and transplanted above
}
]]>
</mx:Script>
</mx:Image>
Flex will attempt to re-use item renderers in lists (which means the lifecycle events you might be expecting -- initialize, creationComplete, etc. -- won't always fire), so if you want to be sure your renderer gets updated when the data item changes (as it will when scroll events happen), the best practice is to override the renderer's data property. That'll most likely fix the problem.
Maybe try to invalidate on creationComplete?
From what I recall with DataGrids (which work somewhat similarly to a tilelist), when an item comes into focus its recreated.
<mx:itemRenderer>
<mx:Image id="myImage" creationComplete="myImage.invalidate()" />
</mx:itemRenderer>
Haven't tried this code but I think this is where you want to start looking. I took a look at your itemRenderer component. Try creationComplete instead of initialize to call your function
What I am trying to accomplish to to get financial data in my Flex Datagrid to be color-coded--green if it's positive; red if it's negative. This would be fairly straightforward if the column I want colored was part of the dataProvider. Instead, I am calculating it based on two other columns that are part of the dataProvider. That would still be fairly straightforward because I could just calculate it again in the ItemRenderer, but another part of the calculation is based on the value of a textBox. So, what I think I need to be able to do is send the value of the textBox to the custom ItemRenderer, but since that value is stored in the main MXML Application, I don't know how to access it. Sending it as a parameter seems like the best way, but perhaps there's another.
Here is the current code for my ItemRenderer:
package {
import mx.controls.Label;
import mx.controls.listClasses.*;
public class PriceLabel extends Label {
private const POSITIVE_COLOR:uint = 0x458B00 // Green
private const NEGATIVE_COLOR:uint = 0xFF0000; // Red
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void {
super.updateDisplayList(unscaledWidth, unscaledHeight);
/* Set the font color based on the item price. */
setStyle("color", (data.AvailableFunding >= 0) ? NEGATIVE_COLOR : POSITIVE_COLOR);
}
}
(data.AvailableFunding doesn't exist)
So does anyone know how I would go about accomplishing this?
You may want to look into ClassFactory from the Flex APIs:
This allows you to set a prototype Object with arbitrary types / values each of which will be passed to the item renderer. From the sample:
var productRenderer:ClassFactory = new ClassFactory(ProductRenderer);
productRenderer.properties = { showProductImage: true };
myList.itemRenderer = productRenderer;
The above code assumed that "ProductRenderer" has a public property called "showProductImage" which will be set with a value of "true."
Ah, so I knew about outerDocument but not parentDocument. I was able to just use parentDocument.*whatever I want from the main App and I can access it as long as it's public.
Example:
setStyle("color", (parentDocument.availableFunding >= 0) ? POSITIVE_COLOR : NEGATIVE_COLOR);
Sweet! :)
You can access the value of the TextBox directly, if you need to, by using the static Application.application object, which is accessible from anywhere in your application.
For example, if you wanted the renderers to be notified when the value of the TextInput control changes, you could do something like this (from within your ItemRenderer, and where myTextInput is the ID of the control defined in your main MXML class):
<mx:Script>
<![CDATA[
import mx.core.Application;
private function creationCompleteHandler(event:Event):void
{
Application.application.myTextInput.addEventListener(TextEvent.TEXT_INPUT, handleTextInput, false, 0, true);
}
private function handleTextInput(event:TextEvent):void
{
if (event.currentTarget.text == "some special value")
{
// Take some action...
}
}
]]>
</mx:Script>
With this approach, each item-renderer object will be notified when the TextInput's text property changes, and you can take appropriate action based on the value of the control at that time. Notice as well that I've set the useWeakReference argument to true in this case, to make sure the listener assignments don't interfere unintentionally with garbage collection. Hope it helps!
There's another technique, which, while it initially feels a little hacky is perhaps less cumbersome and cleaner in actual use.
It involves the little-observed fact that an event dispatch is, of course, synchronous and the event object can be treated as a value object populated by any event handler.
i.e. the ItemRenderer can do something like:
...
var questionEvt:DynamicEvent = new DynamicEvent('answerMeThis', true, true);
if (dispatchEvent(questionEvt))
{
if (questionEvent.answer == "some value")
....
With a corresponding handler somewhere up the view hierarchy above the renderer that has a listener on the event and does something like:
function handleAnswerMeThis(event:DynamicEvent):void
{
event.answer = "another value";
event.dataHelper = new DataHelperThingy();
}
etc.
It need not be a DynamicEvent - I'm just using that for lazy illustrative purposes.
I vote up for cliff.meyers' answer.
Here's another example on setting the properties of an itemRenderer from MXML by building a function that wraps a ClassFactory around the itemRenderer class and that injects the necessary properties.
The static function:
public static function createRendererWithProperties(renderer:Class,
properties:Object ):IFactory {
var factory:ClassFactory = new ClassFactory(renderer);
factory.properties = properties;
return factory;
}
A simple example that adds a Tooltip to each item in a list:
<mx:List dataProvider="{['Foo', 'Bar']}" itemRenderer="{createRendererWithProperties(Label, {toolTip: 'Hello'})}"/>
Reference:
http://cookbooks.adobe.com/post_Setting_the_properties_of_an_itemRenderer_from_MXM-5762.html
You use outerDocument property. Please see the fx:Component reference.
You could create an 'AvailableFunding' static variable in the ItemRenderer and then set it in the parent document.
public class PriceLabel extends Label {
public static var availableFunding:int;
...
...
SetStyle("color", (PriceLabel.availableFunding >= 0) ? NEGATIVE_COLOR : POSITIVE_COLOR);
}
In your parent document, set it when your text box gets updated
PriceLabel.availableFunding = textBox.text;
Obviously it'll be the same value for every ItemRenderer but it looks like that might be what you're doing anyway.
I like to override the set data function of the item renderer to change the renderer when the data provider changes as shown here
When you override the function you could cast the object to your object to make the availableFunding property available.
To access the text box you could try creating a public property and binding the property to the text box in the mxml file:
public var textVar:String;
<mx:itemRenderer>
<mx:Component>
<customrenderer textVar="{txtBox.text}" />
</mx:Component>
</mx:itemRenderer>
Nice ClassFactory Example here
See this example:
itemRenderer="{UIUtils.createRenderer(TextBox,{iconSrc:IconRepository.linechart,headerColor:0xB7D034,subHeaderColor:0xE3007F,textColor:0x75757D})}"