Using the Model-View-ViewModel Pattern
You Will Learn
- How to use the MVVM and dependency injection patterns to increase the testability of an application.
- How to connect view models to views by using a view model locator.
- How to display data by binding elements of the view to properties in the view model.
- How to invoke commands and navigation from a view.
- How to use a custom toast notification to provide a non-disruptive user interface notification.
Now that you've seen how Tailspin designed the UI of the application, including the choice of controls, and the navigation through the application, it's time to look behind the scenes to discover how Tailspin structured the mobile client for the Windows Phone platform.
The developers at Tailspin decided to adopt a Model-View-ViewModel (MVVM) approach to structuring the Windows Phone application. This section provides an overview of MVVM, explains why it is appropriate for Windows Phone applications, and highlights some of the decisions made by the developers at Tailspin about the implementation.
The application is designed and built using the MVVM pattern.
The Premise
The MVVM approach naturally lends itself to XAML application platforms such as Silverlight. This is because the MVVM pattern leverages some of the specific capabilities of Silverlight, such as data binding, commands, and behaviors. MVVM is similar to many other patterns that separate the responsibility for the appearance and layout of the UI from the responsibility for the presentation logic; for example, if you're familiar with the Model-View-Controller (MVC) pattern, you'll find that MVVM has many similar concepts.
Markus Says: | |
---|---|
|
Overview of MVVM
There are three core components in the MVVM pattern: the model, the view, and the view model. Figure 4 illustrates the relationships between these three components.
Figure 4
The Model-View-ViewModel pattern
The view is responsible for defining the structure, layout, and appearance of what the user sees on the screen. Ideally, the view is defined purely with XAML, with a limited code-behind that does not contain business logic.
The model in MVVM is an implementation of the application's domain model that can include a data model along with business and validation logic. Often, the model will include a data access layer. In the case of a Windows Phone application, the data access layer could support retrieving and updating data by using a web service or local storage.
Jana Says: | |
---|---|
|
The view model acts as an intermediary between the view and the model, and is responsible for handling the view logic. The view model provides data in a form that the view can easily use. The view model retrieves data from the model and then makes that data available to the view, and may reformat the data in some way that makes it simpler for the view to handle. The view model also provides implementations of commands that a user of the application initiates in the view. For example, when a user clicks a button in the UI, that action can trigger a command in the view model. The view model may also be responsible for defining logical state changes that affect some aspect of the display in the view, such as an indication that some operation is pending.
In addition to understanding the responsibilities of the three components, it's also important to understand how the components interact with each other. At the highest level, the view "knows about" the view model, and the view model "knows about" the model, but the model is unaware of the view model, and the view model is unaware of the view.
Jana Says: | |
---|---|
|
MVVM leverages the data-binding capabilities in Silverlight to manage the link between the view and view model, along with behaviors and event triggers. These capabilities limit the need to place business logic in the view's code-behind.
Typically, the view model interacts with the model by invoking methods in the model classes. The model may also need to be able to report errors back to the view model by using a standard set of events that the view model subscribes to (remember that the model is unaware of the view model). In some scenarios, the model may need to be able to report changes in the underlying data back to the view model; again, the model should do this using events.
This chapter focuses on the view and view model components of the Tailspin mobile client application. Chapter 3, "Using Services on the Phone," includes a description of the model components in the application.
Benefits of MVVM
MVVM enables a great developer-designer workflow that promotes these benefits:
- During the development process, developers and designers can work more independently and concurrently on their components. The designers can concentrate on the view, and if they are using Expression Blend, they can easily generate sample data to work with, while the developers can work on the view model and model components.
- The developers can create unit tests for the view model and the model without using the view. The unit tests for the view model can exercise exactly the same functionality as used by the view.
- It is easy to redesign the UI of the application without touching the code because the view is implemented entirely in XAML. A new version of the view should work with the existing view model.
- If there is an existing implementation of the model that encapsulates existing business logic, it may be difficult or risky to change. In this scenario, the view model acts as an adapter for the model classes and enables you to avoid making any major changes to the model code.
Although the benefits of MVVM are clear for a complex application with a relatively long shelf life, the additional work needed to implement the MVVM pattern may not be worthwhile for simple applications or applications with shorter expected lifetimes.
Jana Says: | |
---|---|
|
You've seen a high-level description of the MVVM pattern, and the reasons that Tailspin decided to adopt it for their Windows Phone client. The next sections describe the following implementation choices made by the developers at Tailspin when they implemented MVVM for the Surveys application:
- How Tailspin connects the view and the view model components.
- Examples of how Tailspin tests components of the application
- How Tailspin implements commands, asynchronous operations, and notifications to the user.
- Details of data binding and navigation.
For more information about MVVM, see Chapter 5, "Implementing the MVVM Pattern," and Chapter 6, "Advanced MVVM Scenarios," of the Prism documentation on MSDN.
Connecting the View and the View Model
Now is a good time to walk through the code that implements the MVVM pattern in the Windows Phone application in more detail. As you read through this section, you may want to download the Windows Phone Tailspin Surveys application.
There are several ways to connect the view model to the view, including direct relations and data template relations. The developers at Tailspin chose to use a view model locator because this approach means that the application has a single class that is responsible for connecting views to view models. This still leaves developers free to choose to manually perform the connection within the view model locator, or by using a dependency injection container. Figure 5 illustrates the relationships between the view, view model locator, and container locator.
Markus Says: | |
---|---|
|
Figure 5
Connecting the view to the view model
Inside the Implementation
The mobile client application uses a view model locator to connect view models to views. The following code example from the App.xaml file shows how the view model locator class is identified and made available to the application. The application declares the TailSpin.PhoneClient.ViewModels ViewModelLocator class in the <Application.Resources> section of the App.xaml file.
<Application
x:Class="TailSpin.PhoneClient.App"
...
xmlns:viewmodels=
"clr-namespace:TailSpin.PhoneClient.ViewModels"
... >
<!--Application Resources-->
<Application.Resources>
...
<viewmodels:ViewModelLocator x:Key="ViewModelLocator"/>
...
</Application.Resources>
...
</Application>
The following code example from the SurveyListView.xaml file shows how a view can then reference the ViewModelLocator class as a static resource in its data context bindings.
<phone:PhoneApplicationPage
x:Class="
TailSpin.PhoneClient.Views.SurveyList.SurveyListView"
...
DataContext=
"{Binding Source={StaticResource ViewModelLocator},
Path=SurveyListViewModel}"
...
>
...
</phone:PhoneApplicationPage>
The Path attribute identifies the property of the ViewModelLocator instance that returns the view model associated with the current page.
The following code example shows the parts of the ViewModelLocator class that return the SurveyListViewModel instance.
public class ViewModelLocator : IDisposable
{
private readonly ContainerLocator containerLocator;
...
public SurveyListViewModel SurveyListViewModel
{
get
{
return this.containerLocator.Container
.Resolve<SurveyListViewModel>();
}
}
}
Notice how the instance of the ContainerLocator class exposes the Funq Container property to resolve the view model.
The ViewModelLocator class connects view models to views.
Note
The ContainerLocator class is also responsible for instantiating the store and passing it to the view model, which in turn passes it on to the model.
Testing the Application
One of the benefits of combining the MVVM pattern with the dependency injection pattern is that it promotes the testability of the application, making it easy to create tests that exercise the view model.
Tailspin uses a Silverlight unit-testing framework to run unit tests on the phone emulator and on real devices.
Inside the Implementation
Tailspin uses the Silverlight unit test framework for Windows Phone and Silverlight 4.
The Windows Phone project named Tailspin.PhoneClient.Tests contains the unit tests for the Surveys mobile client application. To run the tests, first build and then deploy this project either to the Windows Phone emulator or to a real device. On the Windows Phone device, you can now launch an application named Tailspin.Tests, and then select the unit tests you want to run.
Markus Says: | |
---|---|
Tailspin runs unit tests on the emulator and on a real device to make sure the test behavior is not affected by any behavioral differences in the core libraries on the phone as compared to the emulator. |
The following code example shows a unit test method from the SurveyListViewModelFixture class that tests that the view model returns a list of all the surveys that are marked as favorites.
[TestMethod]
public void FavoritesSectionShowsFavoritedItems()
{
var store = new SurveyStoreMock();
var surveyStoreLocator = new SurveyStoreLocatorMock(store);
store.Initialize();
var allSurveys = store.GetSurveyTemplates();
var vm = new SurveyListViewModel(surveyStoreLocator,
new SurveysSynchronizationServiceMock(),
new MockNavigationService(),
new MockPhoneApplicationServiceFacade(),
new MockShellTile(),
new MockSettingsStore(),
new MockLocationService());
vm.Refresh();
var favoriteSurveys =
vm.FavoriteItems.Cast<SurveyTemplateViewModel>().ToList();
CollectionAssert.AreEquivalent(
allSurveys.Where(p => p.IsFavorite).ToArray(),
favoriteSurveys.Select(t => t.Template).ToArray());
}
This method first instantiates a mock survey store and store locator objects to use in the test. The method then instantiates the view model object from the Tailspin.PhoneClient project to test, passing in the mock store locator object, along with other mock services. The method then executes the test on the view model instance and verifies that the favorite surveys in the view are the same as the ones in the underlying database.
For more information about testing the application see Appendix A, "Unit Testing Windows Phone Applications."
Displaying Data
The application displays data by binding elements of the view to properties in the view model. For example, the Pivot control on the SurveyListView page binds to the SelectedPivotIndex property in the SurveyListViewModel class.
The view can automatically update the values it displays in response to changes in the underlying view model if the view model implements the INotifyPropertyChanged interface. In the Tailspin mobile client, the abstract ViewModel class inherits from the NotificationObject class in the Prism Library. The NotificationObject class implements the INotifyPropertyChanged interface. With the exception of the question view models and the survey template view model, all the other view model classes in the Tailspin mobile client application inherit from the abstract ViewModel class. The application also uses the ObservableCollection class from the System.Collections.ObjectModel namespace that also implements the INotifyPropertyChanged interface.
The view model implements the INotifyPropertyChanged interface indirectly through the NotificationObject class from the Prism Library.
Note
To learn more about Prism, see the Prism CodePlex site and "Prism (Composite Client Application Guidance)" on MSDN.
Inside the Implementation
The following sections describe examples of data binding in the application. The first section describes a simple scenario on the AppSettingsView page, the next sections describe more complex examples using Pivot controls, and the last section describes how Tailspin addressed an issue with focus events on the phone.
Data Binding on the Settings Screen
The AppSettingsView page illustrates a simple scenario for binding a view to a view model. On this screen, the controls on the screen must display property values from the AppSettingsViewModel class, and set the property values in the view model when the user edits the settings values.
Define your data bindings to the view model in the view's XAML.
The following code example shows the DataContext attribute and some of the control definitions in the AppSettingsView.xaml file. Tailspin chose to use the ToggleSwitch control in place of a standard CheckBox control because it better conveys the idea of switching something on and off instead of selecting something. The ToggleSwitch control is part of the Microsoft Silverlight Windows Phone Toolkit available on the Silverlight Toolkit CodePlex site.
<phone:PhoneApplicationPage
x:Class="TailSpin.PhoneClient.Views.AppSettingsView"
...
DataContext="{Binding Source={StaticResource ViewModelLocator},
Path=AppSettingsViewModel}"
...>
...
<shell:SystemTray.ProgressIndicator>
<shell:ProgressIndicator IsIndeterminate="True"
IsVisible="{Binding IsSyncing}"
Text="{Binding ProgressText}"/>
</shell:SystemTray.ProgressIndicator>
...
<TextBox Height="Auto" HorizontalAlignment="Stretch"
Margin="0,28,0,0" Name="textBoxUsername"
VerticalAlignment="Top" Width="Auto"
Text="{Binding UserName, Mode=TwoWay}" Padding="0"
BorderThickness="3">
<i:Interaction.Behaviors>
<prism:UpdateTextBindingOnPropertyChanged/>
</i:Interaction.Behaviors>
</TextBox>
...
<PasswordBox Height="Auto" HorizontalAlignment="Stretch"
Margin="0,124,0,0" Name="passwordBoxPassword"
VerticalAlignment="Top" Width="Auto"
Password="{Binding Password, Mode=TwoWay}">
<i:Interaction.Behaviors>
<prism:UpdatePasswordBindingOnPropertyChanged/>
</i:Interaction.Behaviors>
</PasswordBox>
...
<toolkit:ToggleSwitch Header="Subscribe to Push Notifications"
Margin="0,202,0,0"
IsChecked="{Binding SubscribeToPushNotifications, Mode=TwoWay}" />
...
Markus Says: | |
---|---|
The default binding mode value is OneWay, which is the setting used by the ProgressIndicator control. You need to change this to TwoWay if you want to send the changes back to the view model. |
If a view wants to update its view model, the binding mode must be set to TwoWay. In order for the view model to notify the view of updates, the view model must implement the INotifyPropertyChanged interface. In the Tailspin client application, this interface is implemented by the ViewModel class from which all the view models inherit. A view model notifies a view of a changed property value by invoking the RaisePropertyChanged method. The following code example shows how the AppSettingsViewModel view model class notifies the view that it should display the in-progress indicator to the user.
public bool IsSyncing
{
get { return this.isSyncing; }
set
{
this.isSyncing = value;
this.RaisePropertyChanged(() => this.IsSyncing);
}
}
Markus Says: | |
---|---|
The RaisePropertyChanged method uses an expression for compile-time verification. The Prism PropertySupport class performs the translation of a lambda expression to a property name. |
The code for the AppSettingsView page illustrates a solution to a common issue in Silverlight for the Windows Phone platform. By default, the view does not notify the view model of property value changes until the control loses focus. For example, if the user enters a value in a control and then causes the control to lose focus, the view model will be updated. However, if the user enters a value in a control and then interacts with any ApplicationBarButton elements, the focus lost event will not be fired. For example, new content in the textBoxUserName control is lost if the user tries to save the settings before moving to another control. The UpdateTextBindingOnPropertyChanged behavior from the Prism Library ensures that the view always notifies the view model of any changes in the TextBox control as soon as they happen. The UpdatePasswordBindingOnPropertyChanged behavior does the same for the PasswordBox control. For more information, see the section, "Handling Focus Events," later in this chapter.
Data Binding and the Pivot Control
The application uses the Pivot control to allow the user to view different filtered lists of surveys. Figure 6 shows the components in the mobile client application that relate to the Pivot control as it's used on the SurveyListView page.
Figure 6
Using the Pivot control on the SurveyListView page
The following code example shows how the Pivot control on the SurveyListView page binds to the SelectedPivotIndex property of the SurveyListViewModel instance. This two-way binding determines which PivotItem control, and therefore which list of surveys, is displayed on the page. Remember, the ViewModelLocator class is responsible for locating the correct view model for a view.
<phoneControls:Pivot
Title="TAILSPIN SURVEYS"
Name="homePivot"
SelectedIndex="{Binding SelectedPivotIndex, Mode=TwoWay}"
Visibility="{Binding SettingAreConfigured,
Converter={StaticResource VisibilityConverter}}">
...
</phoneControls:PivotControl>
The following code example shows the definition of the PivotItem control that holds a list of new surveys; it also shows how the ListBox control binds to the NewItems property in the view model.
<phoneControls:PivotItem Header="new">
<Grid>
<ContentControl Template="{StaticResource NoItemsTextBlock}"
Visibility="{Binding NewItems.IsEmpty,
Converter={StaticResource VisibilityConverter}}" />
<ListBox ItemsSource="{Binding NewItems}"
Style="{StaticResource SurveyTemplateItemStyle}"
Visibility="{Binding NewItems.IsEmpty,
Converter={StaticResource NegativeVisibilityConverter}}" >
</ListBox>
</Grid>
</phoneControls:PivotItem>
Note
In the list, the layout and formatting of each survey's information is handled by the SurveyTemplateItemStyle style and the SurveyDataTemplate data template in the Styles.xaml file.
The SurveyListViewModel class uses CollectionViewSource objects to hold the list of surveys to display in the list on each PivotItem control. This allows the view model to notice and to react to changes in the item selected in the view, without needing to know about the view itself. The following code example shows how the SurveyListViewModel class defines the properties that the ListBox controls bind to.
private CollectionViewSource newSurveysViewSource;
private CollectionViewSource byLengthViewSource;
private CollectionViewSource favoritesViewSource;
...
public ICollectionView NewItems
{
get { return this.newSurveysViewSource.View; }
}
public ICollectionView FavoriteItems
{
get { return this.favoritesViewSource.View; }
}
public ICollectionView ByLengthItems
{
get { return this.byLengthViewSource.View; }
}
The following code example shows how the BuildPivotDimensions method populates the CollectionViewSource objects. In this example, to save space, only the code that populates the newSurveysViewSource property is shown.
private void BuildPivotDimensions()
{
...
this.ObservableSurveyTemplates =
new ObservableCollection<SurveyTemplateViewModel>();
List<SurveyTemplateViewModel> surveyTemplateViewModels =
this.surveyStoreLocator.GetStore().GetSurveyTemplates()
.Select(t => new SurveyTemplateViewModel(t,
this.NavigationService,
this.PhoneApplicationServiceFacade,
this.shellTile,
this.locationService)
{
CompletedAnswers = this.surveyStoreLocator.GetStore()
.GetCurrentAnswerForTemplate(t) != null
? this.surveyStoreLocator.GetStore()
.GetCurrentAnswerForTemplate(t).CompletedAnswers
: 0,
Completeness = this.surveyStoreLocator.GetStore()
.GetCurrentAnswerForTemplate(t) != null
? this.surveyStoreLocator.GetStore()
.GetCurrentAnswerForTemplate(t).CompletenessPercentage
: 0,
CanTakeSurvey = () => !IsSynchronizing
}).ToList();
surveyTemplateViewModels.ForEach(
this.observableSurveyTemplates.Add);
...
this.newSurveysViewSource = new CollectionViewSource
{ Source = this.observableSurveyTemplates };
...
this.newSurveysViewSource.Filter +=
(o, e) => e.Accepted =
((SurveyTemplateViewModel)e.Item).Template.IsNew;
...
this.newSurveysViewSource.View.CurrentChanged +=
(o, e) => this.SelectedSurveyTemplate =
(SurveyTemplateViewModel)this.newSurveysViewSource
.View.CurrentItem;
...
// Initialize the selected survey template.
this.HandleCurrentSectionChanged();
...
}
This method creates an ObservableCollection collection of SurveyTemplateViewModel objects. Each SurveyTemplateViewModel object holds a complete definition of a survey, its questions, and its answers. The method then assigns this collection to the Source property of each CollectionViewSource object. Next, the method applies a filter or a sort to each CollectionViewSource object so that it displays the correct set of surveys. The method then attaches an event handler to the CurrentChanged event of the view in each CollectionViewSource object so that the SelectedSurveyTemplate property of the SurveyListViewModel object is updated correctly. Finally, the method calls the HandleCurrentSectionChanged method that causes the view model to set the selected survey to the value of the SelectedSurveyTemplate property.
Markus Says: | |
---|---|
The ObservableCollection class provides notifications when the collection is modified. This means the view automatically updates through the bindings when the data changes. |
The application also uses the Pivot control to display survey questions. This enables the user to scroll left and right through the questions as if the questions are all on a single large page of which the phone's screen shows a small portion.
Figure 7 shows how the Pivot control's binding works on the TakeSurveyView page.
Figure 7
Using the Pivot Control on the TakeSurveyView page
As in the previous examples, the view uses the ViewModelLocator class to create the view model. The following code example shows how the ViewModelLocator object instantiates a TakeSurveyViewModel object.
public TakeSurveyViewModel TakeSurveyViewModel
{
get
{
var vm = new TakeSurveyViewModel(
(
this.containerLocator.Container
.Resolve<INavigationService>(),
this.containerLocator.Container
.Resolve<IPhoneApplicationServiceFacade>(),
this.containerLocator.Container
.Resolve<ILocationService>(),
this.containerLocator.Container
.Resolve<ISurveyStoreLocator>(),
this.containerLocator.Container
.Resolve<IShellTile>()
);
return vm;
}
}
The Pivot control binds to the Questions property of the TakeSurveyViewModel class and a Pivot.ItemTemplate template controls the display of each question in the survey. However, it's necessary to display different question types using different layouts. The following code example from the TakeSurveyView.xaml file shows how the data binding and view layout is defined for the Pivot control using the DataTemplateSelector content selector class from the Prism Library.
<phoneControls:Pivot
SelectionChanged="PivotSelectionChanged"
Loaded="ControlLoaded"
VerticalAlignment="Top"
Name="questionsPivot" Margin="0,0,0,0"
ItemsSource="{Binding Questions}">
<phoneControls:Pivot.ItemTemplate>
<DataTemplate>
<ScrollViewer>
<prismViewModel:DataTemplateSelector Content="{Binding}"
Grid.Row="0" VerticalAlignment="Top"
HorizontalContentAlignment="Left" IsTabStop="False">
<prismViewModel:DataTemplateSelector.Resources>
<DataTemplate x:Key="OpenQuestionViewModel">
<Views:OpenQuestionView DataContext="{Binding}"/>
</DataTemplate>
<DataTemplate x:Key="MultipleChoiceQuestionViewModel">
<Views:MultipleChoiceQuestionView DataContext="{Binding}"/>
</DataTemplate>
...
</prismViewModel:DataTemplateSelector.Resources>
</prismViewModel:DataTemplateSelector>
</ScrollViewer>
</DataTemplate>
</phoneControls:Pivot.ItemTemplate>
<phoneControls:Pivot.HeaderTemplate>
<DataTemplate>
<TextBlock Text="{Binding Title}"/>
</DataTemplate>
</phoneControls:Pivot.HeaderTemplate>
</phoneControls:Pivot>
Note
You'll notice that the XAML declares handlers for the SelectionChanged and Loaded events in the code-behind. For an explanation of why the developers at Tailspin used code-behind here, see the section, "Handling Activation and Deactivation," in Chapter 3, "Using Services on the Phone." The code-behind also contains a workaround method to trim long titles that don't always display correctly when the user scrolls in the Pivot control.
Each question view on the Pivot control binds to a view model object in the list of questions held in the Questions property of the TakeSurveyViewModel class. For example, an OpenQuestionView view binds to an OpenQuestionViewModel object, and a VoiceQuestionView view binds to a VoiceQuestionViewModel object. The following code example shows how the TakeSurveyViewModel class builds this list of question view models.
public IList<QuestionViewModel> Questions { get; set; }
...
public void Initialize(ISurveyStoreLocator surveyStoreLocator)
{
...
this.Questions = this.SurveyAnswer.Answers.Select(
a => Maps[a.QuestionType].Invoke(a)).ToArray();
}
The following code sample shows the definition of the Maps property in the TakeSurveyViewModel. The Maps property maps question types to view models.
private static readonly
Dictionary<QuestionType, Func<QuestionAnswer,
QuestionViewModel>> Maps =
new Dictionary<QuestionType, Func<QuestionAnswer,
QuestionViewModel>>()
{
{ QuestionType.SimpleText,
a => new OpenQuestionViewModel(a) },
{ QuestionType.MultipleChoice,
a => new MultipleChoiceQuestionViewModel(a) },
...
};
Handling Focus Events
The file OpenQuestionView.xaml defines the UI for entering survey results. Tailspin found that when the user clicked the Save button, the last answer wasn't saved by the view model. This is because ApplicationBarIconButton control is not a FrameworkElement control, so it cannot get the focus. As a result, the lost focus event on the text box wasn't being raised; as a result, the bindings didn't tell the view model about the new field contents.
To solve this problem, the developers at Tailspin used a behavior named UpdateTextBindingOnPropertyChanged from the Prism Library. This behavior ensures that the view notifies the view model whenever the user changes the text in the TextBox control, not just when the control loses focus. The following code example shows how this behavior is defined in OpenQuestionView.xaml.
...
xmlns:prism=
"clr-namespace:Microsoft.Practices.Prism.Interactivity;
assembly=Microsoft.Practices.Prism.Interactivity"
...
<TextBox Text="{Binding AnswerText, Mode=TwoWay}" Height="100">
<Interactivity:Interaction.Behaviors>
<prism:UpdateTextBindingOnPropertyChanged/>
</Interactivity:Interaction.Behaviors>
</TextBox>
Commands
In a Silverlight application, you can invoke some action in response to a user action (such as a button click) by creating an event handler in the code-behind class. However, in the MVVM pattern, the responsibility for implementing the action lies with the view model, and you should avoid placing code in the code-behind classes. Therefore, you should connect the control to a method in the view model using a command binding.
Jana Says: | |
---|---|
To keep the responsibilities of the view and view model separate, you should try to avoid placing code in the code-behind files of your views when you are implementing the MVVM pattern. |
In Silverlight 4, the ButtonBase class and the Hyperlink class both support Command and CommandParameter properties. The Command property can reference an ICommand implementation that comes from a view model data source, through a binding. The command is then interpreted at runtime by the Silverlight input system. For more information, see "ButtonBase.Command Property" on MSDN and "Hyperlink.Command Property" on MSDN.
However, because the ApplicationBarIconButton class is not a control, Tailspin uses the ApplicationBarButtonCommand behavior from the Prism Library to bind the click event of the ApplicationBarButtonCommand to the execution of a command.
Windows Phone controls that derive from ButtonBase or Hyperlink support binding to ICommand instances.
For more information about the ICommand interface, see "ICommand Interface" on MSDN.
Inside the Implementation
The developers at Tailspin use bindings to associate actions in the UI with commands in the view model. However, you cannot use the InvokeCommandAction Expression Blend behavior with the ApplicationBarIconButton or ApplicationBarMenuItems controls because they cannot have dependency properties. Tailspin uses a custom behavior to connect commands to the view model.
The following code example from the SurveyListView page shows how the Synchronize button, the Settings button, and the Filter button on the application bar are associated with commands. In addition, it shows how the About menu item on the application bar is associated with an event handler for the Click event.
<phoneNavigation:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar IsVisible="True">
<shell:ApplicationBar.MenuItems>
<shell:ApplicationBarMenuItem Text="about" Click="AboutMenuItem_Click"/>
</shell:ApplicationBar.MenuItems>
<shell:ApplicationBarIconButton Text="Sync" IconUri="..." />
<shell:ApplicationBarIconButton Text="Settings" IconUri="..." />
<shell:ApplicationBarIconButton Text="Filters" IconUri="..."/>
</shell:ApplicationBar>
</phoneNavigation:PhoneApplicationPage.ApplicationBar>
...
<Custom:Interaction.Behaviors>
<prismInteractivity:ApplicationBarButtonCommand
ButtonText="Sync"
CommandBinding="{Binding StartSyncCommand}"/>
<prismInteractivity:ApplicationBarButtonCommand
ButtonText="Settings"
CommandBinding="{Binding AppSettingsCommand}" />
<prismInteractivity:ApplicationBarButtonCommand
ButtonText="Filters"
CommandBinding="{Binding FiltersCommand}" />
</Custom:Interaction.Behaviors>
The attributes attached to the ApplicationBarIconButton controls (Text and IconUri) only affect their appearance. The ApplicationBarButtonCommand elements handle the connection to the commands; they identify the control to associate with the command through the ButtonText attribute and the command through the CommandBinding attribute.
The ApplicationBarButtonCommand class from the Prism Library implements the custom behavior that links a button click in the UI to the StartSyncCommand, AppSettingsCommand, and FiltersCommand properties in the SurveyListViewModel class.
The StartSyncCommand property uses an instance of the DelegateCommand class that implements the ICommand interface. The following code example from the SurveyListViewModel class shows the definition of the StartSyncCommand property.
public DelegateCommand StartSyncCommand { get; set; }
...
public SurveyListViewModel(...) : base(...)
{
...
this.StartSyncCommand = new DelegateCommand(
this.StartSync,
() => !this.IsSynchronizing &&
!this.SettingAreNotConfigured);
...
}
Note
For more information about the DelegateCommand class from Prism, see Chapter 9 of the Prism documentation, "Communicating Between Loosely Coupled Components" on MSDN.
The following code example from the SurveyListViewModel class shows the implementation of the StartSync method and SyncCompleted method. The StartSync method runs the synchronization process asynchronously by invoking the StartSynchronization method. It also uses the ObserveOnDispatcher method from the Reactive Extensions (Rx) for .NET. For more information about how Tailspin uses Rx, see Chapter 3, "Using Services on the Phone."
public void StartSync()
{
if (this.IsSynchronizing)
{
return;
}
this.IsSynchronizing = true;
this.synchronizationService
.StartSynchronization()
.ObserveOnDispatcher()
.Subscribe(this.SyncCompleted);
}
private void SyncCompleted(
IEnumerable<TaskCompletedSummary> taskSummaries)
{
...
this.BuildPivotDimensions();
this.IsSynchronizing = false;
this.UpdateCommandsForSync();
}
The SyncCompleted method also updates the UI to show the new list of surveys following the synchronization process; it also controls the progress indicator in the UI by setting the IsSynchronizing property. The UpdateCommandsForSync method disables the Synchronize button in the UI while the synchronization is running.
Handling Navigation Requests
In addition to invoking commands from the view, the Tailspin mobile client also triggers navigation requests from the view. These requests could be to navigate to a particular view or navigate back to the previous view. In some scenarios, for example if the application needs to navigate to a new view when a command completes, the view model will need to send a message to the view. In other scenarios, you might want to trigger the navigation request directly from the view without involving the view model directly. When you're using the MVVM pattern, you want to be able to do all this without using any code-behind in the view, and without introducing any dependency on the view implementation in the view model classes.
Inside the Implementation
The following code example from the FilterSettingsView.xaml file shows how navigation is initiated in the sample application.
<i:Interaction.Behaviors>
<prismInteractivity:ApplicationBarButtonCommand
ButtonText="Save" CommandBinding="{Binding SaveCommand}"/>
<prismInteractivity:ApplicationBarButtonCommand
ButtonText="Cancel" CommandBinding="{Binding CancelCommand}" />
</i:Interaction.Behaviors>
In both cases, commands are invoked in the view model. The code that implements each command causes the application to navigate back to the previous view if the command succeeds, so the navigation is initiated from the view model. The following code example from the FilterSettingsViewModel class illustrates this.
public DelegateCommand SaveCommand { get; set; }
public DelegateCommand CancelCommand { get; set; }
...
public FilterSettingsViewModel(...)
{
...
this.SaveCommand =
new DelegateCommand(this.Submit, () => !this.CanSubmit);
this.CancelCommand =
new DelegateCommand(this.Cancel, () => true);
...
}
public bool CanSubmit
{
get { return this.canSubmit; }
set
{
if (!value.Equals(this.canSubmit))
{
this.canSubmit = value;
this.RaisePropertyChanged(() => this.CanSubmit);
this.SaveCommand.RaiseCanExecuteChanged();
}
}
}
...
public void Submit()
{
...
if (this.NavigationService.CanGoBack) this.NavigationService.GoBack();
...
}
public void Cancel()
{
if (this.NavigationService.CanGoBack) this.NavigationService.GoBack();
}
public override void OnPageDeactivation(bool isIntentionalNavigation)
{
base.OnPageDeactivation(isIntentionalNavigation);
if (isIntentionalNavigation)
{
this.Dispose();
return;
}
...
}
...
The OnPageDeactivation method is called by the PhoneApplicationFrame Navigating event when the page is intentionally or unintentionally navigated away from. The isIntentionalNavigation parameter indicates whether the current application is both the origin and destination of the navigation. Therefore, when navigating to another page in the application, the Dispose method of the base ViewModel class will be called in order to dispose of the instance of the FilterSettingsViewModel, provided that the navigation is intentional. For more information about the PhoneApplicationFrame Navigating event, see the section, "Handling Activation and Deactivation" in Chapter 3, "Using Service on the Phone."
Navigation is performed using the ApplicationFrameNavigationService class, from the TailSpin.PhoneClient.Adapters project, which is shown in the following code example.
public class ApplicationFrameNavigationService :
INavigationService
{
private readonly PhoneApplicationFrame frame;
private Dictionary<string, bool> tombstonedPages;
public ApplicationFrameNavigationService(
PhoneApplicationFrame frame)
{
this.frame = frame;
this.frame.Navigated += frame_Navigated;
this.frame.Navigating += frame_Navigating;
this.frame.Obscured += frame_Obscured;
this.RecoveredFromTombstoning = false;
}
public bool CanGoBack
{
get { return this.frame.CanGoBack; }
}
public bool RecoveredFromTombstoning { get; set; }
public bool DoesPageNeedtoRecoverFromTombstoning(Uri pageUri)
{
if (!RecoveredFromTombstoning) return false;
if (tombstonedPages == null)
{
tombstonedPages = new Dictionary<string, bool>();
tombstonedPages.Add(pageUri.ToString(), true);
foreach (var journalEntry in frame.BackStack)
{
tombstonedPages.Add(journalEntry.Source.ToString(), true);
}
return true;
}
if (tombstonedPages.ContainsKey(pageUri.ToString()))
{
return tombstonedPages[pageUri.ToString()];
}
return false;
}
public void UpdateTombstonedPageTracking(Uri pageUri)
{
tombstonedPages[pageUri.ToString()] = false;
}
public bool Navigate(Uri source)
{
return this.frame.Navigate(source);
}
public void GoBack()
{
this.frame.GoBack();
}
public event NavigatedEventHandler Navigated;
void frame_Navigated(object sender, NavigationEventArgs e)
{
var handler = this.Navigated;
if (handler != null)
{
handler(sender, e);
}
}
public event NavigatingCancelEventHandler Navigating;
void frame_Navigating(object sender, NavigatingCancelEventArgs e)
{
var handler = this.Navigating;
if (handler != null)
{
handler(sender, e);
}
}
public event EventHandler<ObscuredEventArgs> Obscured;
void frame_Obscured(object sender, ObscuredEventArgs e)
{
var handler = this.Obscured;
if (handler != null)
{
handler(sender, e);
}
}
}
This class, which implements Tailspin's INavigationService interface, uses the phone's PhoneApplicationFrame instance to perform the navigation request for the application.
Christine Says: | |
---|---|
Using the PhoneApplicationFrame instance ensures that the phone maintains the correct navigation stack for the application so that navigating backward works the way users expect. |
A view model can invoke the Navigate method on the ApplicationFrameNavigationService object to cause the application to navigate to a particular view in the application or the GoBack method to return to the previous view.
The ViewModel base class maintains the INavigationService instance for all the view models, and the Funq dependency injection container is responsible for initially creating the ApplicationFrameNavigationService object that implements this interface.
To avoid any code-behind in the view when the view initiates the navigation, the developers at Tailspin use an interaction behavior from the Prism Library. The following code example shows how the Cancel button is declared in the FilterSettingsView.xaml file.
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar ...>
...
<shell:ApplicationBarIconButton Text="Cancel" IconUri="..."/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
...
<i:Interaction.Behaviors>
...
<prismInteractivity:ApplicationBarButtonCommand
ButtonText="Cancel" CommandBinding="{Binding CancelCommand}" />
</i:Interaction.Behaviors>
Markus Says: | |
---|---|
By using a behavior, Tailspin avoids having any code in the view to handle navigation requests. This general pattern is how the view model can handle requests from the view without using "classic" events that require handlers in code-behind. |
User Interface Notifications
The Tailspin Surveys mobile client application performs some tasks asynchronously; one example is the potentially time-consuming synchronization with the Tailspin Surveys web service. Asynchronous tasks must often inform the user of the outcome of the task or provide status information while they're running. It's important to consider the usability of the application when you're deciding on the appropriate way to notify users of significant events or to provide status information. The developers at Tailspin were concerned they would either flood the user with alerts or have the user miss an important piece of information.
For the Tailspin Surveys mobile client application, the developers identified two categories of notification, informational/warning notifications and error notifications.
Informational/Warning Notifications
Informational or warning notifications should not be disruptive, so the user should see the message but not be interrupted in their current task. The user does not need to perform any action in response to this type of message; the Tailspin mobile client application uses this type of notification to inform the user when a synchronization completes successfully, for example. Tailspin uses a custom toast notification for these messages because the application does not have access to the Windows Phone toast notification system.
Tailspin implemented a custom toast notification system.
Error Notifications
Error notifications should be disruptive because the message is informing the user that some expected action of the application will not happen. The notification should inform the user of the actions they need to take to resolve the problem; for example, to retry the synchronization operation again if it fails for some reason. Tailspin uses message boxes for this type of message.
Inside the Implementation
This example shows how Tailspin implements custom toast notifications and error notifications on the SurveyListView page to inform users when the synchronization process finishes or fails. In the sample application, many of the notifications and error messages are not intended for the imaginary user of the sample; instead, they are there to help you, the developer understand what the application is doing as you explore its functionality. You should follow the guidance published in the User Experience Design Guidelines for Windows Phone when you design the user notification system for your application.
The following code example shows the relevant declarations in the SurveyListView.xaml file.
<Custom:EventTrigger SourceObject=
"{Binding SubmitNotificationInteractionRequest}" EventName="Raised">
<prismInteractionRequest:ToastPopupAction PopupElementName=
"SynchronizationToast" />
</Custom:EventTrigger>
<Custom:EventTrigger SourceObject="{Binding SubmitErrorInteractionRequest}"
EventName="Raised">
<prismInteractionRequest:MessageBoxAction />
</Custom:EventTrigger>
...
<Popup x:Name="SynchronizationToast" DataContext="">
<Grid x:Name="grid" Background="{StaticResource PhoneAccentBrush}"
VerticalAlignment="Bottom" Width="480">
<TextBlock Text="{Binding Title}"
HorizontalAlignment="Stretch"
Foreground="{StaticResource PhoneForegroundBrush}"
TextWrapping="Wrap" Margin="14,5,14,5">
<Custom:Interaction.Behaviors>
<pag:PopupHideOnLeftMouseUp/>
</Custom:Interaction.Behaviors>
</TextBlock>
</Grid>
</Popup>
The view model uses the SubmitNotificationInteractionRequest binding to trigger the toast notification and the SubmitErrorInteractionRequest binding to trigger the error message notification. The following code example shows how the SurveyListViewModel displays a toast notification when the synchronization process completes successfully and an error message when it fails.
private readonly InteractionRequest
<InteractionRequest.Notification> submitErrorInteractionRequest;
private readonly InteractionRequest
<InteractionRequest.Notification> submitNotificationInteractionRequest;
...
public IInteractionRequest SubmitErrorInteractionRequest
{
get { return this.submitErrorInteractionRequest; }
}
public IInteractionRequest SubmitNotificationInteractionRequest
{
get { return this.submitNotificationInteractionRequest; }
}
...
private void SyncCompleted(
IEnumerable<TaskCompletedSummary> taskSummaries)
{
...
if (taskSummaries.Any(
t => t.Result != TaskSummaryResult.Success))
{
this.submitErrorInteractionRequest.Raise(
new InteractionRequest.Notification
{
Title = "Synchronization error",
Content = stringBuilder.ToString()
},
n => { });
}
else
{
this.submitNotificationInteractionRequest.Raise(
new InteractionRequest.Notification
{
Title = stringBuilder.ToString(),
Content = null
},
n => { });
}
...
}
Note
This solution uses the InteractionRequest and Notification classes and two trigger actions, MessageBoxAction and ToastPopupAction, from the Prism Library.
Accessing Services
The MVVM pattern identifies three major components: the view, the view model, and the model. This chapter describes the UI of the Tailspin Surveys mobile client application and the way that Tailspin uses the MVVM pattern. The Tailspin mobile client application also includes a number of services. These services can be invoked from the view, view model, or model components and include services that do the following:
- Manage the settings and surveys stores that handle data persistence for the application.
- Save and load the application's state when it's activated and deactivated.
- Synchronize survey data between the client application and the Tailspin Surveys web application.
- Notify the application when new surveys are available.
- Encode audio data, and support application navigation and other infrastructure services.
These services are discussed in the following chapters. Chapter 3, "Using Services on the Phone,"describes how the application uses services offered by the Windows Phone platform, such as local data persistence services and geo-location services. Chapter 4, "Connecting with Services,"describes how the mobile client application accesses services over the network.
Next Topic | Previous Topic | Home
Last built: May 25, 2012