Sunday, February 10, 2013

MVC in Matlab

Some time ago I started to make use of Matlab's object oriented capabilities.  (In true Matlab style, inheritance is notated with a 'less than' sign:  dog < animal)  The application I refactored has a GUIDE front end.  I had used GUIDE to make user interfaces before; and forged ahead using what I had learned from the user's guide.  While I have been really pleased with the benefits of OO, I was struggling to figure out how to apply patterns to my program, specifically MVC.  Well, last weekend I cracked that nut; and moreover, while watching the Grammys I made a little demo app to share.

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);
We want to extract the meat of the template GUIDE into a separate model class.  Here's what it looks like.  I'm using all handle classes throughout because I don't care about instantiating multiple copies of any of them, and I want to be able to pass around references to my classes.  In Matlab, that means handle classes.  The model's only responsibility in the MVC pattern is to make its properties available for other classes to monitor through the observer pattern.  More good news ... Matlab provides events and listeners and even has special property listeners.  All you have to do is use the SetObservable attribute on the properties you want to monitor.

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!

4 comments:

  1. 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?

    ReplyDelete
  2. Hi Jason,
    Sorry 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

    ReplyDelete
  3. A really super example. Thanks.
    I will be sharing this with the guys at work.

    ReplyDelete
  4. 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)

    case 'english'
    ...
    set(handles.unitgroup, 'SelectedObject', handles.english);

    case 'si'
    ...
    set(handles.unitgroup, 'SelectedObject', handles.si);

    ReplyDelete