2.5.1 Model-View-Adapter (MVA, Mediated MVC, Model-Mediator-View)
Motivation
Model-View-Adapter is a variation of the Triad where all communication between Model and View must flow through a Controller, instead of interacting directly as in a Traditional MVC Triad. The Controller becomes a communication hub, accepting change notifications from Model objects and UI events from the View.
This approach might appear excessively strict, but has some advantages: the communication network is artificially constrained, making it easier to evaluate and debug. All action happens in the Controller, and the View can be created from off-the-shelf widgets without any Model-specific variation.
Design
MVA is an implementation of the Mediator pattern. Controllers are generally referred as Adapters or Mediators. The Model and the View do not hold references to each other, they do not exchange data nor interact directly.
The pattern of communication in MVA can be represented with the following interaction diagram
which can be described as
- The View receives a UI event. It calls an appropriate method on the Controller.
- The Controller sets the value on the Model.
- The Model notifies its listeners of the change, among which is the Controller itself. As already pointed out, in a MVC approach this notification would be sent to the View.
- The notified Controller fetches information from the Model and updates the View.
With the Controller in full control on the dialog between the two remaining parties, smart tricks can be performed on the “in transit” data: for example, the Controller could be responsible for formatting, translating or ordering the data from the Model.
Practical Example 1: With Subclassing of the Views
common in Apple OSX Cocoa Framework.
We examine a MVA implementation of the Engine example in two steps. In the first step, we will keep subclassing the Views, to present the general concept. In the second step, we will remove the need for subclassing the Views.
The Model is unchanged: it stores rotations per minute information and notifies about changes
class Engine(BaseModel):
def __init__(self):
super(Engine, self).__init__()
self._rpm = 0
def setRpm(self, rpm):
if rpm < 0:
raise ValueError("Invalid rpm value")
if rpm != self._rpm:
self._rpm = rpm
self._notifyListeners()
def rpm(self):
return self._rpm
The two View classes, Dial
and Slider
, are now unaware of the Model. Instead,
they know about the Controller, and accept changes to their content through the
setRpmValue()
method. A matter of taste can decide the semantic level of this
method. Should it talk “domain language” (i.e. Rpm) or not (i.e. the method
should just be named setValue). In any case, Views behave differently with
respect to the issued value (the Slider needs its value scaled appropriately).
One could handle this difference in the Controller, but this will be considered
in the next step.
When the user interacts with the Dial
, the Controller
changeRpm()
method is directly invoked, in this case via the Qt Signal/Slot
mechanism
class Dial(QtGui.QDial):
def __init__(self, *args, **kwargs):
super(Dial, self).__init__(*args, **kwargs)
self._controller = None
self.setRange(0, 10000)
def setRpmValue(self, rpm_value):
self.setValue(rpm_value)
def setController(self, controller):
self._controller = controller
self.connect(self, QtCore.SIGNAL("valueChanged(int)"),
self._controller.changeRpm)
For the Slider
, the interface is the same, but the internal implementation is
different. Again, the setRpmValue
allows the Controller to change the
View contents. In this case however, a proper transformation of the data is
performed to deal with the specifics of the Slider
behavior, whose range is
from 0 to 10. Similarly, when the User interact with the Slider
, the method
_valueChanged
will be invoked, which in turn will issue a call to the
Controller changeRpm()
method, after transformation of the parameter
class Slider(QtGui.QSlider):
def __init__(self, *args, **kwargs):
super(Slider, self).__init__(*args, **kwargs)
self._controller = None
self.connect(self, QtCore.SIGNAL("valueChanged(int)"),
self._valueChanged)
self.setRange(0,10)
def setRpmValue(self, rpm_value):
self.setValue(rpm_value/1000)
def setController(self, controller):
self._controller = controller
def _valueChanged(self, value):
if self._controller:
self._controller.changeRpm(value*1000)
The Controller class handles the Model and the two Views accordingly. It
registers for notifications on the Model, and it receives notification from the
Views on its changeRpm()
method, where it modifies the contents of the Model.
When the Model communicates a change, it pushes the new value to the Views
class Controller(object):
def __init__(self):
self._views = []
self._model = None
def setModel(self, model):
self._model = model
model.register(self)
def addView(self, view):
view.setController(self)
self._views.append(view)
def changeRpm(self, rpm):
if self._model:
self._model.setRpm(rpm)
def notify(self):
for view in self._views:
view.setRpmValue(self._model.rpm())
Practical Example 2: With generic Views
In the previous example, removing the dependency of the Views on the Model did not allow us to use generic Views. In this example we will move the value translation logic from the Views to the Controller, thus removing the need for subclassing the Views, an important advantage of MVA.
class Controller(object):
def __init__(self, model, slider, dial):
self._slider = slider
self._dial = dial
self.connect(self._slider, QtCore.SIGNAL("valueChanged(int)"),
self._sliderChanged)
self.connect(self._dial, QtCore.SIGNAL("valueChanged(int)"),
self._dialChanged)
self._model = model
model.register(self)
def changeRpm(self, rpm):
if self._model:
self._model.setRpm(rpm)
def notify(self):
for view in self._views:
view.setRpmValue(self._model.rpm())
Practical Example 3: With forwarding of Model events
If the Model emits qualified events, the Controller could simply forward them to the View.