Compiler warns me that binding will not work but why I run the application it does work! - apache-flex

The Flex application below generates the compiler warning: Data binding will not be able to detect assignments to 'dp'. This seems correct since the variable 'dp' is not a bindable property (there is no [Bindable] metadata tag). I have added a button which appends items to the back of 'dp' when it is clicked. Although the compiler warns me that I will not see changes to 'dp', the list shows the new item every time the button is clicked!
I do not understand why I can see new items appear in the list. Can someone explain why this still works although 'dp' is not bindable?
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" minWidth="955" minHeight="600">
<mx:Script>
<![CDATA[
import mx.collections.ArrayCollection;
import mx.rpc.events.ResultEvent;
private var arrayData:Array = [
{name:"banana", cat:"fruit", cost:0.99},
{name:"bread", cat:"bakery", cost:1.99},
{name:"orange", cat:"fruit", cost:0.52},
{name:"donut", cat:"bakery", cost:0.33},
{name:"apple", cat:"fruit", cost:1.05}];
private var dp:ArrayCollection = new ArrayCollection(arrayData);
private function onButtonClick(event:MouseEvent):void
{
var obj:Object = new Object();
obj.name="test";
obj.cat="testcat";
obj.cost=666;
dp.addItem(obj);
}
]]>
</mx:Script>
<mx:HorizontalList dataProvider="{dp}" labelField="name" columnWidth="100" width="80%" height="50"/>
<mx:Button label="Click me" click="onButtonClick(event)" />

The compiler is correct in it's warning.
The compiler is warning you that assignments that change the value of dp from the initial ArrayCollection you specified to another ArrayCollection won't be detected.
However, if you leave the value of dp alone, and only change the contents of it, then your <HorizontalList /> will continue to work.
This may seem trivial, but it's an important distinction, and one that can lead to some very confusing bugs further down the road in your application.
Assignments to the variable dp will not be detected. However, changes to the ArrayCollections list will, because they dispatch a CollectionChangeEvent.
eg:
private var dp:ArrayCollection = new ArrayCollection();
private function test():void
{
// Here, we don't change the value of dp directly,
// instead we just modify it's list.
// The DataGroup will show the strings One,Two
dp.addItem("One")
dp.addItem("Two")
// Here, we change the actual value of dp, by assigning a
// new ArrayCollection to it.
// This change would not be detected, and the list would continue to show
// the contents of the previous value.
// Additionally, the label will show the string "Length: 2",
// even though the length is clearly now 3.
dp = new ArrayCollection();
dp.addItem("Tahi");
dp.addItem("Rua");
dp.addItem("Toru");
}
<s:DataGroup dataProvider="{dp}" />
<s:Label text="Length: {dp.length}" />

Try to use:
[Bindable("__NoChangeEvent__")]
private var dp:ArrayCollection = new ArrayCollection(arrayData);
What about adding elements in list see the code of ListBase:
public function set dataProvider(value:Object):void
{
if (collection)
{
collection.removeEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler);
}
if (value is Array)
{
collection = new ArrayCollection(value as Array);
}
else if (value is ICollectionView)
{
collection = ICollectionView(value);
}
else if (value is IList)
{
collection = new ListCollectionView(IList(value));
}
else if (value is XMLList)
{
collection = new XMLListCollection(value as XMLList);
}
else if (value is XML)
{
var xl:XMLList = new XMLList();
xl += value;
collection = new XMLListCollection(xl);
}
else
{
// convert it to an array containing this one item
var tmp:Array = [];
if (value != null)
tmp.push(value);
collection = new ArrayCollection(tmp);
}
// get an iterator for the displaying rows. The CollectionView's
// main iterator is left unchanged so folks can use old DataSelector
// methods if they want to
iterator = collection.createCursor();
collectionIterator = collection.createCursor(); //IViewCursor(collection);
// trace("ListBase added change listener");
collection.addEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler, false, 0, true);
clearSelectionData();
var event:CollectionEvent = new CollectionEvent(CollectionEvent.COLLECTION_CHANGE);
event.kind = CollectionEventKind.RESET;
collectionChangeHandler(event);
dispatchEvent(event);
itemsNeedMeasurement = true;
invalidateProperties();
invalidateSize();
invalidateDisplayList();
}
So take a look at line:
collection.addEventListener(CollectionEvent.COLLECTION_CHANGE, collectionChangeHandler, false, 0, true);

Related

ArayList as dataProvider for a List: The index 0 is out of range 0

Does anybody please have an idea, why do I get the runtime error:
RangeError: Error #1125: The index 0 is out of range 0.
........
at Popup/update()[Popup.mxml:80]
at PopupTest/showPopup()[PopupTest.mxml:45]
at PopupTest/___btn_click()[PopupTest.mxml:52]
when calling the function:
private function showPopup(event:MouseEvent):void {
_popup.update(new Array('Pass' ,
'6♠', '6♣', '6♦', '6♥', '6 x',
'7♠', '7♣', '7♦', '7♥', '7 x',
'8♠', '8♣', '8♦', '8♥', '8 x',
'9♠', '9♣', '9♦', '9♥', '9 x',
'10♠', '10♣', '10♦', '10♥', '10 x'), true, 80);
}
As if my _list would have no entries at all (but why? I do assign _data.source=args) and thus the _list.ensureIndexIsVisible(0) call would fail at the line 80:
<?xml version="1.0" encoding="utf-8"?>
<s:Panel xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
width="220" height="200"
initialize="init(event)">
<fx:Script>
<![CDATA[
import mx.collections.ArrayList;
import mx.events.FlexEvent;
import mx.utils.ObjectUtil;
private static const FORCE:uint = 20;
[Bindable]
private var _data:ArrayList = new ArrayList();
private var _timer:Timer = new Timer(1000, 120);
private function init(event:FlexEvent):void {
_timer.addEventListener(TimerEvent.TIMER, timerUpdated);
_timer.addEventListener(TimerEvent.TIMER_COMPLETE, timerCompleted);
}
public function close():void {
_timer.reset();
_data.source = null;
visible = false;
}
private function timerUpdated(event:TimerEvent=null):void {
var seconds:int = _timer.repeatCount - _timer.currentCount;
title = 'Your turn! (' + seconds + ')';
// show panel for cards too
if (seconds < FORCE)
visible = true;
}
private function timerCompleted(event:TimerEvent=null):void {
title = 'Your turn!';
close();
}
public function update(args:Array, bidding:Boolean, seconds:int):void {
if (seconds <= 0) {
close();
return;
}
// nothing has changed
if (ObjectUtil.compare(_data.source, args, 0) == 0)
return;
_data.source = args;
if (args == null || args.length == 0) {
close();
return;
}
if (seconds < FORCE || bidding)
visible = true;
_timer.reset();
title = 'Your turn! (' + seconds + ')';
_list.ensureIndexIsVisible(0); // the line 80
_timer.repeatCount = seconds;
_timer.start();
}
]]>
</fx:Script>
<s:VGroup paddingLeft="10" paddingTop="10" paddingRight="10" paddingBottom="10" gap="10" width="100%" height="100%">
<s:List id="_list" dataProvider="{_data}" width="100%" height="100%" fontSize="24" itemRenderer="RedBlack" />
</s:VGroup>
</s:Panel>
the reason
You are adding the new array allright, but then the List starts creating ItemRenderers based on the items that are in that array. This takes some time and happens asynchronously. In the meantime you're saying "show me item 1", but the ItemRenderer for item 1 doesn't exist yet. It will very soon, but not right now. That's why you get an indexoutofrange error.
the solution
You have to be sure the List is done creating ItemRenderers before you call that method. The easiest way to solve this situation - though definitely not the cleanest - is to just wait until the next render cycle by using the infamous callLater().
callLater(_list.ensureIndexIsVisible, [0]);
This essentially saying: wait for the next render cycle and then call ensureIndexIsVisible() on _list with parameter 0.
(On a side note: if you really only want index 0 this whole thing is rather pointless, because I think a List scrolls back to the top when its dataprovider is changed anyway)
a cleaner solution
You can listen on the List for the RendererExistenceEvent#RENDERER_ADD event. This will be dispatched whenever a new ItemRenderer was added to the list and it holds a reference to the item's index in the List, the data and the ItemRenderer itself. However in your case we only need the 'index'. Whenever an ItemRenderer is added at index 0 we'll scroll back to the top:
_list.addEventListener(RendererExistenceEvent.RENDERER_ADD, onRendererAdded);
private function onRendererAdded(event:RendererExistenceEvent):void {
if (event.index == 0) myList.ensureIndexIsVisible(0);
}
This will immediately scroll to the top when the first ItemRenderer is added and doesn't need to wait until all of them are ready.

Flex 3: Can anybody see why this dictionary isn't working?

So, in my main mxml, i have a variable defined as such:
[Bindable] public var studentsListDict:Dictionary = new Dictionary;
I also have the following imported:
import flash.utils.Dictionary;
I then have an HTTPService that imports an XML file:
<mx:HTTPService id="studentsHttp" url="students.xml" resultFormat="e4x" makeObjectsBindable="true" result="createStudentsCollection(event)" />
The createStudentsCollection function is as follows:
private function createStudentsCollection(e:ResultEvent):void
{
var xmlList:XMLList = XML(e.result).student;
var dupString:String = "|";
var tempArray:Array = new Array;
studentsListDict = new Dictionary;
for (var i:int = 0; i < xmlList.length(); i++)
{
if (dupString.indexOf(String("|" + xmlList[i].name) + "|") == -1)
{
tempArray = new Array;
tempArray[0] = xmlList[i].name.#id;
tempArray[1] = xmlList[i].name;
tempArray[2] = xmlList[i].year;
tempArray[3] = xmlList[i].track;
studentsListAC.addItem(tempArray);
studentsListDict[tempArray[0]] = tempArray;
dupString += "|" + xmlList[i].name + "|";
getLen(studentsListDict);
}
}
}
Then, to ensure the items were correctly put into the dictionary, i have the following function:
public static function getLen(d:Dictionary):int
{
var i:int = 0;
for (var key:Object in d)
{
Alert.show(String(key + "\n" + d[key]));
i++;
}
return i;
}
This creates pop up alerts that show that everything was loaded correctly into the dictionary.
Later on, in a child, I call a function that tries to use the dictionary, and i get a return of "undefined".
Here's the function that searches based on key, and returns a value from the array within:
public function getStudentName(sID:Number):String
{
return studentsListDict[sID][1];
}
Unfortunately, the getStudentName function simply returns undefined every time.
If anybody can see something I'm missing, it'd be greatly appreciated.
Thanks,
Brds
EDIT
It wasn't working b/c you can't have numbers as keys in a dictionary. Simply casting them to a string during the declaration and look up seems to work just fine.
Here is some documentation on dictionary keys..
It looks like you're code is setting it as a string and then accessing it as a number. I suspect that is the root of your problem You can try something like this:
public function getStudentName(sID:Number):String
{
return studentsListDict[sID.toString()][1];
}
It is actually perfectly acceptable to use numbers as Keys to a Dictionary. The Dictionary apparently turns the number and a string value of that number to the same key. Here is a sample:
<?xml version="1.0" encoding="utf-8"?>
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx" minWidth="955" minHeight="600" creationComplete="application1_creationCompleteHandler(event)">
<fx:Script>
<![CDATA[
import mx.events.FlexEvent;
public var dict : Dictionary;
protected function application1_creationCompleteHandler(event:FlexEvent):void
{
dict = new Dictionary();
dict["0"] = "hi";
dict["4"] = "hola";
dict["17"] = "bye";
dict["32"] = "adios";
dict[32] = "adios 2";
dict[3.2] = "adios 3";
dict[50] = "Audio ";
dict["50"] = "Audio 2";
trace(dict["0"]);
trace(dict["4"]);
trace(dict["17"]);
trace(dict["32"]);
trace(dict[32]);
trace(dict[3.2]);
trace(dict[50]);
trace(dict["50"]);
}
]]>
</fx:Script>
</s:Application>
I think it's because getStudentName is using a Number as a key, while createStudentCollection is using a string. In this case, because the keys are numbers/strings, you can simply use an Object: var studentsListDict:Object = {}; — it will automatically coerce all the keys to strings.
Also, as an asside: new Dictionary(); more standard, and it's better form in ActionScript to use [] (eg, var foo:Array = []) than new Array(). That way you can put stuff in the array at the same time:
var tempArray:Array = [
xmlList[i].name.#id,
xmlList[i].name,
…
];

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.

How to access the array elements

var count:uint = 0;
var textInputs:Array /* of TextInputs */ = [];
for(var i:String in columnsData){
textInputs[count] = new TextInput();
addChild(textInputs[count]);
count++;
}
here how can i access the first, second of Array of textinputs in the form of string or any thing to further proceed
I am not sure if I understood what your question is, but you're not setting the value for text inputs:
I prefer it the following way:
//make the array an instance variable instead of local var
//so that it can be accessed from other functions if required.
public var textInputs:Array = [];
for(var str:String in columnsData)
{
var ti:TextInput = new TextInput();
ti.text = str;
addChild(ti);
textInputs.push(ti);
}
You can access the values using textInputs[0].text etc.
if(textInput != null){
for(var i:Number = 0; i < textInputs.length; i++)
trace(textInputs[i].text);
}
(getChildAt(0) as TextInput).text //first
(getChildAt(1) as TextInput).text //second
If your sprite contains something other than TextInput at indices 0 & 1 you'll get null reference exception.
<?xml version="1.0"?>
<!-- dpcontrols\UseIList.mxml -->
<s:Application xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx"
initialize="initData();">
<s:layout>
<s:VerticalLayout/>
</s:layout>
<fx:Script>
<![CDATA[
import mx.collections.*;
// The data provider is an Array of Strings
public var myArray:Array = ["AZ", "MA", "MZ", "MN", "MO", "MS"];
// Declare an ArrayList that represents the Array.
[Bindable]
public var myAL:ArrayList;
//Initialize the ArrayList.
public function initData():void {
myAL = new ArrayList(myArray);
}
// The function to change the collection, and therefore
// the Array.
public function changeCollection():void {
// Get the original collection length.
var oldLength:int=myAL.length;
// Remove the invalid first item, AZ.
var removedItem:String=String(myAL.removeItemAt(0));
// Add ME as the second item. (ILists used 0-based indexing.)
myAL.addItemAt("ME", 1);
// Add MT at the end of the Array and collection.
myAL.addItem("MT");
// Change the third item from MZ to MI.
myAL.setItemAt("MI", 2);
// Get the updated collection length.
var newLength:int=myAL.length;
// Get the index of the item with the value ME.
var addedItemIndex:int=myAL.getItemIndex("ME");
// Get the fifth item in the collection.
var index4Item:String=String(myAL.getItemAt(4));
// Display the information in the TextArea control.
ta1.text="Start Length: " + oldLength + ". New Length: " +
newLength;
ta1.text+=".\nRemoved " + removedItem;
ta1.text+=".\nAdded ME at index " + addedItemIndex;
ta1.text+=".\nThe item at index 4 is " + index4Item + ".";
// Show that the base Array has been changed.
ta1.text+="\nThe base Array is: " + myArray.join();
}
]]>
</fx:Script>
<s:ComboBox id="myCB" dataProvider="{myAL}"/>
<s:TextArea id="ta1" height="75" width="300"/>
<s:Button label="rearrange list" click="changeCollection();"/>
</s:Application>

add Dynamic children to an array or arraycollection

I'm a bit new to flex and cannot get my head around this problem. can someone help. thanks in advance.
I have a string list path.
path 1 - "one/two/three"
path 2 - "one/two/four"
path 3 - "five/six"
i need an advanced datagrid to show a tree structure like so
one/
...two/
........three/
............four
five/
.......six
but i want to achieve this dynamicall with arrays, objects, or arraycollection (as applicable)
I need to loop through each string path using string methods which isnt a problem but how do i create "DYNAMIC" (depth) children? please help as i'm about to pull my hair out.
You can try something like:
var paths:Array = ['one/two/three','one/two/four','five/six'];
var pathsCollection:ArrayCollection = new ArrayCollection();
for(var i:int = 0 ; i < paths.length ; i++){
var folderArr:Array = paths[i].split('/');
var folderNum:int = folderArr.length;
var folderLabel:String = '';
for(var j:int = 0 ; j < folderNum; j++){
trace(folderLabel+folderArr[j]);
pathsCollection.addItem({label:folderLabel+folderArr[j],level:j,path:folderArr});
folderLabel += '...';
}
}
and as sharvey says, do have a look at recursion.
Recursion is the way to go when you are operating on objects which contain an unknown number of elements. Try this sample application:
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml"
creationComplete="{init();}"
layout="vertical"
verticalAlign="middle">
<mx:Script>
<![CDATA[
import mx.utils.ObjectUtil;
import mx.collections.HierarchicalData;
private var paths:Array = ['one/two/three','one/two/four','five/six'];
private static const DELIMITER:String = "/";
private function init():void {
var test:Array = buildHierarchy(paths);
dg_test.dataProvider = new HierarchicalData(test);
trace(ObjectUtil.toString(test));
}
private function buildHierarchy(arr:Array):Array {
var ret:Array = new Array();
var o:Object = new Object();
/* Loop over the paths array */
for (var i:int = 0; i < arr.length; i++) {
/* Split the string block according to the delimiter */
var parts:Array = String(arr[i]).split(DELIMITER);
if (parts.length) {
/* Make a new object with a label equal to the first string element */
o = new Object();
o.label = parts[0];
/* Remove the first item in the string list */
parts.splice(0, 1);
/* Important - If the string has remaining members, call this
function again with the remaining members. Assign this to
the 'children' property of the newly created object */
if (parts.length > 0)
o.children = buildHierarchy([parts.join(DELIMITER)]);
/* Add the object to the new array */
ret.push(o);
}
}
return ret;
}
]]>
</mx:Script>
<mx:AdvancedDataGrid id="dg_test" height="200" width="400">
<mx:columns>
<mx:AdvancedDataGridColumn id="col_label" dataField="label"/>
</mx:columns>
</mx:AdvancedDataGrid>
This function will call itself once for every element contained in the 'string/string/string' block. The key to getting this structure displayed in the ADG is to set the adg.dataProvider = new HierarchicalData(myArray);
Hope this works! I can't get the code formatted 100% but you should get the idea. Don't forget to add the closing application tag.

Resources