The Model-View-ViewModel (MVVM) architecture
originated when the Microsoft Windows Presentation Foundation (WPF) team
were building the first version of Expression Blend. WPF is Microsoft's
desktop XAML development model, and Expression Blend is written in WPF.
MVVM is similar to other separation of concerns architectures, like the
tried-and-true Model-View-Controller (MVC) model; however, MVVM is
optimized to take advantage of XAML's rich data binding, data templates,
commands, and event routing capabilities. The next section covers the
architecture in more detail.
1. MVVM Overview
The MVVM
pattern is defined to help you grasp how it works with XAML. If you are
familiar with MVC, MVVM will look somewhat familiar to you – but it is
much more than just MVC. MVVM relies heavily on XAML data binding
capabilities to allow the UI to data bind to both data and commands. Figure 1 depicts the MVVM architecture.
The BasicMVVM and the WcfRemoteServicesSimpleRestJSON projects are configured as the startup project. Four folders are added to the project named Model, View, and ViewModel. The sections that follow cover the major components of MVVM in the BasicMVVM sample.
1.1. BasicMVVM - Model
The Model contains the
building blocks of the application. It consists of the underlying data
objects that are populated via a data access layer. Examples of Model
classes are Customer, Store, Product,
etc. When you create a class to represent an object in an application,
it most likely belongs as part of the Model. The Model sits behind the
ViewModel. The View will data bind to lists of or individual objects
based on classes in the Model.
To get started, copy over the Vendor class from the WcfRemoteServicesSimpleRestJSON services project to the BasicMVVMModels folder. The class implements the INotifyPropertyChanged interface to support data binding at the class level. The INotifyPropertyChanged interface ensures that changes to the underlying object are propagated to the UI and vice versa. See Listing 1 for the code.
Example 1. Vendor Model Class Code File
using System; using System.ComponentModel; using System.Runtime.Serialization;
namespace BasicMVVM.Model { //Copied from services project [DataContract()] public class Vendor : INotifyPropertyChanged { private string AccountNumberField; private byte CreditRatingField; private string NameField;
[DataMemberAttribute()] public string AccountNumber {
get { return this.AccountNumberField; } set { this.AccountNumberField = value; NotifyPropertyChanged("AccountNumber"); } }
[DataMemberAttribute()] public byte CreditRating { get { return this.CreditRatingField; } set { this.CreditRatingField = value; NotifyPropertyChanged("CreditRating"); } }
[DataMemberAttribute()] public string Name { get { return this.NameField; } set { this.NameField = value; NotifyPropertyChanged("Name"); } }
public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged(String propertyName) { if (null != PropertyChanged) { PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } } }
|
1.2. BasicMVVM - ViewModel
Mentioned above is the fact
that the View or UI data binds to the ViewModel, suggesting that a
ViewModel consists of the data containers for the application, which is
correct. Lists of objects defined in the Model are created and managed
by the ViewModel. In addition, the ViewModel consists of the majority of
application logic as well.
Next create the VendorViewModel class. The VendorViewModel class in the BasicMVVM project supports the following four major features:
The Vendor-specific business logic is pretty straightforward. It consists of a read-only collection of Vendor objects from the Model
and two event handlers to add and remove Vendor objects from the
collection. For a professional application, additional methods and
business logic would be present but the implementation is the same.
NOTE
While the VendorViewModel.Vendors collection is read-only – it has just a get property accessor – you can still add and remove Vendor objects in the collection. You just cannot assign a new collection to the property.
It is critical to implement INotifyPropertyChanged
for data binding to work. Otherwise, changes are not propagated back to
the UI, and vice versa. It is simple enough to do. Add an instance of
the PropertyChangedEventHandler class named PropertyChanged and a method that takes a property name as a string and then fires the PropertyChanged event instance.
To detect design-time, the System.ComponentModel.DesignerProperties class has a static bool property named IsInDesignTool that indicates whether the code is running in a design-time tool. The VendorViewModel constructor checks if an instance of the class is running at design-time. If at design-time, the constructor calls LoadSampleData method. Otherwise, at run-time, it calls LoadData, which invokes a remote REST+JSON service.
The last major functionality for the VendorsViewModel class is making the remote service call. The interesting change for this scenario is that the service call and asynchronous callback live in the VendorsViewModel class. The callback cannot have code like the following:
vendorsListbox.Dispatcher.BeginInvoke(...);
The solution is to make the call using this line of code instead:
Deployment.Current.Dispatcher.BeginInvoke(..);
This code ensures that the correct Dispatcher
instance is used to notify the UI that data changes occurred. The next
challenge is that the callback function needs to update the Vendors collection property. Remember that the Vendors
collection is a read-only collection, because we do not want external
classes to be able to assign a new collection to it. We want the data to
only come from the remote services. The code instead assigns the
collection to the underlying _vendors collection private member
variable.
The final issue is that the code still needs to notify the UI that data changes occurred, i.e. that the Vendors collection is loaded. Since the _vendors collection is updated directly, NotifyPropertyChanged("Vendors") is called in the anonymous delegate by BeginInvoke.
Again, the code could make Vendors read/write and have a set accessor
function like this but maintaining data integrity is preferred so the
set function is commented out, as in the following:
set
{
_vendors = value;
NotifyPropertyChanged("Vendors");
}
Listing 2 has the full source code for review.
Example 2. VendorViewModel Class Code File
using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Net; using System.Runtime.Serialization.Json; using System.Windows; using BasicMVVM.Model;
namespace BasicMVVM.ViewModel { public class VendorViewModel : INotifyPropertyChanged { public VendorViewModel() { if (InDesignTime) { LoadSampleData(); } else { LoadData(); } }
#region Design-time support private bool InDesignTime { get { return DesignerProperties.IsInDesignTool; } }
private void LoadSampleData() { _vendors = new ObservableCollection<Vendor>() { new Vendor(){AccountNumber="111111", CreditRating=65, Name="DesignTime - Fabrikam Bikes" }, new Vendor(){AccountNumber="222222", CreditRating=40, Name="Contoso Sports" }, new Vendor(){AccountNumber="333333", CreditRating=30, Name="Duwamish Surfing Gear" }, new Vendor(){AccountNumber="444444", CreditRating=65, Name="Contoso Bikes" }, new Vendor(){AccountNumber="555555", CreditRating=40, Name="Fabrikam Sports" }, new Vendor(){AccountNumber="666666", CreditRating=30, Name="Duwamish Golf" }, new Vendor(){AccountNumber="777777", CreditRating=65, Name="Fabrikam Sun Sports" }, new Vendor(){AccountNumber="888888", CreditRating=40, Name="Contoso Lacross" }, new Vendor(){AccountNumber="999999", CreditRating=30, Name="Duwamish Team Sports" }, }; } #endregion
#region Vendors Data Load HttpWebRequest httpWebRequest; private void LoadData() { httpWebRequest = HttpWebRequest.CreateHttp("http://localhost:9191 /AdventureWorksRestJSON.svc/Vendors"); httpWebRequest.BeginGetResponse(new AsyncCallback(GetVendors), null); }
//add a reference to System.Servicemodel.web to get DataContractJsonSerializer
void GetVendors(IAsyncResult result) { HttpWebResponse response = httpWebRequest.EndGetResponse(result) as HttpWebResponse; DataContractJsonSerializer ser = new DataContractJsonSerializer (typeof(ObservableCollection<Vendor>)); _vendors = ser.ReadObject(response.GetResponseStream()) as ObservableCollection<Vendor>; //Vendors is read-only so cannot set directly //Must call NotifyPropertyChanged notifications on UI thread //to update the UI and have data binding work properly Deployment.Current.Dispatcher.BeginInvoke(() => { NotifyPropertyChanged("Vendors"); }); } #endregion
#region Vendors Business Logic private ObservableCollection<Vendor> _vendors; public ObservableCollection<Vendor> Vendors { get { return _vendors; } //set //{ // _vendors = value; // NotifyPropertyChanged("Vendors"); //} }
public Vendor GetVendorByAccountNumber(string accountNumber) { var vendor = from v in _vendors where v.AccountNumber == accountNumber select v;
return vendor.First<Vendor>(); }
public void AddVendor() { Vendors.Add(new Vendor() { AccountNumber = "111111", CreditRating = 65, Name = "Fabrikam Bikes - Added" }); }
public void RemoveVendor(object vendor) { if (null != vendor) Vendors.Remove((Vendor)vendor);
} #endregion
#region INotifyPropertyChanged interface members public event PropertyChangedEventHandler PropertyChanged;
public void NotifyPropertyChanged(String property) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(property)); } } #endregion } }
|
The next section covers how to make the Model and ViewModel objects available to the UI.
1.3. BasicMVVM - View
The View is the actual XAML of an application. It is the mainpage.xaml
file in a Silverlight project, and is what the user interacts with
directly, presenting the underlying data and application logic. The View
data binds to the ViewModel, which is covered in the previous section.
The goal when building the view is to not have any code in the
code-behind for the .xaml file, if possible. This means that all logic
is in the ViewModel, which is non-visual, making it much more
unit-testable. The other advantage of the separation of concerns here is
that the design-team can focus on building out the View without
interfering with business logic in event handlers. A View always has a
reference to the ViewModel, because it data binds to it.
Remove the MainPage.xaml from the BasicMVVM project and add a new View (.xaml page) to the Views folder named VendorsView.xaml. Next, edit the WMAppManifest.xml file by changing the NavigationPage attribute to point to the new default task, as in the following:
<DefaultTask Name ="_default" NavigationPage="Views/CustomersView.xaml"/>.
NOTE
In general, the WMAppManifest.xml file should not be manually edited, but in this case it is required.
In Expression Blend, add a ListBox to VendorsView.xaml and configure the ItemsSource to data bind to the VendorViewModel.Vendors collection by clicking the Advanced Options button next to the ItemsSource
property in the Expression Blend Properties window and selecting Data
Binding... to bring up the Create Data Binding dialog. Click the +CLR
Object button, select VendorViewModel, and then click OK.
If the VendorViewModel
class – or any .NET CLR class that you want to data bind – does not
show up in the dialog box, make sure to compile the application. Static
collections will not show either.
|
|
This generates a new Data Source named VendorViewModelDataSource in the left pane. Select Vendors in the right pane and then click OK. This configuration updates the XAML in three places. It adds a new resource to the VendorsView page, as in the following:
<phone:PhoneApplicationPage.Resources>
<BasicMVVM_ViewModels:VendorViewModel
x:Key="VendorViewModelDataSource" d:IsDataSource="True"/>
</phone:PhoneApplicationPage.Resources>
It configures LayoutRootGrid's DataContext property to point to the VendorViewModel class:
DataContext="{Binding Source={StaticResource VendorViewModelDataSource}}"
Finally, the work in Expression Blend configures the vendorsListBoxItemsSource property to data bind to the VendorViewModel.Vendors collection like so ItemsSource="{Binding Vendors}."
One of the goals of
MVVM and separating concerns is to make the View as "thin" as possible.
WPF and Silverlight 4 have support for separating concerns by allowing
UI element events like Click to data bind to methods on the ViewModel via Commanding and the ICommand
interface. This means that instead of having event handlers in the
code-behind for the view, everything is instead configured via data
binding in XAML. Figure 2 shows the UI.
Silverlight for Windows Phone
7, which is based on Silverlight 3 plus some additional Silverlight 4
features like the WebBrowser control and offline DRM, does not have full
support for Commanding. For the BasicMVVM sample, the VendorsView has two code-behind events to support adding and removing a Vendor, as in the following:
private void insertVendorAppBarBtn_Click(object sender, EventArgs e)
{
VendorViewModel vm = LayoutRoot.DataContext as VendorViewModel;
vm.AddVendor();
}
private void RemoveVendorAppBarBtn_Click(object sender, EventArgs e)
{
VendorViewModel vm = LayoutRoot.DataContext as VendorViewModel;
vm.RemoveVendor(vendorsListBox.SelectedItem);
}
Notice that the path to the underlying ViewModel is still via the instance of the VendorViewModel class that is data bound to the LayoutRootGrid's DataContext
property. It would be great to be able to avoid this type of code.
Luckily, there are third-party, open-source frameworks that provide
extensions to Silverlight that enable better support for MVVM.