12 min read

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.

Understanding the MVVM pattern

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:

  • Models: Models represent the business entities of an application. When responses come back from an API, they are typically deserialized to models.
  • Views: Views represent the actual pages or screens of an application, along with all of the elements that make them up, including custom controls. Views are very platform-specific and depend heavily on platform APIs to render the application’s user interface (UI).
  • ViewModels: ViewModels control and manipulate the Views by serving as their data context. ViewModels are made up of a series of properties represented by Models. These properties are part of what is bound to the Views to provide the data that is displayed to users, or to collect the data that is entered or selected by users. In addition to model-backed properties, ViewModels can also contain commands, which are action-backed properties that bind the actual functionality and execution to events that occur in the Views, such as button taps or list item selections.
  • Data binding: Data binding is the concept of connecting data properties and actions in a ViewModel with the user interface elements in a View. The actual implementation of how data binding happens can vary and, in most cases is provided by a framework, toolkit, or library. In Windows app development, data binding is provided declaratively in XAML. In traditional (non-Xamarin.Forms) Xamarin app development, data binding is either a manual process or dependent on a framework such as MvvmCross (https://github.com/MvvmCross/MvvmCross), a popular framework in the .NET mobile development community. Data binding in Xamarin.Forms follows a very similar approach to Windows app development.

Adding MVVM to the app

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, this doesn’t mean that separation between the user interface and the app logic isn’t important.

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:

Setting up the app structure

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:

  1. Add a new folder named ViewModels to the root of the TripLog project.
  2. Add a new folder named Views to the root of the TripLog project.
  3. Move the existing XAML pages files (MainPage.xaml, DetailPage.xaml, and NewEntryPage.xaml and their .cs code-behind files) into the Views folder that we have just created.
  4. Update the namespace of each Page from TripLog to TripLog.Views.
  5. Update the x:Class attribute of each Page’s root ContentPage from TripLog.MainPage, TripLog.DetailPage, and TripLog.NewEntryPage to TripLog.Views.MainPage, TripLog.Views.DetailPage, and TripLog.Views.NewEntryPage, respectively.
  6. Update the using statements on any class that references the Pages. Currently, this should only be in the App class in App.xaml.cs, where MainPage is instantiated.

Once the MVVM structure has been added, the folder structure in the solution should look similar to the following screenshot:

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.

Adding ViewModels

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:

  1. Create a new abstract class named BaseViewModel in the ViewModels folder using the following code:
      public abstract class BaseViewModel
      {
          protected BaseViewModel()
          { 
          } 
      }
  1. Update BaseViewModel to implement INotifyPropertyChanged:
      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.

Adding MainViewModel

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:

  1. Create a new class file in the ViewModels folder and name it MainViewModel.
  2. Update the MainViewModel class to inherit from BaseViewModel:
      public class MainViewModel : BaseViewModel
      {
          // ...
      }
  1. Add an ObservableCollection<T> property to the MainViewModel class and name it LogEntries. This property will be used to bind to the ItemsSource property of the ListView element on MainPage.xaml:
public class MainViewModel : BaseViewModel
      {
          ObservableCollection<TripLogEntry> _logEntries;
          public ObservableCollection<TripLogEntry> LogEntries
          {
              get { return _logEntries; }
              set 
              { 
                  _logEntries = value;
                  OnPropertyChanged ();
              }
          }
// ...
}
  1. Next, remove the List<TripLogEntry> that populates the ListView 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>();
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
});
}
  1. Set MainViewModel as the BindingContext property 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:
public MainPage()
      {
          InitializeComponent();
BindingContext = new MainViewModel();
}
  1. Finally, update how the ListView element on MainPage.xaml gets its items. Currently, its ItemsSource property is being set directly in the Page’s code behind. Remove this and instead update the ListView element’s tag in MainPage.xaml to bind to the MainViewModel LogEntries property:
      <ListView ... ItemsSource="{Binding LogEntries}">

Adding DetailViewModel

Next, we will add another ViewModel to serve as the data context for DetailPage, as follows:

  1. Create a new class file in the ViewModels folder and name it DetailViewModel.
  2. Update the DetailViewModel class to inherit from the BaseViewModel abstract class:
      public class DetailViewModel : BaseViewModel 
      {
          // ...
      }
  1. Add a 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:
public class DetailViewModel : BaseViewModel 
      {
          TripLogEntry _entry;
          public TripLogEntry Entry
          {
              get { return _entry; }
              set 
              {
                  _entry = value;
                  OnPropertyChanged ();
              }
          }
// ...

      }
  1. Update the 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;
}
      }
  1. Set DetailViewModel as the BindingContext for DetailPage and pass in the TripLogEntry property that is being passed to DetailPage:
      public DetailPage (TripLogEntry entry)
      {
          InitializeComponent();
BindingContext = new DetailViewModel(entry);

// ... 
}
  1. Next, remove the code at the end of the 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;
}
  1. Next, update the 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}" />
  1. Finally, update the map to get the values it is plotting from the ViewModel. Since the Xamarin.Forms 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 _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)
});
}
}

Adding NewEntryViewModel

Finally, we will need to add a ViewModel for NewEntryPage, as follows:

  1. Create a new class file in the ViewModels folder and name it NewEntryViewModel.
  2. Update the NewEntryViewModel class to inherit from BaseViewModel:
      public class NewEntryViewModel : BaseViewModel 
      {
          // ... 
      }
  1. Add public properties to the 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 { 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();
}
}

// ...
}
  1. Update the NewEntryViewModel constructor to initialize the Date and Rating properties:
      public NewEntryViewModel()
      {
          Date = DateTime.Today;
          Rating = 1;
      }
  1. Add a public 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
{
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);
}
}
  1. In order to keep the 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 { 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.

  1. Next, set NewEntryViewModel as the BindingContext for NewEntryPage:
  1. 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}" />
  1. Finally, we will need to update the Save 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 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.

Read Next:

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

Content Marketing Editor at Packt Hub. I blog about new and upcoming tech trends ranging from Data science, Web development, Programming, Cloud & Networking, IoT, Security and Game development.

LEAVE A REPLY

Please enter your comment!
Please enter your name here