In the recent past, we had a tight coupling between a Model
and an
Item
. The idea was that an Item
represented an individual "thing",
or element of data, in the model. The implementation of the model worked out how to store the item (i.e., as a
std::vector<item*>
, for example).
This meant that the model had to store a collection of Item
s, regardless of what theses items stood for, and the design of the
Item
meant that it stored
all sorts of UI information that meant nothing to the model, such as the item's state, color, font, etc. So what happened is that the
Item
class ended up
storing all sorts of data, some of which was relevant to the item's UI and some relevant to the model. This made it more difficult than necessary to make general purpose models.
So, I'm proposing breaking the direct connection of having a model store Item
s. Instead the model *only* stores data, which is really as it should be. The collection
of Item
s, if appropriate, is maintained instead by the View/Control. Thus the Item
becomes a "bridge" between the model and the control, and is used
by the control/view to help "compartmentalize" the drawing process and allow for things like custom item rendering. You could even think of
the Item as a sort of "mini" View.
The model will allow for data access by using the VariantData
class. Obviously we could use templates for this, but that has it's own issues. I think, in this case,
it's easier to understand and to deal with using VariantData
classes. Remembering that the public interface, common to a specific type of model (for example, a list model)
would use VariantData
types for its methods, but the actual storage could be accomplished however you want. The default storage implementation might be, for a list model,
an array of VariantData
, but a custom design could certainly use something else, so long as methods are implemented correctly.
The Model
class has a generic method for getting and setting data that allows you to get or set an element of data using a
VariantData
class to hold the data,
and also to specify an optional "key" using a VariantData
class to hold the key information. So, if you had a model that represented an array of doubles,
then the "key" might be an integer that indicated the index to use to set or get an element from the array, while the result of the "get"
or the data for the "set", a double value, would be stored in the
VariantData
instance. All of this would be done using the generic get and set methods
of the Model class. Of course you could provide your own more specific set of methods on your concrete model class. For example, the
ListModel
provides
a set of methods for getting and setting values in a collection of data. It also implements the generic Model get/set methods - you can use either one, it's up to you.
So, to reiterate: a Model stores data, an Item stores visual information and serves as a bridge between the control and model. An
Item
does not store data relevant
to the model. An Item
may have methods that allow you to retrieve data from the model, but they are simply convenience methods, you achieve the same thing by calling
the appropriate methods on the specific model class.
Let's look at some changes to the base classes for MVC in the framework, specifically the
Model
and Item
classes, and then move on to looking at some specific model types.
The Model
class doesn't change a great deal. It still has a collection of Views, and it still has delegates that fire events. It does have several new methods
for generically getting or setting data on the model.
class Model {
public:
virtual VariantData getValue( const VariantData& key=VariantData::null() );
virtual VariantData getValueAsString( const VariantData& key=VariantData::null() );
virtual void setValue( const VariantData& value,
const VariantData& key=VariantData::null() );
virtual void setValueAsString( const String& value,
const VariantData& key=VariantData::null() )
}
The exact implementation is up to the designer of the specific model class. The default implementations do nothing or return
VariantData
objects that are set to NULL
(the VariantData
's isNull()
method will return true).
The Item class has a pointer to the model it's associated with. It has a pointer to the
control it's associated with. It stores data related to it's visual display,
specifically, it holds a pointer to a font, display state mask that can hold various values, an image index, a void pointer for some application specific value, if need be,
and a boolean value indicating whether or not it's selected. It has a virtual method that is used to indicate whether or not the item can paint itself, and also
a virtual method for the actual paint()
method.
The Item
class is a base class, and there are specific sub classes of it for use by the various controls. For example, there's a ListItem
for use in list like controls,
a TreeItem
for tree or hierarchical controls, a TableCellItem
, and so forth.
An Item's lifetime is managed by the control it's associated with. An Item may or may not be created at the moment you make a change to the model.
For example, if you have a header control, then for each element you add to the model, a new Item is created. This is OK, as headers rarely have a high number
of elements to them (think of a control with more than 100 hundred header items - not very likely). However other controls, like a
tree control, only create the items when
you need them. This saves enormously on memory since if you have a TreeModel
with 1 million elements in it then creating 1 million corresponding
TreeItem
's doesn't
make a whole lot of sense, especially since none of them will be needed right away. Moral of the story: don't cache Item's directly - store the key or index you used
to retrieve them instead.
Some changes to specific models are a cleaned up interface, for example the
ListModel
now looks like this:
class ListModel : public Model {
void insert( const uint32 & index, const VariantData& item );
void add( const VariantData& item );
void remove( const uint32& index );
void set( const uint32& index, const VariantData& item );
virtual void setAsString( const uint32& index, const String& item );
virtual VariantData get( const uint32& index );
virtual String getAsString( const uint32& index );
virtual uint32 getIndexOf( const VariantData& item );
virtual bool getItems( std::vector<variantdata>& items );
virtual Enumerator<variantdata>* getItems();
virtual bool getRange( const uint32& start, const uint32& end,
std::vector<variantdata>& items );
virtual uint32 getCount();
}
The functions that you have to implement are for read only support are:
virtual VariantData get( const uint32& index );
virtual uint32 getIndexOf( const VariantData& item );
virtual bool getItems( std::vector<variantdata>& items );
virtual Enumerator<variantdata>* getItems();
virtual bool getRange( const uint32& start, const uint32& end,
std::vector<variantdata>& items );
virtual uint32 getCount();
These are fairly self explanatory.
The functions that modify the model, specifically:
void insert( const uint32 & index, const VariantData& item );
void add( const VariantData& item );
void remove( const uint32& index );
void set( const uint32& index, const VariantData& item );
are a little more involved. They are not virtual. Instead they call virtual methods that by default are no ops, and are intended to do the actual work of modifying
the underlying collection or data store (whatever that might be). If these methods return a true value, then the non virtual modifier method do the work of ensuring
that events are fired on the model's correct delegate(s). This makes is simpler on the implementer, and they don't have to worry about writing a lot of boiler plate code.
The virtual methods for modifying the model are:
virtual bool doInsert( const uint32 & index, const VariantData& item );
virtual bool doRemove( const uint32& index );
virtual bool doSet( const uint32& index, const VariantData& item );
When you call ListModel::insert()
, it calls doInsert()
to perform the actual data insertion on the collection, and then fires the appropriate events.
Likewise, set()
calls doSet()
, and so forth.
The ListModel
has support for adding/removing/getting sub items, and these methods work in a similar fashion
as the previous functions. One note: sub items are zero indexed, so if you have a list model with an element at index 0, and it has
two sub items, then the sub item indices
would be 0 and 1, and you could access them by calling ListModel::getSubItem(0,0)
and
ListModel::getSubItem(0,1)
.
The list control's have been modified (or are in the process of being modified) to use a common
ListControl
base class. This provides a common set of functionality
for dealing with a list of items and displaying them to the user. Controls that derive from this are the
ListViewControl
, ListBoxControl
, and DropDownControl
.
The ListControl
handles events from the ListModel
and deals with creating or removing
ListItem
s accordingly. The class looks something like this (only listing methods dealing with Items):
class ListControl {
ListModel* getListModel();
void setListModel(ListModel * model);
ListItem* addItem( const String& caption, const uint32 imageIndex=0 );
ListItem* insertItem( const uint32& index,
const String& caption, const uint32 imageIndex=0 );
bool itemExists( const uint32& index );
ListItem* getItem( const uint32& index );
void setItem( const uint32& index, ListItem* item );
void selectItem( const uint32& index );
Enumerator<uint32>* getSelectedItems();
uint32 getFocusedItem();
uint32 getSelectedItem();
uint32 hitTest( const Point& point );
};
Note that these methods are simply meant as shortcuts to using the list model methods for adding/removing elements. Under the hood the list model ends up getting called.
With these methods, you're limited to the kind of data that you can store since the caption is simply passed to the list model as a string. If the caption is the string
"Hello" and your list model is implemented to expect numbers then the data that gets entered into it will be incorrect.
You can add, insert, or remove items, as well as get or set a specific item. Adding/removing items is fairly straight forward. You add an item by specifying a caption
and optional image index, and you're returned a ListItem
instance.
You can test for the presence of a ListItem
by calling itemExists()
, which will return true if there is an
ListItem
at the specified index.
You can change an existing ListItem
to something completely different (for example, perhaps you have a custom
ListItem
that you want to use instead of the default) by calling
the setItem, passing in an index and a valid instance. The old ListItem
will be deleted and the new one put in it's place. If the new item doesn't have a component owner,
then the list control will take over and you don't have to worry about the ownership (and cleanup) of the item.
These changes have been made to other model classes as well, including the
TreeModel
, the TableModel
, the TextModel
, and so on. Changes to the various controls
to facilitate this have also been made, and the TreeControl
and
ListViewControl
's underlying peer implementation has also changed quite a bit, making them a lot more robust.