1. Problem
You want your application
implementation to have a consistent separation of concerns between
modules. You want the view to have only the task of displaying
information, and to remove from the view everything that requires some
logic or data access. You need to create a view with only a set of
controls (such as TextBlock, TextBox, Button,...) binding them with the DataContext (ViewModel) of the view. Then the view it's not responsible of data-retrieving, but it's a task reserved to the ViewModel that will aggregate data if necessary, and put it in properties, bindable by the view.
2. Solution
You must implement the
Model-View-ViewModel (MVVM) pattern, and because you don't want to
reinvent the wheel, you'll also use the MVVMLight framework, which is
available on CodePlex (http://mvvmlight.codeplex.com). MVVMLight provides some important classes and "gears" necessary to implement MVVM on Silverlight for Windows Phone, such as EventToCommand and MessageBroker (a.k.a. Messenger).
3. How It Works
MVVM (based on a
specialization of Martin Fowler's Presentation Model) is a design
pattern used to realize the separation of concerns. Elements of the MVVM
pattern are as follows:
View is responsible for showing information, as in other MVx patterns (where x could be C = controller, P = presenter, and so forth) to display controls (for example: buttons and text boxes).
ViewModel is responsible for preparing data taken from the model for the visualization.
Model represents the model of your application (that is, your data source).
In order to implement MVVM,
you need to understand some basic concepts such as commands and binding.
Then you'll learn about the basics of MVVM and after that, you will see
unit testing. and their importance within your application life cycle.
Let's start with binding. A binding is simply an association between two properties of two different classes. You can bind the Value of a Slider with the Text property of a TextBox,
or in the case of the MVVM implementation, you can bind the value of a
property in our ViewModel with the property of a control on the view.
Typically, you will use properties with the same or similar types. There
are different options for binding:
Path
Mode
UpdateTrigger
BindsDirectlyToSource
Converter
Path represents the path to the binding source property. For example, if your binding source is a product and you are binding TextBox.Text to show the category name, your path will be Path=Category.Description.
In Silverlight for Windows Phone, as in Silverlight for the Web, the Mode attribute can assume three values: OneWay, OneTime, and TwoWay. By default, the binding Mode is set to OneWay. This means that when you update the source of the binding, the target is updated. As you might guess from its name, TwoWay mode means that updating the target value will reflect on the source value, and vice versa. Finally, when you use OneTime, the target will be updated only the first time, when you create the binding.
UpdateTrigger in Silverlight for Windows Phone, as in its cousin for the Web, accepts only two values: Default and Explicit. With Explicit, you must indicate to the binding engine to update the source. Default mode updates the value when a lost focus (of the control) occurs.
BindsDirectlyToSource, when set to false, causes Path to be evaluated relative to the data item; otherwise, Path is evaluated relative to the data source.
Last but not least is the concept of a converter.
With a converter, you can bind properties of different types, defining
the way in which the view must evaluate the value. For example,
sometimes you will need to bind a Boolean property to the visibility of a
control. As you know, visibility is an enumeration, so when you try to
apply a Boolean, nothing happens. The solution? A converter! You will
see this in the following "The Code" section.
Now you need to
understand what a command is. The concept of a command comes from the
command pattern, which allows you to isolate the part of code that
executes an operation from the part that requests the execution. Inside
the code of our ViewModel, a command is a class that implements the ICommand interface, which represents the contract to implement commanding. As you can see in the class diagram shown in Figure 1, ICommand exposes the methods CanExecute and Execute, and the event CanExecuteChanged. As you can imagine, CanExecute is the method that enables the control bound with the command to be enabled, and Execute
is the method that describes what you want to do with that command.
Unfortunately, in this version of Silverlight for Windows Phone, even if
the ICommand interface is available, the command cannot be bound with controls without using EventToCommand,
because the controls don't expose the Command property. However, we are
confident that this capability will be introduced in future versions.
It's important to remember that when you bind a command to a control
through the Command={Binding MyCommand} syntax, where MyCommand is your command exposed by your ViewModel, this command refers to the default event of the control (for example, the Click event of a button).
4. The Code
Before you begin to create a new project, download the libraries indicated in the preceding "How it Works"
section. Of course, if you want to get the most from MVVMLight, you
should also install the templates for Visual Studio 2010 (Express or
not, depending on your version) that the package contains.
To begin, create a new
Windows Phone application, and add a reference to the MVVMLight DLLs and
to the Coding4Fun libraries, as shown in Figure 2.
Your application is
now ready for a quick implementation of the pattern, and to use a
workaround, notorious among people who work with Silverlight. This
workaround helps you to update the source of the binding, every times a
change accours inside a textbox, then will be raised PropertyChanged event every time you write something in a text box, because as you know the default UpdateSourceTrigger, set the value of the source property only when the control has lost the focus.
To do it you need to import the Coding4Fun.Phone.Controls.Binding namespace in this way
xmlns:local="clr-namespace:Coding4Fun.Phone.Controls.Binding;
assembly=Coding4Fun.Phone.Controls"
Then the only thing that you
need to do now it's to set inside the TextBox the attached property
TextBoxBinding .UpdateSourceOnChange to True in this way:
<TextBox Text="{Binding Text}"local:TextBoxBinding.UpdateSourceOnChange="True" />
After the explanation of the
workaround we are ready to continue the implementation of the pattern.
The first step is to add the ViewModelLocator of MVVMLight to your application. Then you must create the folder ViewModels and add an item of type ViewModelLocator, calling it ViewModelsLocator.
Immediately after this, you must edit your App.xaml by adding the namespace of ViewModelsLocator to it, and creating a resource of type ViewModelsLocator, at shown here:
<Application
x:Class="Wp7Recipe_10_2_MVVM.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:vm="clr-namespace:Wp7Recipe_10_2_MVVM.ViewModels">
<!--Application Resources-->
<Application.Resources>
<vm:ViewModelsLocator xmlns:vm="clr-namespace:Wp7Recipe_10_1_MVVM.ViewModels"
x:Key="Locator" />
</Application.Resources>
...
</Application>
Now that you have a way
of accessing your ViewModels, you can start creating them. You'll begin
with the ViewModel for the main page.
A naming convention for Views and ViewModels is important. If you have a view named CategoriesView, you could name the relative ViewModel CategoriesViewModel.
Although you do not have to always have a 1:1 correspondence of Views
and ViewModels, perhaps there is some case where you will have only one
ViewModel for CRUD (Create, Read, Update, and Delete) operations for all
your typological entities.
|
|
As before, add a new item to your ViewModels folder, of type ViewModel, and call it MainPageViewModel. As you can see in the constructor, this will ready a piece of code that has been commented out:
////if (IsInDesignMode)
////{
//// // Code runs in Blend --> create design time data.
////}
////else
////{
//// // Code runs "for real": Connect to service, etc...
////}
This is the first important
feature to consider, because by using this code, you will have the
ViewModel available at design time, and this is significant when you
want to test your binding inside DataTemplate. In addition, you must
specify in ViewModelsLocator a property of type MainPageViewModel, and bind it to the data context of the view (MainPage, in this case).
#region MainPageViewModel
private static MainPageViewModel _mainPageViewModel;
/// <summary>
/// Gets the MainPageViewModel property.
/// </summary>
public static MainPageViewModel MainPageViewModelStatic
{
get
{
if (_mainPageViewModel == null)
{
CreateMainPageViewModel();
}
return _mainPageViewModel;
}
}
/// <summary>
/// Gets the MainPageViewModel property.
/// </summary>
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance",
"CA1822:MarkMembersAsStatic",
Justification = "This non-static member is needed for data binding purposes.")]
public MainPageViewModel MainPageViewModel
{
get
{
return MainPageViewModelStatic;
}
}
/// <summary>
/// Provides a deterministic way to delete the MainPageViewModel property.
/// </summary>
public static void ClearMainPageViewModel()
{
_mainPageViewModel.Cleanup();
_mainPageViewModel = null;
}
/// <summary>
/// Provides a deterministic way to create the MainPageViewModel property.
/// </summary>
public static void CreateMainPageViewModel()
{
if (_mainPageViewModel == null)
{
_mainPageViewModel = new MainPageViewModel();
}
}
/// <summary>
/// Cleans up all the resources.
/// </summary>
public static void Cleanup()
{
ClearMainPageViewModel();
}
#endregion
To quickly write these
parts of the code, you must install snippets that are available inside
the MVVMLight package that you download from CodePlex. The mvvmlocatorproperty snippet creates a property of ViewModel inside your ViewModelLocator. Another really helpful snippet is mvvminpc, which rapidly creates a bindable property in your ViewModel that raises PropertyChangedEvent.
|
|
Now you have the most important
step to do: binding the data context of the view to your ViewModel. In
this way, all properties exposed by the ViewModel will be available for
binding controls inside the view. You do this with a simple binding, as
you can see in the following code:
<phone:PhoneApplicationPage
x:Class="Wp7Recipe_10_2_MVVM.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:phone="clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone"
xmlns:shell="clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="480" d:DesignHeight="768"
FontFamily="{StaticResource PhoneFontFamilyNormal}"
FontSize="{StaticResource PhoneFontSizeNormal}"
Foreground="{StaticResource PhoneForegroundBrush}"
SupportedOrientations="Portrait" Orientation="Portrait"
shell:SystemTray.IsVisible="True"
DataContext="{Binding Source={StaticResource Locator}, Path=MainPageViewModel}">
...
In this way, you have the view (MainPage) data context bound to the property MainPageViewModel of the Locator resource that you have defined in App.xaml.
These are all the
preliminary steps to start with MVVM, but it's clear that you must add
properties at your ViewModel to bind them to the controls on the user
interface, and you should start with simply the strings ApplicationName and PageName.
...
/// <summary>
/// The name of our application
/// </summary>
public string ApplicationName { get { return "Learning MVVM"; } }
/// <summary>
/// The name of the page
/// </summary>
public string PageName { get { return "Main Page"; } }
...
Binding is important in implementing MVVM, and so you bind the properties to the TextBlocks of the view, called ApplicationTitle and PageTitle:
<StackPanel x:Name="TitlePanel" Grid.Row="0" Margin="12,17,0,28">
<TextBlock x:Name="ApplicationTitle" Text="{Binding Path=ApplicationName}"
Style="{StaticResource PhoneTextNormalStyle}"/>
<TextBlock x:Name="PageTitle" Text="{Binding Path=PageName}" Margin="9,-7,0,0"
Style="{StaticResource PhoneTextTitle1Style}"/>
</StackPanel>
At this point, if you use
Microsoft Blend, you can see another point of strange of MVVM Light that
supports the "Blendability" then you will see the ApplicationTitle TextBlock Text property valued with text of property ApplicationName of the ViewModel (Figure 3) and PageTitle set with PageName.
All of this introduces you to
the concept of MVVM, but you want to use it in a real case—for example,
with an application that helps the user to keep track of expenses. Then
in MainPage, you would have a form
for inserting data, with fields such as Date, Amount, and Motivation,
and a button to bind to a command. Unfortunately, buttons in Windows
Phone 7 don't support "clean" commands (for example, Command={Binding SaveCommand}) in this release.
At this point, your view XAML will be something like this:
...
<Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.421*"/>
<ColumnDefinition Width="0.579*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="80"/>
<RowDefinition Height="80"/>
<RowDefinition Height="80"/>
<RowDefinition Height="80"/>
<RowDefinition/>
</Grid.RowDefinitions>
<TextBlock Margin="0" TextWrapping="Wrap" Text="Date" TextAlignment="Center"
VerticalAlignment="Center"/>
<TextBlock Margin="0" Grid.Row="1" TextWrapping="Wrap" Text="Amount"
TextAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Margin="0" Grid.Row="2" TextWrapping="Wrap" Text="Motivation"
TextAlignment="Center" VerticalAlignment="Center"/>
<TextBox Margin="0" Grid.Column="1" Grid.Row="1" TextWrapping="Wrap"
TextAlignment="Center" VerticalAlignment="Center"
Text="{Binding Path=Amount, Mode=TwoWay}"
local:TextBoxBinding.UpdateSourceOnChange="True" />
<TextBox Margin="0" Grid.Column="1" Grid.Row="2" TextWrapping="Wrap"
TextAlignment="Center" VerticalAlignment="Center"
Text="{Binding Path=Motivation, Mode=TwoWay}"
local:TextBoxBinding.UpdateSourceOnChange="True"/>
<toolkit:DatePicker Grid.Column="1" VerticalAlignment="Bottom" Height="77"
HorizontalContentAlignment="Center"
Value="{Binding Path=Date, Mode=TwoWay}"
local:TextBoxBinding.UpdateSourceOnChange="True" />
<Button Content="Save It" Grid.Column="0" HorizontalAlignment="Center" Grid.Row="3"
Width="152" Grid.ColumnSpan="2" >
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<cmd:EventToCommand Command="{Binding SaveCommand}"/>
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</Grid>
...
As you can see, in this XAML you have two "new" namespaces, cmd and i, which come from the MVVMLight libraries. In this way, you have bound the event Click with a command (inside ViewModel) named SaveCommand demanding to this command the interest to execute the logic of this operation, so when Button.Click raises, SaveCommand.Execute will be called. Of course, to use these namespaces, you must import them in your XAML, in this way:
...
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WP7"
...
As you can see from the XAML, you bound your controls to the properties of ViewModel named Date, Amount, and Motivation. In the same way as before, you can use a snippet (mvvminpc)
to create these properties inside your ViewModel. The advantage of
using this snippet is that you don't need to add any call to the RaisePropertyChanged method that raises the PropertyChanged event for subscribed handlers (in this case by binding engine).
NOTE
The RaisePropertyChanged method call raises the PropertyChanged event in ViewModelBase (a class inside of the MVVMLight toolkit) that derives from the interface INotifyPropertyChanged.
The binding engine will handle the event, looking for updated
properties, and informing the source of the binding that something has
changed.
As you can imagine, the ViewModel has become a little longer because you have to bind more properties and a command:
...
public class MainPageViewModel : ViewModelBase
{
/// <summary>
/// The name of our application
/// </summary>
public string ApplicationName { get { return "Learning MVVM"; } }
/// <summary>
/// The name of the page
/// </summary>
public string PageName { get { return "Main Page"; } }
public GalaSoft.MvvmLight.Command.RelayCommand SaveCommand { get; set; }
#region DateProperty
/// <summary>
/// The <see cref="Date" /> property's name.
/// </summary>
public const string DatePropertyName = "Date";
private DateTime _date = DateTime.Now.AddDays(-4);
public DateTime Date
{
get
{
return _date;
}
set
{
if (_date == value)
{
return;
}
var oldValue = _date;
_date = value;
// Update bindings, no broadcast
RaisePropertyChanged(DatePropertyName);
}
}
#endregion
#region Amount Property
/// <summary>
/// The <see cref="Amount" /> property's name.
/// </summary>
public const string AmountPropertyName = "Amount";
private decimal _amount = 0;
public decimal Amount
{
get
{
return _amount;
}
set
{
if (_amount == value)
{
return;
}
var oldValue = _amount;
_amount = value;
// Update bindings, no broadcast
RaisePropertyChanged(AmountPropertyName);
}
}
#endregion
#region Motivation Property
/// <summary>
/// The <see cref="Motivation" /> property's name.
/// </summary>
public const string MotivationPropertyName = "Motivation";
private string _motivation = string.Empty;
public string Motivation
{
get
{
return _motivation;
}
set
{
if (_motivation == value)
{
return;
}
var oldValue = _motivation;
_motivation = value;
// Update bindings, no broadcast
RaisePropertyChanged(MotivationPropertyName);
}
}
#endregion
/// <summary>
/// Initializes a new instance of the MainPageViewModel class.
/// </summary>
public MainPageViewModel()
{
SaveCommand = new GalaSoft.MvvmLight.Command.RelayCommand(SaveCommandExecute);
}
private void SaveCommandExecute()
{
//put your logic to save here
}
}
}
5. Usage
This application can run
in the emulator or the physical device. This recipe is a good start
point for a real financial tracking application, all you need to do is
to add your preferred logic for saving data and start the application. Make sure that you have
decoupled the view from the related ViewModel, so that the only interest
of the view is to know how to show data (ViewModel is view-ignorant).
In this way, if you change your logic for saving information, nothing
must be done on your view. Furthermore, if you want to test your
ViewModel, you can do it with unit tests without problems—as opposed to
the events approach, that requires that an event be raised