Model View Controller (MVC) is a compound pattern whose goal is to separate responsibilities into modular pieces that can be interchanged relatively easily. If you didn't need a user interface, all you'd have to worry about is the model. Why should you have to burden the model with user interface code? Obviously you shouldn't, thus the model view separation. Introduction of a controller loosens the coupling between the model and the view and, if done well, allows the behavior of the system to be modified by replacing the controller with a different one. After the jump, I'll show you how to apply the pattern to a traditional Matlab GUI.
Model View Controller |
I'm following the MVC example laid out in the excellent book Head First Design Patterns. My starting point is the quick start GUIDE 'GUI with Uicontrols'. If you choose this GUIDE template and view the m file, you can see how it started and compare it to the MVC version I am presenting. Of course there are a few idiosyncrasies that the Matlab scripting language imposes. The main one is that the user interface is not a class; rather, it is a handle graphics object. The good news is that it's not hard to work around.
We start with a top-level main function that instantiates the model class as well as the controller class and passes the model into the controller.
function main() mymodel = model(); mycontroller = controller(mymodel);
classdef model < handle
properties (SetObservable) density volume units mass end methods function obj = model() obj.reset(); end function reset(obj) obj.density = 0; obj.volume = 0; obj.units = 'english'; obj.mass = 0; end function setDensity(obj,density) obj.density = density; end function setVolume(obj,volume) obj.volume = volume; end function setUnits(obj,units) obj.units = units; end function calculate(obj) obj.mass = obj.density * obj.volume; end end end
How do we deal with this handle graphics user interface? Simple, we wrap it in a class. The view class is passed a reference to the controller object when it is created. It also instantiates the GUIDE graphics object, which is called measures here. Notice that we also pass the controller into the GUI. Then we register listeners to the model properties and specify a method to handle the triggered events. This method has access to the visual elements of the GUI and modifies them appropriately in response to events. The addlistener call has been modified here with an anonymous function definition that adds the self object, obj. The event handler needs obj to get the GUI handles structure.
classdef view < handle properties gui model controller end methods function obj = view(controller) obj.controller = controller; obj.model = controller.model; obj.gui = measures('controller',obj.controller); addlistener(obj.model,'density','PostSet', ... @(src,evnt)view.handlePropEvents(obj,src,evnt)); addlistener(obj.model,'volume','PostSet', ... @(src,evnt)view.handlePropEvents(obj,src,evnt)); addlistener(obj.model,'units','PostSet', ... @(src,evnt)view.handlePropEvents(obj,src,evnt)); addlistener(obj.model,'mass','PostSet', ... @(src,evnt)view.handlePropEvents(obj,src,evnt)); end end methods (Static) function handlePropEvents(obj,src,evnt) evntobj = evnt.AffectedObject; handles = guidata(obj.gui); switch src.Name case 'density' set(handles.density, 'String', evntobj.density); case 'volume' set(handles.volume, 'String', evntobj.volume); case 'units' switch evntobj.units case 'english' set(handles.text4, 'String', 'lb/cu.in'); set(handles.text5, 'String', 'cu.in'); set(handles.text6, 'String', 'lb'); case 'si' set(handles.text4, 'String', 'kg/cu.m'); set(handles.text5, 'String', 'cu.m'); set(handles.text6, 'String', 'kg'); otherwise error('unknown units') end case 'mass' set(handles.mass,'String',evntobj.mass); end end end end
The controller knows about the model and the view; but it's only function in this demo is call model methods as requested by the GUI. In more realistic applications, it may interact with the view and the model in more complicated ways.
classdef controller < handle properties model view end methods function obj = controller(model) obj.model = model; obj.view = view(obj); end function setDensity(obj,density) obj.model.setDensity(density) end function setVolume(obj,volume) obj.model.setVolume(volume) end function setUnits(obj,units) obj.model.setUnits(units) end function calculate(obj) obj.model.calculate() end function reset(obj) obj.model.reset() end end end
Finally, you can compare the m file created by the template GUIDE to the one in the listing below. You would see that the model was originally held in the handles structure of the GUI. The handles structure is one of the ways to store user data within a GUI. Now there is a separate model class that stands on its own. You may also notice that function listed below doesn't have the responsibility of modifying the graphics elements directly. Rather, it bumps that responsibility to the wrapping view class, which can respond to events from the model. Nor does it modify the model, but passes that responsibility to the controller.
function varargout = measures(varargin) % MEASURES M-file for measures.fig % MEASURES, by itself, creates a new MEASURES or raises the existing % singleton*. % % H = MEASURES returns the handle to a new MEASURES or the handle to % the existing singleton*. % % MEASURES('CALLBACK',hObject,eventData,handles,...) calls the local % function named CALLBACK in MEASURES.M with the given input arguments. % % MEASURES('Property','Value',...) creates a new MEASURES or raises % the existing singleton*. Starting from the left, property value pairs are % applied to the GUI before measures_OpeningFcn gets called. An % unrecognized property name or invalid value makes property application % stop. All inputs are passed to measures_OpeningFcn via varargin. % % *See GUI Options on GUIDE's Tools menu. Choose "GUI allows only one % instance to run (singleton)". % % See also: GUIDE, GUIDATA, GUIHANDLES % Edit the above text to modify the response to help measures % Last Modified by GUIDE v2.5 10-Feb-2013 20:14:47 % Begin initialization code - DO NOT EDIT gui_Singleton = 1; gui_State = struct('gui_Name', mfilename, ... 'gui_Singleton', gui_Singleton, ... 'gui_OpeningFcn', @measures_OpeningFcn, ... 'gui_OutputFcn', @measures_OutputFcn, ... 'gui_LayoutFcn', [] , ... 'gui_Callback', []); if nargin && ischar(varargin{1}) gui_State.gui_Callback = str2func(varargin{1}); end if nargout [varargout{1:nargout}] = gui_mainfcn(gui_State, varargin{:}); else gui_mainfcn(gui_State, varargin{:}); end % End initialization code - DO NOT EDIT % --- Executes just before measures is made visible. function measures_OpeningFcn(hObject, eventdata, handles, varargin) % This function has no output args, see OutputFcn. % hObject handle to figure % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) % varargin command line arguments to measures (see VARARGIN) % Choose default command line output for measures handles.output = hObject; % get handle to the controller for i = 1:2:length(varargin) switch varargin{i} case 'controller' handles.controller = varargin{i+1}; otherwise error('unknown input') end end handles.metricdata.density = 0; handles.metricdata.volume = 0; set(handles.density, 'String', 0); set(handles.volume, 'String', 0); set(handles.mass, 'String', 0); set(handles.unitgroup, 'SelectedObject', handles.english); set(handles.text4, 'String', 'lb/cu.in'); set(handles.text5, 'String', 'cu.in'); set(handles.text6, 'String', 'lb'); % Update handles structure guidata(hObject, handles); % UIWAIT makes measures wait for user response (see UIRESUME) % uiwait(handles.figure1); % --- Outputs from this function are returned to the command line. function varargout = measures_OutputFcn(hObject, eventdata, handles) % varargout cell array for returning output args (see VARARGOUT); % hObject handle to figure % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) % Get default command line output from handles structure varargout{1} = handles.output; % --- Executes during object creation, after setting all properties. function density_CreateFcn(hObject, eventdata, handles) % hObject handle to density (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: popupmenu controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end function density_Callback(hObject, eventdata, handles) % hObject handle to density (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) % Hints: get(hObject,'String') returns contents of density as text % str2double(get(hObject,'String')) returns contents of density as a double density = str2double(get(hObject, 'String')); if isnan(density) density = 0; errordlg('Input must be a number','Error'); end handles.controller.setDensity(density) % --- Executes during object creation, after setting all properties. function volume_CreateFcn(hObject, eventdata, handles) % hObject handle to volume (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles empty - handles not created until after all CreateFcns called % Hint: popupmenu controls usually have a white background on Windows. % See ISPC and COMPUTER. if ispc && isequal(get(hObject,'BackgroundColor'), get(0,'defaultUicontrolBackgroundColor')) set(hObject,'BackgroundColor','white'); end function volume_Callback(hObject, eventdata, handles) % hObject handle to volume (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) % Hints: get(hObject,'String') returns contents of volume as text % str2double(get(hObject,'String')) returns contents of volume as a double volume = str2double(get(hObject, 'String')); if isnan(volume) volume = 0; errordlg('Input must be a number','Error'); end handles.controller.setVolume(volume) % --- Executes on button press in calculate. function calculate_Callback(hObject, eventdata, handles) % hObject handle to calculate (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) handles.controller.calculate() % --- Executes on button press in reset. function reset_Callback(hObject, eventdata, handles) % hObject handle to reset (see GCBO) % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) handles.controller.reset() % --- Executes when selected object changed in unitgroup. function unitgroup_SelectionChangeFcn(hObject, eventdata, handles) % hObject handle to the selected object in unitgroup % eventdata reserved - to be defined in a future version of MATLAB % handles structure with handles and user data (see GUIDATA) if (hObject == handles.english) units = 'english'; else units = 'si'; end handles.controller.setUnits(units)
So, the model notifies the view when its properties change. the view updates the GUI as appropriate. The GUI sends requests to the controller in response to user interaction. The controller instructs the model to update. You would not want to use such a heavy solution for this simple model; but it doesn't take much more complexity for the benefits of MVC to shine through.
The complete source code for this demo is available at Matlab Central File Exchange.
Have fun with OO and MVC in Matlab!
Looks great,but what if I create my GUI(and is that means 'view' in MVC?) without GUIDE, what should I do and things I should notice?
ReplyDeleteHi Jason,
ReplyDeleteSorry to leave you hanging for so long.
As far as I can tell, there should be no difference. If you forgo using GUIDE, then you have access to the entire figure, rather than a partial, so I wonder if that would open up any new possibilities to you. I can't think of anything; but would love to learn from your experience.
Best,
Chris
A really super example. Thanks.
ReplyDeleteI will be sharing this with the guys at work.
The model reset() wasn't re-setting the radio buttons, so I added the following to the view handlePropEvents() call: (lines ~43,49) to force this (I believe this still follows MVC contraints)
ReplyDeletecase 'english'
...
set(handles.unitgroup, 'SelectedObject', handles.english);
case 'si'
...
set(handles.unitgroup, 'SelectedObject', handles.si);
Wow, Awesome example. Thank you so much. This article has greatly accelerated my understanding of MVC. Don't know if you still track this blog but I have a two follow-up questions.
ReplyDelete1) In the code above, the addlistener method calls a function in the View class. Would it be better (more akin to MVC) to call a function in the Controller class? I am under the impression that the View is "dumb" and the Controller "manages" the views; therefore it seems that we would prefer to keep the View (and Model) clean of any code that links the two.
2) If the GUI has an edit text uicontrol that "controls" the Model state (and subsequently the View), should the uicontrol's callback point to a Controller function? Or should the Controller have a listener on the View? The former seems to make more sense to me but is there a preferred way per MVC?
Thanks All,
Adam
Hi Adam,
DeleteThanks for the kind words. I'm glad you found it useful.
To me it makes sense that the view should 'listen' to the model, while the controller 'talks' to the model; but there are exceptions to every rule. So, I would answer your second question in the affirmative, i.e. the view should talk to the controller, which in turn should modify a setting in the model. There may be other ways to construct the code, but this was the best one I could come up with given the constraints (and allowances) of Matlab OO design.
Cheers,
Chris
Does the view interact with the model besides property event changes? E.g. View gets property directly from model, or should this still go through the controller?
ReplyDelete