Order of calls to set functions when invoking a flex component - apache-flex

I have a component called a TableDataViewer that contains the following pieces of data and their associated set functions:
[Bindable]
private var _dataSetLoader:DataSetLoader;
public function get dataSetLoader():DataSetLoader {return _dataSetLoader;}
public function set dataSetLoader(dataSetLoader:DataSetLoader):void {
trace("setting dSL");
_dataSetLoader = dataSetLoader;
}
[Bindable]
private var _table:Table = null;
public function set table(table:Table):void {
trace("setting table");
_table = table;
_dataSetLoader.load(_table.definition.id, "viewData", _table.definition.id);
}
This component is nested in another component as follows:
<ve:TableDataViewer width="100%" height="100%" paddingTop="10" dataSetLoader="{_openTable.dataSetLoader}"
table="{_openTable.table}"/>
Looking at the trace in the logs, the call to set table is coming before the call to set dataSetLoader. Which is a real shame because set table() needs dataSetLoader to already be set in order to call its load() function.
So my question is, is there a way to enforce an order on the calls to the set functions when declaring a component?

The Flex documentation mentions (somewhere, I can't find it al the moment) that the order of initialisation of properties set in MXML is undefined. That is, sometimes dataSetLoader might have its value set first and then table, or sometimes the other way round.
To get around this you can use the Flex invalidation methods like invalidateProperties() and invalidateDisplayList() to wait until all of your properties are set and then perform your processing all at once.
As an example, here is how you might handle your issue.
Note that we move the call to the _dataSetLoader.load(...) method to the commitProperties() method, when we know we have both the table and dataSetLoader values:
[Bindable]
private var _dataSetLoader:DataSetLoader;
private var dataSetLoaderChanged:Boolean = false;
public function get dataSetLoader():DataSetLoader {return _dataSetLoader;}
public function set dataSetLoader(dataSetLoader:DataSetLoader):void{
trace("setting dSL");
_dataSetLoader = dataSetLoader;
dataSetLoaderChanged = true;
invalidateProperties();
}
[Bindable]
private var _table:Table = null;
private var tableChanged:Boolean = false;
public function set table(table:Table):void {
trace("setting table");
_table = table;
tableChanged = true;
invalidateProperties();
}
override protected function commitProperties():void
{
super.commitProperties();
if (tableChanged || dataSetLoaderChanged)
{
if (_dataSetLoader != null)
{
_dataSetLoader.load(_table.definition.id, "viewData", _table.definition.id);
}
tableChanged = false;
dataSetLoaderChanged = false;
}
}

Related

Flex 3: is it possible to add an event listender to a boolean variable?

I have a boolean variable, projectsLoaded that is set to false when my application loads. As i'm sure you can imagine, when the final project module loads, I set the variable to be true. Is there a way I can trigger a series of functions to run once that variable is set to true?
You can use setters and getters to execute code when value changes. Just be sure to use the setter instead of setting the private variable value.
EDIT : I just saw you tagged your question with addeventlistener. I edited the code to use that instead.
private _projectsLoaded:Boolean = false;
//this could be done elsewhere, that's just an example
private function init():void
{
addEventListener("projectsLoaded", onProjectsLoaded);
}
public function get projectsLoader():Boolean
{
return _projectsLoaded;
}
public function set projectsLoaded(value:Boolean):void
{
if(_projectsLoaded!=value)
{
_projectsLoaded = value;
if(value)
dispatchEvent(new Event("projectsLoaded"));
}
}
protected function onProjectsLoaded(event:Event):void
{
//your logic here
}

Custom Component Not Updating Display List

I've created a component which is basically a meter (extends skinnable container). The skin to the component contains two rectangles (background and the actual meter). This component receives an arraycollection with the following fields value, max and min(the arraycollection I am passing is Bindable). When I initialize and pass the data to be used to draw It works great (keep in mind - only works the first time). I've created a button that changes the data array and shows the current meter value. Apparently the meter's value is changing everytime I modify the ArrayCollection I've set to be used for rendering.(dont want to say "dataProvider" because it is not a Flex dataprovider, just a variable )... here is the code...
public class meter extends SkinnableContainer {
[SkinPart( required = "true" )]
public var meter:spark.primitives.Rect;
[SkinPart( required = "true" )]
public var meterBackground:spark.primitives.Rect;
private var _dataProvider:ArrayCollection;
private var _renderDirty:Boolean = false;
public function Meter() {
super();
}
public function set dataProvider( value:Object ):void {
if ( value )
{
if(value is ArrayCollection)
{
_renderDirty = true;
_dataProvider = value as ArrayCollection;
}
if(_dataProvider)
{
_dataProvider.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, propertyChanged);//used both eventlisteners but none actually ever fire off
_dataProvider.addEventListener(CollectionEvent.COLLECTION_CHANGE,collectionChanged);
}
invalidateDisplayList();//only happens the first time
}
}
private function collectionChanged(event:CollectionEvent):void
{
Alert.show("In collection Change");//this never goes off when I change the "dataProvider"
_renderDirty = true;
invalidateDisplayList();
}
private function propertyChanged(event:PropertyChangeEvent):void
{
Alert.show("In property Change");//this never goes off when I change the "dataProvider"
_renderDirty=true;
invalidateDisplayList();
}
public function get dataProvider():Object {
return _dataProvider;
}
override protected function updateDisplayList( unscaledWidth:Number, unscaledHeight:Number ):void {
if(_dataProvider))
{
var span:Number = unscaledWidth / _dataProvider[0].max;
var meterWidth:Number = _dataProvider[0].value * span;
meter.width = meterWidth;
_renderDirty = false;
}
}
And this is the mxml code where I change the "value" field....
<fx:Script>
<![CDATA[
[Bindable]
private var meterData:ArrayCollection = new ArrayCollection([
{value:80, max:100, min:0}
]);
protected function mySlider_changeHandler(event:Event):void
{
meterData[0].value = Math.round(mySlider.value)*10;
}
protected function button1_clickHandler(event:MouseEvent):void
{
// TODO Auto-generated method stub
var array:ArrayCollection = testMeter.dataProvider as ArrayCollection;
var value:Number = array[0].value as Number;
Alert.show(value.toString());
// testMeter.meter.width= Math.random()*100;
}
]]>//initial value in "meterData" does get drawn...but when it's changed with the slider..nothing happens..
<custom:Meter id="testMeter" dataProvider="{meterData}" />
<s:HSlider id="mySlider"
liveDragging="true"
dataTipPrecision="0"
change="mySlider_changeHandler(event)"
value="3"/>
<s:Button click="button1_clickHandler(event)"/>
Can you help me figure out what's going on??Thanks guys!!!
The issue is that you are not resetting the dataProvider, nor doing anything to fire off a collectionChange event. I'll deal with each one individually. Here is how you reset the dataProvider:
testMeter.dataProvider = newDataPRovider;
But, you aren't doing that. So the set method will never execute after the first initial set.
If you need the collectionChange event to fire, you need to change the collection. You aren't actually doing that. You are changing the object in the collection. This code does not change the collection:
meterData[0].value = Math.round(mySlider.value)*10;
It just changes a single property in one of the objects of the collection. Try something like this:
var newObject = meterData[0];
newObject['value'] = Math.round(mySlider.value)*10
meterData.addItem(newObject);
This code should fire off the colletionChange event, even though the code you have does not.
I have a few more thoughts, unrelated to your primary question. Be sure to removeEventListeners in the dataProvider set method, like this:
public function set dataProvider( value:Object ):void {
if ( value )
{
// remove the event listeners here before changing the value
// that should allow the object to have no extra 'references' that prevent it from being garbage collected
_dataProvider.removeEventListener(PropertyChangeEvent.PROPERTY_CHANGE, propertyChanged);
_dataProvider.removeEventListener(CollectionEvent.COLLECTION_CHANGE,collectionChanged);
if(value is ArrayCollection)
{
_renderDirty = true;
_dataProvider = value as ArrayCollection;
}
if(_dataProvider)
{
_dataProvider.addEventListener(PropertyChangeEvent.PROPERTY_CHANGE, propertyChanged);//used both eventlisteners but none actually ever fire off
_dataProvider.addEventListener(CollectionEvent.COLLECTION_CHANGE,collectionChanged);
}
invalidateDisplayList();//only happens the first time
}
}
And you said:
(dont want to say "dataProvider" because it is not a Flex
dataprovider, just a variable )
A dataProvider on the Flex List, or DataGrid, or DataGroup is also "just a variable." I don't see how the "Flex Framework" implementation is all that different than what you've done, conceptually anyway. Since you're specifically expecting a min and max value perhaps you should use a Value Object with those explicit properties instead of an ArrayCollection.

What event is fired when all SkinSparts are available in a SkinnableComponent?

I'm (slowly) learning Flex 4 and working on skinning a custom component that extends SkinnableComponent. The component is all in ActionScript and essentially looks like this:
package components
{
import spark.components.supportClasses.SkinnableComponent
[SkinState("normal")]
[SkinState("over")]
[SkinState("selected")]
public class AccountSummary extends SkinnableComponent
{
[Bindable]
public var itemIndex:int = 0;
[Bindable]
public var accountName:String = "";
[Bindable]
public var accountNumber:String = "";
[Bindable]
public var currentBalance:String = "";
[SkinPart(required="true")]
public var lblAccountName:Label;
[SkinPart(required="true")]
public var lblCurrentBalance:Label;
[SkinPart(required="true")]
public var lblAccountNumber:Label;
[SkinPart(required="true")]
public var lblLastUpdated:Label;
public function AccountSummary()
{
super();
lblAccountName.text = accountName;
lblCurrentBalance.text = currentBalance;
lblAccountNumber.text = "Acc: " + accountNumber;
lblLastUpdated.text = "Last Updated: ";
}
override protected function getCurrentSkinState():String
{
return "normal";
}
}
}
The issue I'm having is on NULL REFERENCES in the Constructor for the various SkinParts, because they're not created yet. I'm trying to find out when is the best time to access them to assign their .text values. I know I can override partAdded() and add the .text value as each part is added, but I'd rather just listen for some magic event that is dispatched when they're all available.
I'm not sure whether or not it matters, but the accountName, accountNumber variables are assigned by using a repeater with a dataProvider on the host component. I'm not sure if that too has something to do with when the data is available to the AccountSummary component - which may also needed to be waited on. I was able to successfully use FlexEvent.CREATION_COMPLETE to assign all my .text values to the SkinParts, but is that the right event/best practice? Any help would be greatly appreciated!
You need to read up on the Flex 4 Component LifeCycle, specifically the piece about component instantiation. Nothing will be created at the time the constructor executes. The values you are referencing (accountName, lblCurrentBalance. etc...) will not have moved beyond the default states either.
You want to set your default values in the partAdded() method. Rewrite your constructor code to something like this:
public function AccountSummary()
{
super();
}
override protected function partAdded(partName : String, instance: Object):void{
super.partAdded(partName, instance);
if(instance == lblAccountName){
lblAccountName.text = accountName;
else if(instance == lblCurrentBalance){
} else if (instance == lblCurrentBalance){
lblCurrentBalance.text = currentBalance;
} else if (instance == lblAccountNumber){
lblAccountNumber.text = "Acc: " + accountNumber;
} else if (instance == lblLastUpdated){
lblLastUpdated.text = "Last Updated: ";
}
}
If you're a new programmer, you may want to devote some time to learning some basic programming concepts. This ActionScript 3 Guide may help you get started. If you have experience in other languages, but are new to Flex, I strongly suggest spending a couple of days reading through the full flex documentation to help get your head around it.

Extending Flex DataGridColumn for custom sorting function

I extended the DataGridColumn because I wanted to include a custom itemToLabel function (to be able to show nested data in the DataGrid. See this question.
Anyways, it also needs a custom sorting function. So I have written the sorting function like so:
private function mySortCompareFunction(obj1:Object, obj2:Object):int{
var currentData1:Object = obj1;
var currentData2:Object = obj2;
//some logic here to get the currentData if the object is nested.
if(currentData1 is int && currentData2 is int){
var int1:int = int(currentData1);
var int2:int = int(currentData2);
var result:int = (int1>int2)?-1:1;
return result;
}
//so on for string and date
}
And in the constructor of my CustomDataGridColumn, I've put:
super(columnName);
sortCompareFunction = mySortCompareFunction;
Whenever I try to sort the column, I get the error "Error: Find criteria must contain at least one sort field value."
When I debug and step through each step, I see that the first few times, the function is being called correctly, but towards the end, this error occurs.
Can someone please shed some light on what is happening here?
Thanks.
I have seen this error too, and I tracked it down to one of the cells containing 'null'.
And if I remember correctly, this error also shows up, when one of the columns has a wrong 'dataField' attribute.
hth,
Koen Weyn
Just to specify how exactly I solved this problem (for the benefit of others):
Instead of using the dataField property (to which I was assigning something like data.name, data.title since I was getting data from a nested object), I created my own field nestedDataField and made it bindable. So my code basically looks like this now:
public class DataGridColumnNested extends DataGridColumn{
[Bindable] public var nestedDataField:String;
private function mySortCompareFunction(obj1:Object, obj2:Object):int{
var currentData1:Object = obj1;
var currentData2:Object = obj2;
//some logic here to get the currentData if the object is nested.
if(currentData1 is int && currentData2 is int){
var int1:int = int(currentData1);
var int2:int = int(currentData2);
var result:int = (int1>int2)?-1:1;
return result;
}
//so on for string and date
}
}
Then I assign to this new variable my dataField entry
<custom:DataGridColumnNested headerText="Name" nestedDataField="data.name"/>
And lo and behold! it works without a hitch.
I often find it easier to use the standard datafield and just write a getter function in my valueobject to use as a datafield.
For example:
//file SomeObject.as with a nested object as property
public class SomeObject
{
public var someProperty:AnotherObject;
public function get someString():String;
{
if(someProperty)
return someProperty.someString;
else
return "";
}
}
//your nested class, AnotherObject.as
public class AnotherObject
{
public var someString:String;
}
//this way sorting and the label will work without custom label/compare functions
<mx:DataGridColumn headerText="" dataField="someString"/>
The easiest way to solve the problem is change the dataField="obj.atributte" by a labelFunction. If you want you can add a sortCompareFunction too.
<mx:DataGridColumn headerText="email"
dataField="user.email" textAlign="center" />
change by
<mx:DataGridColumn headerText="email"
labelFunction="emailLabelFunction"
sortCompareFunction="emailsCompareFunction2"
textAlign="center" />
public static function emailLabelFunction(item:Object, column:DataGridColumn):String{
if (item!=null){
var user:User = (item as TpAnnouncementUser).user as User;
return user.email;
}else{
return "";
}
}
public static function emailCompareFunction(obj1:Object, obj2:Object):int{
var dato1:String = User(obj1).email.toLowerCase();
var dato2:String = User(obj2).email.toLowerCase();
return ObjectUtil.compare(dato1, dato2);
}
public static function emailsCompareFunction2(obj1:Object, obj2:Object):int{
var dato3:User = (TpAnnouncementUser(obj1).user as User);
var dato4:User = (TpAnnouncementUser(obj2).user as User);
return emailCompareFunction(dato3, dato4);

How do I create a Hierarchical Cursor from the dataProvider of an AdvancedDataGrid?

In a previous application that I had written, I had a Class that extended AdvancedDataGrid (ADG). It contained the following code:
package
{
public class CustomADG extends AdvancedDataGrid
{
....
// This function serves as the result handler for a webservice call that retrieves XML data.
private function webServiceResultHandler(event:ResultEvent):void
{
var resultXML:XML = new XML(event.result);
dataProvider = new HierarchicalData(resultXML.children);
}
....
public function setOpenNodes(maxDepth:int = 0):void
{
var dataCursor:IHierarchicalCollectionViewCursor = dataProvider.createCursor();
while (dataCursor.current != null)
{
if (dataCursor.currentDepth < maxDepth)
dataProvider.openNode(dataCursor.current);
dataCursor.moveNext();
}
dataProvider.refresh();
}
}
}
In this implementation, the function setOpenNodes() worked fine - it did exactly what I intended it to do - pass it a number, and open all nodes in the dataProvider at or below that level.
Now, I am creating a new ADG Class and want to reproduce this functionality:
package view
{
import mx.collections.IHierarchicalCollectionViewCursor;
public class ReportADG extends AdvancedDataGrid
{
public function ReportADG()
{
super();
}
public function setOpenNodes(maxDepth:int = 0):void
{
var dataCursor:IHierarchicalCollectionViewCursor =
dataProvider.createCursor();
while (dataCursor.current != null)
{
if (dataCursor.currentDepth < maxDepth)
dataProvider.openNode(dataCursor.current);
dataCursor.moveNext();
}
dataProvider.refresh();
}
}
}
The dataProvider is set in the parent component:
<view:ReportADG id="reportADG" dataProvider="{reportData}" />
reportData is set in another file:
reportData = new HierarchicalData(resultXML.children);
However, I am getting runtime errors:
TypeError: Error #1034: Type Coercion failed: cannot convert ListCollectionViewCursor#6f14031 to mx.collections.IHierarchicalCollectionViewCursor.
I've tried casting dataProvider as ICollectionView. I've tried then casting the ICollectionView as IHierarchicalCollectionView. I've tried all sorts of casting, but nothing seems to work. Why won't this work in this new implementation as it did in the past implementation? What do I need to do?
*** Update:
I started debugging this. I added an override setter to my ADG Class to see when dataProvider was being set:
override public function set dataProvider(value:Object):void
{
super.dataProvider = value;
}
I added a breakpoint to this setter and to my setOpenNodes() function. Sure enough, the dataProvider is being set BEFORE setOpenNodes() is called, and it is HierarchicalData. But, when the setOpenNodes() the debugger says that the dataProvider is a null ArrayCollection. It seems like this is the root issue.
I needed to call commitProperties before attempting to access the dataProvider property.
public function setOpenNodes(maxDepth:int = 0):void
{
super.commitProperties();
var dataCursor:IHierarchicalCollectionViewCursor =
dataProvider.createCursor();
while (dataCursor.current != null)
{
if (dataCursor.currentDepth < maxDepth)
dataProvider.openNode(dataCursor.current);
dataCursor.moveNext();
}
dataProvider.refresh();
}

Resources