XAM320 Design an MVVM ViewModel in Xamarin.Forms

Exercise 2: Using commands to run behavior (XAM320)

You can control the behavior of an application in your ViewModels. For example, what happens when you tap a button or scroll a list? In this exercise, you'll examine commanding and use it to add behavior to your bindable ViewModel.

Screenshot of the completed exercise.
Screenshot of the completed exercise.
Screenshot of the completed exercise.
Screenshot of the completed exercise.
To complete the exercise, you will need Visual Studio for Windows or macOS with the Xamarin development tools installed. You will also need either an emulator/simulator or a device to run the exercise on. Please see the setup page if you need help installing the Xamarin development environment.

Explore the starter application

Let's start by exploring the new starter application to see the changes.

  1. Open the starter solution from the Exercise 2 > Start folder in your copy of the cloned or downloaded course materials in either Visual Studio on Windows or Visual Studio for Mac.
  2. There are several changes in the project. In particular, we've added a new NuGet package - XamarinUniversity.Infrastructure which contains a collection of classes to work with Xamarin.Forms and MVVM.

    You can get full details on the library (and source code) from the Github repo but here are the highlights:

    • DependencyServiceExtension This is a custom markup extension which allows XAML to lookup a dependency by type from the DependencyService class in Xamarin.Forms. You'll use this to lookup the MainViewModel for each of our pages. Use it like this:
      BindingContext="{infrastructure:DependencyService Type={x:Type viewModels:MainViewModel}}"
      
      Where infrastructure: and viewModels: are both namespaces defined in the XAML.
    • PickerBindBehavior This class allows a Picker.Item property to be data bound to a collection with a selection. By default, the picker does not support data binding (the property is not bindable). This behavior overcomes that restriction through a Items and SelectedItem bindable property. You can see it in use in the EditQuotePage XAML.
    • TaskExtensions This is a simple static class with a single static extension method for Task which will remove compiler warnings from un-awaited tasks.
    • IMessageVisualizerService This is an abstraction over the Page.DisplayAlert API which allows a View Model to show UI alerts without taking a dependency on a page or the Xamarin.Forms API directly. It is registered through the DependencyService in App.xaml.cs by a call to XamUInfrastructure.Init() to an implementation (FormsMessageVisualizerService) and you can find it being used to prompt the user when a quote is deleted in the MainViewModel code.
    • INavigationService This is an abstraction over the navigation services provided by Xamarin.Forms. It provides a set of methods to register screens, display a screen, and navigate backwards. The default implementation is contained in the FormsNavigationPageService class.
    • FormsNavigationPageService This is the Xamarin.Forms navigation implementation that allows a View Model to change the current page view without a dependency on Xamarin.Forms. It is specifically tailored to NavigationPage - it knows the pages and how to show them. It is registered through the DependencyService in App.xaml.cs by calling XamUInfrastructure.Init() and you can find it being used in the MainViewModel code. You need to access this type directly to register your pages.
    • SimpleViewModel An enhanced version of the SimpleViewModel you used in earlier versions of the lab is included in the library. It has the same methods, but the source file has been removed so you'll use the NuGet version.
    Navigation Details: The FormsNavigationPageService implementation provides a coupling between a "key" and a Page and has a NavigateAsync method which will locate the underlying NavigationPage in your application and direct it to a new page based on the key. In this application, the page keys are defined through an enumeration - AppPages and have two values: Edit and Detail. Normally, you would use the RegisterPage method on FormsNavigationPageService to associate each page key to a specific Page type. However if you examine the App.xaml.cs file, you will find that only the Edit page is registered. This is not a bug - it's intentional. Since your application is using a MasterDetailPage as the root "main" page, it has two pages: a "master" page and a "details" page. The FormsNavigationPageService implementation understands MasterDetailPage and expects that all navigation occurs on the "details" page (e.g. that the page is a NavigationPage. When you call the NavigateAsync method, it will automatically hide the "master" page when running on a phone device and perform the navigation on the "details" page. If there is no page to display (because the passed key does not exist), then it just displays the details page. So, when the code makes a call like navService.NavigateAsync(AppPages.Detail), even though there is no registered page for that key, it will still cause behavior when we are running on a phone device. This automatic treatment for MasterDetailsPage can be turned off through a property - see the documentation for more information.

    In addition, there is a new EditQuotePage which is used to edit a quote and the QuoteListPage and QuoteDetailPage have been updated to include toolbar buttons to add, delete and edit the quotes. These are currently wired up to event handlers in the code behind which then use some shared code in the view model. These pages have also been changed to set the BindingContext in XAML. Finally, the App.xaml.cs writes the changed quotes back to disk using the QuoteManager.Save method.

    Finally, we have MainViewModel.cs which exposes the primary view model used by the application. It is registered with the Xamarin.Forms DependencyService in App.xaml.cs. The QuoteListPage.xaml and QuoteDetailPage.xaml have both been updated to look up this view model and set it as their BindingContext. We have a custom markup extension which provides this functionality (DependencyServiceExtension.cs).


Run the application

  1. Run the application and go through the various operations - select a quote, then tap the "Quotes" button on the top left to get back to the list. You can also try running it in a tablet form factor - in this mode, when you are in landscape orientation, the master and detail views will be shown simultaneously.
  2. When the details screen is showing, you can use the Edit and Delete buttons in the toolbar. This same functionality is exposed through either a swipe to the left gesture (iOS) or a long-click (Windows and Android) when looking at the master page (list of quotes).
  3. The master page (list of quotes) has an "Add" button which lets you add a new quote.

Add in commanding support

The toolbar buttons and ListView actions are all wired up in the code behind. Our first goal is to execute these using commands instead of events.

  1. Open the MainViewModel and add a new public property named AddQuote which is a System.Windows.Input.ICommand.
  2. In the constructor, set the value of your new command to an instance of a Command. Recall that this is a built-in implementation of an ICommand that uses delegates to forward the command to our ViewModel logic. Pass in the method OnAddQuote which is already part of the view model as the action. There is no need for a CanExecute handler - this command should always be available.
  3. Since the OnAddQuote method is async and returns a Task, you won't be able to point the delegate directly at the method. Instead, use an async lambda to invoke it like this:

    new Command(async () => await OnAddQuote());
    
    public ICommand AddQuote { get; private set; }
    
    public MainViewModel()
    {
        Quotes = new ObservableCollection<QuoteViewModel>(
            QuoteManager.Load()
                        .Select(q => new QuoteViewModel(q)));
    
        SelectedQuote = Quotes.FirstOrDefault();
    
        AddQuote = new Command(async () => await OnAddQuote());
    }
    
  4. Open the QuoteListPage.xaml UI definition and locate the ToolbarItem for the Add operation. Remove the Click event handler and replace it with a Command property bound to your AddQuote command.
  5. Open the code behind file and remove the existing handler OnAddQuote which was calling the view model - this is no longer necessary.
  6. You can change the OnAddQuote method in your view model to be private since it will only be used by the command now.
  7. Run the application and test the add logic - it should still work. Try setting a breakpoint in the OnAddQuote logic and check the call stack if you don't completely understand how this is executing.

    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Add" Icon="ic_action_new.png" Command="{Binding AddQuote}" />
    </ContentPage.ToolbarItems>
    
  8. Next, let's implement the Delete logic as a command. Add a new command into the MainViewModel named DeleteQuote, this time, have it take a parameter of type QuoteViewModel - there's a Command<T> which allows you to cast the parameter to a type. Point the implementation at the OnDeleteQuote implementation that is already present. Again, there is no need for a CanExecute handler.
  9. You will need to again use an async lambda - this one taking the VM and calling the OnDeleteQuote async method. Check the code hint below if you need some guidance.

    public ICommand AddQuote { get; private set; }
    public ICommand DeleteQuote { get; private set; }
    
    public MainViewModel()
    {
        Quotes = new ObservableCollection<QuoteViewModel>(
            QuoteManager.Load()
                        .Select(q => new QuoteViewModel(q)));
    
        SelectedQuote = Quotes.FirstOrDefault();
    
        AddQuote = new Command(async () => OnAddQuote());
        DeleteQuote = new Command<QuoteViewModel>(async vm => OnDeleteQuote(vm));
    }
    
  10. Remove the Clicked handler from the menu item.
  11. In the QuoteListPage.xaml UI, locate the MenuItem associated with the Delete operation and let's bind the Command property to your new DeleteQuote command. However, you need to pause and think for a minute.

    • The binding context for the page is the MainViewModel. But the binding context for the MenuItem is the QuoteViewModel being displayed in the cell. That means that a simple statement like {Binding DeleteQuote} won't work here because the BindingContext is not correct. This is a common issue with commands - binding to the correct object.
    • One way you can address this is to identify the Binding's source by setting the Source property on the binding. This allows you to specify a specific object to use as the binding source, if you don't set it, then it uses BindingContext. That's the behavior you've been relying on up to now. In order to fix this problem, you'll need to set this Source property to the MainViewModel. Can you think of a way to do that?
    How do I find the proper binding source? The MainViewModel is assigned to the Page.BindingContext property. Is there any way to reference this? For example, if you gave the root tag an x:Name value? Think about the x:Reference directive in XAML. Remember that you want to bind to the DeleteQuote property of the page's binding context! It's a little tricky so think about it. Check the Command property set in the code below if you need further help.
  12. Since the command needs a parameter, bind the CommandParameter property to {Binding}. This will provide the current binding context as the parameter - this will be the row in the ListView - which happens to be a QuoteViewModel, exactly what you want.

    <ContentPage x:Class="GreatQuotes.QuoteListPage"
        x:Name="RootPage" ...>
        ...
        <MenuItem Command="{Binding BindingContext.DeleteQuote, Source={x:Reference RootPage}}"
            CommandParameter="{Binding}"
            IsDestructive="True"
            Text="Delete" />
        ...
    
  13. Switch to the code behind file and remove the OnDeleteQuote event handler which is no longer used.
  14. Do the same change to the QuoteDetailPage.xaml and QuoteDetailPage.xaml.cs to bind to the command.

    • Since the Page's BindingContext is already MainViewModel, you don't need to set a Source - just set the Path for the binding.
    • The Command parameter in this case should be data bound to the SelectedQuote property since the binding context is the MainViewModel. As you can see, it's very important to know what your binding context is at each level of the XAML when you are using commands!
    • Make sure to remove the event handlers in the code behind file!
    <ContentPage.ToolbarItems>
        <ToolbarItem Text="Delete" Icon="ic_delete.png" Command="{Binding DeleteQuote}" CommandParameter="{Binding SelectedQuote}" />
        ...
    </ContentPage.ToolbarItems>
    ...
    
  15. Run the application and test the delete logic. Set breakpoints to see how you get to the OnDeleteQuote method in the view model.
  16. Do the same steps for the Edit command - the code is almost identical, except you want to use the OnEditQuote method in the MainViewModel. Make sure to make the changes for both the QuoteListPage and the QuoteDetailPage.

Support navigation

There is a simple INavigationService abstraction included in the XamarinUniversity.Infrastructure library. It is already used in most places in the starter project, but there is still a spot in the QuoteListPage which works directly with the navigation system: the ItemTapped handler. Our next goal is to remove this code and push it totally into the view model.

The default implementation code in StackNavigationService is fairly simple. It has a dictionary of string to Page types and you add known pages to the navigation system using the RegisterPage method. You can then navigate to a page by name using the NavigateAsync method and you can go backwards in the stack with the GoBackAsync method. This is one way to manage navigation, and ultimately, the goal is to create some sort of dynamic page registry which the navigation service uses to look up pages based on the view model or some sort of key passed in. Never use pages directly in your ViewModels!

There are a few ways you could handle the ItemTapped event in the View Model. Since this is a ListView, you could use the SelectedItem property change notification and perform your navigation there if you select a new quote, this is simple and intuitive. The only problem is that navigation is inherently an asynchronous operation which cannot be awaited in a property setter and will therefore produce a warning if you don't block/await it. There's an extension method named IgnoreResult in the XamarinUniversity.Infrastructure library which lets you ignore the warnings produced by Visual Studio.

Another option is to turn the ItemTapped event into a Command which you can then bind to in our View Model. This requires a bit of code to perform, but luckily, the infrastructure library already has this code in place in the form of a reusable class: EventToCommandBehavior.

  1. Let's start by adding a new ICommand into our MainViewModel.

    • Name it "ShowQuoteDetail".
    • Create a Command object and assign it to the property; have it take a QuoteViewModel as a parameter.
    • Create a method "OnShowQuoteDetail" which returns a Task and have the command object call the method.
    • Have the OnShowQuoteDetail method set the SelectedQuote, lookup the INavigationService using DependencyService.Get and use the NavigateAsync method to go to the AppPages.Detail page. Look at the implementation of the OnEditQuote method if you'd like a little help. It's essentially the same code, just a different page. Since the QuoteDetailPage sets the BindingContext itself, you can omit the second parameter to the NavigateAsync method. See the note above on Navigation Details to get a full explanation of navigating to pages in the app.
    private async Task OnShowQuoteDetails(QuoteViewModel qvm)
    {
        SelectedQuote = qvm;
        await DependencyService.Get<INavigationService>()
            .NavigateAsync(AppPages.Detail);
    }
    
  2. Open the QuoteListPage.xaml.cs and locate the ItemTapped event handler and remove it. This was originally using the page navigation API directly. We've moved this logic into our MainViewModel.
  3. Next, open the QuoteListPage.xaml UI and remove the ItemTapped event on the ListView.

    The EventToCommandBehavior converts an event into a command, and it can optionally pass information to the command in the form of the CommandParameter property, just like the other places where you used the command. The problem is that the information you want is in the ItemTappedEventArgs passed with the event. You don't want to rely directly on this data structure since it's UI and Forms specific. That's why the EventArgsConverter property exists. It lets you assign a IValueConverter to the behavior which will take the sender/EventArgs and turn it into some object passed as the command parameter. In this case, you just want the ItemTappedEventArgs.Item property. EventToCommandBehavior will pass the sender as the value parameter and the EventArgs as the parameter parameter to the IValueConverter. See the documentation for more detail if you are interested.
  4. Add a new source file to your project in the Converters folder. Name it "ItemTappedValueConverter".
  5. The Convert method should take the parameter and cast it to a ItemTappedEventArgs and then return the Item property.
  6. Leave the default throw in place for the ConvertBack implementation. It won't be used.

    using System;
    using System.Globalization;
    using Xamarin.Forms;
    
    namespace GreatQuotes.Converters
    {
        public class ItemTappedValueConverter : IValueConverter
        {
            public object Convert(object value, Type targetType,
                                  object parameter, CultureInfo culture)
            {
                ItemTappedEventArgs e = (ItemTappedEventArgs)parameter;
                return e.Item;
            }
    
            public object ConvertBack(object value, Type targetType,
                                      object parameter, CultureInfo culture)
            {
                throw new NotImplementedException();
            }
        }
    }
    
  7. Now, create a new ResourceDictionary and apply it to the QuoteListPage.xaml page. Create an instance of your ItemTappedValueConverter and add it to the resource dictionary and name it "itemTappedConverter". Remember that you'll need to define a XAML namespace to help XAML find the class. You can do this globally, on the ContentPage tag, or directly on your resource. Check the code below if you need a hint.

    <ContentPage.Resources>
        <ResourceDictionary>
            <ItemTappedValueConverter xmlns="clr-namespace:GreatQuotes.Converters;assembly=GreatQuotes" x:Key="itemTappedConverter" />
        </ResourceDictionary>
    </ContentPage.Resources>
    
  8. Finally, let's use the EventToCommandBehavior. This ties an event on a UI element to a command on the BindingContext. Here's the code you need to add:

    <ListView ItemsSource="{Binding Quotes}"
        SeparatorColor="#c0c0c0"
        SelectedItem="{Binding SelectedQuote, Mode=TwoWay}">
        <ListView.Behaviors>
            <infrastructure:EventToCommandBehavior EventName="ItemTapped"
                Command="{Binding ShowQuoteDetail}"
                EventArgsConverter="{StaticResource itemTappedConverter}" />
        </ListView.Behaviors>
        ...
    
  9. Run the code and tap a quote. Make sure the details change, and if in the phone form factor, you navigate to the details screen. Just as you did before.

Exercise summary

In this exercise, you've created commands to model the Add and Delete logic. You also utilized those commands in XAML so that you have most of the business style logic all present in the view models.

You can view the completed solution in the Exercise 2 > Completed folder of your copy of the cloned or downloaded course materials.

Go Back