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 ...
Related
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
I'm an experienced AS developer, but this is something very basic about flex that I can't figure out. In the class below, I'm not sure why the function imageLoaded would not be executed when the image loads a url. Is there a race condition happening here? The application is completely loaded by the time this object is created and setPicture is called. How can I get the Event.COMPLETE event to fire properly in this case?
The line and Log.info functions are conveniences for me, and they definitely work.
Here's my MXML definition:
<?xml version="1.0" encoding="utf-8"?>
<photo:PhotoClass xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:photo="com.xxx.widgets.photo.*">
<mx:Image id="image"/>
</photo:PhotoClass>
Here's my actionscript:
public class PhotoClass extends Box {
public var image : Image;
public function PhotoClass() {
image = new Image();
image.addEventListener(Event.COMPLETE, imageLoaded);
}
private function line(txt : *) : void {
Log.info(txt, "PhotoClass");
}
public function setPicture(url : String) : void {
line("setPicture: " + url);
image.source = url;
}
public function imageLoaded(event : Event) : void {
line("image loaded");
}
}
eventListeners won't register unless the sprite is attached to the application.
Can you give a reference? Because I believe the issue is something else.
When you declare an mxml file with PhotoClass as its root tag, you're extending the PhotoClass. The <mx:Image> tag in the mxml defines a public variable of type Image in the mxml class; and you already have a public var image:Image; in the super class. This will create a conflict - I'd expect a compile time error.
My guess is that since constructor is called first, it assigns an Image object to the public variable image. When the mxml is executed and the children are created, it assigns a new Image object to the public variable image (instead of correctly throwing an error). Now, the event listener was added to the Image instance created in the constructor, but by the time you set image.source in setProperty, it is a different object altogether. That is why it works when you move the event listener to setPicture - there you're assigning it to the new object.
Change the id of the mxml Image tag to something else and it will work even if the event listener was added from the constructor. Change the name of public var image to something else and you'll get a compile time error in PhotoClass.
Btw, what does the Image tag in the mxml do - is that for showing a different image? In that case you must change its id.
<!-- MyCanvas.mxml -->
<mx:Canvas xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Label id="lbl"/>
</mx:Canvas>
<!-- the main application class -->
<mx:Application xmlns:mx="http://www.adobe.com/2006/mxml" xmlns:local="*"
creationComplete="onCreate()" layout="vertical">
<local:MyCanvas id="canvas1"/>
<mx:Script>
<![CDATA[
public var canvas2:MyCanvas;
public function onCreate():void
{
this.canvas1.lbl.text = "MyCanvas_1";
canvas2 = new MyCanvas();
this.addChild(canvas2);
canvas2.addEventListener(FlexEvent.CREATION_COMPLETE, onCanvas2Created);
}
public function onCanvas2Created(e:FlexEvent):void
{
this.canvas2.lbl.text = "MyCanvas_2";
}
]]>
</mx:Script>
</mx:Application>
I figured it out...
eventListeners won't register unless the sprite is attached to the application. The eventListener in this case was being added in the constructor, before the sprite was added to its parent class. I moved image.addEventListener to setPicture and it worked.
I am going to use a HSlider to set a range of values. I would like the left thumb to look like ( and the right thumb to lok like ) so they appear to encompass the range like (range) instead of |range|. I only know how to set the skin for SliderThumb which will set the skin for both. Does anyone know of a way to set a different skin for each thumb?
Thanks.
UPDATE
I have this code now:
<?xml version="1.0" encoding="utf-8"?>
<mx:HSlider xmlns:mx="http://www.adobe.com/2006/mxml">
<mx:Style>
.thumbTickLeft
{
disabledSkin: Embed(source="skins.swf", symbol="thumbTickLeft_disabledSkin");
downSkin: Embed(source="skins.swf", symbol="thumbTickLeft_downSkin");
overSkin: Embed(source="skins.swf", symbol="thumbTickLeft_overSkin");
upSkin: Embed(source="skins.swf", symbol="thumbTickLeft_upSkin");
}
.thumbTickRight
{
disabledSkin: Embed(source="skins.swf", symbol="thumbTickRight_disabledSkin");
downSkin: Embed(source="skins.swf", symbol="thumbTickRight_downSkin");
overSkin: Embed(source="skins.swf", symbol="thumbTickRight_overSkin");
upSkin: Embed(source="skins.swf", symbol="thumbTickRight_upSkin");
}
</mx:Style>
<mx:Script>
<![CDATA[
override protected function commitProperties():void
{
super.commitProperties();
updateThumbSkins();
}
private function updateThumbSkins():void
{
this.getThumbAt(0).setStyle('styleName','thumbTickLeft');
this.getThumbAt(1).setStyle('styleName','thumbTickRight');
}
]]>
</mx:Script>
</mx:HSlider>
The thumb ticks just dont show at all? By the way I have made sure that the skins are loading in correctly because I can set them to a button like this:
<mx:Button styleName="thumbTickRight"/>
Well I was able to get it to work this way..not sure if this is the best way or not.
<?xml version="1.0" encoding="utf-8"?>
<mx:HSlider
xmlns:mx="http://www.adobe.com/2006/mxml"
sliderThumbClass="RangeSliderThumb"
creationComplete="initThumbs()">
<mx:Script>
<![CDATA[
import mx.controls.sliderClasses.SliderThumb;
[Embed(source="skins.swf", symbol="thumbTickLeft_upSkin")]
private var leftUp:Class;
[Embed(source="skins.swf", symbol="thumbTickRight_upSkin")]
private var rightUp:Class;
[Embed(source="skins.swf", symbol="thumbTickLeft_downSkin")]
private var leftDown:Class;
[Embed(source="skins.swf", symbol="thumbTickRight_downSkin")]
private var rightDown:Class;
[Embed(source="skins.swf", symbol="thumbTickLeft_overSkin")]
private var leftOver:Class;
[Embed(source="skins.swf", symbol="thumbTickRight_overSkin")]
private var rightOver:Class;
[Embed(source="skins.swf", symbol="thumbTickLeft_disabledSkin")]
private var leftDisabled:Class;
[Embed(source="skins.swf", symbol="thumbTickRight_disabledSkin")]
private var rightDisabled:Class;
private function initThumbs():void
{
this.thumbCount = 2;
var thumb1:SliderThumb = this.getThumbAt(0);
thumb1.setStyle("thumbUpSkin", leftUp);
thumb1.setStyle("thumbDownSkin", leftDown);
thumb1.setStyle("thumbOverSkin", leftOver);
thumb1.setStyle("thumbDisabledSkin", leftDisabled);
var thumb2:SliderThumb = this.getThumbAt(1);
thumb2.setStyle("thumbUpSkin", rightUp);
thumb2.setStyle("thumbDownSkin", rightDown);
thumb2.setStyle("thumbOverSkin", rightOver);
thumb2.setStyle("thumbDisabledSkin", rightDisabled);
}
]]>
</mx:Script>
</mx:HSlider>
you may not read this again, but for the benefit of others who are having as much pain as I am trying to manipulate multiple thumbs to each have different skins, I thought I'd point out where you went wrong in your original code. I followed your original code example and also could not get the thumbs to render, then it dawned on me why your final solution worked.
The problem is that in the original code you are using style properties upSkin, downSkin, etc, whereas in the code which works you are using thumbUpSkin, thumbDownSkin, etc. Subtle change but it makes all the difference!
Hope this helps someone save a day of their lives down the track... ;-)
Cheers
Drew
I think you could do this by subclassing the Slider class (or HSlider), and adding a method to apply skins to each thumb individually.
In Slider.as there is a method called createThumbs. If you look it up, you'll see that when a thumb is created, its skins are assigned to it based on whatever is set for thumbUpSkin, etc.
thumb = SliderThumb(new _thumbClass());
thumb.owner = this;
thumb.styleName = new StyleProxy(this, thumbStyleFilters);
thumb.thumbIndex = i;
thumb.visible = true;
thumb.enabled = enabled;
thumb.upSkinName = "thumbUpSkin";
thumb.downSkinName = "thumbDownSkin";
thumb.disabledSkinName = "thumbDisabledSkin";
thumb.overSkinName = "thumbOverSkin";
thumb.skinName = "thumbSkin";
So, you could create a method called skinThumbs and have it override the skin settings that are applied in createThumbs. You can get each thumb by calling getThumbAt(int). If you need a hand with overriding the skin settings, leave me a comment.
I would override the commitProperties method and call skinThumbs from there - just make sure you put the call the skinThumbs after the call to super.commitProperties. Incidentally, the createThumbs method is called only from commitProperties, and is only called from one location in commitProperties, so you don't have to worry about dealing with your modification being replaced by another internal call to createThumbs.
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.