2.1.3 Application Model (MMVC)

Motivation

In Traditional MVC we pointed out that a Model object should not contain GUI state. In practice, some applications need to preserve and manage state that is only relevant for visualization. Traditional MVC has no place for it, but we can satisfy this need with a specialized Compositing Model: the Application Model, also known as Presentation Model. Its submodel, called Domain Model, will be kept unaware of such state.

An Application Model is closer to the View than a Domain Model, and therefore able to take into account specific needs of the View it is addressing: in a scrollable area, where only a part of the overall Model is visible it can hold information about the currently visible portion of the Domain Model, and suppress those notifications reporting changes in data currently not visible, preventing a useless refresh. It can also be used to distill information from multiple Domain Models, producing something that is relevant for its View. For example, our Domain Model may be made of objects representing the employees in a company, company departments and so on, in a rather elaborate network. If the View wants to display a list of employees regardless of the department, maybe with a checkbox to select them for further processing, it is convenient to have an Application Model presenting data to the View as a list, gathering the details from the Domain Model objects (non-graphical information) while at the same time keeping track and presenting the checkbox state as well (graphical information). As a drawback, it is much less reusable: multiple Views can interact with the same Application Model only if they agree on the visual state representation (e.g. we want both the Dial and the Slider red when over the rpm limit).

Some implementations of Application Model push its responsibilities even further than purely GUI state: it is, quite literally, the model of the application, and it is responsible for modifying application state directly on the application itself. For example, it might enable/disable menus, show or hide widgets, validation of the events. Most of the visual logic will be responsibility of this model object, rather than the controllers. This interpretation has deep implications for the Dolphin Model View Presenter, which will be examined later.

FIXME: Application model represents the GUI state without the GUI. it contains the logic for enabling/disabling checkboxes, for example. FIXME: Application model can contain selection.

FIXME: Some logic may not be possible to extract from the View and put into the presentation model, especially if this logic is deeply rooted in the graphical characteristics of the visual state. Examples are options that depends on the screen resolution, or the visual positioning of the mouse within the window.

Design

Practical Example

To present a practical example. imagine having a Domain Model representing an engine

class Engine(BaseModel):
   def __init__(self): 
       super(Engine, self).__init__()  
       self._rpm = 0 

   def setRpm(self, rpm):
       if rpm != self._rpm:
           self._rpm = rpm
           self._notifyListeners()

   def rpm(self):
       return self._rpm

Initial specifications require to control the revolution per minute (rpm) value through two Views: a Slider and a Dial. Two View/Controller pairs observe and act on a single Model

Suppose an additional requirement is added to this simple application: the Dial should be colored red for potentially damaging rpm values above 8000 rpm, and green otherwise.

We could violate Traditional MVC and add visual information to the Model, specifically the color

class Engine(BaseModel):
  # <proper adaptations to init method>

  def dialColor(self):
     if self._rpm > 8000:
        return Qt.red
     else:
        return Qt.green

With this setup, when the Dial receives a change notification, it can inquire for both the rpm value to adjust its position and for the color to paint itself appropriately. However, the Slider has no interest in this information and now the Engine object is carrying a Qt object, gaining a dependency against GUI. This reduces reuse of the Model in a non-GUI application. The underlying problem is that the Engine is deviating from business nature, and now has to deal with visual nature, something it should not be concerned about. Additionally, this approach is unfeasible if the Model object cannot be modified.

An alternative solution is to let the Dial View decide the color when notified, like this

class Dial(View):
   def notify(self):
       self.setValue(self._model.rpm())
       palette = QtGui.Qpalette()

       color = Qt.green
       if self._model.rpm() > 8000:
           color = Qt.red

       palette.setColor(QtGui.Qpalette.Button, color)
       self.setPalette(palette)

Once again, this solution is impractical, and for a complementary reason: the View has to know what is a dangerous rpm amount, a business-related concern that should be in the Model. This solution may be acceptable for those limited cases when the logic connecting the value and its visual representation is simple, and the View is designed to be agnostic of the meaning of what is showing to the User. For example, a label displaying negative values in red may be used to show bank account balances. The real meaning of a negative balance, the account is overdrawn, is ignored. A better solution would be to have the BankAccount Model object provide this logic as isOverdrawn(), and the label color should honor this semantic, not the one implied by the numerical value.

Given the point above, it is clear that the Engine object is the only entity that can know what rpm value is too high. It has to provide this information, leaving its visual representation strategy to the View. A better design provides a query method isOverRpmLimit

class Engine(BaseModel):
   <...>
   def isOverRpmLimit(self):
       return self._rpm > 8000

The View can now query the Model for the information and render it appropriately

class Dial(View):
   def notify(self):
       <...>
       color = Qt.red if self._model.isOverRpmLimit() else Qt.green

       palette.setColor(QtGui.QPalette.Button, color)
       self.setPalette(palette)

This solution respects the semantic level of the business object, and allows to keep the knowledge about excessive rpm values in the proper place. It is an acceptable solution for simple state.

With this implementation in place we can now extract logic and state from Dial View into the Application Model DialEngine. The resulting design is known as Model-Model-View-Controller

The DialEngine will handle state about the Dial color, while delegating the rpm value to the Domain Model. View and Controller will interact with the Application Model and listen to its notifications. Our Application Model will be implemented as follows. In the initializer, we register for notifications on the Domain Model, and initialize the color

class DialEngine(BaseModel):
 def __init__(self, engine):
   super(DialEngine, self).__init__()
   self._dial_color = Qt.green
   self._engine = engine
   self._engine.register(self)

The accessor method for the color just returns the current value

class DialEngine(BaseModel):
    # ...
    def dialColor(self):
        return self._dial_color

The two accessors for the rpm value trivially delegate to the Domain Model

class DialEngine(BaseModel):
    # ...
    def setRpm(self, rpm):
        self._engine.setRpm(rpm)

    def rpm(self):
        return self._engine.rpm()

When the DialController issues a change to the Application Model through the above accessor methods, this request will be forwarded and will generate a change notification. Both the Slider and the Application Model will receive this notification on their method notify. The Slider will change its position, and the Application Model will change its color and reissue a change notification

class DialEngine(BaseModel):
    # ...
    def notify(self):
        if self._engine.isOverRpmLimit():  
          self._dial_color = Qt.red
        else: 
          self._dial_color = Qt.green

        self._notifyListeners() 

The DialView will handle this notification, query the Application Model (both the rpm value and the color) and repaint itself. Note that changing the self._dial_color in DialEngine.setRpm, as in

class DialEngine(BaseModel):
    # ...
    def setRpm(self, rpm):
        self._engine.setRpm(rpm)

        if self._engine.isOverRpmLimit():  
            self._dial_color = Qt.red
        else: 
            self._dial_color = Qt.green

instead of using the notify solution given before, would introduce the following problems:

  • the dial color would not change as a consequence of external changes on the Domain Model (in our case, by the Slider)
  • There is no guarantee that issuing self._engine.setRpm() will trigger a notification from the Domain Model, because the value might be the same. On the other hand, the Application Model might potentially change (although probably not in this example), and should trigger a notification to the listeners. Solving this problem by adding a self._notifyListeners call to DialEngine.setRpm will end up producing two notifications when the Domain Model does issue a notification.

FIXME In practice, application model is a UI model. FIXME VisualWorks. FIXME application model may need to talk to the view or the controller directly, instead of notification.