It appears as though the new spark List component does not honour the IDropInItemRenderer interface.
Ie - if I implement IDropInItemRenderer on my renderer, the setter of listData is never called.
Am I missing something, or is this interface now deprecated?
If so, What is the suggested approach for providing similar dataProvider context information to the renderer?
For example, I want the renderer for the last item in a collection to behave slightly differently.
I see that IItemRenderer now defines a listIndex property, however this approach doesn't work without knowing the count of the source dataProvider.
Here's the workaround I've ended up using.
In it's own way, the DataGroup is dripping Spark's compositional goodness, in that it exposes a rendererUpdateDelegate property, which you can set with your own class to provide whatever custom functionliaty you're after.
While it's frustrating that the interface got dropped without really being advertised, this approach is much more powerful.
Here's an example class. In my example, I want the last renderer to have it's collapsable property set to false:
/**
* Decorates another IItemRendererOwner (eg., a DataGroup) and augments the updateRenderer method
* to set the isCollapsable property */
public class ThreadMessageRendererUpdateDelegate implements IItemRendererOwner
{
private var _dataGroup:DataGroup;
public function get dataGroup():DataGroup
{
return _dataGroup;
}
public function set dataGroup(value:DataGroup):void
{
_dataGroup = value;
if (dataGroup)
{
dataGroup.rendererUpdateDelegate = this;
}
}
public var dataProvider:ArrayCollection;
public function ThreadMessageRendererUpdateDelegate(owner:DataGroup=null)
{
this.dataGroup = owner;
}
public function itemToLabel(item:Object):String
{
return dataGroup.itemToLabel(item);
}
public function updateRenderer(renderer:IVisualElement, itemIndex:int, data:Object):void
{
dataGroup.updateRenderer(renderer,itemIndex,data);
if (renderer is ThreadMessageRenderer)
{
ThreadMessageRenderer(renderer).collapsable = itemIndex < dataProvider.length - 1;
}
}
}
And here's it's example usage:
<fx:Declarations>
<viewer:ThreadMessageRendererUpdateDelegate dataProvider="{dataProvider}" dataGroup="{threadList}" />
</fx:Declarations>
<fx:Script>
<![CDATA[
[Bindable]
public var dataProvider:ArrayCollection
]]>
</fx:Script>
<s:DataGroup height="100%"
width="100%"
dataProvider="{dataProvider}"
itemRenderer="ThreadMessageRenderer"
id="threadList"
>
</s:DataGroup>
Man! Just spent ages trying to find DataGroup.rendererUpdateDelegate(...), eventually discovering why I couldn't, courtesy of this SO post.
Anyway, thinking about the (disappearance of) rendererUpdateDelegate property and your offering a little bit more, I realise neither are really necessary.
DataGroup has the rendererAdd event which gives you enough info, at the right time, to do what you want; for example:
...
<s:DataGroup id="dg"
dataProvider="{model.dataProvider}"
itemRenderer="{model.itemRendererFactory}"
rendererAdd="model.updateRenderer(event.data, event.index, event.renderer)">
...
...and in the model we have:
public function updateRenderer(data:Object, index:int, renderer:IVisualElement):void
{
if (renderer is ICollapsable)
{
ICollapsable(renderer).collapse = index < dataProvider.length - 1;
}
}
Fewer lines of code and clearer intent
Related
I am moving away from MXML and have built a custom component control within ActionScript.
I have the control displaying correctly. The problem comes after I remove it from the display list and add it back in again with the .addElement(control) method.
Here is the code that adds it back in again.
private function displayParameters(parameters:ArrayCollection):void{
for(var index:int = 0; index<parameters.length; index++){
if(parameters[index] is ReportControl){
var control:ReportControl = parameters[index] as ReportControl;
control.percentWidth = 100;
vgParameters.addElement(control);
}
}
}
ReportControl is the base class for comboBoxMultiSelect which is shown below. There is nothing graphically special about ReportControl, it only serves as a programmatic interface for its concrete implementations (polymorphic).
public class comboBoxMultiSelect extends ReportControl{
[Embed("../Assets/Icons/plus-16.png")]
private var plusIcon:Class;
[Embed("../Assets/Icons/minus-16.png")]
private var minusIcon:Class;
private var expanded:Boolean = false;
private var buttonIconChanged:Boolean = false;
private var _drp:ComboBox;
private var _btnMultiple:Button;
private var _horizontalGroup:HGroup;
private var _multiSelector:ReportGridSelector;
private var _multiSelection:Boolean = true;
private var bMultiSelectionChanged:Boolean = false;
public function ToggleExpanded():void{
expanded = !_expanded;
buttonIconChanged = true;
invalidateSize();
invalidateProperties();
invalidateDisplayList();
}
public function comboBoxMultiSelect(){
super();
}
override protected function createChildren():void{
super.createChildren();
if(!_horizontalGroup){
_horizontalGroup = new HGroup();
_horizontalGroup.gap = 0;
_horizontalGroup.percentWidth = 100;
_horizontalGroup.height = ReportControl.SIZE_DEFAULT_HEIGHT;
addChild(_horizontalGroup);
}
if(!_drp){
_drp = new ComboBox();
_drp.text = GuiText;
_drp.percentWidth = 100;
_drp.height = ReportControl.SIZE_DEFAULT_HEIGHT;
_horizontalGroup.addElement(_drp);
}
if(!_btnMultiple && _multiSelection){
_btnMultiple = new Button;
_btnMultiple.setStyle("icon", plusIcon);
_btnMultiple.width = 20;
_btnMultiple.height = ReportControl.SIZE_DEFAULT_HEIGHT;
_btnMultiple.visible = true;
_btnMultiple.addEventListener(MouseEvent.CLICK,
function(event:MouseEvent):void{
ToggleExpanded();
});
_horizontalGroup.addElement(_btnMultiple);
}
}
override protected function commitProperties():void{
super.commitProperties();
if(buttonIconChanged){
if(_expanded==true){
_btnMultiple.setStyle("icon", minusIcon);
}
else{
_btnMultiple.setStyle("icon", plusIcon);
}
buttonIconChanged = false;
}
}
override protected function updateDisplayList(unscaledWidth:Number,
unscaledHeight:Number):void{
super.updateDisplayList(unscaledWidth, unscaledHeight);
_horizontalGroup.width = unscaledWidth;
_horizontalGroup.height = unscaledHeight;
}
override protected function measure():void{
super.measure();
measuredMinWidth = measuredWidth = ReportControl.SIZE_DEFAULT_WIDTH;
//minimum size //default size
if(_expanded==true)
measuredMinHeight= measuredHeight = 200;
else
measuredMinHeight= measuredHeight =
ReportControl.SIZE_DEFAULT_HEIGHT;
}
}
When I add the control back in using vgParameters.addElement(control), the comboBoxMultiSelect is not rendering properly. The plusIcon inside the button _btnMultiple is not postioned correctly at first, but then quickly corrects itself about 0.5-1 secs later.
I pretty sure the problem lies within comboBoxMultiSelect, just not sure how to force the icon to stay in the same place.
This is highly annoying after all my hard work, anyone have ideas as to what I am doing wrong?
Thanks :)
UPDATE -----> Here is the ReportControl code
[Event (name= "controlChanged", type="Reporting.ReportControls.ReportControlEvent")]
[Event (name= "controlIsNowValid", type="Reporting.ReportControls.ReportControlEvent")]
public class ReportControl extends UIComponent
{
private var _guiText:String;
private var _amfPHPArgumentName:String;
private var _reportResult:ReportResult;
private var _sequence:int;
private var _reportId:int;
private var _controlConfiguration:ReportParameterVO;
private var _isValid:Boolean = false;
internal var _selection:Object;
/**
* SIZE_DEFAULT_HEIGHT = 22
*/
internal static const SIZE_DEFAULT_HEIGHT:int = 22;
/**
* SIZE_DEFAULT_WIDTH = 150
*/
internal static const SIZE_DEFAULT_WIDTH:int = 150;
public function get ControlConfiguration():ReportParameterVO{
return _controlConfiguration;
}
public function set ControlConfiguration(value:ReportParameterVO):void{
_controlConfiguration = value;
_guiText = (value ? value.GuiText:"");
_amfPHPArgumentName = (value ? value.AMFPHP_ArgumentName: "");
_sequence = (value ? value.Sequence : null);
_reportId = (value ? value.ReportId : null);
}
public function get IsValid():Boolean{
return _isValid;
}
public function get ReportID():int{
return _reportId;
}
public function get Sequence():int{
return _sequence;
}
public function get ControlRepResult():ReportResult{
return _reportResult;
}
public function set ControlRepResult(value:ReportResult):void{
_reportResult = value;
}
internal function set Selection(value:Object):void{
_selection = value;
}
internal function get Selection():Object{
return _selection;
}
public function get ParameterSelection():Object{
return _selection;
}
public function get GuiText():String{
return _guiText;
}
public function get AmfPHPArgumentName():String{
return _amfPHPArgumentName;
}
public function ReportControl(){
//TODO: implement function
super();
}
public function dispatchControlChanged():void{
this.dispatchEvent(new ReportControlEvent(ReportControlEvent.CONTROL_CHANGED, this, true));
}
public function dispatchControlIsNowValid():void{
this.dispatchEvent(new ReportControlEvent(ReportControlEvent.CONTROL_IS_NOW_VALID, this, true));
}
public function addSelfToValueObject(valueObject:Object):Object{
valueObject[AmfPHPArgumentName] = _selection;
return valueObject;
}
}
I'll try to give you an example of what I mean with the Spark skinning architecture we've discussed in the comments above. It's not directly an answer to your question, but I thought you might find it interesting. I will have to make it somewhat simpler than your component for brevity's sake and because you seem to have stripped out some of the code for your question so I can't know exactly what it's supposed to do.
This will be a component that will let you toggle between a normal and an expanded state through the click of a Button. First we'll create the skin class. Normally you'd create the host component first, but it'll be easier to explain this way.
<!-- my.skins.ComboBoxMultiSelectSkin -->
<s:Skin xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
height.normal="25" height.expanded="200">
<fx:Metadata>
[HostComponent("my.components.ComboBoxMultiSelect")]
</fx:Metadata>
<s:states>
<s:State name="normal" />
<s:State name="expanded" />
</s:states>
<s:layout>
<s:HorizontalLayout gap="0" />
</s:layout>
<s:ComboBox id="comboBox" width="100%" />
<s:Button id="toggleButton" width="20"
icon.normal="#Embed('../Assets/Icons/plus-16.png')"
icon.expanded="#Embed('../Assets/Icons/minus-16.png')"/>
</s:Skin>
Thus we've set up completely how your component will look and how it will lay out. Do you feel your headaches dissipating? I for one find this quite elegant. We have the two states and the height of the component will adjust to the currently selected state as will the icon of the Button. How and when the state is toggled is component behaviour and will be defined in the host component.
Now let's create that host component in plain ActionScript. For this we'll extend SkinnableComponent (note that it could also extend your ReportControl if that would extend SkinnableComponent instead of UIComponent).
[SkinState("normal")]
[SkinState("expanded")]
public class ComboBoxMultiSelect extends SkinnableComponent {
[SkinPart(required="true")]
public var toggleButton:IEventDispatcher;
[SkinPart(required="true")]
public var comboBox:ComboBox;
private var expanded:Boolean;
override protected function partAdded(partName:String, instance:Object):void {
super.partAdded(partName, instance);
switch (instance) {
case toggleButton:
toggleButton.addEventListener(MouseEvent.CLICK, handleToggleButtonClick);
break;
case comboBox:
comboBox.addEventListener(IndexChangeEvent.CHANGE, handleComboSelection);
break;
}
}
private function handleToggleButtonClick(event:MouseEvent):void {
toggleExpanded();
}
private function handleComboSelection(event:IndexChangeEvent):void {
//handle comboBox selection
}
protected function toggleExpanded():void {
expanded = !expanded;
invalidateSkinState();
}
override protected function getCurrentSkinState():String {
return expanded ? "expanded" : "normal";
}
}
Allright, there's a lot more going on here.
First look at the SkinState metadata declarations: when a skin class is assigned to the component, the compiler will check whether that skin has the required states implemented.
Then the SkinPart declarations: the name of the property on the host component must exactly match the id of the tag in the skin class. As required is set to true the compiler will check whether these components do really exist in the skin. If you want optional skin parts, you set it to false.
Note that the type of toggleButton is IEventDispatcher: from the host component's point of view, all toggleButton has to do, is dispatching CLICK events. This means that we could now create a skin with <s:Image id="toggleButton" source="..." /> and the whole thing would keep working the same way. See how powerful this is?
Because the skinpart properties are not assigned immediately, we override the partAdded() method which will be executed whenever a component becomes available. In most cases this is the place where you hook up your event listeners.
In the toggleExpanded() method, we toggle the boolean just like the component in your question, however we only invalidate the skin state. This will cause the skin to call the getCurrentSkinState() method and update its state to whatever value is returned.
Et voilà! You have a working component with the behaviour nicely separated into an actionscript class and you didn't have to worry about the layout intricacies. And if you ever wish to create a component with the same behaviour, but it should expand horizontally instead of vertically: just create a new skin that adjusts the width instead of the height and assign that to the same host component.
Oh wait! I nearly forgot to tell you how to assign the skin to the components. You can do it either inline:
<c:ComboBoxMultiSelect skinClass="my.skins.ComboBoxMultiSelectSkin" />
or through styling:
#namespace c "my.components.*";
c|ComboBoxMultiSelect {
skinClass: ClassReference("my.skins.ComboBoxMultiSelectSkin")
}
One thing that stands out is in your implementation of updateDisplayList(). As you know, this is where your component should size and position it's child objects (and/or do any programatic drawing).
But rather than set the child object's width/height directly, you should use one of the Flex lifecycle methods: setActualSize() or setLayoutBoundsSize(). Use setLayoutBoundsSize() with spark components.
When you set a Flex component's width/height, the component will invalidate itself so that on the next update cycle it can be re-rendered. But since you are trying to render the component in updateDisplayList() you should be careful to not invalidate your child objects inside this method.
The setActualSize() and setLayoutBoundsSize() methods set the width/height on a Flex component, but do not invalidate the component.
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
_horizontalGroup.setLayoutBoundsSize(unscaledWidth, unscaledHeight);
// if you wanted to position objects, you would set their x/y coordinates
// here with the move() or setLayoutBoundsPosition() methods
}
Note, it looks like some child objects are being sized in createChildren() as well ... and it's not really clear what the base Flex component is in this case (what class does ReportControl extend?
Doing it this way may get rid of that rendering glitch. It will most certainly execute less code than if you set width/height properties directly.
[Edit]
It may be an interaction with the HGroup which is kind of unnecessary in this component. While I think making components this way is fun, it can be more tedious... which is why #RIAStar is wisely pointing out another approach.
Some further ideas, if you want to continue down this path:
1) Take a look at the sizing you are doing in createChildren() - for example, the HGroup is given a percentWidth, but in updateDisplayList() it is given a fixed width (this may be a red herring, but I would not set the percentWidth).
2) You might be able to trick the component into validating itself after you remove it or before you re-add it. A hacky hunch that may be a waste of time.
3) Remove the 'HGroup' from your component. It's kind of unnecessary: the layout requirements are simple enough to do w/a few lines of Actionscript. Your mileage will vary as the layout requirements get more complex!
In createChildren() add the combo box and button directly to the UIComponent. Then size and position them in updateDisplayList(), something like this:
override protected function updateDisplayList(unscaledWidth:Number, unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
var padding:Number = 10;
var gap:Number = 0;
// make the ComboBox consume all of the width execpt for 20px and gap + padding
var availableWidth:Number = unscaledWidth - 20 - gap - (2*padding);
_drp.setLayoutBoundsSize(availableWidth, unscaledHeight); // combo box 100% width
_btnMultiple.setLayoutBoundsSize(20, unscaledHeight); // button is 20px wide
// now position them ...
// probably should not use 0, rather calculate a Y coordinate that centers them
// in the unscaledHeight
_drp.setLayoutBoundsPosition(padding, 0);
_btnMultiple.setLayoutBoundsPosition(unscaledWidth - padding - 20, 0);
}
Thank you both so much for your answers! The consideration and attention to detail in explaining the concepts is awesome! Très bien!
#RIAstar However due to the amount of code already in place, changing my architecture (separating visual element from behavioural) would force to large a re-factor of the code and would cost to much for a feature that hasn't been explicitly requested. (visual representation of the control being able to change at runtime) It certainly is interesting and I will be adding that into a future version.
That said, I think I've been able to find a solution to my problem. I decided to build off of #SunilD.'s suggestion of validating the control before it's added back in. A hack I know, but humans aren't perfect and thus the code aint either. ;-)
When looking at the control, I noticed it was only the button that was having issues with rendering its image. So to test, I added and removed JUST a button instance with an icon, and I saw the same behaviour! (regardless of how comboBoxMultiSelect was implemented) I ALSO noticed that I didn't see the button do this when it was first created. So why not just reconstruct the button when it gets removed from the display list?
I ended up wiring comboBoxMultiSelect to the FlexEvent.REMOVE event, destroy the button reference, create a new one, and add it back in with AddChild(). Below is an explanation of the event.
"Dispatched when the component is removed from a container as a content
child by using the removeChild(), removeChildAt(), removeElement(), or
removeElementAt() method. If the component is removed from the
container as a noncontent child by using the rawChildren.removeChild()
or rawChildren.removeChildAt() method, the event is not dispatched.
This event only dispatched when there are one or more relevant
listeners attached to the dispatching object."
Sure enough, this fixed the icon from displaying incorrectly and explains what happening. For some reason the button is taking more than one render event to apply its style when it's added back in. Anyone else able to replicate this behaviour?
I guess the real question now is "what is the best way to remove and add-back-in a button to the display list, so that its embedded icon is unaffected?"
In my example below I have several objects. I want to change the label of all objects in one go, without calling each element by id. I know how to do this in HTML, but not in Flex.
// HTML
<div class="text" id="text1">SomeText</div>
<div class="text" id="text2">SomeText</div>
<div class="text" id="text3">SomeText</div>
// jQuery
$(".text").css("color", "#333333");
This is how I would usually set the color of 3 objects to grey in one line.
// Flex
<s:Button id="button1" label="Button 1"/>
<s:Button id="button2" label="Button 2"/>
<s:Button id="button3" label="Button 3"/>
// AS3
button1.label = 'Something else';
button2.label = 'Something else';
button3.label = 'Something else';
Is there any way I can change the labels of all 3 buttons with a single line of code similar to the jQuery example? Thanks in advance.
I'm pretty sure the answer is no, with a caveat.
Keep in mind that JQuery is a framework that hides the complexity of what it is doing. ( A lot of frameworks do that including the flex Framework ). In Flex, I can create a DataGrid in one line of code. However, there are thousands of lines of code, and multiple classes already written that allow me to do that. I suspect the same is true for a lot of JQuery functionality.
there is no reason you can't encapsulate that functionality to make the change and then call it with one line of code.
As #www.Flextras.com pointed out - you can write a class to do this.
I'd encourage you to consider an alternative approach however, as looping through the children looking for a specific property is quite slow. That said - it does make for an interesting coding challenge.
Here's a class & example that should acheive what you're after.
package com.mangofactory.util
{
import flash.display.DisplayObject;
import mx.core.UIComponent;
/**
* Utility class to set a given property of all matching children on a target.
* Named to act as an operator, rather than a class (hence no captial letter)
*
* eg., on(someTarget).of(SomeClass).someProperty = someValue;
* */
public class on
{
private var root:UIComponent;
private var requiredPropertyName:String;
private var requiredType:Class;
public function on(root:UIComponent)
{
this.root = root;
}
/**
* Returns a list of targets which match the defined criteria.
* Note - the root component is also evaluated
* */
private function get targets():void
{
var result:Array = [];
if (matches(root))
{
result.push(root);
}
for (var i:int = 0; i < root.numChildren; i++)
{
var child:DisplayObject = root.getChildAt(i);
if (matches(child))
result.push(child);
}
}
/**
* Returns true if the target param matches the defined criteria.
* If a propertyName has been given (by calling 'having') that is checked first.
* Otherwise, the type is checked against the value passed calling ('of')
* */
private function matches(target:Object):Boolean
{
if (requiredPropertyName && target.hasOwnProperty(requiredPropertyName))
return true;
if (requiredType && target is requiredType)
return true;
return false;
}
public function having(propertyName:String):PropertyCatcher
{
this.requiredPropertyName = propertyName;
}
public function setOnTargets(propertyName:*,value:*):void
{
for each (var matchedTarget:Object in targets)
{
if (matchedTarget.hasOwnProperty(propertyName))
matchedTarget[propertyName] = value;
}
}
public function of(type:Class):PropertyCatcher
{
this.requiredType = type;
}
}
}
import com.mangofactory.util.on;
import flash.utils.Proxy;
import flash.utils.flash_proxy;
use namespace flash_proxy;
dynamic class PropertyCatcher() extends Proxy
{
private var callbackTarget:on;
public function PropertyCatcher(callbackTarget:on)
{
this.callbackTarget = callbackTarget;
}
override flash_proxy function setProperty(name:*, value:*):void {
callbackTarget.setOnTargets(name,value);
}
}
And an example:
<?xml version="1.0" encoding="utf-8"?>
<mx:Canvas xmlns:fx="http://ns.adobe.com/mxml/2009"
xmlns:s="library://ns.adobe.com/flex/spark"
xmlns:mx="library://ns.adobe.com/flex/mx">
<s:Button />
<s:Button />
<s:Button />
<mx:Canvas />
<fx:Script>
<![CDATA[
public function doTest():void
{
// Sets the label of all Buttons to "Hello World"
on(this).of(Button).label = "Hello World";
// Sets the visible property of all children which declare a "alpha" property to false.
on(this).having("alpha").visible = false;
}
]]>
</fx:Script>
</mx:Canvas>
Note - I haven't tested this, but in theory it should work.
I don't know of any selector of the css3/jquery type for flex. But a workaround will be to use an array of buttons instead of many buttons variables and then just iterate through all of them (button[i] instead of buttoni)
In my flex app I have a public bindable property.
I want it so that every time the value of that property changes, a function gets triggered.
I tried using ChangeWatchers, but it seems those only apply to built-in components like a text box change.
I would like to do that same behavior with a property that changes at runtime.
One option is to use BindingUtils.bindSetter (which incidentally returns a ChangeWatcher):
<?xml version="1.0" encoding="utf-8"?>
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" layout="absolute" creationComplete="this_creationComplete()">
<mx:Script>
<![CDATA[
import mx.binding.utils.BindingUtils;
import mx.binding.utils.ChangeWatcher;
[Bindable]
public var myValue:int = 0;
private function this_creationComplete():void
{
var cw:ChangeWatcher = BindingUtils.bindSetter(myValueChanged, this, "myValue");
}
private function setValue():void
{
myValue = getTimer();
}
private function myValueChanged(o:Object):void
{
trace("myValue: " + myValue.toString());
// You can also use o.toString() -- the new value will be passed into the function
}
]]>
</mx:Script>
<mx:Button label="Click Me" click="setValue()" />
</mx:Application>
Here, myValueChanged gets called whenever the myValue property changes. There are other ways, of course, but I often use this approach with good results. Hope it helps! Post back with questions and I'll keep an eye out.
Look into BindUtils class as back2dos suggests.
And, also, you can set the name of the event that will be triggered when a change is done to a property (default is propertyChange) like this:
[Bindable("change")]
var myProperty : SomeClass;
That is if ChangeWatchers adds listeners for the change event instead of propertyChange event. Which would be kind of weird, but not impossible with all the mishaps of the flex SDKs.
But again, I think BindUtils class should do the trick for you.
Use the class ObjectProxy or its subclass and wrap up the class that has a property you need to watch. In my example, I'm calling a func if someone is changing the property salary giving it a value of more than 55000 in an object Person:
package com.farata
{
import mx.utils.ObjectProxy;
import flash.utils.*;
use namespace flash_proxy;
public dynamic class MyPersonProxy extends ObjectProxy
{
// The object to wrap up
private var person:Person;
public function MyPersonProxy(item:Person){
super(item);
person=item;
}
flash_proxy override function setProperty(name:*, value:*):void {
if ( name == 'salary'&& value>55000) {
// add a new property to this instance of the
// class Person, which can be used in the calculations
// of the total compensation
setProperty("pension", 0.02);
}
super.setProperty(name, value);
}
}
}
well, the easiest way is to listen to PropertyChangeEvent.PROPERTY_CHANGE ... if you declare a property bindable, then mxmlc generates the code to dispatch this event ... if you let the compiler keep the generated ActionScript, then you'll see it ...
other than that, you might want to have a look at BindingUtils ...
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})}"
When I am editing a cell in a dataGrid, the changes are not applied to the dataProvider until I finish editing. Is there a way that I can make the changes appear in the dataProvider whilst editing?
I would assume that the way of doing this would be to subclass the editor I am using, in this case NumericStepper, but I don't know how I would go about it.
Is there some sort of event that I need to trigger?
if you create your own itemEditor/itemRenderer you can do something like:
<mx:TextInput xmlns:mx="..." change="onChange(event)"
implements="mx.controls.listClasses.IDropInListItemRenderer">
<mx:Script>
<![CDATA[
import mx.controls.dataGridClasses.DataGridListData;
import mx.controls.listClasses.BaseListData;
[Bindable("dataChange")] private var _listData : BaseListData;
public function get listData():BaseListData
{
return _listData;
}
public function set listData( value : BaseListData ) : void
{
_listData = value;
}
private function onChange(event:Event):void
{
this.data[ (listData as DataGridListData).dataField ] = this.text;
}
]]>
</mx:Script>
</mx:TextInput>
hope this helps.
If you need to reference anything outside of an itemeditor the way I have done this is through outerDocument.somePublicVar.
So if you need to reference the dataprovider of the datagrid you are editing you can update the var you have binded to the datagrid but it must be public (i think) or you can edit the datagrids dataprovider directly.
Within the item editor you can just catch the change event and update the value in the dataprovider from there. But remember that the end edit item event will be thrown and if your doing any processing in there that might mess with your data provider as well.