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

EmmanueleBassi/GController (last edited 2010-05-04 16:24:59 by EmmanueleBassi)