In this chapter, we will take a look at the Model-View-ViewModel (MVVM) pattern, the MVVM elements that are offered with the Xamarin.Forms toolkit, and how we can expand on them to truly take advantage of the power of the pattern. As we dig into these topics, we will apply what we have learned to the TripLog app that we started building in Chapter 1, Getting Started.
In this chapter, we will cover the following topics:
Before we start applying the MVVM pattern to our app we will review the basics of the pattern in the following section.
At its core, MVVM is a presentation pattern designed to control the separation between user interfaces and the rest of an application. The key elements of the MVVM pattern are as follows:
Now that we have an understanding of the key pieces of the MVVM pattern and how those pieces relate to one another we can start updating our app architecture to follow the pattern. In the next section we will add ViewModels for each of the pages we added in the previous chapter and refactor those pages with data bindings.
The first step of introducing MVVM into an app is to set up the structure by adding folders that will represent the core tenants of the pattern, such as Models, ViewModels, and Views. Traditionally, the Models and ViewModels live in a core library (usually, a portable class library or .NET standard library), whereas the Views live in a platform-specific library.
Thanks to the power of the Xamarin.Forms toolkit and its abstraction of platform-specific UI APIs, the Views in a Xamarin.Forms app can also live in the core library.
Just because the Views can live in the core library with the ViewModels and Models doesn't mean that separation between the UI and the app logic isn't important. As we will see in this chapter and throughout the rest of the book, the separation between the UI and app logic is instrumental in keeping the codebase maintainable, testable, and shareable.
When implementing a specific structure to support a design pattern, it is helpful to have your application namespaces organized in a similar structure. This is not a requirement, but it is something that can be useful. By default, Visual Studio for Mac will associate namespaces with directory names, as shown in the following screenshot:
Figure 1: .NET Naming Policies settings in Visual Studio
For the TripLog app, we will let the Views, ViewModels, and Models all live in the same core .NET standard library project. In our solution, this is the project called TripLog. We have already added the Views
and Models
folders in Chapter 1, Getting Started, so we just need to add a ViewModels
folder to the project to complete the MVVM structure:
ViewModels
to the root of the TripLog project.Once the MVVM structure has been added, the folder structure in the solution should look similar to the following screenshot:
Figure 2: The TripLog solution in Visual Studio
In MVVM, the term View is used to describe a screen. Xamarin.Forms uses the term View to describe controls, such as buttons or labels, and uses the term Page to describe a screen. In order to avoid confusion, I will stick with the Xamarin.Forms terminology and refer to screens as Pages, and will only use the term Views in reference to screens for the folder where the Pages will live, in order to stick with the MVVM pattern.
In most cases, Views (Pages) and ViewModels have a one-to-one relationship. However, it is possible for a View (Page) to contain multiple ViewModels or for a ViewModel to be used by multiple Views (Pages). For now, we will simply have a single ViewModel for each Page. Before we create our ViewModels, we will start by creating a base ViewModel class, which will contain the basic functionality that each of our ViewModels will inherit. Initially, the base ViewModel class will only contain a couple of members and will implement INotifyPropertyChanged
, but we will add to this class as we continue to build upon the TripLog app throughout this book.
In order to create a base ViewModel, perform the following steps:
BaseViewModel
in the ViewModels
folder using the following code:public class BaseViewModel
{
protected BaseViewModel()
{
}
}
BaseViewModel
to implement INotifyPropertyChanged
:using System.ComponentModel;
using System.Runtime.CompilerServices;
public class BaseViewModel : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected BaseViewModel()
{
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
The implementation of INotifyPropertyChanged
is key to the behavior and role of the ViewModels and data binding. It allows a Page to be notified when the properties of its ViewModel have changed.
Now that we have created a base ViewModel, we can start adding the actual ViewModels that will serve as the data context for each of our Pages. We will start by creating a ViewModel for MainPage
.
The main purpose of a ViewModel is to separate the business logic, for example, data access and data manipulation, from the UI logic. Right now, our MainPage
directly defines the list of data that it is displaying. This data will eventually be dynamically loaded from an API but for now, we will move this initial static data definition to its ViewModel so that it can be data bound to the UI.
In order to create the ViewModel for MainPage
, perform the following steps:
ViewModels
folder and name it MainViewModel
.MainViewModel
class to inherit from BaseViewModel
:public class MainViewModel : BaseViewModel
{
// ...
}
ObservableCollection<T>
property to the MainViewModel
class and name it LogEntries
. This property will be used to bind to the ItemsSource
property of the CollectionView
element on MainPage.xaml
:using System.Collections.ObjectModel;
using TripLog.Models;
public class MainViewModel : BaseViewModel
{
ObservableCollection<TripLogEntry> _logEntries;
public ObservableCollection<TripLogEntry> LogEntries
{
get => _logEntries;
set
{
_logEntries = value;
OnPropertyChanged();
}
}
// ...
}
MainPage.xaml.cs
that creates the List<TripLogEntry>
that populates the CollectionView
element on MainPage.xaml
and repurpose that logic in the MainViewModel
– we will put it in the constructor for now:public MainViewModel()
{
LogEntries = new ObservableCollection<TripLogEntry>
{
new TripLogEntry
{
Title = "Washington Monument",
Notes = "Amazing!",
Rating = 3,
Date = new DateTime(2019, 2, 5),
Latitude = 38.8895,
Longitude = -77.0352
},
new TripLogEntry
{
Title = "Statue of Liberty",
Notes = "Inspiring!",
Rating = 4,
Date = new DateTime(2019, 4, 13),
Latitude = 40.6892,
Longitude = -74.0444
},
new TripLogEntry
{
Title = "Golden Gate Bridge",
Notes = "Foggy, but beautiful.",
Rating = 5,
Date = new DateTime(2019, 4, 26),
Latitude = 37.8268,
Longitude = -122.4798
}
};
}
MainViewModel
as the BindingContext
for MainPage
. Do this by simply setting the BindingContext
property of MainPage
in its code-behind file to a new instance of MainViewModel
. The BindingContext
property comes from the Xamarin.Forms.ContentPage
base class:using System;
using Xamarin.Forms;
using TripLog.Models;
using TripLog.ViewModels;
public partial class MainPage : ContentPage
{
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
// ...
}
CollectionView
element on MainPage.xaml
gets its items. Currently, its ItemsSource
property is being set directly in the Page's code behind, which we removed in step four. Now update the CollectionView
element's tag in MainPage.xaml
to bind to the MainViewModel LogEntries
property:<CollectionView x:Name="trips"
SelectionMode="Single"
ItemsSource="{Binding LogEntries}"
SelectionChanged="Trips_SelectionChanged">
The Main Page is now all setup with a ViewModel and data binding. Next, we will add a ViewModel for the Detail Page.
Now we will add another ViewModel to serve as the data context for DetailPage
, as follows:
ViewModels
folder and name it DetailViewModel
.DetailViewModel
class to inherit from the BaseViewModel
class:public class DetailViewModel : BaseViewModel
{
// ...
}
TripLogEntry
property to the class and name it Entry
. This property will be used to bind details about an entry to the various labels on DetailPage
:using TripLog.Models;
public class DetailViewModel : BaseViewModel
{
TripLogEntry _entry;
public TripLogEntry Entry
{
get => _entry;
set
{
_entry = value;
OnPropertyChanged();
}
}
// ...
}
DetailViewModel
constructor to take a TripLogEntry
parameter named entry
. Use this constructor property to populate the public Entry
property created in the previous step:public class DetailViewModel : BaseViewModel
{
// ...
public DetailViewModel(TripLogEntry entry)
{
Entry = entry;
}
}
DetailViewModel
as the BindingContext
for DetailPage
and pass in the TripLogEntry
property that is being passed to DetailPage
:using System;
using Xamarin.Forms;
using TripLog.Models;
using TripLog.ViewModels;
public partial class DetailPage : ContentPage
{
public DetailPage(TripLogEntry entry)
{
InitializeComponent();
BindingContext = new DetailViewModel(entry);
// ...
}
// ...
}
In Chapter 3, Navigation, we will refactor how we are passing the entry parameter to DetailViewModel
.
DetailPage
constructor that directly sets the Text
properties of the Label
elements:public DetailPage(TripLogEntry entry)
{
// ...
// Remove these lines of code:
//title.Text = entry.Title;
//date.Text = entry.Date.ToString("M");
//rating.Text = $"{entry.Rating} star rating";
//notes.Text = entry.Notes;
}
Label
element tags in DetailPage.xaml
to bind their Text
properties to the DetailViewModel Entry
property:<Label ... Text="{Binding Entry.Title}" />
<Label ... Text="{Binding Entry.Date, StringFormat='{0:M}'}" />
<Label ... Text="{Binding Entry.Rating, StringFormat='{0} star rating'}" />
<Label ... Text="{Binding Entry.Notes}" />
Map
control does not have bindable properties, the values have to be set directly to the ViewModel properties. The easiest way to do this is to add a private field to the Page that returns the value of the page's BindingContext
and then use that field to set the values on the map:public partial class DetailPage : ContentPage
{
DetailViewModel ViewModel => BindingContext as DetailViewModel;
public DetailPage(TripLogEntry entry)
{
InitializeComponent();
BindingContext = new DetailViewModel(entry);
map.MoveToRegion(MapSpan.FromCenterAndRadius(
new Position(
ViewModel.Entry.Latitude,
ViewModel.Entry.Longitude),
Distance.FromMiles(.5)));
map.Pins.Add(new Pin
{
Type = PinType.Place,
Label = ViewModel.Entry.Title,
Position = new Position(ViewModel.Entry.Latitude, ViewModel.Entry.Longitude)
});
}
}
With the Main Page and Detail Page updated with ViewModels and data binding we just need to update the New Entry Page. In the next section we will create a ViewModel to bind to the user's input on the New Entry Page.
Finally, we will need to add a ViewModel for NewEntryPage
, as follows:
ViewModels
folder and name it NewEntryViewModel
.NewEntryViewModel
class to inherit from BaseViewModel
:public class NewEntryViewModel : BaseViewModel
{
// ...
}
NewEntryViewModel
class that will be used to bind it to the values entered into the EntryCell
elements in NewEntryPage.xaml
:public class NewEntryViewModel : BaseViewModel
{
string _title;
public string Title
{
get => _title;
set
{
_title = value;
OnPropertyChanged();
}
}
double _latitude;
public double Latitude
{
get => _latitude;
set
{
_latitude = value;
OnPropertyChanged();
}
}
double _longitude;
public double Longitude
{
get => _longitude;
set
{
_longitude = value;
OnPropertyChanged();
}
}
DateTime _date;
public DateTime Date
{
get => _date;
set
{
_date = value;
OnPropertyChanged();
}
}
int _rating;
public int Rating
{
get => _rating;
set
{
_rating = value;
OnPropertyChanged();
}
}
string _notes;
public string Notes
{
get => _notes;
set
{
_notes = value;
OnPropertyChanged();
}
}
// ...
}
NewEntryViewModel
constructor to initialize the Date
and Rating
properties:public NewEntryViewModel()
{
Date = DateTime.Today;
Rating = 1;
}
Command
property to NewEntryViewModel
and name it SaveCommand
. This property will be used to bind to the Save ToolbarItem
in NewEntryPage.xaml
. The Xamarin.Forms Command
type implements System.Windows.Input.ICommand
to provide an Action
to run when the command is executed, and a Func
to determine whether the command can be executed:public class NewEntryViewModel : BaseViewModel
{
// ...
Command _saveCommand;
public Command SaveCommand =>
_saveCommand ?? (_saveCommand = new Command(Save, CanSave));
void Save()
{
var newItem = new TripLogEntry
{
Title = Title,
Latitude = Latitude,
Longitude = Longitude,
Date = Date,
Rating = Rating,
Notes = Notes
};
// TODO: Persist entry in a later chapter
}
bool CanSave() => !string.IsNullOrWhiteSpace(Title);
}
CanExecute
function of the SaveCommand
up to date, we will need to call the SaveCommand.ChangeCanExecute()
method in any property setters that impact the results of that CanExecute
function. In our case, this is only the Title
property:public string Title
{
get => _title;
set
{
_title = value;
OnPropertyChanged();
SaveCommand.ChangeCanExecute();
}
}
The CanExecute
function is not required, but by providing it, you can automatically manipulate the state of the control in the UI that is bound to the Command
so that it is disabled until all of the required criteria are met, at which point it becomes enabled.
NewEntryViewModel
as the BindingContext
for NewEntryPage
:using System;
using Xamarin.Forms;
using TripLog.ViewModels;
public NewEntryPage()
{
InitializeComponent();
BindingContext = new NewEntryViewModel();
// ...
}
EntryCell
elements in NewEntryPage.xaml
to bind to the NewEntryViewModel
properties:<EntryCell Label="Title" Text="{Binding Title}" />
<EntryCell Label="Latitude" Keyboard="Numeric"
Text="{Binding Latitude}" />
<EntryCell Label="Longitude" Keyboard="Numeric"
Text="{Binding Longitude}" />
<EntryCell Label="Date"
Text="{Binding Date, StringFormat='{0:d}'}" />
<EntryCell Label="Rating" Keyboard="Numeric"
Text="{Binding Rating}" />
<EntryCell Label="Notes" Text="{Binding Notes}" />
ToolbarItem
element in NewEntryPage.xaml
to bind to the NewEntryViewModel SaveCommand
property:<ToolbarItem Text="Save" Command="{Binding SaveCommand}" />
Now, when we run the app and navigate to the New Entry Page, we can see the data binding in action, as shown in the following screenshots. Notice how the Save button is disabled in the first set of screenshots until the title field contains a value, as shown in the second set of screenshots:
Figure 3: The TripLog new entry page with Save button disabled
Figure 4: The TripLog new entry page with Save button enabled
In software, data validation is a process that ensures the validity and integrity of user input and usually involves checking that data is in the correct format and contains an acceptable value. There are typically two types of validation when building apps: server-side and client-side. Both play an important role in the lifecycle of an app's data. Server-side validation is critical when it comes to security, making sure malicious data or code doesn't make its way into the server or backend infrastructure. Client-side validation is usually more about user experience than security. A mobile app should always validate its data before sending it to a backend (such as a web API) for a number of reasons, including the following:
Just as a backend server should never assume all incoming data has been validated by the client side before being received, a mobile app should also never assume the backend will do its own server-side validation, even though it's a good security practice. For this reason, mobile apps should perform as much client-side validation as possible.
When adding validation to a mobile app the actual validation logic can go in a few areas of the app architecture. It could go directly in the UI code (the View layer of an MVVM architecture), it could go in the business logic or controller code (the ViewModel layer of an MVVM architecture), or it could even go in the HTTP code. In most cases when implementing the MVVM pattern it will make the most sense to include validation in the ViewModels for the following reasons:
As discussed earlier in this section, validation makes the most sense in the ViewModel. To do this we will start by creating a new base ViewModel that will provide some base-level methods, properties, and events for subclassed ViewModels to leverage. This new base ViewModel will be called BaseValidationViewModel
and will subclass the BaseViewModel
we created earlier in the chapter. It will also implement an interface called INotifyDataErrorInfo
from the System.ComponentModel
namespace. INotifyDataErrorInfo
works a lot like INotifyPropertyChanged
– it specifies some properties about what errors have occurred and as well as an event for when the error state of particular property changes:
ViewModels
folder named BaseValidationViewModel
that subclasses BaseViewModel
:public class BaseValidationViewModel : BaseViewModel
{
public BaseValidationViewModel()
{
}
}
BaseValidationViewModel
to implement INotifyDataErrorInfo
as follows:public class BaseValidationViewModel : BaseViewModel,
INotifyDataErrorInfo
{
readonly IDictionary<string, List<string>> _errors =
new Dictionary<string, List<string>>();
public BaseValidationViewModel()
{
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public bool HasErrors =>
_errors?.Any(x => x.Value?.Any() == true) == true;
public IEnumerable GetErrors(string propertyName)
{
if (string.IsNullOrWhiteSpace(propertyName))
{
return _errors.SelectMany(x => x.Value);
}
if (_errors.ContainsKey(propertyName)
&& _errors[propertyName].Any())
{
return _errors[propertyName];
}
return new List<string>();
}
}
INotifyDataErrorInfo
– ErrorsChanged
, HasErrors
, and GetErrors()
– we also need to add a method that actually handles validating ViewModel properties. This method needs a validation rule parameter in the form of a Func<bool>
and an error message to be used if the validation rule fails. Add a protected method named Validate
to BaseValidationViewModel
as follows:
public class BaseValidationViewModel : BaseViewModel,
INotifyDataErrorInfo
{
// ...
protected void Validate(Func<bool> rule, string error,
[CallerMemberName] string propertyName = "")
{
if (string.IsNullOrWhiteSpace(propertyName)) return;
if (_errors.ContainsKey(propertyName))
{
_errors.Remove(propertyName);
}
if (rule() == false)
{
_errors.Add(propertyName, new List<string> { error });
}
OnPropertyChanged(nameof(HasErrors));
ErrorsChanged?.Invoke(this,
new DataErrorsChangedEventArgs(propertyName));
}
}
If the validation rule Func<bool>
returns false
, the error message that is provided is added to a private list of errors – used by HasErrors
and GetErrors()
– mapped to the specific property that called into this Validate()
method. Lastly, the Validate()
method invokes the ErrorsChanged
event with the caller property's name included in the event arguments.
Now any ViewModel that needs to perform validation can subclass BaseValidationViewModel
and call the Validate()
method to check if individual properties are valid.
In the next section we will use BaseValidationViewModel
to add validation to the New Entry Page and its supporting ViewModel.
In this section we will add some simple client-side validation to a couple of the entry fields on the New Entry Page.
NewEntryViewModel
to subclass BaseValidationViewModel
instead of BaseViewModel
:public class NewEntryViewModel : BaseValidationViewModel
{
// ...
}
Because BaseValidationViewModel
subclasses BaseViewModel
, NewEntryViewModel
is still able to leverage everything in BaseViewModel
as well.
Validate()
in the Title
property setter that includes a validation rule specifying that the field cannot be left blank:public string Title
{
get => _title;
set
{
_title = value;
Validate(() => !string.IsNullOrWhiteSpace(_title),
"Title must be provided.");
OnPropertyChanged();
SaveCommand.ChangeCanExecute();
}
}
Validate()
in the Rating
property setter that includes a validation rule specifying that the field's value must be between 1 and 5:public int Rating
{
get => _rating;
set
{
_rating = value;
Validate(() => _rating >= 1 && _rating <= 5,
"Rating must be between 1 and 5.");
OnPropertyChanged();
SaveCommand.ChangeCanExecute();
}
}
Notice we also added SaveCommand.ChangeCanExecute()
to the setter as well. This is because we want to update the SaveCommand
's canExecute
value when this value is changed since it will now impact the return value of CanSave()
, which we will update in the next step.
CanSave()
– the method used for the SaveCommand
's canExecute
function – to prevent saving if the ViewModel has any errors:bool CanSave() => !string.IsNullOrWhitespace(Title) && !HasErrors;
// NewEntryPage.xaml:
<EntryCell x:Name="title" Label="Title" Text="{Binding Title}" />
// ...
<EntryCell x:Name="rating" Label="Rating" Keyboard="Numeric"
Text="{Binding Rating}" />
// NewEntryPage.xaml.cs:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Xamarin.Forms;
using TripLog.ViewModels;
public partial class NewEntryPage : ContentPage
{
NewEntryViewModel ViewModel =>
BindingContext as NewEntryViewModel;
public NewEntryPage()
{
InitializeComponent();
BindingContextChanged += Page_BindingContextChanged;
BindingContext = new NewEntryViewModel();
}
void Page_BindingContextChanged(object sender, EventArgs e)
{
ViewModel.ErrorsChanged += ViewModel_ErrorsChanged;
}
void ViewModel_ErrorsChanged(object sender,
DataErrorsChangedEventArgs e)
{
var propHasErrors = (ViewModel.GetErrors(e.PropertyName)
as List<string>)?.Any() == true;
switch (e.PropertyName)
{
case nameof(ViewModel.Title):
title.LabelColor = propHasErrors
? Color.Red : Color.Black;
break;
case nameof(ViewModel.Rating):
rating.LabelColor = propHasErrors
? Color.Red : Color.Black;
break;
default:
break;
}
}
}
Now when we run the app and navigate to the New Entry Page and enter an invalid value in either the Title
or Rating
field, we will see the field label turn red and the Save button will be disabled, as shown in the following screenshots. Once the error has been corrected the field label color returns to black and the Save button is re-enabled.
Figure 5: The TripLog new entry page with client side validation
In this chapter, we updated the app that we started creating in Chapter 1, Getting Started, by removing data and data-related logic from the Pages, offloading it to a series of ViewModels, and then binding the Pages to those ViewModels. In the next chapter, we will expand on the Xamarin.Forms navigation service so that we can also move navigation code from the Pages to the ViewModels.
18.188.142.146