Type-Safe DataProviders for Flex Lists with the ActionScript Vector

by David Salahi on February 21, 2011

Update, 2/25/11: I’ve updated the code to provide both complete type-safety and event dispatching so that Lists will update their views automatically when the list contents are changed. If you’re interested in this topic, you will probably still want to start by reading this post. But be sure to continue with my follow-up post An Enhanced Type-Safe Data Provider Technique for Flex IList-Based Controls to get the new, improved code.

Coming to Flex from a primarily C++/C# background I have always been a bit dismayed by the loose typing that is possible with ActionScript. Strict typing is an inherent advantage of languages like C++, C# and Java because it allows the compiler to catch incorrect-type errors at compile-time and this leads to greater developer productivity. Any time you can catch an error at compile-time instead of at run-time you’re multiplying your productivity.

With ActionScript 3 you do have the ability to require strict typing for most of your objects but there are some areas in which Flex still falls down. One of these in its List-based components and its DataGrids. Data providers for these objects are typically ArrayLists or ArrayCollections of generic Objects. Unfortunately, this means there is no way to do type-checking of the objects added to these containers. You may intend to have an ArrayCollection of Dog objects but there’s nothing to prevent you from slipping a couple of Cats in there. Imagine the mayhem that will ensue!

The ActionScript Vector—a (mostly) Type-Safe Container Class

ActionScript does have a container class, the Vector, which allows for type checking. Unfortunately, it’s little-used and that’s too bad. Part of the reason is probably because it cannot be used with Flex’s list and grid components. But I encourage you to use it whenever possible—which includes any time you might use a regular Array. And, in fact, it is possible to use it for List-based components by simply implementing the IList interface. I’ll show an example of that shortly.

But, first, I want to back up and demonstrate the basics of the Vector class. Any time you have a collection of things of a single type you can declare a Vector of that type. So, to keep Cats out of my collection of Dogs, I could do the following:

var dogs:Vector.<Dog> = new Vector.<Dog>;

The dot-notation will probably look a little odd the first time you see it but this is how you tell ActionScript that you want an array (speaking generically, here) consisting of only Dog objects.  To add Dog instances to the Vector just do the following:

var dog1:Dog = new Dog();
dogs.push(dog1);

And if BorderCollie extends Dog you can, of course, also do:

var dog2:BorderCollie = new BorderCollie();
dogs.push(dog2);

But if you try this:

var cat:Cat = new Cat();
dogs.push(cat);

you will get an error at run-time. It’s nice to get the run-time error but it’s unfortunate that the Flex compiler doesn’t detect this at compile time. The docs say the reason is because push() can accept multiple arguments. Because of the way ActionScript handles functions with an indeterminate number of arguments it’s just not possible to do type-checking at compile-time.

But you will, at least,  get a run-time error as soon as you attempt to push the Cat in there amongst all those Dogs. That’s better than adding a Cat to your Dogs collection and not finding out about the error until sometime later. If you tried this with an ArrayList or ArrayCollection it would, of course, work and then when the inevitable cat-and-dog-fight ensues you’d have to backtrack to find out who let the Cat in.

Next, I’ll show how to wrap a Vector in a class so that it can be used as a DataProvider. And, later I’ve got a solution which even allows for compile-time type checking with the Vector. More on that below.

Flex Lists with Vectors as DataProviders

This application wraps a Vector object in a class to provide type-safe access. This class implements IList so that the List, ComboBox and DropDownList can use it as a DataProvider. Right-click the app to view source or download an FXP file here.

As I mentioned earlier, one downside of Vectors is that you cannot use them with List-based components like Spark Lists, ComboBoxes, DropDownLists and ButtonBars—at least, not directly. You can, however, adapt Vectors so that they can be used with these Flex components. These components require, as data providers, an object which implements IList. ArrayList and ArrayCollection both implement IList which is why they work as data providers.

The basics of implementing IList are straightforward. The interface consists primarily of methods like addItem, addItemAt, getItem and removeItem. So, you can create a class which implements IList and include a Vector.<YourType> as a member variable. Then, in the implementation of each of these setter and getter methods just do the appropriate action on your Vector instance. That’s it! You can then use your Vector wrapper with a List, ComboBox or DropDownList.

If you want a full-featured IList implementation you’ll need to do some extra work like calling listeners when data items change. But for many purposes you won’t need to bother with these extra features. Here’s a simple implementation of IList which wraps a Vector.<Person> instance. Here’s the Person class:

public class Person
{
    public var firstName:String;
    public var lastName:String;
 
    public function Person()
    {
    }
 
    public function toString():String
    {
        return firstName + " " + lastName;
    }
}

Here’s the PersonVectorDP class with the (partial) IList implementation:

public class PersonVectorDP implements IList
{
    private var _people:Vector.<Person> = new Vector.<Person>;
 
    public function PersonVectorDP()
    {
    }
 
    //
    // IList
    //
    public function get length():int
    {
        return _people.length;
    }
 
    public function addItem(item:Object):void
    {
        _people.push(item);
    }
 
    public function addItemAt(item:Object, index:int):void
    {
        _people.push(item); // TODO: insert at specified index; throw RangeError if index is out of range
    }
 
    public function getItemAt(index:int, prefetch:int=0):Object
    {
        return _people[index] as Object;
    }
 
    public function getItemIndex(item:Object):int
    {
        for (var i:int = 0; i < _people.length; i++)
        {
            var element:Person = _people[i] as Person;
            if(element == item)
            {
                return i;
            }
        }
        return -1;
    }
 
    public function itemUpdated(item:Object, property:Object=null, oldValue:Object=null, newValue:Object=null):void
    {
        //TODO: implement function
        trace("itemUpdated NOT IMPLEMENTED");
    }
 
    public function removeAll():void
    {
        _people.splice(0, _people.length);
    }
 
    public function removeItemAt(index:int):Object
    {
        var removedStrings:Vector.<Person> = _people.splice(index,1);
        return removedStrings[0];
    }
 
    public function setItemAt(item:Object, index:int):Object
    {
        if(index < 0 || index >= _people.length)
        {
            throw new RangeError("setItemAt index: " + index + " is out of range");
        }
        _people.splice(index, 0, item);
        return null;
    }

And here it is in use:

<s:List id="list"/>
<s:ComboBox id="combo"/>
<s:DropDownList id="drop"/>
 
protected function createPersonVectorDP():void
{
    var people:PersonVectorDP = new PersonVectorDP;
    list.dataProvider = people;
    combo.dataProvider = people;
    drop.dataProvider = people;
 
    var person1:Person = new Person;
    person1.firstName = "Don";
    person1.lastName = "Draper";
    people.addItem(person1);
 
    var person2:Person = new Person;
    person2.firstName = "Betty";
    person2.lastName = "Draper";
    people.addItem(person2);
 
    var person3:Person = new Person;
    person3.firstName = "Roger";
    person3.lastName = "Sterling";
    people.addItem(person3);
 
    var person4:Person = new Person;
    person4.firstName = "Peggy";
    person4.lastName = "Olson";
    people.addItem(person4);
}

Download the sample code at the end of this post.

Making the DataProvider Type-Safe

Although this does work there’s a problem with using the IList interface to add elements to the Vector DataProvider. The problem is that those IList methods accept Objects. So, although we were only inserting Persons into our PersonVectorDP container the compiler wouldn’t stop us from inserting Dogs or Cats:

var d:Dog = new Dog;
people.addItem(d); // compiles, but throws error at run-time

We would get an error at run-time but it sure would be nice to get the error at compile-time instead. Fortunately, there’s a solution:  never use the IList interface in your own code. Instead, create your own type-safe interface and use it as a proxy to access the Vector object. We can do this with the following simple interface:

public interface IPersonVectorDP
{
    function pop():Person;
    function push(value:Person):uint;
}

We’ll implement this interface, in addition to IList, in PersonVectorDP:

public function pop():Person
{
    return _people.pop();
}
 
public function push(value:Person):uint
{
    return _people.push(value);
}

Now, we can add and remove elements to _people in a completely type-safe manner. Any attempt to push a Dog in where a Person belongs will be rejected by the compiler.

If you want a more robust set of accessor methods you’re free to add them. As long as you use only these strongly-typed methods you get the best of both worlds: List-compatible collections and type-safety. Just make sure never to use the IList accessor methods. To avoid this mistake you should always access the only through the type-safe interface, never through the Vector wrapper:

var people:PersonVectorDP = new PersonVectorDP;
list.dataProvider = people;
combo.dataProvider = people;
drop.dataProvider = people;
 
// Always use this interface to access the personVectorDP instance:
var peopleInterface:IPersonVectorDP = people as IPersonVectorDP; 
people = null; // To be safe, null this out ASAP. That way, you can't inadvertently use the wrong method.
var person1:Person = new Person;
person1.firstName = "Don";
person1.lastName = "Draper";
peopleInterface.push(person1);
 
var person2:Person = new Person;
person2.firstName = "Betty";
person2.lastName = "Draper";
peopleInterface.push(person2);
 
var person3:Person = new Person;
person3.firstName = "Roger";
person3.lastName = "Sterling";
peopleInterface.push(person3);
 
var person4:Person = new Person;
person4.firstName = "Peggy";
person4.lastName = "Olson";
peopleInterface.push(person4);
 
// Never access people directly. Always use the interface as shown above.
// people.addItem(new Dog()); // This will fail at run-time since people is now null.

Download the sample code (FXP)

In an upcoming post, I’ll show an implementation of PersonVectorDP which dispatches events so that the Flex List classes will reflect changes to the data source. I’ve written an updated post, An Enhanced Type-Safe Data Provider Technique for Flex IList-Based Controls, which includes a new, enhanced version of the code. Please get that version of the code instead.

Brain Teaser

Substitute a StringVectorDP class for the PersonVectorDP class above. This class is the same as the PersonVectorDP class except that its field variable is a Vector of Strings. And, of course, all the IList methods operate on this _strings var.

private var _strings:Vector.<String> = new Vector.<String>;

Consider the following code:

var vectorDP:StringVectorDP = new StringVectorDP();
var d:Dog = new Dog;
vectorDP.addItem(d); // No error either at compile-time or run-time

No error occurs when adding the Dog object either at compile-time or run-time. Why not? Click to see answer

Previous post:

Next post: