Tom Sugden: Writing Genuinely Reusable Flex Components

« Modularity in Flex Applications | Main | The Flexible Configuration Options of Parsley »

December 10, 2009

Writing Genuinely Reusable Flex Components

On larger projects and within enterprises, there's often a case for extracting a set of reusable components into a Flex library project. In theory, the same components can be reused across modules and sub-applications of multiple Flex or AIR clients, bringing greater consistency and more rapid development. However, in practice there are some common mistakes that limit the reusability of components. This post explains what makes a component genuinely reusable and highlights some techniques from the Flex SDK that can be applied to your own components to make them more reusable.

What Makes a Component Genuinely Reusable?

There are different levels of reusability, but a fully reusable component should be able to render any kind of data. It should be equally comfortable with an array of basic, dynamic Objects or a collection of concrete Kangaroos. The Flex DataGrid has this quality:

<mx:DataGrid dataProvider="{ kangaroos }">
   <mx:columns>
      <mx:DataGridColumn headerText="Name" dataField="name"/>
      <mx:DataGridColumn headerText="Weight" labelFunction="calculateWeight"/>
   </mx:columns>
</mx:DataGrid>

Notice how the dataField and labelFunction properties tell the component how to get its data from the Kanagoo objects without imposing any dependency. These are two of the mechanisms available to make a component genuinely reusable. Even if the developer has no control over the Kangaroo class itself, perhaps it's part of a 3rd-party library, they can still easily render these objects within a DataGrid.

The Data Interface Anti-Pattern

One common mistake is to insist that the data being rendered by a component implements a specific interface. For example, consider a DistributionBar component that renders a simple graph, like that shown in Figure 1.

DistributionBar.png

Figure 1 - A Distribution Bar Component

The distribution bar shows a number of regions with different sizes, each containing a label. It's tempting to configure this using an array of IRegion objects:

public interface IRegion
{
   function get label() : String;
   function get size() : int;
}

The distribution bar can then extract the size and label information for each region through this interface. The rationale for this is that the interface decouples the component from the concrete objects it renders. Anything can be rendered so long as it implements IRegion, but that so long is in fact the design flaw. By imposing the use of the IRegion interface the reusability is limited. The interface needs to be added to existing model classes before they can be rendered in a distribution bar, and worse, if the models were produced by another library or separate team, it might not be an option to change them, so they'd need to be wrapped. For these reasons, the component is not genuinely reusable.

Reusable Components of the Flex SDK

The Flex SDK contains many reusable components and this is achieved by applying a few standard approaches:

  1. Data Fields
  2. Data Functions
  3. Data Descriptors
  4. Factory Objects

These approaches are now described and the same techniques can be applied to your own components to make them reusable.

Data Fields

A data field is a String property that specifies the name of another property. For example, the labelField property of the ComboBox or the dataField and dataTipField properties of the DataGridColumn:

<mx:ComboBox dataProvider="{ items }" labelField="name"/>

The component implementation uses the data field to read the data values from the items it renders. For example:

for each (var item:Object in dataProvider)
{
    var value:Object = item[dataField];
    // do something with the value
}

This is a simple approach but it offers great flexibility. The component can render any readable property of any class of object.

Data Functions

A data function is a property of type Function that is used to specify a reference to another function. For example, the labelFunction property of the ComboBox or the dataFunction property of the DataGridColumn.

<mx:DataGridColumn headerText="weight" dataFunction="calculateWeight"/>

The component then invokes the data function, typically passing through an item of data as a parameter. For example:

for each (var item:Object in dataProvider)
{
    var value:Object = dataFunction(item);
    // do something with the value
}

This approach is similar to using a data field, but offers even more flexibility, since the function can perform calculations or formatting before returning a value to the component for rendering.

Data Descriptors

A data descriptor is an interface through which a component can analyze the items of data it renders. The developer can then pass their own implementation of the interface to the component in order to configure it. An example can be seen in the Tree component of the Flex SDK:

<mx:Tree dataProvider="{ items }">
    <mx:dataDescriptor><my:MyDataDescriptor/></mx:dataDescriptor>
</mx:Tree>

The tree can then discover characteristics of the data by querying its data descriptor interface. For example:

for each (var item:Object in dataProvider)
{
    var isBranch:Boolean = dataDescriptor.isBranch(item, dataProvider);
    // do something with the outcome
}

This approach is very powerful, but is only necessary for complex components such as the Tree. The effort of using the component is more than a simple List or ComboBox, but the component is still completely decoupled from the data it renders. If a developer needs to render a new class of object in a tree, they typically write a new implementation of the ITreeDataDescriptor interface.

Factory Objects

A factory object, in terms of component developent, is a property of type IFactory that is used to instantiate children at runtime. For example, the itemRenderer property of the List and DataGrid or the dropdownFactory property of the ComboBox.

<mx:List dataProvider="{ items }" itemRenderer="my.package.MyItemRenderer"/>

The component uses the standard IFactory interface of the Flex SDK to create new objects at runtime:

var itemRenderer:Object = itemRenderer.newInstance();

Then it passes data items into the new object through the IDataRenderer interface:

if (itemRenderer is IDataRenderer)
{
    IDataRenderer(itemRenderer).data = item;
}

This approach gives great control over the visual appearance of parts of the component. By providing custom item renderers, completely different results can be achieved. The logic for processing the item of data can be as simple or complex as needed and it can be encapsulated inside the item renderer class. Having said that, components that use factories should define sensible default values, so the component is easy to use in simple cases without setting special factories. This is the case for all the ListBase components, such as DataGrid, which uses the general purpose DataGridItemRenderer by default.

It's worth noting here that the Flex compiler has a special relationship with properties of type IFactory. When it notices such a property being set in MXML, it will automatically generate code to convert class names and in-line components into instances of ClassFactory. This makes the components easier to use, so developers don't usually need to instantiate class factories manually, but instead just specify a class name or declare an in-line component.

Conclusion

When fully reusable components are needed, it's good to remember the simple rule: a reusable component should be able to render any kind of data. This is best achieved by following the conventions laid out in the Flex SDK, such as data fields, data functions, data descriptors and object factories. It's important to resist the urge to introduce new interfaces that impose unnecessary obligations on users of your component, because to do so limits its reusability.

Postscript: Since fully reusable components tend to use mechanisms such as dynamic property lookups and function references, there are some trade-offs to consider. These techniques are slower than accessing properties of strongly-typed objects and they're not resiliant to compile-time type checking. However, the advantages of flexibility and reduced dependencies can outweigh these drawbacks for larger projects and enterprises.

Posted by tsugden at December 10, 2009 9:18 AM

Comments

Nice post Tom.

It looks familiar ! ;-)

Posted by: Alex Galays at December 12, 2009 9:16 PM

Great post Tom!

Posted by: Victor at January 20, 2010 8:18 AM

Post a comment




Remember Me?

(you may use HTML tags for style)