Document-View: dividing state from GUI

To solve the shortcomings of Smart UI, we take advantage of the intrinsic
division into visual rendering, interaction and business logic expressed by a GUI
application. In Smart UI, these three roles happen to be assigned to the same
class, but we can reorganize our code so that the business logic part is kept
separated. The resulting design is a two-class system known in literature as
Document View or Model Delegate.

The Document class is responsible for handling the business logic.
It has no part in dealing with graphical rendering, nor with GUI events. It
simply stores application relevant state, and provides an interface to obtain this
state or change it according to the rules of the application. Additionally, it
provides a mechanism to inform interested objects of changes.

The View class instead handles user events, renders itself visually,
performs operations on the Document and keeps its visual aspect synchronized
against the Document’s state when it changes.

The Document-View design achieves separation of the state from its graphical
representation, allowing them to change independently. The Document has become
a fully non-GUI entity that can act and be tested independently. Any registered
View always keeps itself up-to-date against the Document contents through the
notification system, and carry full responsibility for graphical rendering of
the Document information and the handling of user interaction.

This design removes some of the concerns expressed for Smart UI. Testing of the
state and business logic becomes easier: the Document object can be modified or
accessed programmatically by issuing calls to its methods. This object is now
independent and can work and manipulated with different Views, if desired. An
additional price in complexity is introduced in having to keep the View (or Views) notified of changes to the Document.

Implementation example

We can implement this design to our Click counter application through progressive refactorings. The first step is to partition out the data, represented by the self._value variable, into the Document class. For our system to continue to work, the visual part View must now be informed of changes to this data. The Document will therefore not only hold self._value, but also provide an interface to query and modify this data and a strategy to notify other objects when changes occur. This is expressed in the following implementation code

  1. class CounterDocument(object):
  2. def __init__(self):
  3. self._value = 0
  4. self._listeners = set()

In addition to the value, the self._listeners member variable holds references to Views interested in being notified about changes. We use a python set instead of a list to prevent accidental registration of the same object twice. Interested objects can register and unregister through the following methods

  1. class CounterDocument(object):
  2. # ...
  3. def register(self, listener):
  4. self._listeners.add(listener)
  5. listener.notify()
  6. def unregister(self, listener):
  7. self._listeners.remove(listener)

We then provide a getter method [^1] for self._value:

  1. class CounterDocument(object):
  2. # ...
  3. def value(self):
  4. return self._value

We also provide a setter to directly set a specific value. Note in particular how the method notifies the registered listeners when the value changes. This is
done by calling the listeners’ notify method, as you can see in
self._notifyListeners

  1. class CounterDocument(object):
  2. # ...
  3. def setValue(self, value):
  4. if value != self._value:
  5. self._value = value
  6. self._notifyListeners()
  7. def _notifyListeners(self):
  8. for l in self._listeners:
  9. l.notify()

The method notify is therefore the interface that a registered listener
must provide in order to receive notifications about the mutated state of the
Document object. Our View need to implement this method.

Finally, we also provide a method that increments the value according to the
expected logic

  1. class CounterDocument(object):
  2. # ...
  3. def incrementValue(self):
  4. self._value += 1
  5. self._notifyListeners()

The View class will be responsible for rendering the information contained in
an instance of CounterDocument. This instance is passed at initialization,
and after a few formalities, the View register itself for notifications

  1. class CounterView(QtGui.QPushButton):
  2. def __init__(self, document):
  3. super(CounterView, self).__init__()
  4. self._document = document
  5. self._document.register(self)

When this happens, the Document adds the View as a listener. A notification is
immediately delivered to the newly added listener so that it can update
itself. The notify method on the View is then called, which will query the current value from the Document, and update the text on the button

  1. class CounterView(QtGui.QPushButton):
  2. # ...
  3. def notify(self):
  4. self.setText(unicode(self._document.value()))

Note how this method inquires the Document through its interface (calling
CounterDocument.value). The View must therefore have detailed knowledge of its associated Model’s interface and must deal with the semantic level it presents. Through this knowledge, the View extracts data from the Model, and converts “Model language” into “View language” to present the data into the visual widgets it is composed of.

Handling of the click event from the User is performed in mouseReleaseEvent, as in Smart-UI. This time however, the action will involve the Document, again through its interface

  1. class CounterView(QtGui.QPushButton):
  2. # ...
  3. def mouseReleaseEvent(self, event):
  4. super(CounterView, self).mouseReleaseEvent(event)
  5. self._document.incrementValue()

the setValue call will then issue a change notification that will update the
button text via notify.

We can now provide multiple Views with different representation modes for the
same information, or modify it through different sources, either visual or
non-visual. We can for example add a Progress Bar

  1. class ProgressBarView(QtGui.QProgressBar):
  2. def __init__(self, document):
  3. super(ProgressBarView, self).__init__()
  4. self._document = document
  5. self._document.register(self)
  6. self.setRange(0,100)
  7. def notify(self):
  8. self.setValue(self._document.value())

and register it on the same Document instance at initialization

  1. app = QtGui.QApplication(sys.argv)
  2. document = CounterDocument()
  3. counter = CounterView(document)
  4. progress = ProgressBarView(document)
  5. counter.show()
  6. progress.show()
  7. app.exec_()

When the button is clicked, both its label and the progress bar are kept
updated with the current value in the Document.

[^1] Python properties can be used for the same goal. However, python properties are harder to connect to the signal/slots mechanism in PyQt.

[^2] When registration of the View on the Document is done in the View’s
initializer, as we are doing here, it should be done only when the
initialization is completed, so that notify can be called on a fully
initialized object. An alternative strategy is to delay this setup and perform it through a View.setDocument method.


Notification system in strongly typed languages

A possible implementation of the notification system in strongly typed
languages uses an interface class ListenerInterface with one abstract method
notify(). For example, in C++ we could write the following code

  1. class ListenerIface
  2. {
  3. public:
  4. virtual void notify() = 0;
  5. };

Concrete listeners will implement this interface

  1. class View : public ListenerIface
  2. {
  3. public:
  4. void notify();
  5. };

The Model will accept and handle pointers to the Listener interface, thus
not requiring a dependency toward specific Views or Controllers

  1. class Model
  2. {
  3. public:
  4. void register(ListenerIface *listener)
  5. {
  6. listeners.push_back(listener);
  7. }
  8. private:
  9. void notifyListeners()
  10. {
  11. std::vector<ListenerIface *>::iterator it;
  12. for (it = listeners.begin(); it != listeners.end(); ++it) {
  13. (*it)->notify();
  14. }
  15. std::vector<ListenerIface *> listeners;
  16. };

A similar approach can be used in Java.