In our previous tutorial, we created a basic travel app using Xamarin.Forms. In this post, we will look at adding the Model-View-View-Model (MVVM) pattern to our travel app. The MVVM elements are offered with the Xamarin.Forms toolkit and we can expand on them to truly take advantage of the power of the pattern. As we dig into MVVM, we will apply what we have learned to the TripLog app that we started building in our previous tutorial.
This article is an excerpt from the book Mastering Xamaring.Forms by Ed Snider.
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:
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.
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:
For the TripLog app, we will let the Views, ViewModels, and Models all live in the same core portable class library. In our solution, this is the project called TripLog. We have already added a Models folder in our previous tutorial, so we just need to add a ViewModels folder and a Views folder to the project to complete the MVVM structure. In order to set up the app structure, perform the following steps:
Once the MVVM structure has been added, the folder structure in the solution should look similar to the following screenshot:
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 be an abstract class containing the basic functionality that each of our ViewModels will inherit.
Initially, the base ViewModel abstract 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:
public abstract class BaseViewModel { protected BaseViewModel() { } }
public abstract class BaseViewModel : INotifyPropertyChanged { protected BaseViewModel() { }
public event PropertyChangedEventHandler PropertyChanged;
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 user interface 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 user interface.
In order to create the ViewModel for MainPage, perform the following steps:
public class MainViewModel : BaseViewModel { // ... }
public class MainViewModel : BaseViewModel
{
ObservableCollection<TripLogEntry> _logEntries;
public ObservableCollection<TripLogEntry> LogEntries
{
get { return _logEntries; }
set
{
_logEntries = value;
OnPropertyChanged ();
}
}
// ...
}
public MainViewModel() { LogEntries = new ObservableCollection<TripLogEntry>();
LogEntries.Add(new TripLogEntry
{
Title = "Washington Monument",
Notes = "Amazing!",
Rating = 3,
Date = new DateTime(2017, 2, 5),
Latitude = 38.8895,
Longitude = -77.0352
});
LogEntries.Add(new TripLogEntry
{
Title = "Statue of Liberty",
Notes = "Inspiring!",
Rating = 4,
Date = new DateTime(2017, 4, 13),
Latitude = 40.6892,
Longitude = -74.0444
});
LogEntries.Add(new TripLogEntry
{
Title = "Golden Gate Bridge",
Notes = "Foggy, but beautiful.",
Rating = 5,
Date = new DateTime(2017, 4, 26),
Latitude = 37.8268,
Longitude = -122.4798
});
}
public MainPage()
{
InitializeComponent();
BindingContext = new MainViewModel();
}
<ListView ... ItemsSource="{Binding LogEntries}">
Next, we will add another ViewModel to serve as the data context for DetailPage, as follows:
public class DetailViewModel : BaseViewModel { // ... }
public class DetailViewModel : BaseViewModel
{
TripLogEntry _entry;
public TripLogEntry Entry
{
get { return _entry; }
set
{
_entry = value;
OnPropertyChanged ();
}
}
// ...
}
public class DetailViewModel : BaseViewModel { // ...
public DetailViewModel(TripLogEntry entry)
{
Entry = entry;
}
}
public DetailPage (TripLogEntry entry) { InitializeComponent();
BindingContext = new DetailViewModel(entry);
// ...
}
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 ... 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}" />
public partial class DetailPage : ContentPage { DetailViewModel _vm { get { return BindingContext as DetailViewModel; } }
public DetailPage(TripLogEntry entry)
{
InitializeComponent();
BindingContext = new DetailViewModel(entry);
TripMap.MoveToRegion(MapSpan.FromCenterAndRadius(
new Position(_vm.Entry.Latitude, _vm.Entry.Longitude),
Distance.FromMiles(.5)));
TripMap.Pins.Add(new Pin
{
Type = PinType.Place,
Label = _vm.Entry.Title,
Position =
new Position(_vm.Entry.Latitude, _vm.Entry.Longitude)
});
}
}
Finally, we will need to add a ViewModel for NewEntryPage, as follows:
public class NewEntryViewModel : BaseViewModel { // ... }
public class NewEntryViewModel : BaseViewModel { string _title; public string Title { get { return _title; } set { _title = value; OnPropertyChanged(); } } double _latitude; public double Latitude { get { return _latitude; } set { _latitude = value; OnPropertyChanged(); } }
double _longitude;
public double Longitude
{
get { return _longitude; }
set
{
_longitude = value;
OnPropertyChanged();
}
}
DateTime _date;
public DateTime Date
{
get { return _date; }
set
{
_date = value;
OnPropertyChanged();
}
}
int _rating;
public int Rating
{
get { return _rating; }
set
{
_rating = value;
OnPropertyChanged();
}
}
string _notes;
public string Notes
{
get { return _notes; }
set
{
_notes = value;
OnPropertyChanged();
}
}
// ...
}
public NewEntryViewModel() { Date = DateTime.Today; Rating = 1; }
public class NewEntryViewModel : BaseViewModel { // ...
Command _saveCommand;
public Command SaveCommand
{
get
{
return _saveCommand ?? (_saveCommand =
new Command(ExecuteSaveCommand, CanSave));
}
}
void ExecuteSaveCommand()
{
var newItem = new TripLogEntry
{
Title = Title,
Latitude = Latitude,
Longitude = Longitude,
Date = Date,
Rating = Rating,
Notes = Notes
};
}
bool CanSave ()
{
return !string.IsNullOrWhiteSpace (Title);
}
}
public string Title { get { return _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.
public NewEntryPage()
{
InitializeComponent();
BindingContext = new NewEntryViewModel();
// ...
}
Next, update the EntryCell elements in NewEntryPage.xaml to bind to the NewEntryViewModel properties:
<EntryCell Label="Title" Text="{Binding Title}" /> <EntryCell Label="Latitude" Text="{Binding Latitude}" ... /> <EntryCell Label="Longitude" Text="{Binding Longitude}" ... /> <EntryCell Label="Date" Text="{Binding Date, StringFormat='{0:d}'}" />
<EntryCell Label="Rating" Text="{Binding Rating}" ... /> <EntryCell Label="Notes" Text="{Binding Notes}" />
<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 until the title field contains a value:
To summarize, we updated the app that we had created in this article; Create a basic travel app using Xamarin.Forms. We removed data and data-related logic from the Pages, offloading it to a series of ViewModels and then binding the Pages to those ViewModels.
If you liked this tutorial, read our book, Mastering Xamaring.Forms , to create an architecture rich mobile application with good design patterns and best practices using Xamarin.Forms.
Xamarin Forms 3, the popular cross-platform UI Toolkit, is here!
Five reasons why Xamarin will change mobile development
Creating Hello World in Xamarin.Forms_sample