Developing MATLAB Apps Using the Model-View-Controller Pattern - MATLAB & Simulink

Technical Articles

Developing MATLAB Apps Using the Model-View-Controller Pattern

By Laura Dempsey and Ken Deeley, MathWorks


As you develop apps in MATLAB®, often your requirements change or new features are added. As a result, apps increase in size and complexity, and the code becomes unwieldy and time-consuming to maintain.

Building an app programmatically gives you extra control over the structure of the code. As an app’s code base becomes more complex, it becomes increasingly important to follow the separation of concerns (SoC) design principle. To improve maintainability, we recommend organizing the app code base using the model-view-controller (MVC) software architecture pattern (Figure 1), a specific instance of the SoC design principle.

A diagram of the model-view-controller (MVC) software architecture pattern. The user interacts with the Controller, which manipulates the Model, which Communicates the View, which the user sees.

Figure 1. The model-view-controller (MVC) software architecture pattern.

The MVC approach to application development separates the code base into model, view, and controller classes, each with well-defined roles and responsibilities. The benefits of writing separate classes include improved development, testing, and collaboration; different developers can work independently on separate parts of the application without conflicts arising. Further, managing the complexity of a large-scale application becomes easier when each class is relatively short and has a limited set of responsibilities.

This article addresses four concerns that often arise in large-scale application development:

  • Structuring a code base to allow for future growth
  • Combining functional and object-oriented programming
  • Working with MATLAB graphics and user interface control objects
  • Separating the application into components that can be developed and tested independently

We provide a step-by-step guide for building scalable apps in MATLAB using the MVC design pattern. The code used in this article is available on GitHub.

This article covers the following topics:

The Model-View-Controller Architecture

The model-view-controller (MVC) software architecture pattern is used to build desktop and web-based apps in a wide variety of programming languages. MVC separates the application code into three main areas of responsibility:

  • Model. Models organize and manage the application data, store the state of the system, and communicate with other parts of the application when relevant data changes occur.
  • View. Views provide app users with visualizations of the application data and the system state.
  • Controller. Controllers provide mechanisms for app users to interact with and modify the application data and system state.

Models, views, and controllers are separate classes with specific responsibilities and defined relationships. The model does not communicate directly with views or controllers, but instead broadcasts events notifying that its data or state has changed. Views and controllers have a reference to the model but not to each other. They also share a significant amount of code. This shared code can be moved into a component superclass to reduce code duplication and make it easier to develop additional views and controllers. The MATLAB ComponentContainer class simplifies this development workflow. It provides a panel-like container to hold view and controller graphics objects and takes care of routine operations such as object lifecycle management.

Within the MVC framework, the application code is separated into multiple classes, enabling the distinct parts of the application to be worked on independently by different team members. In Figure 2, the bottom arrow indicates that both the view and controller share common code located in the component class (inheritance). The component class inherits from the MATLAB component container. The diamond connector indicates that Component has a reference to the model. The arrow connectors represent inheritance. Since View and Controller inherit from Component, they will also have a reference to the model. Italics indicate abstract classes.

Figure 2. Class diagram of the MVC application built in the Worked Example section.

Value and Handle Classes

There are two types of classes in MATLAB: value and handle. Value classes provide pass-by-value behavior, while handle classes provide pass-by-reference behavior. All fundamental datatypes in MATLAB, such as numeric arrays, tables, and strings, are value classes. All MATLAB graphics objects, such as those created by the uifigure and plot functions, are handle classes.

Value Class Behavior

Consider a value object a. When b is assigned to a, we are creating a copy of a. When a changes to a new value, b remains the same.

>> a = 1;
>> b = a;
>> a = 2;
a = 
     2 
>> b 
b = 
     1 

Handle Class Behavior

For handle objects, if a new variable is assigned to an existing handle, we are creating an additional reference to the same underlying object. When we change the properties of the underlying object using one of the references, the other reference reflects those changes.

>> f1 = uifigure; 
>> f2 = f1; 
>> f2.Color 
    0.9400    0.9400    0.9400 
>> f1.Color = [1, 0, 0]; % Set the Color property of f1 to red.
>> f2.Color % The Color property of f2 has also changed to red.
     1     0     0 

Value or Handle Classes for Apps

All classes within the MVC framework are implemented as handle classes. The model must be a handle class since the entire application must refer to a single source of application data. If the model were a value class, it would be easy to create unnecessary copies. Misleading application behavior can occur if views and controllers hold references to different copies of the model, and thus to different data states. Both views and controllers exhibit graphics-like behavior and are therefore handle classes.

To define a handle class in MATLAB, we derive it from the built-in handle class.

classdef MyHandleClass < handle
    ...
end

Worked Example

The following sections will explore the MVC pattern in more detail via a worked example. We are using a simple example for demonstration purposes, but the same principles apply to apps of any size.

The example comprises a small app that creates and displays new random data each time the app user clicks a button (Figure 3). A reset button in the toolbar allows the app user to clear the plot and its underlying data. The roles of the MVC classes within the application are as follows:

  1. The app user clicks Generate Random Data.
  2. Clicking this button triggers its callback function. The controller class manages the button together with its callback function (a controller method). In turn, the callback function invokes the random method in the model class.
  3. The random method in the model class modifies the underlying application data, which is stored as a model property.
  4. The model broadcasts an event named DataChanged.
  5. The view class has a listener for the DataChanged event which, when triggered, updates the line visualizing the data. Both the line and the listener are stored as view properties, with the listener callback as a view method.

Similarly, when the app user clicks the reset button on the toolbar, the reset model method is invoked. The model resets its application data and broadcasts the DataChanged event. This event is detected automatically by the view, which updates the line accordingly.

Figure 3. Animation of the final MVC app: a random data generator app.

These implementation details are hidden from the app user, who simply observes that the plot updates whenever they click one of the buttons.

In the next sections, we discuss the individual application classes in more detail.

Models

The model is usually developed first, as it is the central part of the application. In general, the model organizes the application data, stores the system state, provides methods for interacting with the data, and broadcasts events.

In our example, the model is responsible for the following tasks:

  • Storing the current random data: this is done by defining a property, Data (a numeric column vector).
  • Generating new random data: this is implemented via a method called random.
  • Resetting the data to an empty vector: this is implemented via a method called reset.
  • Broadcasting an event when the data changes: first, the model defines a custom event DataChanged. After generating new random data or resetting empty data, the model broadcasts the event using the handle class notify method.

In the Model class, we provide the methods random and reset for changing the application data, which is stored as the Data property. (Note that this property cannot be modified directly.) When developing the class we also use property validation to enable tab completion and reduce the risk of errors.

Events

The events and listeners framework in MATLAB allows an object to broadcast changes to another object. In our example, the object that changes is the model, and the object that needs to respond to those changes is the view.

An event is a notification of any activity that can be detected programmatically. In our example, the activity of interest is the application data being changed. In a class, events are defined using a separate events block listing the event names. You broadcast events by invoking the handle class notify method from inside class methods whenever the event occurs. To prevent code external to the model from notifying the event, we set the NotifyAccess event attribute to private.

Calling one of the model’s public methods (random or reset) changes the Data property. It is the responsibility of the model to broadcast an event (DataChanged) after the Data property has been changed so that views can listen to this event and respond appropriately.

Other code can detect when events occur and respond dynamically via a callback function.

The complete code listing for the model is

classdef Model < handle 
    %MODEL Application data model.

    properties ( SetAccess = private ) 
        % Application data.
        Data(:, 1) double = double.empty( 0, 1 ) 
    end

    events ( NotifyAccess = private ) 
        % Event broadcast when the data is changed.
        DataChanged
    end

    methods

        function random( obj ) 
            %RANDOM Generate new application data.

            % Generate column vector of random data.
            obj.Data = randn( 20, 1 ); 
            notify( obj, "DataChanged" ) 

        end

        function reset( obj ) 
            %RESET Restore the application data to its default value.

            % Reset the data to an empty column vector.
            obj.Data = double.empty( 0, 1 ); 
            notify( obj, "DataChanged" ) 

        end

    end

end

Views

Views reveal the model’s data and state by maintaining a collection of graphics objects such as lines, histograms, and tables. Views store a reference to the model and refresh their graphics objects dynamically in response to changes in the model.

In our example, the view is responsible for the following tasks:

  • Storing a reference to the model: this is done by defining a property, Model.
  • Visualizing the model’s random data: this is achieved by creating an axes object together with a line object to plot the data. The line object is stored as a property, Line, for future updates.
  • Creating a listener to respond automatically to model changes: this is implemented by setting up a listener object in the view constructor. This object listens for the model’s DataChanged event and responds via a callback function, which is implemented as a private view method. This method updates the XData and YData of the line object.

We derive the view from the MATLAB ComponentContainer class. The view inherits many useful graphics-related properties such as Parent, Position, Units, Layout, and Visible.

To obtain a valid subclass of ComponentContainer, we need to implement two methods in the view: setup and update. The setup method runs once when the view is created. It holds initialization code for the view, such as the code for creating the axes and line objects. The update method runs whenever a public property of the view is changed. Because the view in our example does not have public properties, we leave this method empty. When the view is created, the component container framework ensures that the setup method runs first, followed by the view constructor and finally, by the update method.

The view constructor takes in a reference to the model as the first input argument. Subsequent constructor inputs are name-value pairs applicable to the view. For example, you would usually specify the parent of the view on construction. The model is stored as a private property of the view, and it acts as the data source when the view needs to refresh its graphics.

Graphics objects managed by the view are added directly to the object. In our example, the view manages axes and line objects (Figure 4).

Figure 4. Random data generator app view containing two graphics objects: an axes object parented to the component and a line object parented to the axes.

As generating new data requires us to update the XData and YData of the line object, we must maintain a reference to the line. We store the line as a private property of the view, preventing it from being modified or deleted outside of the class.

Although our app doesn’t require any data to be loaded, most apps require this as a first step. To avoid assuming that any data exists when the app is launched, all components in the app must support an empty state.

Setting the XData and YData of the line to NaN in the view’s setup method results in an empty launch state. We make further customizations to the axes and line here (see code listing below), including styles such as the axes property FontSize and the line property Color.

Listeners

The final responsibility of the view is to listen for events broadcast by the model and to respond dynamically by updating the graphics objects for which it is responsible. MATLAB listeners automatically trigger their callback function whenever they detect a specific event from the source object (in this case, the model). To demonstrate this, we can create a listener at the command line that displays the string "Changed" when the model broadcasts its DataChanged event.

>> m = Model;
>> l = listener( m, "DataChanged", @(~, ~) disp( "Changed" ) );
>> random( m )
Changed

Instead of displaying a statement, the listener in the view class needs to refresh the view’s graphical objects. The callback for the Listener property is a private method onDataChanged. If the source and event data will not be used within the callback function, we can replace these input arguments with the ~ placeholder.

Within onDataChanged, we must update the XData and YData of the line to match the new model data. This could mean a 20x1 vector of data if the model’s random method was called, or a 0x1 empty double column vector if the reset method was called. To update both properties simultaneously, we use the set function rather than the dot notation for individual properties.

Note that onDataChanged is also called as the final step in the view constructor to ensure that the app displays the correct data if a model with nonempty Data is specified on construction.

classdef View < matlab.ui.componentcontainer.ComponentContainer 
    %VIEW Visualizes the data, responding to any relevant model events.

    properties ( Access = private )         
        % Line object used to visualize the model data.
        Line(1, 1) matlab.graphics.primitive.Line
        % Application data model.
        Model(1, 1) Model
        % Listener object used to respond dynamically to model events.
        Listener(:, 1) event.listener {mustBeScalarOrEmpty}
    end

    methods

        function obj = View( model, namedArgs ) 
            %VIEW View constructor.

            arguments
                model(1, 1) Model
                namedArgs.?View 
            end % arguments

            % Do not create a default figure parent for the component, and
            % ensure that the component spans its parent. By default,
            % ComponentContainer objects are auto-parenting - that is, a
            % figure is created automatically if no parent argument is
            % specified.
            obj@matlab.ui.componentcontainer.ComponentContainer( ... 
                "Parent", [], ... 
                "Units", "normalized", ... 
                "Position", [0, 0, 1, 1] ) 

            % Store the model.
            obj.Model = model; 

            % Listen for changes to the data. 
            obj.Listener = listener( obj.Model, ... 
                "DataChanged", @obj.onDataChanged ); 

            % Set any user-specified properties.
            set( obj, namedArgs ) 

            % Refresh the view. 
            onDataChanged( obj ) 

        end 

    end 

    methods ( Access = protected ) 

        function setup( obj ) 
            %SETUP Initialize the view. 

            % Create the view graphics. 
            ax = axes( "Parent", obj ); 
            obj.Line = line( ... 
                "Parent", ax, ... 
                "XData", NaN, ... 
                "YData", NaN, ... 
                "Color", ax.ColorOrder(1, :), ... 
                "LineWidth", 1.5 ); 

        end

        function update( ~ ) 
            %UPDATE Update the view. This method is empty because there are 
            %no public properties of the view. 

        end

    end

    methods ( Access = private ) 

        function onDataChanged( obj, ~, ~ ) 
            %ONDATACHANGED Listener callback, responding to the model event
            %"DataChanged"

            % Retrieve the most recent data and update the line.
            data = obj.Model.Data; 
            set( obj.Line, "XData", 1:numel( data ), "YData", data ) 

        end

    end

end

Controllers

Controllers allow the app user to modify the application data and system state. This is done by providing UI components, such as buttons and check boxes, whose callback functions change model properties or invoke model methods. In general, controllers store a reference to the model, create the interactive control objects, and implement their callbacks as private methods of the controller class.

In our example, the controller is responsible for the following tasks:

  • Storing a reference to the model. This is done by defining a property, Model.
  • Creating a button to allow the app user to generate new random data. This is done using the uibutton function in the controller’s setup method.
  • Calling the appropriate model method when the app user presses the button. This is done by implementing the button’s callback function as a private method that invokes the model’s random method.

As with the view, we derive the controller from ComponentContainer. The setup method creates a 1-by-1 grid and places a button inside it. In practice, the controller is usually responsible for multiple control objects, which are convenient to arrange within a grid layout.

As before, we leave the update method empty since we have no public controller properties.

To ensure that a visible change occurs when the user clicks a button in the app, we implement the button callback onButtonPushed as a private class method. This callback invokes the random method of the model, which will broadcast the DataChanged event and lead to a view refresh.

As with the view, the controller constructor takes in a reference to the model as the first input argument. Subsequent constructor inputs are name-value pairs applicable to the controller. The model is stored as a private property of the controller and is required by control object callbacks to invoke model methods or change model properties.

classdef Controller < matlab.ui.componentcontainer.ComponentContainer
    %CONTROLLER Provides an interactive control to generate new data.
    %
    % Copyright 2021-2022 The MathWorks, Inc.

    properties ( Access = private )
        % Application data model.
        Model(1, 1) Model
    end % properties ( Access = private )
    
    methods
        
        function obj = Controller( model, namedArgs )
            % CONTROLLER Controller constructor.
            
            arguments
                model(1, 1) Model
                namedArgs.?Controller
            end % arguments
            
            % Do not create a default figure parent for the component, and
            % ensure that the component spans its parent. By default,
            % ComponentContainer objects are auto-parenting - that is, a
            % figure is created automatically if no parent argument is
            % specified.
            obj@matlab.ui.componentcontainer.ComponentContainer( ...
                "Parent", [], ...
                "Units", "normalized", ...
                "Position", [0, 0, 1, 1] )

            % Store the model.
            obj.Model = model;
            
            % Set any user-specified properties.
            set( obj, namedArgs )
            
        end % constructor
        
    end % methods
    
    methods ( Access = protected )
        
        function setup( obj )
            %SETUP Initialize the controller.
            
            % Create grid and button.
            g = uigridlayout( ...
                "Parent", obj, ...
                "RowHeight", "1x", ...
                "ColumnWidth", "1x", ...
                "Padding", 0 );
            uibutton( ...
                "Parent", g, ...
                "Text", "Generate Random Data", ...
                "ButtonPushedFcn", @obj.onButtonPushed );
            
        end % setup
        
        function update( ~ )
            %UPDATE Update the controller. This method is empty because 
            %there are no public properties of the controller.
            
        end % update
        
    end % methods ( Access = protected )
    
    methods ( Access = private )
        
        function onButtonPushed( obj, ~, ~ )
            
            % Invoke the random() method of the model.
            random( obj.Model )
            
        end % onButtonPushed
        
    end % methods ( Access = private )
    
end % classdef

Centralizing Common Code

As the previous two sections have shown, views and controllers often share the same code. In our example, both the view and the controller are derived from the ComponentContainer, and both have a reference to the model. Views and controllers may also have the same model listeners. To minimize code duplication, it is useful to define a component superclass from which the views and controllers will inherit. This superclass stores common properties and methods.

Because the view and controller are both derived from ComponentContainer in our example, we can also derive Component from ComponentContainer. All subclasses of Component will then inherit the properties and methods available from ComponentContainer.

In all apps, the view and controller classes need to maintain a reference to the model. In our example, the Component class has the Model property. This property can now be removed from the view and controller class definitions.

To prevent downstream errors, the SetAccess of the Model is set to immutable, ensuring that this property can only be set in the constructor. The GetAccess is protected so that any view/controller subclasses of Component can retrieve these properties when needed, while preventing external code from modifying them (for reasons previously stated).

The Component constructor accepts a model reference as its only input argument and invokes the ComponentContainer constructor with values for the Parent, Units, and Position properties. We do not rely on the ComponentContainer constructor to create a default parent object; the graphics parent must be specified when the view and controller are created, otherwise each component will be created in a separate figure window.

With the Component superclass defined, we can simplify the view and controller classes. As mentioned above, we remove the Model property. We also call the Component constructor from both the view and controller constructors, which centralizes the common code that previously appeared there. This simplified version of the code is available on the GitHub repository.

classdef ( Abstract ) Component < matlab.ui.componentcontainer.ComponentContainer 
    %COMPONENT Superclass for implementing views and controllers.

    properties ( SetAccess = immutable, GetAccess = protected ) 
        % Application data model.
        Model(1, 1) Model
    end

    methods

        function obj = Component( model ) 
            %COMPONENT Component constructor.

            arguments
                model(1, 1) Model
            end % arguments

            % Do not create a default figure parent for the component, and
            % ensure that the component spans its parent. By default,
            % ComponentContainer objects are auto-parenting - that is, a
            % figure is created automatically if no parent argument is
            % specified.
            obj@matlab.ui.componentcontainer.ComponentContainer( ... 
                "Parent", [], ... 
                "Units", "normalized", ... 
                "Position", [0, 0, 1, 1] ) 

            % Store the model.
            obj.Model = model; 

        end

    end 

end

Launching the App

The core parts of our app are now complete. To summarize, when the app user clicks the button on the controller, the controller calls the random method of the model. The model subsequently changes its Data property and broadcasts an event. The view detects this event and updates the XData and YData of the line in the plot.

We build up our application incrementally to test that the line plot updates when we click the button.

>> m = Model; % Create the Model.
>> f = uifigure; % Create application window.
>> g = uigridlayout( ...
    "Parent", f, ...
    "RowHeight", {"1x", 50}, ...
    "ColumnWidth", "1x" ); % Create the layout.
>> View( m, "Parent", g ); % Create the View.
>> Controller( m, "Parent", g ); % Create the Controller.

Writing a Launcher Function

A launcher function provides a convenient way for the app user to start the app. We provide a figure handle as an optional input argument to this launcher function. In the next section, we will use this input to embed the MVC application into App Designer, where it can be compiled as a web app.

The figure handle should be an optional output argument of the launcher function. This enables MATLAB to add or remove the application from the path when the app is packaged and shared via an .mlappinstall file.

Within the launcher, it is also possible to add controls that will interact with the model. Some controls can only be parented to the figure, such as menus and toolbars (see uimenu and uitoolbar). In this example, we have added a toolbar containing a button for resetting the model (Figure 5). When the app user clicks this button, the reset method of the model is called. We have implemented the button callback using a nested function onReset (note that the model reference is a shared variable in the launcher function).

Screenshot showing the reset button in the figure toolbar for a small MVC app.

Figure 5. A reset button in the figure toolbar allows the user to clear the application data.

function varargout = launchMVCApp( f ) 
%LAUNCHMVCAPP Launch the small MVC application. 

arguments
    f(1, 1) matlab.ui.Figure = uifigure() 
end

% Rename figure.
f.Name = "Small MVC App"; 

% Create the layout.
g = uigridlayout( ... 
    "Parent", f, ... 
    "RowHeight", {"1x", 40}, ... 
    "ColumnWidth", "1x" ); 

% Create the model.
m = Model; 

% Create the view.
View( m, "Parent", g ); 

% Create the controller.
Controller( m, "Parent", g ); 

% Create toolbar to reset the model.
icon = fullfile( matlabroot, ...
"toolbox", “matlab”, “icons”, “tool_rotate_3d.png” ); 
tb = uitoolbar( "Parent", f ); 
uipushtool( ... 
    "Parent", tb, ... 
    "Icon", icon, ... 
    "Tooltip", "Reset the data.", ... 
    "ClickedCallback", @onReset ); 

    function onReset( ~, ~ ) 
        %ONRESET Callback function for the toolbar reset button.

        % Reset the model.
        reset( m ) 

    end

% Return the figure handle if requested.
if nargout > 0 
    nargoutchk( 1, 1 ) 
    varargout{1} = f; 
end % if

end

Sharing MVC Applications

Methods for sharing are the same whether the app was developed in App Designer or programmatically with MVC. For example, MVC apps can be shared as standalone apps and web apps. Web apps are generated from .mlapp files, either interactively using App Designer or programmatically with compiler.build.webAppArchive. To obtain an .mlapp wrapper for an MVC application, create a new blank app in App Designer, then call your application launcher from the app’s startup function, as shown below.

classdef app < matlab.apps.AppBase

    % Properties that correspond to app components
    properties (Access = public)
        Figure  matlab.ui.Figure
    end

    % Callbacks that handle component events
    methods (Access = private)

        % Code that executes after component creation
        function startupFcn(app)
            launcher( app.Figure )
        end

    end

...

end

Summary

In this article, we described the MVC software architecture pattern and corresponding best practices for the implementation of large-scale MATLAB apps using a simple random data generator app as an example. As your MATLAB apps increase in size and complexity, with multiple developers contributing to your projects, the benefits of using the structured MVC approach become clear. While some upfront effort is required to design an application using this framework, in the long term you will save time when requirements change or when your app users request new features.

Published 2023

View Articles for Related Capabilities