2.9.4 Customizing Proxies and Models

From our example, proxy and instance appear to be completely coupled - even the names of the components and attributes are tied. This is a precise interpretation of the situation, but in my opinion, this coupling is not only unavoidable, it is the essence of UI architecture. Treating the interface as a completely separate entity from the object it manipulates is, honestly, a bad idea, because the interface is a representation of the object, and as such, essentially coupled to it. Would it make sense to remove the url attribute from the model and not remove it from the interface in question?

Because the UI is really a representation, however, there are times where the contents of the widget and its attached proxy attribute must differ in some way. Often, it is a matter of cardinality: more than one widget defines a single proxy attribute, or vice-versa; at other times, the data format in the widget does not match the format in the model (think of dates, represented by strings in the interface, but stored as DateTime objects in the object)7. The Kiwi Proxy was designed to cater to these different requirements, using accessor functions when available.

Accessors provide an easy way to translate the model value to the interface value: a pair of get_*() and set_*() functions implemented in the model that perform internal manipulation of its variables, removing the need to directly manipulate the instance variable. You can define accessors for as few or as many model attributes you want.

To make the process clearer, it is worth discussing how the model and the UI are updated. The heuristics for updating a model are:

  1. If a change happens in the Proxy's widget X, it looks at the model attached to it.
  2. If the model offers a set_X() method, it is called, with the new value as its only parameter.
  3. If not, it manipulates the model directly by using setattr(), which is the equivalent of model.X = value. If Proxies.set_attr_warnings(True) has been called, a warning like the following will be printed:

        Kiwi warning: could not find method set_title in model
        <__main__.NewsItem instance at 0x82011ac>, using setattr()
    

  4. The model is updated (If multiple proxies are attached to the model, special things happen, additionally, as you will see in section 2.9).

The heuristics for updating the interface widget are:

  1. On startup, the proxy queries the model for the value for attribute X.
  2. It tries first using an accessor method get_X() (which should return a single value). If the accessor does not exist, it will attempt to access the model's variable directly (using getattr()). As with setattr() above, a warning will be printed if attribute warnings are enabled.
  3. The interface is updated with this initial value, and normal event processing begins.
  4. If a model's state for X is altered (item.X = "foo"), a notification is sent to the proxy, with the attribute that was altered. If a callback calls self.update("X"), a notification is sent to the proxy, with the attribute that was altered.
  5. The proxy receives the notification; it gets the value of X from the model using the same method as in step 2, and updates the widget contents accordingly.

Summarizing: if you would like to customize the connection between model and proxy for an attribute, implement accessor functions (get_foo() and set_foo()) for it. If you would like to verify that no direct instance manipulations are happening, use the module function set_attr_warnings() and check the output printed to the console's standard error.

Let's extend our previous example to provide an accessor and explain how things work out (newsform3.py).

    class NewsItem(FrameWork.Model):
        """An instance representing an item of news.
           Attributes: title, author, url"""
        def set_url(self, url):
            """sets the url, prefixing "http://" if missing"""
            http = "http://"
            if len(url) > len(http) and string.find(url, http) != 0:
                url = http + url
            self.url = url

In this example, we provide an accessor for setting the url (a "setter") prefixed by "http://". The accessor is called when the entry's text changes (for each character inserted or deleted), which is why I have to check for the length of the url typed in. Note that I don't provide a get_url() method (a "getter"), which means the raw url would be loaded from the instance into the interface. In this specific case, this is not an issue, because data is only loaded from the instance at instantiation time and when the model attribute is changed. However, for most cases both setter and getter need to convert to and from the model format.



Footnotes

... object)7
Apart from these reasons, some people insist that it is bad or wrong to manipulate the instance directly, and that accessors should always be used.