Introduction
When designing your own Flex applications, you deal with different types of object's attributes, like string, date, enumeration, number and boolean. Each of the types can be visualized in different ways in the user interface, for example a boolean
type can be represented as a label, as a checkbox, as a pair of radio buttons or as a dropdown list. For these different visualizations, the different controls are used.
In this article, we will get through some examples and learn how the tasks, we deal with, when working with dynamically created controls in Flex, could be simplified with the usage of interfaces and data binding.
Imagine we have a panel that contains several controls, which display some property of the object. The controls are dynamically loaded during startup of the panel. Each of the controls may provide its own style of property visualization.
Creating a Control
Firstly we will create a common interface for the components that will display values (the interface will ease our future work, when we will need to add new types of the controls). To simplify the example, we assume all the components operate with a single value:
package controls
{
public interface IDetailsControl
{
function set value(value: Object): void;
}
}
Now we create a control, which implements that interface and displays the supplied value. This will be a simple control, which extends the mx.controls.Label
functionality:
package controls
{
import mx.controls.Label;
public class ELable extends Label implements IDetailsControl
{
public function set value(value: Object): void
{
if (value is String)
{
text = value as String;
}
else if (value != null)
{
text = "#object#";
}
else
{
text = "#null value#"
}
}
}
}
We will also make a factory, which will provide us with the controls. This factory will operate only with the Elable
control at the moment:
package controls
{
public class ControlFactory
{
public static function getControl(type: String): IDetailsControl
{
var ctrlClass: Class = null;
switch (type)
{
case "label": ctrlClass= ELable;break;
}
var ctrl: IDetailsControl =
ctrlClass != null ? new ctrlClass : null;
return ctrl;
}
}
}
Making Visualization
As soon as we have an interface, a control and a factory, we may start to create an application. We create a property obj
, which will act as a value to be displayed by controls. We will also add a VBox
with the number of buttons, which will make the changes to the obj
property:
="1.0"="utf-8"
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical">
<mx:Script>
<![CDATA[
</mx:Script>
<mx:VBox id="target" width="100%" height="100%" horizontalAlign="center">
<mx:Button label="String" click="obj = Math.random().toString()"/>
<mx:Button label="Object" click="obj = {}"/>
<mx:Button label="Null" click="obj = null"/>
</mx:VBox>
</mx:WindowedApplication>
Our application will display a number of controls. The controls to be shown will be defined in the conf
string array by their names. We will initialize the controls using our ControlFactory
:
var conf: Array = ["label", "label", "unknown control"];
for each (var controlType: String in conf)
{
var ctrl: IDetailsControl = ControlFactory.getControl(controlType);
}
We will then add controls to a VBox layout and will also set the obj
property to be the value for the controls (for simplicity, we will use the same value for all of the controls):
if (ctrl is DisplayObject)
{
target.addChild(ctrl as DisplayObject);
ctrl.value=obj;
}
All the code will be placed in the creationComplete
handler, which will be invoked after the application will be initialized:
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
creationComplete="onCreationComplete(event)">
<mx:Script>
<![CDATA[
import mx.events.FlexEvent;
import controls.IDetailsControl;
import controls.ControlFactory;
public var obj: Object = "initial state";
private function onCreationComplete(event: FlexEvent): void
{
var conf: Array =
["label", "label", "unknown control"];
for each (var controlType: String in conf)
{
var ctrl: IDetailsControl =
ControlFactory.getControl(controlType);
if (ctrl is DisplayObject)
{
target.addChild(ctrl as
DisplayObject);
ctrl.value=obj;
}
}
}
]]>
</mx:Script>
<mx:VBox id="target" width="100%" height="100%" horizontalAlign="center">
<mx:Button label="String" click="obj = Math.random().toString()"/>
<mx:Button label="Object" click="obj = {}"/>
<mx:Button label="Null" click="obj = null"/>
</mx:VBox>
</mx:WindowedApplication>
We may start our application now.
As you see, the two Label controls show the initial value of obj
variable.
Applying Bindings
Now by the use of data binding, we will make the labels listen to the changes made with obj
variable, so when some button is pressed, we will be able to see on the labels the up to date value of obj
property.
First, we will make our obj
property to be bindable:
[Bindable]
public var obj: Object = "initial state";
Then we will replace the simple setting of ctrl.value=obj;
with the binding to the setter function so this function will be invoked every time an obj
property changes. For that, we will use the bindSetter
method from mx.binding.utils.BindingUtils
:
BindingUtils.bindSetter(createSetter(lbl),this,"obj");
The setter function, which we pass to the bindSetter method
, will look like follows:
private function createSetter(ctrl: IDetailsControl): Function
{
return function (value: Object): void
{
ctrl.value = value;
}
}
As you see, we enclosed a setter function within another function closure. Due to this, the reference to the ctrl
control will be stored in a function closure and during binding process, the setter will be correctly invoked once per control. If we would bind a setter directly, ctrl
variable, which is stored within the setter scope and changes in the previously defined "for
" loop, will contain a reference to the last control in all the scopes, which is not what we really want to have.
Our full application code looks as follows:
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical"
creationComplete="onCreationComplete(event)">
<mx:Script>
<![CDATA[
import controls.IDetailsControl;
import controls.ControlFactory;
import mx.events.FlexEvent;
import mx.binding.utils.BindingUtils;
[Bindable]
public var obj: Object = "initial state";
private function onCreationComplete(event: FlexEvent): void
{
var conf: Array =
["label", "label", "unknown control"];
for each (var controlType: String in conf)
{
var ctrl: IDetailsControl =
ControlFactory.getControl(controlType);
if (ctrl is DisplayObject)
{
target.addChild
(ctrl as DisplayObject);
BindingUtils.bindSetter(
createSetter(ctrl),
this,
"obj"
);
}
}
}
private function createSetter(ctrl: IDetailsControl): Function
{
return function (value: Object): void
{
ctrl.value = value;
}
}
]]>
</mx:Script>
<mx:VBox id="target" width="100%" height="100%" horizontalAlign="center">
<mx:Button label="String" click="obj = Math.random().toString()"/>
<mx:Button label="Object" click="obj = {}"/>
<mx:Button label="Null" click="obj = null"/>
</mx:VBox>
</mx:WindowedApplication>
Now we may start our application, click on the buttons and see that the labels will react to the changes of obj
property:
Adding More Controls
So, when an object is changed, a setter is executed for each of the controls. When there is a need to introduce a new control, we only should make it implement the IDetailsControl
interface, register it in a ControlFactory
and define it in the configuration. Let us create one more control, which this time will extend the mx.controls.TextInput
:
package controls
{
import mx.controls.TextInput;
public class ETextField extends TextInput implements IDetailsControl
{
public function set value(value: Object): void
{
if (value is String)
{
text = value as String;
}
else if (value != null)
{
text = "#object#";
}
else
{
text = "#null value#"
}
}
}
}
Now, we add a control to the ControlFactory
:
switch (type)
{
case "text":
ctrlClass= ETextField; break;
case "label":
ctrlClass= ELable;break;
}
And finally we define it in a configuration conf
array:
var conf: Array = ["label", "text", "label", "unknown control"];
We may now open the application, click on some button and see the result:
Everything is fine, all the controls including new text control display up to date values for the obj
property.
Extending Setter Logic
The previous screenshot shows the application state, when we clicked on a "Null
" button. The displayed values are not very nice to see, so let us now implement the logic to hide the controls, when the obj
property is set to the null value.
Since we work with the IDetailsControl
interface, we will add a new visible
method there so the interface will look as shown below:
package controls
{
public interface IDetailsControl
{
function set value(value: Object): void;
function set visible(value: Boolean): void;
}
}
Now we need to implement this visible
method in our Elable
and ETextField
controls. The good thing is that this method was already implemented in the classes we extended our controls from (mx.controls.Label
and mx.controls.TextInput
).
So, we may just start using this new interface method. We will add it to the createSetter
function:
private function createSetter(ctrl: IDetailsControl): Function
{
return function (value: Object): void
{
ctrl.visible = value != null;
ctrl.value = value;
}
}
And that’s it. When we click on a "Null
" button, the components will hide:
When dealing with dynamically created controls, the usage of interfaces and data binding simplifies the developer tasks, reduces the code size, and makes the model to be easily extendable.
History
- 16th March, 2010: Initial post