Contents
GController
The story so far...
Given a model-view-controller design, the current approach in GLib-based libraries is to encapsulate the data structure used for storage and add storage accessors that notify the view(s) of changes - thus conflating the model and the controller sides into a single data structure.
Encapsulation of storage and controller has invariably led to some issues:
data replication: unless the model is an interface that resides at a lower level in the dependency chain, and that can be bolted on top of an existing (simpler) data storage (e.g. GPtrArray, GSequence, GList), there has to be a copy from the (simpler) data storage types provided by GLib (GArray, GList, GHashTable, etc.) into the model itself.
data access replication: be the model an interface or an abstract class, it requires the implementation of an API to access the data from the storage, including iteration over the data storage.
loss of performance/generality: if a model has to be accessible from higher levels of the platform stack, it has to lose every knowledge of data types, or re-implement them abstractly, incurring in a loss of performance; conversely, if it can be placed at higher levels of the stack, it will become tied to the view structure, losing generality.
These issues have been nominally approached by different libraries in different ways - but always trying to maintain the model (data storage) and the controller angles together.
GController: An alternative angle of attack
Instead of collapsing both model and controller inside the same class it should be possible to devise a way to use simple data structures already provided by GLib for storage, access and iteration, and decouple the controller into a separate class; every operation on the data storage (now truly a model) will notify the controller, and each view will use the controller to get updates.
Design requirements
- must work with GLib data types:
GArray/GPtrArray
GSequence
GList/GSList
GHashTable
- must not require changes to those data types
- must be able to notify actions on the data types:
- addition
- removal
- change
- must defer data access and iteration over to the data types
GController
GController is the base, abstract class for controllers.
A specialized implementation of GController for a data type might be provided - e.g. GArrayController for GArray - but it is not necessary; a specialized implementation of GController for a model class might also be provided, though it's not necessary.
Every time an action is performed on the GArray, the GController has to:
create a GControllerReference
emit the GController::changed signal
For instance, given a GController created for a given GArray:
GController *controller = g_array_controller_new (array);
The application code for appending a value inside the GArray should be:
g_array_append (array, value); GControllerReference *ref = g_controller_create_reference (controller, G_CONTROLLER_ADD, G_TYPE_UINT, 1, array->len - 1); g_controller_emit_changed (controller, ref); g_object_unref (ref);
This will result in the GController::changed signal being emitted; the signal has the following prototype:
void (* changed) (GController *controller, GControllerAction action, GControllerReference *reference);
Implementations of the view for a given controller should use the changed signal to update themselves, for instance:
static void on_changed (GController *controller, GControllerAction action, GControllerReference *reference) { gint n_indices = g_controller_reference_get_n_indices (reference); GArray *array = g_array_controller_get_array (G_ARRAY_CONTROLLER (controller)); for (gint i = 0; i < n_indices; i++) { /* GArray indices are unsigned integers */ guint array_index = g_controller_reference_get_index_uint (reference, i); /* get the value out of the array */ }
GControllerReference
A GControllerReference is an object created by each GController, and it references four things:
- the action that led to the creation of the reference
the type of the index inside the data storage controlled by the GController
the number of indices affected by the action on the data storage that caused the emission of the changed signal
- the indices affected by the action on the data storage
The GControllerReference does not store or replicate the data affected by the action recorded by the GController: it merely references where the View might find the data inside the storage.
Indices
Indices are a data storage dependent information: they are the description of how to access the data in the storage; for instance:
- array index
- the result of an hashing function
- a SELECT query on a SQL database
The only caveat is that they can effectively be represented by the GType system.
Bulk actions
Since the GControllerReference stores indices it is immediately usable for bulk operations on the storage. For instance, appending a range of items on a GArray requires the indices of the newly added slots:
/* create an empty reference */ GControllerReference *ref = g_controller_create_reference (controller, G_CONTROLLER_ADD, G_TYPE_UINT, 0); /* insert the contents of a C array "new_items" starting * from the index "10" inside the GArray */ for (gint i = 10; i < G_N_ELEMENTS (new_items); i++) { g_array_insert (array, i, new_value[i - 10]); /* add the GArray index of the element to the reference */ g_controller_reference_add_index (ref, i); } /* emit the changed signal for the operation */ g_controller_emit_changed (controller, ref);
Implementation
An initial implementation of GController is hosted on git.gnome.org and it can be browsed online here.
WARNING: the API of the implementation is still in flux and it might change without prior notice.
Example
A model implementation
The following example shows a model implementation using a GController for a GArray storage
static void model_init (Model *model) { GArray *array = g_array_new (FALSE, FALSE, sizeof (Item)); model->controller = g_array_controller_new (array); model->items = array; /* the controller owns the storage, now */ g_array_unref (array); } void model_add_item (Model *model, Item *item) { GControllerReference *ref; Item copy; /* we store a struct, not a pointer to a struct */ copy = *item; g_array_append (model->items, copy); ref = g_controller_create_reference (model->controller, G_CONTROLLER_ADD, G_TYPE_UINT, 1, model->items->len - 1); g_controller_emit_changed (model->controller, ref); g_object_unref (ref); } void model_remove_item (Model *model, Item *item) { GControllerReference *ref; gint i, pos; pos = -1; for (i = 0; i < model->items; i++) { Item *cur = &g_array_index (model->items, Item, i); if (cur->id == item->id) { pos = i; break; } } /* we emit the changed signal before removing the item; this is really an * implementation detail of the model: we could remove the item as part * of the GController::changed signal emission as well */ ref = g_controller_create_reference (model->controller, G_CONTROLLER_REMOVE, G_TYPE_UINT, 1, pos); g_controller_emit_changed (model->controller, ref); g_object_unref (ref); g_array_remove_index (array, pos); } void model_clear (Model *model) { GArray *array = g_array_new (FALSE, FALSE, sizeof (Item)); g_array_controller_set_array (model->controller, array); g_array_unref (array); model->items = array; ref = g_controller_create_reference (model->controller, G_CONTROLLER_CLEAR, G_TYPE_UINT, 0); g_controller_emit_changed (model->controller, ref); g_object_unref (ref); } void model_set_items (Model *model, GArray *items) { /* will unref the existing GArray and take a reference on the new one */ g_array_controller_set_array (G_ARRAY_CONTROLLER (model->controller), items); model->items = items; ref = g_controller_create_reference (model->controller, G_CONTROLLER_REPLACE, G_TYPE_UINT, 0); g_controller_emit_changed (model->controller, ref); g_object_unref (ref); } GArray * model_get_items (Model *model) { return model->items; // or: return g_array_controller_get_array (model->controller); } GController * model_get_controller (Model *model) { return model->controller; }
Object Controller
A GController using a GObject as the storage model could be used to notify state changes affecting one or more properties:
{ g_object_freeze_notify (object); g_object_notify (object, "property-0"); g_object_notify (object, "property-1"); ... g_object_notify (object, "property-N"); g_object_thaw_notify (object); }
A GObjectController could be seen as an object oriented design of the GObject property notification; it can be used to bind two object instances through a property. The object oriented design allows:
encapsulation and information hiding: multiple GObject::notify connections are handled internally, including bookkeeping;
performance improvement: bulk notifications on more than one property is still coalesced into a single signal instead of multiple ones.
Implementation
The GObjectController would inject some code inside the GObject::dispatch_properties_changed virtual function for the controlled GObject and create a single G_CONTROLLER_UPDATE reference for all the properties that have been changed:
static void on_dispatch_properties_changed (GObject *gobject, guint n_pspecs, GParamSpec **pspecs, GController *controller) { GControllerReference *ref; gint i; ref = g_controller_create_reference (controller, G_CONTROLLER_UPDATE, G_TYPE_STRING, 0); for (i = 0; i < n_pspecs; i++) g_controller_reference_add_index (ref, i, pspec->name); g_controller_emit_changed (controller, ref); g_object_unref (ref); }
CAVEAT: currently, there is no way to inject per-instance third party code inside GObject::dispatch-properties-changed. Using the notify signal would always yield a reference with n-indices of 1 and offer no advantages over a notify signal handler. See bug #617446.
Improvements
It should be possible to have a similar behaviour as the current GObject::notify signal by using a filter for the notification:
g_object_controller_add_filter_regexp (controller, "(x|y|with|height)"); g_object_controller_add_filter_name (controller, "opacity");
It should be possible to use the whole GParamSpec as the index, instead of just the name, to allow validation without a costly call to g_object_class_find_property:
GObject *object = g_object_controller_get_object (controller); gint n_properties = g_controller_reference_get_n_indices (ref); for (gint i = 0; i < n_properties; i++) { GParamSpec *pspec; g_controller_reference_get_index (ref, i, &pspec); // unfortunately, this does not exist, but it would be a good addition g_object_get_value_with_pspec (gobject, pspec, &value); }
GController Improvements
Remove sub-classes for basic types
In theory, we could pass the GType to the GController::create_reference() virtual, thus avoiding the need of simple sub-classes altogether.
done, though the sub-classes of GController still make sense.
Storing the action on the Reference
If the GControllerReference stored the action it could be possible to collect multiple references coming from the same GController; then we could collapse references according to some desired pattern.
done
Representation of clear and replace actions
The GControllerAction enumeration should be extended to include the CLEAR and REPLACE actions, to correctly identify them from within a view.
done