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—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.

Tip

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:

Adding MVVM to the app

Figure 1 .NET naming policy settings to associate namespaces with the directory structure.

Setting up the app structure

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:

  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. Copy the Pages files (MainPage.cs, DetailPage.cs and NewEntryPage.cs) and paste them into the Views folder that we just created.
  4. Update the namespace of each Page from TripLog to TripLog.Views.
  5. Update the 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:

Setting up the app structure

Figure 2 TripLog app structure with MVVM

Note

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.

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 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:

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

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 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:

  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
    {
        //...
    }
  3. Add an 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 ();
        }
    }
  4. Next, remove the 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
        });
    }
  5. Next, set 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 ();
        // ...
    }
  6. Finally, update how the 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");
    

Adding DetailViewModel

Next, we will add a 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
    {
        //...
    }
  3. Add a 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 ();
        }
    }
  4. Update the 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;
    }
  5. Next, set 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);
        // ...
    }
  6. Next, update the labels on the 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");
    
  7. Finally, update the map to get the values it is plotting from the ViewModel. Because 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:
    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)
        });
    
        // ...
    }

Adding NewEntryViewModel

Finally, we need to add the ViewModel for NewEntryPage.

  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
    {
        //...
    }
  3. Add properties to the class that will be used to bind to the values entered into 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 ();
        }
    }
  4. Update the NewEntryViewModel constructor to initialize the Date and Rating properties:
    public NewEntryViewModel ()
    {
        Date = DateTime.Today;
        Rating = 1;
    }
  5. Add a 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);
    }
  6. In order to keep the 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.

  7. Next, set NewEntryViewModel as the BindingContext for NewEntryPage.
    public NewEntryPage ()
    {
        BindingContext = new NewEntryViewModel ();
    
        // ...
}
  8. Next, update the EntryCells 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);
    
  9. Finally, we need to update the Save 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:

Adding NewEntryViewModel

Figure 3 CanExecute in action: The Save button is enabled automatically when Title has a value.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.135.188.121