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
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 load an SWF with SWFLoader. Within the loaded .SWF, this.parentApplication is returning NULL. Been searching the internet for eight hours.
code listing as requested:
<?xml version="1.0"?>
<mx:Application
xmlns:mx="http://www.adobe.com/2006/mxml"
initialize= "Init();"
visible="false"
>
<mx:Script>
<![CDATA[
import flextrace.Dumper;
private var txt_event:TextEvent = new TextEvent(TextEvent.LINK,false,false,"next.xml");
private var timer:Timer = new Timer(10000);
private function Init():void {
timer.addEventListener(TimerEvent.TIMER,timer_handlr);
timer.start();
}
private function timer_handlr(event:Event) {
Dumper.info("timer_handlr");
if (this.parentApplication == null)
Dumper.info("null");
parentApplication.dispatchEvent(new TextEvent(TextEvent.LINK,false,false,"next.xml"));
}
]]>
</mx:Script>
</mx:Application>
I just was looking through Flex docs and saw an answer on your question, if I understood you correctly:
The parentApplication property of an Application object is never itself; it is either the Application object into which it was loaded, or it is null (for the Application object).
Since, your calling it from the application, it should be null.
Just a quick link for you about accessing nested applications (I haven't tested the solution yet tho, but still, may give you some ideas): Nesting Flex applications - weird issues..
Hope, this would help :)
RYAN GUILL: I tried your suggestion - didn't work. I replaced the dispatchEvent call in the code from the OP with the following: this.dispatchEvent(new TextEvent(TextEvent.LINK,true,false,"next.xml")); Were you saying that this should have triggered the Parent application's event handler for TextEvent.LINK? It didn't work.
Try FlexGlobals.topLevelApplication?
http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/mx/core/FlexGlobals.html?filter_flex=4.1&filter_flashplayer=10.1&filter_air=2
Say I have the following custom component:
<s:Group 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[
[Bindable]
public var prop:String;
private function formatProp() : String {
return "Hello, " + prop;
}
]]>
</fx:Script>
<s:Label text="User: {prop}"/>
<s:Label text="Greeting: {formatProp()}"/>
</s:Group>
If I add it to my application like this:
<local:MyComponent prop="Hello"/>
The result looks like:
User: Mark
Greeting: Hello, null
It seems Flex is setting prop on my custom component after it has already initialized the child labels, so it's reliant on the property changed event to set the user label.
Is there an elegant way to make Flex wait for all of my component's properties to be set before initially evaluating bindings?
Note: I realize the formatProp function is trivial and could be included inline, but this is just a simplified example.
The "elegant way" would be to actually provide data binding, so that you can change your property also afterwards. Your initial idea looked good, working with the answer provided by Cornel. I just wanted to mention this as your actual question sounded more like that you know your data binding is not working and you just wanted to postpone the initial setting of the variable.
Btw, should you plan to create custom components in Actionscript (instead of mxml) you'll face the opposite problem: properties are set before you had a chance to actually create your children, so you may need to buffer them if they actually should influence some childs properties.
it is not related to component livecycle, more to binding rules. Your function "formatProp" should recieve the parameter "prop" as a parameter in order to be called when the prop is changed. Try this code:
private function formatProp(props:String) : String {
return "Hello, " + props;
}
<s:Label text="Greeting: {formatProp(prop)}"/>
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.
I have a ToggleButtonBar with a DataProvider setup like this:
<mx:ToggleButtonBar itemClick="clickHandler(event);" selectedIndex="0">
<mx:dataProvider>
<mx:String>{resourceManager.getString('dashboard','daily')}</mx:String>
<mx:String>{resourceManager.getString('dashboard','monthly')}</mx:String>
<mx:String>{resourceManager.getString('dashboard','quarterly')}</mx:String>
<mx:String>{resourceManager.getString('dashboard','yearly')}</mx:String>
</mx:dataProvider>
</mx:ToggleButtonBar>
To switch locale to Chinese, I have a combobox with this handler:
resourceManager.localeChain = "zh_CN";
My problem is that on locale change, while the labels for all the other controls on the screen dynamically reload for the new locale, the dataProvider values don't refresh.
I can manually reset them in code, but is there a cleaner solution?
I would abstract out the data for your data provider into a bindable variable, then just reset the data provider when you change locals.
<mx:Script>
<![CDATA[
[Bindable]
myArray:Array = new Array(
[resourceManager.getString('dashboard','daily')]
, [resourceManager.getString('dashboard','monthly')]
, [{resourceManager.getString('dashboard','quarterly')]
, [resourceManager.getString('dashboard','yearly')]);
]]>
</mx:Script>
<mx:ToggleButtonBar itemClick="clickHandler(event);"
selectedIndex="0" id="myToggleButtonBar" dataprovider="{myArray}" />
Then you can just say
myToggleButtonBar.dataProvider = myArray;
after you swap the locals and it should work.
Disclaimer, there may be some minor errors in my code, I obviously am not able to test it and I don't have flex builder available right now to even check my syntax so I hope I didn't make any spelling mistakes. But this should get you in the ballpark.
Maybe if you make a getter bindable to a custom event for ex: "langChange"
[Bindable("langChange")]
public function get dataProviderToggleB():ArrayCollection
{
var arr :ArrayCollection = new ArrayCollection();
arr.addItem(resourceManager.getString('dashboard','daily'));
arr.addItem(resourceManager.getString('dashboard','monthly'));
return arr;
}
and in your "resourceManager.localeChain" setter you dispatch:
dispatchEvent(new Event("langChange"));
and you can used like this:
<mx:ToggleButtonBar dataProvider="{dataProviderToggleB} itemClick="clickHandler(event);" selectedIndex="0">
I hope this would help you.
You should keep 'daily', ... in your array and use a labelFunction to translate the label.
When the resourceManager sends a change event you should do a combo.labelFunction = labelFunction
The trick is to add brackets around each element in the dataProvider array, that way it gets parsed correctly. Note that this also binds correctly to locale changes in flex, no custom event dispatching is needed.
<mx:ToggleButtonBar itemClick="clickHandler(event);" selectedIndex="0"
dataProvider="{[ (resourceManager.getString('dashboard','daily')),
(resourceManager.getString('dashboard','monthly')),
(resourceManager.getString('dashboard','quarterly')),
(resourceManager.getString('dashboard','yearly')) ]}">
</mx:ToggleButtonBar>