In Part 1 (CFDJ, Vol. 8, issue 10) we introduced the destination-aware grid, formatters, and renderers. In this article we are continuing our discussion about datagrid renderers and...
RadioButtonGroupBox as Drop-In Renderer
We can apply similar techniques to RadioButton controls. The following code snippet suggests how the group of RadioButton controls can be used as a drop-in item renderer (and editor). Instead of an onValue/offValue pair, we are introducing an array of options (we could have gone further and upgraded <fx:options> to <fx:dataProvider>, which is similar to the ButtonBar and LinkBar controls):
<fx:DataGridColumn dataField="STATUS" width="300" headerText="Status" rendererIsEditor="true"
itemRenderer="com.theriabook.containers.RadioButtonGroupBox">
<fx:options>
<mx:Array id="options">
<mx:Object data="A" label="Active"/>
<mx:Object data="T" label="Terminated"/>
<mx:Object data="L" label="On leave"/>
</mx:Array>
</fx:options>
</fx:DataGridColumn>
To support this use case, we need to build the renderer class and make the DataGridColumn pass it the array of options. The latter can be done by adding the following options setter to our DataGridColumn:
package com.theriabook.controls.dataGridClasses{
. . . . .
public class DataGridColumn extends mx.controls.dataGridClasses.DataGridColumn {
. . . . .
public function set options(val:Array):void {
if (itemRenderer) UIClassFactory(itemRenderer).properties = {options:val};
}
}
}
Now let's build the renderer. By definition, to be an item renderer, the component has to implement the IListItemRenderer interface. To qualify as drop-in, a component has to additionally implement IDropInListItemRenderer. The Standard CheckBox implements both interfaces. Because of that, when we extended CheckBox in the previous article, we did not have to mention a single implementation and just merrily used data and listData at our convenience.
This is not the case now. Had RadioButtonGroup been at least a UIComponent, we'd need to implement the IDataRenderer and IDropInListItemRenderer interfaces and be done. But RadioButtonGroup is not even a DisplayObject, so we will base our renderer on mx.containers.Box with RadioButtonGroup embedded:
private var group:RadioButtonGroup=null ;
public function RadioButtonGroupBox() {
super();
group = new RadioButtonGroup();
}
Having RadioButtonGroup is just the beginning. Whenever our component gets assigned any options, we need to translate them into a set of RadioButton controls.
Each RadioButton will be added as a child of the renderer (container):
private var _options:Array=null;
public function set options(opt:Array):void {
var i:int;
. . . .
_options=opt;
for (i= 0; i < opt.length; i++) {
var rb:RadioButton = new RadioButton();
rb.label = opt[i].label;
rb.value = opt[i].data;
addChild(rb);
}
}
override public function addChild(child:DisplayObject):DisplayObject {
if (child is RadioButton) {
(child as RadioButton).group = group;
group.addInstance(child as RadioButton);
}
return super.addChild(child);
}
}
Please note how subscribing a RadioButton to the group is delegated to the overridden method addChild():
(child as RadioButton).group = group;
group.addInstance(child as RadioButton);
Had we done it directly in the options setter, there wouldn't be any need in addChild(). Why did we take the convoluted way? We did it to enable the potential use of RadioButtonGroupBox as a regular container, outside of the renderer context. In other words, no matter how a RadioButton gets added to the component - via options or not - it will get associated with the group.
Next, since we want the component to be a drop-in renderer, we need to implement the IDropInListItemRenderer interface so that the extra information about the hosting List is at our fingertips:
private var _listData:BaseListData=null;
public function get listData():BaseListData {
return _listData;
}
public function set listData(value:BaseListData):void {
_listData = value;
}
Once we have the listData, we can offer the following override of the data setter of IDataRenderer:
override public function set data(item:Object):void {
super.data = item;
if( item!=null ) {
group.selectedValue = item[DataGridListData(listData).dataField];
}
}
Similarly, while implementing the property value, we again consider both use cases: the standalone component and the item renderer. In case of the item renderer, our component updates the underlying data:
public function get value():Object {
return group.selectedValue;
}
public function set value(v:Object) : void {
group.selectedValue = v;
if (listData) {
data[DataGridListData(listData).dataField] = group.selectedValue;
}
}
Finally, how about capturing the selection of a radiobutton? Since we need to listen to the change event on the RadioButtonGroup, we'll set up the listener right in the constructor method, handling the Event.CHANGE with the anonymous function:
public function RadioButtonGroupBox() {
. . . .
group = new RadioButtonGroup();
group.addEventListener(Event.CHANGE,
function event:Event):void {
value = event.target.selectedValue;
}
);
}
The complete code of the RadioButtonGroupBox is presented in Listing 1. (Listings 1-13 can be downloaded from the online version of this article at http://coldfusion.sys-con.com.) See if you can discover the discrepancies between the listing and what we outlined in our snippets:
Now let's test the itemRenderer/itemEditor scenario. Not that it's required, but we would prefer to lay out the radio buttons horizontally. Accordingly, we will create RadioButtonGroupHBox as a simple extension of the RadioButtonGroupHBox, taking care of the box's direction and adding a couple of padding pixels:
The screenshot of the test application RadioButtonGroupHBoxDemo is presented in Figure 2. As you may notice in Listing 4, we declared the entire DataGrid as editable, and marked the "status" column with rendererIsEditor="true":
Computed Column Color
Getting back to the
DataGrid, we will look at controlling the styles of the DataGridColumn:
color, backGroundcolor, font and so on. We'll focus on defining the
styles in such a way that they are re-evaluated along with the data
changes. Many times we meet business requirements that call the styles
to be different from row to row. Supposedly, we need to highlight high
paid employees ($50K or more) in red, otherwise the salary value is
shown in green. One solution is to use the in-line itemRenderer:
<mx:DataGridColumn dataField="SALARY" textAlign="right">
<mx:itemRenderer>
<mx:Component>
<mx:Label>
<mx:Script>
<![CDATA[
override protected function updateDisplayList(unscaledWidth: Number,
unscaledHeight:Number):void
{
super.updateDisplayList(unscaledWidth, unscaledHeight);
if (data && listData) { // Check that we are in a List
if (data.SALARY > 50000) {
setStyle("color", "red");
} else {
setStyle("color", "green");
}
}
}
]]>
</mx:Script>
</mx:Label>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
As a component base we've used Label, an immediate descendant of the UIComponent, because updateDisplayList(), an UIComponent method, would be out of reach for a standard DataGridItemRenderer based on UITextField. Alernatively, we could have achieved the same functionality with a more elegant binding expression syntax:
<mx:DataGridColumn dataField="SALARY" textAlign="right">
<mx:itemRenderer>
<mx:Component>
<mx:Label
color="{data.SALARY>50000?255*256*256:255*256}"
>
</mx:Label>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
Listing 5 presents the complete code of the sample application StandardDynamicStyleDemo. In addition to the DataGrid, we have thrown in "Increase" and "Decrease" buttons, which allow us to modify the salary values in increments of 10K. When you run a StandardSynamicStyleDemo, you will see the picture shown in Figure 3.
Computed Column Background
We can't apply the same
technique for a background color, because Label does not support the
backgroundColor style. In comparison, TextInput does and, although, we
could have resorted to TextInput instead of Label, the mere absence of
backgroundColor does not seem reason enough to give up a lightweight
Label in favor of the heavier TextInput. After all, the beauty of Flex
is that the framework is extensible. Our custom Label extended with
backgroundColor support is shown in Listing 6.
We have defined not one, but two styles - backgroundAlpha and backgroundColor - and we use both values with graphics.beginFill() inside the overridden implementation of updateDisplayList(). Once we add the Label.as to theriabook.swc and register it within the component manifest XML, the DataGridColumn can be redefined as is done in the following snippet:
<mx:DataGridColumn dataField="SALARY" textAlign="right">
<mx:itemRenderer>
<mx:Component>
<fx:Label
backgroundColor="{data.SALARY>50000?255*256*256:255*256}"
/>
</mx:Component>
</mx:itemRenderer>
</mx:DataGridColumn>
The difference between the above approach and the way we defined a similar DataGridColumn in Listing 5 is that now we compute backgroundColor instead of the color and use fx:Label instead of the Label from mx namespace. If you do the replacement and run the program, you'll see the picture as shown in Figure 4.
Despite the decent result, we still don't feel satisfied: employing the powerful mechanism of item renderers only for the purpose of managing styles seems to be design overkill. Let's speak our mind: we are after dynamic runtime styles, right? Wouldn't it be nice if we added an extra DataGridColumn attribute, called, say, runtimeStyles, where we could list all the styles and the abstract from the implementation. Here is an example:
<fx:DataGridColumn dataField="SALARY" textAlign="right" formatString="money">
<fx:runtimeStyles>
<mx:Object
backgroundColor="{function(item:Object):String {return (item.SALARY>50000)?'red':'green';}}"
/>
</fx:runtimeStyles>
</fx:DataGridColumn>
This approach would let developers concentrate on the substance rather than on the process. Let's make it happen.
Runtime Column Styles Unleashed
Here is the plan:
we will upgrade the default itemRenderer of the fx:DataGrid from
UITextField to our custom fx:Label by extending fx:DataGridColumn with
an extra property - runtimeStyles. Finally, we will intercept data
changes to an item renderer, whether default or not, to re-assign all
runtimeStyles on each.
Here is how the constructor of the standard DataGrid assigns itemRenderer:
package mx.controls {
public class DataGrid extends DataGridBase implements IIMESupport
{
public function DataGrid()
{
super();
itemRenderer = new ClassFactory(DataGridItemRenderer);
. . . . . .
}
}
}
We would have to fight the instant temptation to replace the assignment of the itemRenderer with:
itemRenderer = new ClassFactory(com.theriabook.controls.Label);
Here is why: a ClassFactory instance is a "factory object," which is used to generate instances of another class (aka generator class) with the newInstance() method. According to our plan, we need to intercept data changes to any instance of the generator class item renderer. In other words, we'll need to listen to the event FlexEvent.DATA_CHANGE on every instance of the com.theriabook.controls.Label created by "the factory." Hmm, what could be simpler than adding the needed event listener to the controls? Although we are content with the fx:Label as a default renderer, by no means do we propose to take the power of the custom item renderers away. To make the mechanism of the runtimeStyles control agnostic, we would like to listen to FlexEvent.DATA_CHANGE on the instances of any generator class.
It only sounds difficult. After all, a ClassFactory is nothing but an implementation of the IFactory interface with a single property - properties and single method - newInstance(). So we can easily wrap a standard ClassFactory inside our custom one for the purpose of intercepting the newInstance() call. We will call our wrapping class factory the UIClassFactory:
function DataGrid() {
super();
itemRenderer = new UIClassFactory(ClassFactory(com.theriabook.controls.Label));
}
The constructor of the UIClassFactory would simply store the reference to the instance of the real class factory - cf. The newInstance() would delegate the call to the cf.newInstance(). In addition, it would also register the listener to FlexEvent.DATA_CHANGE event, as shown below:
public class UIFactory implements IFactory
{
. . . . .
public function UIClassFactory( cf:ClassFactory ) {
wrappedClassFactory = cf;
}
public function newInstance():* {
var obj:* = wrappedClassFactory.newInstance();
obj.addEventListener(FlexEvent.DATA_CHANGE, onDataChange);
return obj;
}
private function onDataChange(event:FlexEvent):void{
. . . . .
}
}
As a reminder, the properties of the factory-manufactured objects get assigned by iterating over properties of the specific property of the factory object. The name of this aggregating property is properties. That way all instances are initialized with the same values. Wrapping up the property properties is quite simple:
public function set properties(v:Object):void {
wrappedClassFactory.properties = v;
}
public function get properties():* {
return wrappedClassFactory.properties ;
}
The complete code for UIClassFactory is available in Listing 7.
Let's talk about the onDataChange() handler. The implementations of both IDropInListItemRenderer and IDataRenderer events are sending us DATA_CHANGE events. We start by filtering out the events coming from the IDropInListItemRenderer part and leave only the ones coming from IDataRenderer. Then we single out the runtimeStyles property and treat it as a dynamic object carrying the property names that correspond to the style names. The values of these properties may be literal or, alternatively, the function references. In the latter case, we apply the call operator () prior to setting the style value with the setStyle().
One chore remains. As long as we want to communicate the runtimeStyles to any item renderer, including the ones that are individually set on a per column basis, we need to modify our DataGridColumn, overriding the implementation of the DataGridColumn's itemRenderer setter:
override public function set itemRenderer( val : IFactory ) : void {
super.itemRenderer = new UIClassFactory(val as ClassFactory);
}
The code for our DataGridColumn is shown in Listing 8, and Listing 9 has our test application RuntimeStyleDemo. Figure 5 depicts the RuntimeStylesDemo running.
What's the cost of our automation? We have replaced the ultralight UITextField with the heavier UIComponent - Label so there must be a potential for performance degradation. On the other end, the DataGrid recycles item renderers by maintaining a pool of them just enough to cover the visible portion of the column, which outright limits possible damage.
o far we have shown that it is possible to control runtime styles via anonymous or explicit functions (backgroundColor versus computedFontWeight in the above demo application). You can take our approach further and completely outsource the dynamic styling to a separate controller object flexibly instantiated via the getDefinitionByName() method. Come to think of it, you would completely shield developers from formatting and styling problems of a particular project.
Masked Input and Numeric Input
The input masking
stops a user from entering non-appropriate characters. Stricter masks
can prevent users from entering non-complete numbers, lesser than the
required text, etc. - details are always implementation-specific. Taken
to an extreme, masks can completely obliterate validation programming,
albeit at a cost of fixing a specific user experience.
The first class of this section - MaskedInput - has been created by Peter Ent from Adobe. We will be using it for illustrative purposes only; the code walkthrough of MaskedInput is beyond the scope of this article. MaskedInput is a lightweight mask in which you can indicate the maximum number of positions in the mask and prescribe the type of characters the user can type. The movement of the insertion point is controlled by the control. For example, if you set a mask for entering a U.S. phone number as (###) ###-####, in response to the end user typing 6175551212, the control will display (617) 555-1212.
Alpha keys will be blocked, but the completeness of the phone is up to the user. The control is an extension of mx.controls.TextInput and its text returns the "mask-free" data input, i.e., 61755551212, in our use case. The main controlling property of MaskedInput is inputMask, which can consist of any characters except:
public override function set data(data:Object):void {
super.data = data;
var dgListData:DataGridListData = DataGridListData(listData);
text = data[dgListData.dataField];
}
The complete code for com.theriabook.controls.MaskedInput as well of all other samples from this article are available at http://samples.faratasystems.com/AdvancedDataGrid/index.html .
The second mask that we present in this section is NumericInput. It doesn't control the insertion point so typing and pasting is not restricted. However, it blocks any typing or pasting that contains invalid characters. Listing 10 shows the first iteration of NumericInput code.
As you can see in the listing, we listen to TextEvent.TEXT_INPUT, which corresponds to a character or a sequence of characters (paste) entered by the user. Whatever has been entered comes as an event.text and we test it with a regular expression, trying to find illegal characters. The string literal that we used for RegExp reads as "anything but characters in the range 0-9 or comma or dot or minus." If an illegal character is found, we reject the typing or pasting by cancelling the default behavior of the event with:
event.preventDefault();
That's all it takes to bullet proof your input fields from undesired characters. Two more small patches before we leave the NumericInput, though.
In the course of marshalling the Java data across the wire, chances are your numeric data will come as Number.NaN, a direct counterpart of Java Double.NaN or Float.NaN. In general, unless you use special DTOs with embedded null indicators; this is the natural way to marshal numeric nulls. No one would appreciate the lettering NaN staring at the user instead of the empty cell. Following the established pattern, we are going to make our NumericInput cognizant of the value, then have it produce the appropriate text as required for the presentation. Hence the following addition to NumericInput code:
private var _value:*;
[Bindable("change")]
public function set value(v:*):void {
_value = v;
if ((isNaN(v)) || (v==null /*null or undefined*/)) {
text="";
} else {
text = String(v as Number);
}
}
public function get value():* {
// Preserve NaN | null | undefined, when nothing has been entered
if (((_value!=null )&& (String(_value)!="NaN")) || (text!="") ) {
_value = Number(text.replace(/,/g,"")); // deformat first
}
return _value;
}
public override function set data(item:Object):void {
if (listData && listData is DataGridListData) {
var dgListData:DataGridListData = DataGridListData(listData);
value = item[dgListData.dataField];
}
super.data = item;
}
In the code fragment above we intervened in the setter for data property for when NumericInput is embedded in the DataGrid as a renderer. There we modify the value and let the value, in turn, modify the text.
In the value getter we return the original content: null, undefined, or Number.NaN, provided the user has not entered anything. Otherwise, we use a regular expression to convert the text to a Number, globally eliminating the spaces and the thousand separators first:
_value = Number(text.replace(/,/g,""));
Strictly speaking, we should have operated with a locale-specific thousands separator character instead of ",". For reference, U.S. English-specific definitions of String constants decimalSeparator and thousandsSeparator ("." and ",") are located in the file Flex SDK/ 2/frameworks/locale/en_US/validator.properties.
If you are delivering your application to Brazil, you could create a similar or smaller file in the Flex SDK/ 2/frameworks/locale/pr_BR folder, where you would redefine decimalSeparator as "," and thousandsSeparator as ".". Supposedly you would keep the original name - validators.properties. Then you would modify the compiler options for theriabook.swc to specify the pr_BR locale and rebuild NumericInput after adding the following code:
import mx.resources.ResourceBundle;
import mx.resources.ResourceBundle;
public class NumericInput extends TextInput {
. . . .
[ResourceBundle("validators")]
private static var rb:ResourceBundle;
public static var decimalSeparator:String;
public static var thousandsSeparator:String;
// Load resources during class definition loading
loadResources();
private static function loadResources():void {
decimalSeparator = rb.getString("decimalSeparator");
thousandsSeparator = rb.getString("thousandsSeparator");
}
}
Let's move on and present the testing application - NumericInputDemo. When you run it, try to type anything but digits, a comma and dot into the "Number" column; you won't be able to. Again, please make no mistake: NumericInput is a light-weight mask and it does not replace a need for validation. In particular, if you like regular expressions as much as we do, you may base the validation of the currency field on the mx.vaidators.RegExpValidator applying the regular expression which is the best match for your use case, for instance, something like:
^\$?([1-9]{1}[0-9]{0,2}(\,[0-9]{3})*(\.[0-9]{0,2})?|[1-9]{1}[0-9]
{0,}(\.[0-9]{0,2})?|0(\.[0-9]{0,2})?|(\.[0-9]{1,2})?)
Another feature to notice while running the demo app is how NaN, null, and undefined values are preserved in the absence of a meaningful input in the corresponding cells (see Figure 6).
The code of the testing application is shown in Listing 11, and the helper class NumberScope.as is presented in Listing 12.
Finally, Listing 13 depicts the complete code for NumericInput.
Summary
In this article we've continued our
journey into not so obvious techniques of working with Adobe Flex
DataGrid. In the final version of the article, we'll talk about data
grids with dynamically computed item editors, data-driven programming
and pivoted data grid.