MVUX Overview
Model, View, Update, eXtended (MVUX) is an implementation of the Model-View-Update design pattern, that encourages the flow of immutable data in a single direction. What differentiates MVUX from other MVU implementations is that it has been designed to support data binding.
Why MVUX?
To better understand the need for MVUX, let us consider a weather application that will display the current temperature, obtained from an external weather service. At face value, this seems simple enough. All the app has to do is call a service to retrieve latest temperature and display the returned value.
Weather App Example - MVVM
For example, using a Model-View-ViewModel (MVVM) approach, the following MainViewModel
initializes the CurrentWeather
property with the information obtained from the weather service. The XAML binds the CurrentWeather
property of the DataContext
(an instance of the MainViewModel
) to the Text property of a TextBlock
public partial class MainViewModel : ObservableObject
{
private readonly IWeatherService _weather;
[ObservableProperty]
private WeatherInfo? _currentWeather;
public MainViewModel(IWeatherService Weather)
{
_weather = Weather;
_ = LoadWeather();
}
private async Task LoadWeather()
{
CurrentWeather = await _weather.GetCurrentWeather();
}
}
The ObservableObject
comes from the CommunityToolkit.Mvvm
package and provides an implementation of the INotifyPropertyChanged
interface, which is used to notify the UI when property values change. The ObservableProperty
attribute (also from the CommunityToolkit.Mvvm
package) is used to instruct the source code generator to emit properties that include raising the PropertyChanged
event when the value changes. In this case the CurrentWeather
property is generated, from the _currentWeather
field, and will raise the PropertyChanged
event when the value is set in the LoadWeather
method.
Here's this simple application running:
The code required to call the GetCurrentWeather
and displaying the resulting Temperature
using XAML is simple enough. However, there are a few things we should consider:
- If the
GetCurrentWeather
method takes a finite amount of time to complete, what should be displayed while the app is waiting for the result? - If the
GetCurrentWeather
service fails, for example due to network issues, should the app display an error? - If the
GetCurrentWeather
service returns no data, what should the app show? - How can the user force the data to be refreshed?
The previous example has been updated in the following code to addresses these points. As you can see the updated code is significantly more complex than the original code.
public partial class MainViewModel : ObservableObject
{
private readonly IWeatherService _weather;
[ObservableProperty]
private WeatherInfo? _currentWeather;
[ObservableProperty]
private bool _isError;
[ObservableProperty]
private bool _noResults;
public MainViewModel(IWeatherService Weather)
{
_weather = Weather;
LoadWeatherCommand.Execute(default);
}
[RelayCommand]
public async Task LoadWeather()
{
try
{
IsError = false;
CurrentWeather = await _weather.GetCurrentWeather();
NoResults = CurrentWeather is null;
}
catch
{
IsError = true;
}
}
}
IsError
and NoResults
properties are generated from the _isError
and _noResults
fields respectively. The LoadWeather
method now has the RelayCommand
attribute (also from the CommunityToolkit.Mvvm
package) which will generate an ICommand
, LoadWeatherCommand
. The LoadWeatherCommand
implementation also includes an IsRunning
property that returns true when the ICommand
is being executed. The IsError
, NoResults
and IsRunning
properties are used to control the visibility of the UI elements in the XAML.
Here's the updated application running, showing the progress ring in action while the data is loading and a Get Weather Button for refreshing the data.
This simple example illustrates how quickly simple code can grow in complexity when we consider the various states that an application can be in.
Weather App Example - MVUX
With Model-View-Update-eXtended (MVUX) we can simplify this code, making it easier to maintain and less error prone. Let's take a look at an equivalent weather application written using MVUX.
public partial record MainModel(IWeatherService WeatherService)
{
public IFeed<WeatherInfo> CurrentWeather => Feed.Async(this.WeatherService.GetCurrentWeather);
}
The MainModel
(as distinct from the MainViewModel
used in the MVVM example) includes a single CurrentWeather
property that exposes an IFeed
that will call GetCurrentWeather
to retrieve the current weather. The IFeed
interface is used to represent a stream, or sequence, of values that are loaded asynchronously.
Here's a quick summary of the changes:
- MainModel defines a single property
CurrentWeather
that returns anIFeed
of typeWeatherInfo
. - Instead of defining additional properties to reflect the state of the
GetCurrentWeather
call, this information is encapsulated in theIFeed
. - The
MainPage
XAML has been simplified to use theFeedView
control, which automatically handles the various states of theIFeed
. - The
FeedView
control has aSource
property that is bound to theCurrentWeather
property of theMainModel
. - The
FeedView
control has aValueTemplate
that defines the UI to display when theIFeed
has a value,ProgressTemplate
that defines the UI to display when theIFeed
is loading,ErrorTemplate
that defines the UI to display when the feed has an error, andNoneTemplate
that defines the UI to display when theIFeed
has no results. - The
Get Weather
Button is data bound to theRefresh
command of theFeedView
control, which will cause theIFeed
to invoke theGetCurrentWeather
method when the Button is pressed.
At this point, don't worry if you don't understand all of the details of the code. We'll cover this in more detail in the following sections. For now, it's important to understand that the MainModel
code is much simpler than the previous MainViewModel
, and that the FeedView
control can be used to encapsulate the various visual states of the application.
What is MVUX?
Now that you've seen an example of MVUX in action, let's discuss the main components of MVUX.
Model
As we saw in the example, a Model in MVUX (eg MainModel) is similar in many ways to a ViewModel in MVVM (eg MainViewModel). Both define the properties that will be available for data binding and any methods that will handle user interactions.
In the context of MVUX the term Model also includes any data entities that are used by the application. For example, the WeatherInfo
class, returned by the GetCurrentWeather
method, is considered part of the Model.
In MVUX the entities that make up the Model are assumed to be immutable, meaning that both MainModel
and WeatherInfo
can be defined as record
types. This is a key difference between MVUX and MVVM, where the ViewModel is typically mutable.
For the weather application example, MainModel
is the Model for the MainPage
, and defines a property named CurrentWeather
.
public partial record MainModel(IWeatherService WeatherService)
{
public IFeed<WeatherInfo> CurrentWeather => Feed.Async(this.WeatherService.GetCurrentWeather);
}
The CurrentWeather
property returns an IFeed
of WeatherInfo
entities. An IFeed
represents a stream, or sequence, of values. For those familiar with Reactive this is similar to an IObservable
.
When the CurrentWeather
property is accessed, an IFeed
is created via the Feed.Async
factory method, which will asynchronously call the GetCurrentWeather
service, and return the result as a WeatherInfo
entity.
Feeds are covered in more detail in the Feeds documentation.
View
In MVUX, the View is the UI, which can be written in XAML, C#, or a combination of the two. For example, the following can be used to data bind the Text
property of a TextBlock
to the CurrentWeather.Temperature
property.
<Page x:Class="WeatherApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<TextBlock>
<Run Text="Current temperature: " />
<Run Text="{Binding CurrentWeather.Temperature}" />
</TextBlock>
</StackPanel>
</Page>
Unlike MVVM where the DataContext
of the page would be set to an instance of the ViewModel, in MVUX the DataContext
is set to an instance of a generated bindable proxy that wraps the Model. In this case, the bindable proxy for MainModel is named BindableMainModel
.
public MainPage()
{
this.InitializeComponent();
DataContext = new BindableMainModel(new WeatherService());
}
The generated BindableMainModel
exposes the IFeed
properties of the MainModel
in a way that they can be data bound using simple data binding expressions, for example {Binding CurrentWeather.Temperature}
.
What's unique to MVUX is the additional information that IFeed
exposes. The IFeed
includes information about the state of the asynchronous operation, such as whether the operation is in progress, whether the operation returned data, or not, and whether there was an error. This information can be used to display the appropriate UI to the user.
To simplify working with an IFeed
, we can leverage the MVUX FeedView
control. The FeedView
has been designed to work with IFeed
sources and exposes an simple way for developers to define what the layout should be for the different states of the asynchronous operation.
The following XAML shows how the FeedView
can be used to display the current temperature. The Source
property is bound to the CurrentWeather
property of the BindableMainModel
. Inside the DataTemplate
the Data
property contains the data returned by the CurrentWeather
property, which will be a WeatherInfo
entity.
<Page x:Class="WeatherApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mvux="using:Uno.Extensions.Reactive.UI">
<StackPanel HorizontalAlignment="Center"
VerticalAlignment="Center">
<mvux:FeedView Source="{Binding CurrentWeather}">
<mvux:FeedView.ValueTemplate>
<DataTemplate>
<TextBlock>
<Run Text="Current temperature: " />
<Run Text="{Binding Data.Temperature}" />
</TextBlock>
</DataTemplate>
</mvux:FeedView.ValueTemplate>
</mvux:FeedView>
</StackPanel>
</Page>
The FeedView
control has different visual states that align with the different states that an IFeed
can be in (e.g. loading, refreshing, error, etc.). The above XAML defines the ValueTemplate
, which is used when the IFeed
has data. Other templates include ProgressTemplate
, ErrorTemplate
and NoneTemplate
These automatically control what's displayed based on the state of the IFeed
.
<Page x:Class="WeatherApp.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mvux="using:Uno.Extensions.Reactive.UI">
<mvux:FeedView Source="{Binding CurrentWeather}">
<mvux:FeedView.ValueTemplate>
<DataTemplate>
<TextBlock>
<Run Text="Current temperature: " />
<Run Text="{Binding Data.Temperature}" />
</TextBlock>
</DataTemplate>
</mvux:FeedView.ValueTemplate>
<mvux:FeedView.ProgressTemplate>
<DataTemplate>
<ProgressRing />
</DataTemplate>
</mvux:FeedView.ProgressTemplate>
<mvux:FeedView.ErrorTemplate>
<DataTemplate>
<TextBlock Text="Error" />
</DataTemplate>
</mvux:FeedView.ErrorTemplate>
<mvux:FeedView.NoneTemplate>
<DataTemplate>
<TextBlock Text="No Results" />
</DataTemplate>
</mvux:FeedView.NoneTemplate>
</mvux:FeedView>
</Page>
Update
An Update is any action that will result in a change to the Model. Whilst an Update is often triggered via an interaction by the user with the View, such as editing text or clicking a button, an Update can also be triggered from a background process (for example a data sync operation or perhaps a notification triggered by a hardware sensor, such as a GPS).
We can make the weather example a bit more realistic by passing a city name as a parameter to the GetCurrentWeather
service. In order for our WeatherModel
to accept a user entered city, it needs to define an IState
property, City
.
public partial record MainModel(IWeatherService WeatherService)
{
public IState<string> City => State<string>.Empty(this);
...
}
An IState
is a special type of property that is used to store state. The bindable proxy generated for the MainModel
will now include a City
property that can be two-way data bound in order to accept user input.
<TextBox Text="{Binding City, Mode=TwoWay}" />
The City
property can now be combined with the CurrentWeather
property in order to pass the city name to the GetCurrentWeather
service. The IFeed
returned by the CurrentWeather
property will now await the City
property and pass the value to the GetCurrentWeather
service.
public IFeed<WeatherInfo> CurrentWeather => Feed.Async(async ct =>
{
var city = await City;
if (city is not null)
{
return await this.WeatherService.GetCurrentWeather(city, ct);
}
return default;
});
This can be expressed in a more declarative way using the SelectAsync
extension method. As the value of the City
property changes, the SelectAsync
method will automatically trigger a refresh of the CurrentWeather
feed. The GetCurrentWeather
method will be invoked, passing the current value of the City
property.
public IFeed<WeatherInfo> CurrentWeather => City.SelectAsync(this.WeatherService.GetCurrentWeather);
This is just one example of how user input can be accepted in order to trigger a change to the Model. Another example is for a Button
to trigger a refresh of the weather data, which was shown earlier with the Get Weather
Button that was data bound to the Refresh
command on the FeedView
.
eXtended
In summary, MVUX is a set of abstractions that are designed to work well with the data binding engine. The use of IFeed
and IState
properties allow the Model to be expressed more declaratively. The source code generator then generates bindable proxies for each Model. The bindable proxies are used as a bridge that enables immutable entities to work with the data-binding engine.
MVVM vs MVUX Data Flow
The Model-View-ViewModel (MVVM) pattern is a popular pattern for building XAML applications. The MVVM pattern is a specialization of the Presentation Model pattern, where the ViewModel is responsible for exposing data from the Model to the View. The ViewModel is also responsible for handling user interactions and updating the Model. The ViewModel is often referred to as the glue between the Model and the View.
The Model-View-Update-eXtended (MVUX) pattern still leverages data binding to present information to the user and capture input. However, instead of being bound directly to the Model, the View is data bound to bindable proxies that are generated by MVUX. It's the use of bindable proxies that ensure the single direction of flow of data. When an update occurs, either by the user triggering an action, or entering some data, this generates new instances of the Model. The bindable proxies then detect the change and update the View accordingly. The bindable proxies also ensure that the Model is updated in a thread-safe manner.
Key points
- Feeds are reactive in nature.
- Models and associated entities are immutable.
- Operations are asynchronous by default.
- Feeds include additional information such as loading, if there's data or if an error occurred.
- States are used to accept input from the user and can be two-way data bound.
- MVUX combines the unidirectional flow of data of MVU, with the data binding capabilities of MVVM.
Creating your own
You can get started with MVUX by creating a new project. Follow the How to set up an MVUX project tutorial to get started.
You can then use the MVUX example above as a reference to create your own IFeed
and IState
properties, and use the FeedView
to display data.
In the Model
- Define your own Models
- MVUX recommends using record types for the Models in your app as they're immutable.
- The MVUX analyzers auto-generate a bindable proxy for each
partial
class
orrecord
named with a Model suffix. - For every public
IFeed
property found in the model, a corresponding property is generated on the bindable proxy. - You can use
IState
properties to accepting input from the user.
In the View
- Create your views and add data binding to the XAML elements as required
- Customize the layout of your application using the FeedView control.
- Use two-way binding to a state to allow input from the user
WeatherApp Sample
You can find the code for our weather app here.