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.
![]() |
![]() |
![]() |
![]() |
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.
- 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.
-
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 theMainViewModel
for each of our pages. Use it like this:
WhereBindingContext="{infrastructure:DependencyService Type={x:Type viewModels:MainViewModel}}"
infrastructure:
andviewModels:
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 theEditQuotePage
XAML. -
TaskExtensions
This is a simple static class with a single static extension method for
Task
which will remove compiler warnings from un-await
ed 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 theDependencyService
in App.xaml.cs by a call toXamUInfrastructure.Init()
to an implementation (FormsMessageVisualizerService) and you can find it being used to prompt the user when a quote is deleted in theMainViewModel
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 theDependencyService
in App.xaml.cs by callingXamUInfrastructure.Init()
and you can find it being used in theMainViewModel
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 aPage
and has aNavigateAsync
method which will locate the underlyingNavigationPage
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 theRegisterPage
method onFormsNavigationPageService
to associate each page key to a specificPage
type. However if you examine theApp.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 aMasterDetailPage
as the root "main" page, it has two pages: a "master" page and a "details" page. TheFormsNavigationPageService
implementation understandsMasterDetailPage
and expects that all navigation occurs on the "details" page (e.g. that the page is aNavigationPage
. When you call theNavigateAsync
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 likenavService.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 forMasterDetailsPage
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 theQuoteListPage
andQuoteDetailPage
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 theBindingContext
in XAML. Finally, the App.xaml.cs writes the changed quotes back to disk using theQuoteManager.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 theirBindingContext
. We have a custom markup extension which provides this functionality (DependencyServiceExtension.cs). -
DependencyServiceExtension
This is a custom markup extension which allows XAML to lookup a dependency by type from the
Run the application
- 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.
- 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).
- 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.
- Open the
MainViewModel
and add a new public property named AddQuote which is aSystem.Windows.Input.ICommand
. - 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 anICommand
that uses delegates to forward the command to our ViewModel logic. Pass in the methodOnAddQuote
which is already part of the view model as the action. There is no need for aCanExecute
handler - this command should always be available. -
Since the
OnAddQuote
method is async and returns aTask
, 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()); }
-
Open the QuoteListPage.xaml UI definition and locate the
ToolbarItem
for the Add operation. Remove theClick
event handler and replace it with aCommand
property bound to yourAddQuote
command. - Open the code behind file and remove the existing handler
OnAddQuote
which was calling the view model - this is no longer necessary. - You can change the
OnAddQuote
method in your view model to be private since it will only be used by the command now. -
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>
-
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 typeQuoteViewModel
- there's aCommand<T>
which allows you to cast the parameter to a type. Point the implementation at theOnDeleteQuote
implementation that is already present. Again, there is no need for aCanExecute
handler. -
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)); }
-
Remove the
Clicked
handler from the menu item. -
In the QuoteListPage.xaml UI, locate the
MenuItem
associated with the Delete operation and let's bind theCommand
property to your newDeleteQuote
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 theMenuItem
is theQuoteViewModel
being displayed in the cell. That means that a simple statement like{Binding DeleteQuote}
won't work here because theBindingContext
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 theSource
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 usesBindingContext
. That's the behavior you've been relying on up to now. In order to fix this problem, you'll need to set thisSource
property to theMainViewModel
. Can you think of a way to do that?
How do I find the proper binding source? The
MainViewModel
is assigned to thePage.BindingContext
property. Is there any way to reference this? For example, if you gave the root tag anx:Name
value? Think about the x:Reference directive in XAML. Remember that you want to bind to theDeleteQuote
property of the page's binding context! It's a little tricky so think about it. Check theCommand
property set in the code below if you need further help. - The binding context for the page is the
-
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 theListView
- which happens to be aQuoteViewModel
, 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" /> ...
-
Switch to the code behind file and remove the
OnDeleteQuote
event handler which is no longer used. -
Do the same change to the QuoteDetailPage.xaml and QuoteDetailPage.xaml.cs to bind to the command.
- Since the Page's
BindingContext
is alreadyMainViewModel
, you don't need to set aSource
- just set thePath
for the binding. - The
Command
parameter in this case should be data bound to theSelectedQuote
property since the binding context is theMainViewModel
. 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> ...
- Since the Page's
-
Run the application and test the delete logic. Set breakpoints to see how you get to the
OnDeleteQuote
method in the view model. - Do the same steps for the Edit command - the code is almost identical, except you want to use the
OnEditQuote
method in theMainViewModel
. 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 ofstring
toPage
types and you add known pages to the navigation system using theRegisterPage
method. You can then navigate to a page by name using theNavigateAsync
method and you can go backwards in the stack with theGoBackAsync
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.
-
Let's start by adding a new
ICommand
into ourMainViewModel
.- Name it "ShowQuoteDetail".
- Create a
Command
object and assign it to the property; have it take aQuoteViewModel
as a parameter. - Create a method "OnShowQuoteDetail" which returns a
Task
and have the command object call the method. - Have the
OnShowQuoteDetail
method set theSelectedQuote
, lookup theINavigationService
usingDependencyService.Get
and use theNavigateAsync
method to go to the AppPages.Detail page. Look at the implementation of theOnEditQuote
method if you'd like a little help. It's essentially the same code, just a different page. Since the QuoteDetailPage sets theBindingContext
itself, you can omit the second parameter to theNavigateAsync
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); }
-
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 ourMainViewModel
. -
Next, open the QuoteListPage.xaml UI and remove the
ItemTapped
event on theListView
.The
EventToCommandBehavior
converts an event into a command, and it can optionally pass information to the command in the form of theCommandParameter
property, just like the other places where you used the command. The problem is that the information you want is in theItemTappedEventArgs
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 theEventArgsConverter
property exists. It lets you assign aIValueConverter
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 theItemTappedEventArgs.Item
property.EventToCommandBehavior
will pass the sender as the value parameter and the EventArgs as the parameter parameter to theIValueConverter
. See the documentation for more detail if you are interested. - Add a new source file to your project in the Converters folder. Name it "ItemTappedValueConverter".
- The
Convert
method should take the parameter and cast it to aItemTappedEventArgs
and then return theItem
property. -
Leave the default
throw
in place for theConvertBack
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(); } } }
-
Now, create a new
ResourceDictionary
and apply it to the QuoteListPage.xaml page. Create an instance of yourItemTappedValueConverter
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 theContentPage
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>
-
Finally, let's use the
EventToCommandBehavior
. This ties an event on a UI element to a command on theBindingContext
. 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> ...
- 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.