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.
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.
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:
- The app user clicks Generate Random Data.
- 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. - The
random
method in the model class modifies the underlying application data, which is stored as a model property. - The model broadcasts an event named
DataChanged
. - 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.
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 classnotify
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 theXData
andYData
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).
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’ssetup
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).
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