|
by Dubrovka Eugene, IntexSoft
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
{
// receiving control class
var ctrlClass: Class = null;
switch (type)
{
case "label": ctrlClass= ELable;break;
}
// instantiating control class
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 do the
changes to the obj property:
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml"
layout="vertical">
<mx:Script>
<![CDATA[
public var obj: Object = "initial 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>
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 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
{
// specify controls we want to display
var conf: Array = ["label", "label", "unknown control"];
for each (var controlType: String in conf)
{
// receiving control
var ctrl: IDetailsControl =
ControlFactory.getControl(controlType);
// we work with DisplayObject only
if (ctrl is DisplayObject)
{
//adding a control to layout
target.addChild(ctrl as DisplayObject);
//setting the value for the control
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 to 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 as 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 each 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 like 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
{
// specify controls we want to display
var conf: Array = ["label", "label", "unknown control"];
for each (var controlType: String in conf)
{
// receiving control
var ctrl: IDetailsControl =
ControlFactory.getControl(controlType);
// adding a control to layout,
// we work with DisplayObject only
if (ctrl is DisplayObject)
{
target.addChild(ctrl as DisplayObject);
// applying binding
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 we 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 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
Previous screenshot shows the application state, when we clicked
on a "Null" button. The displayed values are not very nice to see, so
let we 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 like as follows:
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. |