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—Models, ViewModels, and Views. Traditionally, the Models and ViewModels live in a core library (usually a portable class library), while the Views live in a platform-specific library. However, 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 user interface and the app logic isn't important.
In some mobile app architectures, the Models portion of the app might live all alone in its own library so that the models can be shared by other applications or also be used for other purposes.
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 something I find useful. By default, Xamarin Studio does not associate namespaces with directory structures; however, this can be changed in the solution options.
For the TripLog app, I've turned on the naming policy settings to associate namespaces with the directory structure. See Figure 1 for what these settings should look like in Xamarin Studio:
For the TripLog app, we are going to 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 already added a Models
folder in Chapter 1, Getting Started, so we just need to add a ViewModels
and Views
folder to the project to complete the MVVM structure. In order to set up the app structure, perform the following steps:
ViewModels
to the root of the TripLog project.Views
to the root of the TripLog project.MainPage.cs
, DetailPage.cs
and NewEntryPage.cs
) and paste them into the Views
folder that we just created.TripLog
to TripLog.Views
.using
statements on any classes that reference the Pages. Currently, this should only be in the App
class where MainPage
is instantiated.Once the MVVM structure has been added, the folder structure in the solution should look similar to Figure 2:
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 to mean 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 are simply going to 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 that contains basic functionality that each of our ViewModels will inherit. Initially, the base ViewModel abstract class will only contain a couple of members and implement INotifyPropertyChanged
, but we will add to this class as we continue to build upon the TripLog app throughout the rest of this book.
In order to create a base ViewModel, perform the following steps:
BaseViewModel
in the ViewModels
folder using the following code:public abstract class BaseViewModel { protected BaseViewModel () { } }
BaseViewModel
to implement INotifyPropertyChanged
:public abstract class BaseViewModel : INotifyPropertyChanged { protected BaseViewModel () { } public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged( [CallerMemberName] string propertyName = null) { var handler = PropertyChanged; if (handler != null) handler(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
is directly defining 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 MainPage's ViewModel, 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 class called LogEntries
. This property will be used to bind to the ItemsSource
property of the ListView
on MainPage
:ObservableCollection<TripLogEntry> _logEntries; public ObservableCollection<TripLogEntry> LogEntries { get { return _logEntries; } set { _logEntries = value; OnPropertyChanged (); } }
List<TripLogEntry>
that populates the ListView
in MainPage
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(2015, 2, 5), Latitude = 38.8895, Longitude = -77.0352 }); LogEntries.Add (new TripLogEntry { Title = "Statue of Liberty", Notes = "Inspiring!", Rating = 4, Date = new DateTime(2015, 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(2015, 4, 26), Latitude = 37.8268, Longitude = -122.4798 }); }
MainViewModel
as the BindingContext
for MainPage
so that it knows where to get the LogEntries
binding that we created for the ListView
. Do this by simply setting the MainPage
's BindingContext
property to a new instance of MainViewModel
. The BindingContext
property comes from the Xamarin.Forms.ContentPage
base class:public MainPage ()
{
BindingContext = new MainViewModel ();
// ...
}
ListView
on MainPage
gets its items. Currently, its ItemsSource
property is being set directly when the ListView
is created. Remove this setting from the ListView
object initializer and instead, bind the MainViewModel
LogEntries
property to the ListView
using the SetBinding
method:var entries = new ListView { // ItemsSource = items, <-- Remove this ItemTemplate = itemTemplate }; entries.SetBinding (ListView.ItemsSourceProperty, "LogEntries");
Next, we will add a ViewModel to serve as the data context for DetailPage
, as follows:
ViewModels
folder and name it DetailViewModel
.DetailViewModel
class to inherit from the BaseViewModel
abstract class:public class DetailViewModel : BaseViewModel
{
//...
}
TripLogEntry
property to the class and name it Entry
. This property will be used to bind to the various labels on DetailPage
:TripLogEntry _entry; public TripLogEntry Entry { get { return _entry; } set { _entry = value; OnPropertyChanged (); } }
DetailViewModel
constructor to take a TripLogEntry
parameter named entry
. Use this constructor property to populate the DetailViewModel
public Entry
property:public DetailViewModel (TripLogEntry entry) { Entry = entry; }
DetailViewModel
as the BindingContext
for DetailPage
. Pass in the TripLogEntry
property that is being passed to the DetailPage
. In Chapter 3, Navigation Services, we will refactor how we are passing the entry parameter to DetailPage
when we implement a custom navigation service:public DetailPage (TripLogEntry entry)
{
BindingContext = new DetailViewModel(entry);
// ...
}
DetailPage
to bind to the properties of the DetailViewModel
Entry
property. Do this by removing the code that directly sets the Text
property of the labels and by using the SetBinding
method on each label to bind to the appropriate property on the Entry
model:var title = new Label { HorizontalOptions = LayoutOptions.Center }; // title.Text = entry.Title; <-- Remove this title.SetBinding (Label.TextProperty, "Entry.Title"); var date = new Label { HorizontalOptions = LayoutOptions.Center }; // date.Text = entry.Date.ToString ("M"); <-- Remove this date.SetBinding (Label.TextProperty, "Entry.Date", stringFormat: "{0:M}"); var rating = new Label { HorizontalOptions = LayoutOptions.Center }; // rating.Text = $"{entry.Rating} star rating"; <-- Remove rating.SetBinding (Label.TextProperty, "Entry.Rating", stringFormat: "{0} star rating"); var notes = new Label { HorizontalOptions = LayoutOptions.Center }; // notes.Text = entry.Notes; <-- Remove this notes.SetBinding (Label.TextProperty, "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
:DetailViewModel _vm { get { return BindingContext as DetailViewModel; } } public DetailPage (TripLogEntry entry) { BindingContext = new DetailViewModel(entry); // ... map.MoveToRegion (MapSpan.FromCenterAndRadius ( new Position (_vm.Entry.Latitude, _vm.Entry.Longitude), Distance.FromMiles (.5))); map.Pins.Add (new Pin { Type = PinType.Place, Label = _vm.Entry.Title, Position = new Position (_vm.Entry.Latitude, _vm.Entry.Longitude) }); // ... }
Finally, we need to add the ViewModel for NewEntryPage
.
NewEntryViewModel
.NewEntryViewModel
class to inherit from BaseViewModel
:public class NewEntryViewModel : BaseViewModel
{
//...
}
EntryCell
's on NewEntryPage
: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 (); } }
NewEntryViewModel
constructor to initialize the Date
and Rating
properties:public NewEntryViewModel () { Date = DateTime.Today; Rating = 1; }
Command
property to the class and name it SaveCommand
. This property will be used to bind to the Save ToolbarItem
on NewEntryPage
. 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:Command _saveCommand; public Command SaveCommand { get { return _saveCommand ?? (_saveCommand = new Command (ExecuteSaveCommand, CanSave)); } } void ExecuteSaveCommand() { var newItem = new TripLogEntry { Title = this.Title, Latitude = this.Latitude, Longitude = this.Longitude, Date = this.Date, Rating = this.Rating, Notes = this.Notes }; // TODO: Implement logic to persist Entry in a later chapter. } bool CanSave () { return !string.IsNullOrWhiteSpace (Title); }
SaveCommand
's CanExecute
function up to date, we need to call the SaveCommand.ChangeCanExecute()
method in any property setters that impact the results of the 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. Figure 3 shows how the CanExecute
function is used to disable the Save ToolbarItem
until Title contains a value.
NewEntryViewModel
as the BindingContext
for NewEntryPage
.public NewEntryPage ()
{
BindingContext = new NewEntryViewModel ();
// ...
}
EntryCell
s on the NewEntryPage
to bind to the properties of the DetailViewModel
. Do this by using the SetBinding
method on each EntryCell
to bind to the appropriate ViewModel property:var title = new EntryCell { Label = "Title" }; title.SetBinding (EntryCell.TextProperty, "Title", BindingMode.TwoWay); var latitude = new EntryCell { Label = "Latitude", Keyboard = Keyboard.Numeric }; latitude.SetBinding (EntryCell.TextProperty, "Latitude", BindingMode.TwoWay); var longitude = new EntryCell { Label = "Longitude", Keyboard = Keyboard.Numeric }; longitude.SetBinding (EntryCell.TextProperty, "Longitude", BindingMode.TwoWay); var date = new EntryCell { Label = "Date" }; date.SetBinding (EntryCell.TextProperty, "Date", BindingMode.TwoWay, stringFormat: "{0:d}"); var rating = new EntryCell { Label = "Rating", Keyboard = Keyboard.Numeric }; rating.SetBinding (EntryCell.TextProperty, "Rating", BindingMode.TwoWay); var notes = new EntryCell { Label = "Notes" }; notes.SetBinding (EntryCell.TextProperty, "Notes", BindingMode.TwoWay);
ToolbarItem
on the NewEntryPage
to bind to the NewEntryViewModel
SaveCommand
property:var save = new ToolbarItem { Text = "Save" }; save.SetBinding (ToolbarItem.CommandProperty, "SaveCommand");
Now when we run the app and navigate to the new entry page, we can see the data-binding in action:
3.135.188.121