MVVM

 

 
Patterns
WPF Apps With The Model-View-ViewModel Design Pattern
Josh Smith
 

This article discusses:
  • Patterns and WPF
  • MVP pattern
  • Why MVVM is better for WPF
  • Building an application with MVVM
This article uses the following technologies:
WPF, data binding 
Code download available from the MSDN Code Gallery 
Browse the Code Online
Developing the user interface of a professional software application is not easy. It can be a murky blend of data, interaction design, visual design, connectivity, multithreading, security, internationalization, validation, unit testing, and a touch of voodoo. Considering that a user interface exposes the underlying system and must satisfy the unpredictable stylistic requirements of its users, it can be the most volatile area of many applications.
There are popular design patterns that can help to tame this unwieldy beast, but properly separating and addressing the multitude of concerns can be difficult. The more complicated the patterns are, the more likely that shortcuts will be used later on which undermine all previous efforts to do things the right way.
It is not always the design patterns at fault. Sometimes we use complicated design patterns, which require writing a lot of code because the UI platform in use does not lend itself well to a simpler pattern. What's needed is a platform that makes it easy to build UIs using simple, time-tested, developer-approved design patterns. Fortunately, Windows Presentation Foundation (WPF) provides exactly that.
As the software world continues to adopt WPF at an increasing rate, the WPF community has been developing its own ecosystem of patterns and practices. In this article, I'll review some of those best practices for designing and implementing client applications with WPF. By leveraging some core features of WPF in conjunction with the Model-View-ViewModel (MVVM) design pattern, I will walk through an example program that demonstrates just how simple it can be to build a WPF application the "right way."
By the end of this article, it will be clear how data templates, commands, data binding, the resource system, and the MVVM pattern all fit together to create a simple, testable, robust framework on which any WPF application can thrive. The demonstration program that accompanies this article can serve as a template for a real WPF application that uses MVVM as its core architecture. The unit tests in the demo solution show how easy it is to test the functionality of an application's user interface when that functionality exists in a set of ViewModel classes. Before diving into the details, let's review why you should use a pattern like MVVM in the first place.

Order vs. Chaos
It is unnecessary and counterproductive to use design patterns in a simple "Hello, World!" program. Any competent developer can understand a few lines of code at a glance. However, as the number of features in a program increases, the number of lines of code and moving parts increase accordingly. Eventually, the complexity of a system, and the recurring problems it contains, encourages developers to organize their code in such a way that it is easier to comprehend, discuss, extend, and troubleshoot. We diminish the cognitive chaos of a complex system by applying well-known names to certain entities in the source code. We determine the name to apply to a piece of code by considering its functional role in the system.
Developers often intentionally structure their code according to a design pattern, as opposed to letting the patterns emerge organically. There is nothing wrong with either approach, but in this article, I examine the benefits of explicitly using MVVM as the architecture of a WPF application. The names of certain classes include well-known terms from the MVVM pattern, such as ending with "ViewModel" if the class is an abstraction of a view. This approach helps avoid the cognitive chaos mentioned earlier. Instead, you can happily exist in a state of controlled chaos, which is the natural state of affairs in most professional software development projects!

The Evolution of Model-View-ViewModel
Ever since people started to create software user interfaces, there have been popular design patterns to help make it easier. For example, the Model-View-Presenter (MVP) pattern has enjoyed popularity on various UI programming platforms. MVP is a variation of the Model-View-Controller pattern, which has been around for decades. In case you have never used the MVP pattern before, here is a simplified explanation. What you see on the screen is the View, the data it displays is the model, and the Presenter hooks the two together. The view relies on a Presenter to populate it with model data, react to user input, provide input validation (perhaps by delegating to the model), and other such tasks. If you would like to learn more about the Model View Presenter, I suggest you read Jean-Paul Boodhoo's August 2006 Design Patterns column .
Back in 2004, Martin Fowler published an article about a pattern named Presentation Model(PM). The PM pattern is similar to MVP in that it separates a view from its behavior and state.The interesting part of the PM pattern is that an abstraction of a view is created, called the Presentation Model. A view, then, becomes merely a rendering of a Presentation Model. In Fowler's explanation, he shows that the Presentation Model frequently updates its View, so that the two stay in sync with each other. That synchronization logic exists as code in the Presentation Model classes.
In 2005, John Gossman, currently one of the WPF and Silverlight Architects at Microsoft, unveiled the Model-View-ViewModel (MVVM) pattern on his blog. MVVM is identical to Fowler's Presentation Model, in that both patterns feature an abstraction of a View, which contains a View's state and behavior. Fowler introduced Presentation Model as a means of creating a UI platform-independent abstraction of a View, whereas Gossman introduced MVVM as a standardized way to leverage core features of WPF to simplify the creation of user interfaces. In that sense, I consider MVVM to be a specialization of the more general PM pattern, tailor-made for the WPF and Silverlight platforms.
In Glenn Block's excellent article " Prism: Patterns for Building Composite Applications with WPF " in the September 2008 issue, he explains the Microsoft Composite Application Guidance for WPF. The term ViewModel is never used. Instead, the term Presentation Model is used to describe the abstraction of a view. Throughout this article, however, I'll refer to the pattern as MVVM and the abstraction of a view as a ViewModel. I find this terminology is much more prevelant in the WPF and Silverlight communities.
Unlike the Presenter in MVP, a ViewModel does not need a reference to a view. The view binds to properties on a ViewModel, which, in turn, exposes data contained in model objects and other state specific to the view. The bindings between view and ViewModel are simple to construct because a ViewModel object is set as the DataContext of a view. If property values in the ViewModel change, those new values automatically propagate to the view via data binding. When the user clicks a button in the View, a command on the ViewModel executes to perform the requested action. The ViewModel, never the View, performs all modifications made to the model data.
The view classes have no idea that the model classes exist, while the ViewModel and model are unaware of the view. In fact, the model is completely oblivious to the fact that the ViewModel and view exist. This is a very loosely coupled design, which pays dividends in many ways, as you will soon see.

Why WPF Developers Love MVVM
Once a developer becomes comfortable with WPF and MVVM, it can be difficult to differentiate the two. MVVM is the lingua franca of WPF developers because it is well suited to the WPF platform, and WPF was designed to make it easy to build applications using the MVVM pattern (amongst others). In fact, Microsoft was using MVVM internally to develop WPF applications, such as Microsoft Expression Blend, while the core WPF platform was under construction. Many aspects of WPF, such as the look-less control model and data templates, utilize the strong separation of display from state and behavior promoted by MVVM.
The single most important aspect of WPF that makes MVVM a great pattern to use is the data binding infrastructure. By binding properties of a view to a ViewModel, you get loose coupling between the two and entirely remove the need for writing code in a ViewModel that directly updates a view. The data binding system also supports input validation, which provides a standardized way of transmitting validation errors to a view.
Two other features of WPF that make this pattern so usable are data templates and the resource system. Data templates apply Views to ViewModel objects shown in the user interface. You can declare templates in XAML and let the resource system automatically locate and apply those templates for you at run time. You can learn more about binding and data templates in my July 2008 article, " Data and WPF: Customize Data Display with Data Binding and WPF ."
If it were not for the support for commands in WPF, the MVVM pattern would be much less powerful. In this article, I will show you how a ViewModel can expose commands to a View, thus allowing the view to consume its functionality. If you aren't familiar with commanding, I recommend that you read Brian Noyes's comprehensive article, " Advanced WPF: Understanding Routed Events and Commands in WPF ," from the September 2008 issue.
In addition to the WPF (and Silverlight 2) features that make MVVM a natural way to structure an application, the pattern is also popular because ViewModel classes are easy to unit test.When an application's interaction logic lives in a set of ViewModel classes, you can easily write code that tests it. In a sense, Views and unit tests are just two different types of ViewModel consumers. Having a suite of tests for an application's ViewModels provides free and fast regression testing, which helps reduce the cost of maintaining an application over time.
In addition to promoting the creation of automated regression tests, the testability of ViewModel classes can assist in properly designing user interfaces that are easy to skin. When you are designing an application, you can often decide whether something should be in the view or the ViewModel by imagining that you want to write a unit test to consume the ViewModel. If you can write unit tests for the ViewModel without creating any UI objects, you can also completely skin the ViewModel because it has no dependencies on specific visual elements.
Lastly, for developers who work with visual designers, using MVVM makes it much easier to create a smooth designer/developer workflow. Since a view is just an arbitrary consumer of a ViewModel, it is easy to just rip one view out and drop in a new view to render a ViewModel.This simple step allows for rapid prototyping and evaluation of user interfaces made by the designers.
The development team can focus on creating robust ViewModel classes, and the design team can focus on making user-friendly Views. Connecting the output of both teams can involve little more than ensuring that the correct bindings exist in a view's XAML file.

The Demo Application
At this point, I have reviewed MVVM's history and theory of operation. I also examined why it is so popular amongst WPF developers. Now it is time to roll up your sleeves and see the pattern in action. The demo application that accompanies this article uses MVVM in a variety of ways. It provides a fertile source of examples to help put the concepts into a meaningful context. I created the demo application in Visual Studio 2008 SP1, against the Microsoft .NET Framework 3.5 SP1. The unit tests run in the Visual Studio unit testing system.
The application can contain any number of "workspaces," each of which the user can open by clicking on a command link in the navigation area on the left. All workspaces live in a TabControl on the main content area. The user can close a workspace by clicking the Close button on that workspace's tab item. The application has two available workspaces: "All Customers" and "New Customer." After running the application and opening some workspaces, the UI looks something like Figure 1.
Figure 1 Workspaces
Only one instance of the "All Customers" workspace can be open at a time, but any number of "New Customer" workspaces can be open at once. When the user decides to create a new customer, she must fill in the data entry form in Figure 2.
Figure 2 New Customer Data Entry Form
After filling in the data entry form with valid values and clicking the Save button, the new customer's name appears in the tab item and that customer is added to the list of all customers. The application does not have support for deleting or editing an existing customer, but that functionality, and many other features similar to it, are easy to implement by building on top of the existing application architecture. Now that you have a high-level understanding of what the demo application does, let's investigate how it was designed and implemented.

Relaying Command Logic
Every view in the app has an empty codebehind file, except for the standard boilerplate code that calls InitializeComponent in the class's constructor. In fact, you could remove the views' codebehind files from the project and the application would still compile and run correctly.Despite the lack of event handling methods in the views, when the user clicks on buttons, the application reacts and satisfies the user's requests. This works because of bindings that were established on the Command property of Hyperlink, Button, and MenuItem controls displayed in the UI. Those bindings ensure that when the user clicks on the controls, ICommand objects exposed by the ViewModel execute. You can think of the command object as an adapter that makes it easy to consume a ViewModel's functionality from a view declared in XAML.
When a ViewModel exposes an instance property of type I­Command, the command object typically uses that ViewModel object to get its job done. One possible implementation pattern is to create a private nested class within the ViewModel class, so that the command has access to private members of its containing ViewModel and does not pollute the namespace. That nested class implements the ICommand interface, and a reference to the containing ViewModel object is injected into its constructor. However, creating a nested class that implements ICommand for each command exposed by a ViewModel can bloat the size of the ViewModel class. More code means a greater potential for bugs.
In the demo application, the RelayCommand class solves this problem. RelayCommand allows you to inject the command's logic via delegates passed into its constructor. This approach allows for terse, concise command implementation in ViewModel classes. RelayCommand is a simplified variation of the DelegateCommand found in the Microsoft Composite Application Library The Relay­Command class is shown in Figure 3.
 
public class RelayCommand : ICommand
{
    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;        

    #endregion // Fields

    #region Constructors

    public RelayCommand(Action<object> execute)
    : this(execute, null)
    {
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;           
    }
    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand Members
}
The CanExecuteChanged event, which is part of the ICommand interface implementation, has some interesting features. It delegates the event subscription to the CommandManager.RequerySuggested event. This ensures that the WPF commanding infrastructure asks all RelayCommand objects if they can execute whenever it asks the built-in commands. The following code from the CustomerViewModel class, which I will examine in-depth later, shows how to configure a RelayCommand with lambda expressions:
 
RelayCommand _saveCommand;
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null)
        {
            _saveCommand = new RelayCommand(param => this.Save(),
                param => this.CanSave );
        }
        return _saveCommand;
    }
}

ViewModel Class Hierarchy
Most ViewModel classes need the same features. They often need to implement the INotifyPropertyChanged interface, they usually need to have a user-friendly display name, and, in the case of workspaces, they need the ability to close (that is, be removed from the UI).This problem naturally lends itself to the creations of a ViewModel base class or two, so that new ViewModel classes can inherit all of the common functionality from a base class. The ViewModel classes form the inheritance hierarchy seen in Figure 4.
Figure 4 Inheritance Hierarchy
Having a base class for all of your ViewModels is by no means a requirement. If you prefer to gain features in your classes by composing many smaller classes together, instead of using inheritance, that is not a problem. Just like any other design pattern, MVVM is a set of guidelines, not rules.

ViewModelBase Class
ViewModelBase is the root class in the hierarchy, which is why it implements the commonly used INotifyPropertyChanged interface and has a DisplayName property. The INotifyPropertyChanged interface contains an event called PropertyChanged. Whenever a property on a ViewModel object has a new value, it can raise the PropertyChanged event to notify the WPF binding system of the new value. Upon receiving that notification, the binding system queries the property, and the bound property on some UI element receives the new value.
In order for WPF to know which property on the ViewModel object has changed, the PropertyChangedEventArgs class exposes a PropertyName property of type String. You must be careful to pass the correct property name into that event argument; otherwise, WPF will end up querying the wrong property for a new value.
One interesting aspect of ViewModelBase is that it provides the ability to verify that a property with a given name actually exists on the ViewModel object. This is very useful when refactoring, because changing a property's name via the Visual Studio 2008 refactoring feature will not update strings in your source code that happen to contain that property's name (nor should it). Raising the PropertyChanged event with an incorrect property name in the event argument can lead to subtle bugs that are difficult to track down, so this little feature can be a huge timesaver. The code from ViewModelBase that adds this useful support is shown inFigure 5.
 
// In ViewModelBase.cs
public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
    this.VerifyPropertyName(propertyName);

    PropertyChangedEventHandler handler = this.PropertyChanged;
    if (handler != null)
    {
        var e = new PropertyChangedEventArgs(propertyName);
        handler(this, e);
    }
}

[Conditional("DEBUG")]
[DebuggerStepThrough]
public void VerifyPropertyName(string propertyName)
{
    // Verify that the property name matches a real,  
    // public, instance property on this object.
    if (TypeDescriptor.GetProperties(this)[propertyName] == null)
    {
        string msg = "Invalid property name: " + propertyName;

        if (this.ThrowOnInvalidPropertyName)
            throw new Exception(msg);
        else
            Debug.Fail(msg);
    }
}

CommandViewModel Class
The simplest concrete ViewModelBase subclass is CommandViewModel. It exposes a property called Command of type I­Command. MainWindowViewModel exposes a collection of these objects through its Commands property. The navigation area on the left-hand side of the main window displays a link for each CommandViewModel exposed by MainWindowView­Model, such as "View all customers" and "Create new customer." When the user clicks on a link, thus executing one of those commands, a workspace opens in the TabControl on the main window. The Command­ViewModel class definition is shown here:
 
public class CommandViewModel : ViewModelBase
{
    public CommandViewModel(string displayName, ICommand command)
    {
        if (command == null)
            throw new ArgumentNullException("command");

        base.DisplayName = displayName;
        this.Command = command;
    }

    public ICommand Command { get; private set; }
}
In the MainWindowResources.xaml file there exists a Data­Template whose key is "CommandsTemplate". MainWindow uses that template to render the collection of CommandViewModels mentioned earlier. The template simply renders each CommandViewModel object as a link in an ItemsControl. Each Hyperlink's Command property is bound to the Command property of a Command­ViewModel. That XAML is shown in Figure 6.
 
<!-- In MainWindowResources.xaml -->
<!--
This template explains how to render the list of commands on 
the left side in the main window (the 'Control Panel' area).
-->
<DataTemplate x:Key="CommandsTemplate">
  <ItemsControl ItemsSource="{Binding Path=Commands}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <TextBlock Margin="2,6">
          <Hyperlink Command="{Binding Path=Command}">
            <TextBlock Text="{Binding Path=DisplayName}" />
          </Hyperlink>
        </TextBlock>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</DataTemplate>

MainWindowViewModel Class
As previously seen in the class diagram, the WorkspaceViewModel class derives from ViewModelBase and adds the ability to close. By "close," I mean that something removes the workspace from the user interface at run time. Three classes derive from WorkspaceViewModel: MainWindowViewModel, AllCustomersViewModel, and CustomerViewModel. MainWindowViewModel's request to close is handled by the App class, which creates the MainWindow and its ViewModel, as seen in Figure 7.
 
// In App.xaml.cs
protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    MainWindow window = new MainWindow();

    // Create the ViewModel to which 
    // the main window binds.
    string path = "Data/customers.xml";
    var viewModel = new MainWindowViewModel(path);

    // When the ViewModel asks to be closed, 
    // close the window.
    viewModel.RequestClose += delegate 
    { 
        window.Close(); 
    };

    // Allow all controls in the window to 
    // bind to the ViewModel by setting the 
    // DataContext, which propagates down 
    // the element tree.
    window.DataContext = viewModel;

    window.Show();
}
MainWindow contains a menu item whose Command property is bound to the MainWindowViewModel's CloseCommand property. When the user clicks on that menu item, the App class responds by calling the window's Close method, like so:
 
<!-- In MainWindow.xaml -->
<Menu>
  <MenuItem Header="_File">
    <MenuItem Header="_Exit" Command="{Binding Path=CloseCommand}" />
  </MenuItem>
  <MenuItem Header="_Edit" />
  <MenuItem Header="_Options" />
  <MenuItem Header="_Help" />
</Menu>
MainWindowViewModel contains an observable collection of WorkspaceViewModel objects, called Workspaces. The main window contains a TabControl whose ItemsSource property is bound to that collection. Each tab item has a Close button whose Command property is bound to the CloseCommand of its corresponding WorkspaceViewModel instance. An abridged version of the template that configures each tab item is shown in the code that follows. The code is found in MainWindowResources.xaml, and the template explains how to render a tab item with a Close button:
 
<DataTemplate x:Key="ClosableTabItemTemplate">
  <DockPanel Width="120">
    <Button
      Command="{Binding Path=CloseCommand}"
      Content="X"
      DockPanel.Dock="Right"
      Width="16" Height="16" 
      />
    <ContentPresenter Content="{Binding Path=DisplayName}" />
  </DockPanel>
</DataTemplate>
When the user clicks the Close button in a tab item, that Workspace­ViewModel's CloseCommand executes, causing its Request­Close event to fire. MainWindowViewModel monitors the RequestClose event of its workspaces and removes the workspace from the Workspaces collection upon request. Since the Main­Window's TabControl has its ItemsSource property bound to the observable collection of WorkspaceViewModels, removing an item from the collection causes the corresponding workspace to be removed from the TabControl.That logic from Main­WindowViewModel is shown in Figure 8.
 
// In MainWindowViewModel.cs

ObservableCollection<WorkspaceViewModel> _workspaces;

public ObservableCollection<WorkspaceViewModel> Workspaces
{
    get
    {
        if (_workspaces == null)
        {
            _workspaces = new ObservableCollection<WorkspaceViewModel>();
            _workspaces.CollectionChanged += this.OnWorkspacesChanged;
        }
        return _workspaces;
    }
}

void OnWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.NewItems != null && e.NewItems.Count != 0)
        foreach (WorkspaceViewModel workspace in e.NewItems)
            workspace.RequestClose += this.OnWorkspaceRequestClose;

    if (e.OldItems != null && e.OldItems.Count != 0)
        foreach (WorkspaceViewModel workspace in e.OldItems)
            workspace.RequestClose -= this.OnWorkspaceRequestClose;
}

void OnWorkspaceRequestClose(object sender, EventArgs e)
{
    this.Workspaces.Remove(sender as WorkspaceViewModel);
}
In the UnitTests project, the MainWindowViewModelTests.cs file contains a test method that verifies that this functionality is working properly. The ease with which you can create unit tests for ViewModel classes is a huge selling point of the MVVM pattern, because it allows for simple testing of application functionality without writing code that touches the UI. That test method is shown in Figure 9.
 
// In MainWindowViewModelTests.cs
[TestMethod]
public void TestCloseAllCustomersWorkspace()
{
    // Create the MainWindowViewModel, but not the MainWindow.
    MainWindowViewModel target = 
        new MainWindowViewModel(Constants.CUSTOMER_DATA_FILE);

    Assert.AreEqual(0, target.Workspaces.Count, "Workspaces isn't empty.");

    // Find the command that opens the "All Customers" workspace.
    CommandViewModel commandVM = 
        target.Commands.First(cvm => cvm.DisplayName == "View all customers");

    // Open the "All Customers" workspace.
    commandVM.Command.Execute(null);
    Assert.AreEqual(1, target.Workspaces.Count, "Did not create viewmodel.");

    // Ensure the correct type of workspace was created.
    var allCustomersVM = target.Workspaces[0] as AllCustomersViewModel;
    Assert.IsNotNull(allCustomersVM, "Wrong viewmodel type created.");

    // Tell the "All Customers" workspace to close.
    allCustomersVM.CloseCommand.Execute(null);
    Assert.AreEqual(0, target.Workspaces.Count, "Did not close viewmodel.");
}

Applying a View to a ViewModel
MainWindowViewModel indirectly adds and removes Workspace­ViewModel objects to and from the main window's Tab­Control. By relying on data binding, the Content property of a TabItem receives a ViewModelBase-derived object to display. ViewModelBase is not a UI element, so it has no inherent support for rendering itself. By default, in WPF a non-visual object is rendered by displaying the results of a call to its ToString method in a TextBlock. That clearly is not what you need, unless your users have a burning desire to see the type name of our ViewModel classes!
You can easily tell WPF how to render a ViewModel object by using typed DataTemplates. A typed DataTemplate does not have an x:Key value assigned to it, but it does have its DataType property set to an instance of the Type class. If WPF tries to render one of your ViewModel objects, it will check to see if the resource system has a typed DataTemplate in scope whose DataType is the same as (or a base class of) the type of your ViewModel object. If it finds one, it uses that template to render the ViewModel object referenced by the tab item's Content property.
The MainWindowResources.xaml file has a Resource­Dictionary. That dictionary is added to the main window's resource hierarchy, which means that the resources it contains are in the window's resource scope. When a tab item's content is set to a ViewModel object, a typed DataTemplate from this dictionary supplies a view (that is, a user control) to render it, as shown in Figure 10.
 
<!-- 
This resource dictionary is used by the MainWindow. 
-->
<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:vm="clr-namespace:DemoApp.ViewModel"
  xmlns:vw="clr-namespace:DemoApp.View"
  >

  <!-- 
  This template applies an AllCustomersView to an instance 
  of the AllCustomersViewModel class shown in the main window.
  -->
  <DataTemplate DataType="{x:Type vm:AllCustomersViewModel}">
    <vw:AllCustomersView />
  </DataTemplate>

  <!-- 
  This template applies a CustomerView to an instance  
  of the CustomerViewModel class shown in the main window.
  -->
  <DataTemplate DataType="{x:Type vm:CustomerViewModel}">
    <vw:CustomerView />
  </DataTemplate>

 <!-- Other resources omitted for clarity... -->

</ResourceDictionary>
You do not need to write any code that determines which view to show for a ViewModel object. The WPF resource system does all of the heavy lifting for you, freeing you up to focus on more important things. In more complex scenarios, it is possible to programmatically select the view, but in most situations that is unnecessary.

The Data Model and Repository
You have seen how ViewModel objects are loaded, displayed, and closed by the application shell. Now that the general plumbing is in place, you can review implementation details more specific to the domain of the application. Before getting deep into the application's two workspaces, "All Customers" and "New Customer," let's first examine the data model and data access classes. The design of those classes has almost nothing to do with the MVVM pattern, because you can create a ViewModel class to adapt just about any data object into something friendly to WPF.
The sole model class in the demo program is Customer. That class has a handful of properties that represent information about a customer of a company, such as their first name, last name, and e-mail address. It provides validation messages by implementing the standard IDataErrorInfo interface, which existed for years before WPF hit the street. The Customer class has nothing in it that suggests it is being used in an MVVM architecture or even in a WPF application. The class could easily have come from a legacy business library.
Data must come from and reside somewhere. In this application, an instance of the CustomerRepository class loads and stores all Customer objects. It happens to load the customer data from an XML file, but the type of external data source is irrelevant. The data could come from a database, a Web service, a named pipe, a file on disk, or even carrier pigeons: it simply does not matter. As long as you have a .NET object with some data in it, regardless of where it came from, the MVVM pattern can get that data on the screen.
The CustomerRepository class exposes a few methods that allow you to get all the available Customer objects, add new a Customer to the repository, and check if a Customer is already in the repository. Since the application does not allow the user to delete a customer, the repository does not allow you to remove a customer. The CustomerAdded event fires when a new Customer enters the CustomerRepository, via the AddCustomer method.
Clearly, this application's data model is very small, compared to what real business applications require, but that is not important. What is important to understand is how the ViewModel classes make use of Customer and CustomerRepository. Note that Customer­ViewModel is a wrapper around a Customer object. It exposes the state of a Customer, and other state used by the Customer­View control, through a set of properties.CustomerViewModel does not duplicate the state of a Customer; it simply exposes it via delegation, like this:
 
public string FirstName
{
    get { return _customer.FirstName; }
    set
    {
        if (value == _customer.FirstName)
            return;
        _customer.FirstName = value;
        base.OnPropertyChanged("FirstName");
    }
}
When the user creates a new customer and clicks the Save button in the CustomerView control, the Customer­ViewModel associated with that view will add the new Customer object to the Customer­Repository. That causes the repository's CustomerAdded event to fire, which lets the AllCustomers­ViewModel know that it should add a new Customer­ViewModel to its AllCustomers collection. In a sense, Customer­Repository acts as a synchronization mechanism between various ViewModels that deal with Customer objects. Perhaps one might think of this as using the Mediator design pattern. I will review more of how this works in the upcoming sections, but for now refer to the diagram in Figure 11 for a high-level understanding of how all the pieces fit together.
Figure 11 Customer Relationships

New Customer Data Entry Form
When the user clicks the "Create new customer" link, MainWindowViewModel adds a new CustomerViewModel to its list of workspaces, and a CustomerView control displays it. After the user types valid values into the input fields, the Save button enters the enabled state so that the user can persist the new customer information. There is nothing out of the ordinary here, just a regular data entry form with input validation and a Save button.
The Customer class has built-in validation support, available through its IDataErrorInfo interface implementation. That validation ensures the customer has a first name, a well-formed e-mail address, and, if the customer is a person, a last name. If the Customer's IsCompany property returns true, the LastName property cannot have a value (the idea being that a company does not have a last name). This validation logic might make sense from the Customer object's perspective, but it does not meet the needs of the user interface. The UI requires a user to select whether a new customer is a person or a company. The Customer Type selector initially has the value "(Not Specified)". How can the UI tell the user that the customer type is unspecified if the IsCompany property of a Customer only allows for a true or false value?
Assuming you have complete control over the entire software system, you could change the IsCompany property to be of type Nullable<bool>, which would allow for the "unselected" value. However, the real world is not always so simple. Suppose you cannot change the Customer class because it comes from a legacy library owned by a different team in your company. What if there is no easy way to persist that "unselected" value because of the existing database schema? What if other applications already use the Customer class and rely on the property being a normal Boolean value? Once again, having a ViewModel comes to the rescue.
The test method in Figure 12 shows how this functionality works in CustomerViewModel.CustomerViewModel exposes a CustomerTypeOptions property so that the Customer Type selector has three strings to display. It also exposes a CustomerType property, which stores the selected String in the selector. When CustomerType is set, it maps the String value to a Boolean value for the underlying Customer object's IsCompany property. Figure 13 shows the two properties.
 
// In CustomerViewModelTests.cs
[TestMethod]
public void TestCustomerType()
{
    Customer cust = Customer.CreateNewCustomer();
    CustomerRepository repos = new CustomerRepository(
        Constants.CUSTOMER_DATA_FILE);
    CustomerViewModel target = new CustomerViewModel(cust, repos);

    target.CustomerType = "Company"
    Assert.IsTrue(cust.IsCompany, "Should be a company");

    target.CustomerType = "Person";
    Assert.IsFalse(cust.IsCompany, "Should be a person");

    target.CustomerType = "(Not Specified)";
    string error = (target as IDataErrorInfo)["CustomerType"];
    Assert.IsFalse(String.IsNullOrEmpty(error), "Error message should 
        be returned");
}
 
// In CustomerViewModel.cs

public string[] CustomerTypeOptions
{
    get
    {
        if (_customerTypeOptions == null)
        {
            _customerTypeOptions = new string[]
            {
                "(Not Specified)",
                "Person",
                "Company"
            };
        }
        return _customerTypeOptions;
    }
}
public string CustomerType
{
    get { return _customerType; }
    set
    {
        if (value == _customerType || 
            String.IsNullOrEmpty(value))
            return;

        _customerType = value;

        if (_customerType == "Company")
        {
            _customer.IsCompany = true;
        }
        else if (_customerType == "Person")
        {
            _customer.IsCompany = false;
        }

        base.OnPropertyChanged("CustomerType");
        base.OnPropertyChanged("LastName");
    }
}
The CustomerView control contains a ComboBox that is bound to those properties, as seen here:
 
<ComboBox 
  ItemsSource="{Binding CustomerTypeOptions}"
  SelectedItem="{Binding CustomerType, ValidatesOnDataErrors=True}"
  />
When the selected item in that ComboBox changes, the data source's IDataErrorInfo interface is queried to see if the new value is valid. That occurs because the SelectedItem property binding has ValidatesOnDataErrors set to true. Since the data source is a Customer­ViewModel object, the binding system asks that Customer­ViewModel for a validation error on the CustomerType property. Most of the time, CustomerViewModel delegates all requests for validation errors to the Customer object it contains. However, since Customer has no notion of having an unselected state for the IsCompany property, the CustomerViewModel class must handle validating the new selected item in the ComboBox control. That code is seen in Figure 14.
 
// In CustomerViewModel.cs
string IDataErrorInfo.this[string propertyName]
{
    get
    {
        string error = null;

        if (propertyName == "CustomerType")
        {
            // The IsCompany property of the Customer class 
            // is Boolean, so it has no concept of being in
            // an "unselected" state. The CustomerViewModel
            // class handles this mapping and validation.
            error = this.ValidateCustomerType();
        }
        else
        {
            error = (_customer as IDataErrorInfo)[propertyName];
        }

        // Dirty the commands registered with CommandManager,
        // such as our Save command, so that they are queried
        // to see if they can execute now.
        CommandManager.InvalidateRequerySuggested();

        return error;
    }
}

string ValidateCustomerType()
{
    if (this.CustomerType == "Company" ||
       this.CustomerType == "Person")
        return null;

    return "Customer type must be selected";
}
The key aspect of this code is that CustomerViewModel's implementation of IDataErrorInfo can handle requests for ViewModel-specific property validation and delegate the other requests to the Customer object. This allows you to make use of validation logic in Model classes and have additional validation for properties that only make sense to ViewModel classes.
The ability to save a CustomerViewModel is available to a view through the SaveCommand property. That command uses the RelayCommand class examined earlier to allow CustomerViewModel to decide if it can save itself and what to do when told to save its state. In this application, saving a new customer simply means adding it to a CustomerRepository.Deciding if the new customer is ready to be saved requires consent from two parties. The Customer object must be asked if it is valid or not, and the Customer­ViewModel must decide if it is valid. This two-part decision is necessary because of the ViewModel-specific properties and validation examined previously. The save logic for Customer­ViewModel is shown inFigure 15.
 
// In CustomerViewModel.cs
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null)
        {
            _saveCommand = new RelayCommand(
                param => this.Save(),
                param => this.CanSave
                );
        }
        return _saveCommand;
    }
}

public void Save()
{
    if (!_customer.IsValid)
        throw new InvalidOperationException("...");

    if (this.IsNewCustomer)
        _customerRepository.AddCustomer(_customer);

    base.OnPropertyChanged("DisplayName");
}

bool IsNewCustomer
{
    get 
    { 
        return !_customerRepository.ContainsCustomer(_customer); 
    }
}

bool CanSave
{
    get 
    { 
        return 
            String.IsNullOrEmpty(this.ValidateCustomerType()) && 
            _customer.IsValid; 
    }
}
The use of a ViewModel here makes it much easier to create a view that can display a Customer object and allow for things like an "unselected" state of a Boolean property. It also provides the ability to easily tell the customer to save its state. If the view were bound directly to a Customer object, the view would require a lot of code to make this work properly. In a well-designed MVVM architecture, the codebehind for most Views should be empty, or, at most, only contain code that manipulates the controls and resources contained within that view. Sometimes it is also necessary to write code in a View's codebehind that interacts with a ViewModel object, such as hooking an event or calling a method that would otherwise be very difficult to invoke from the ViewModel itself.

All Customers View
The demo application also contains a workspace that displays all of the customers in a ListView. The customers in the list are grouped according to whether they are a company or a person. The user can select one or more customers at a time and view the sum of their total sales in the bottom right corner.
The UI is the AllCustomersView control, which renders an AllCustomersViewModel object.Each ListView­Item represents a CustomerViewModel object in the AllCustomers collection exposed by the AllCustomerViewModel object. In the previous section, you saw how a CustomerViewModel can render as a data entry form, and now the exact same CustomerViewModel object is rendered as an item in a ListView. The CustomerViewModel class has no idea what visual elements display it, which is why this reuse is possible.
AllCustomersView creates the groups seen in the ListView. It accomplishes this by binding the ListView's ItemsSource to a Collection­ViewSource configured like Figure 16.
 
<!-- In AllCustomersView.xaml -->
<CollectionViewSource
  x:Key="CustomerGroups" 
  Source="{Binding Path=AllCustomers}"
  >
  <CollectionViewSource.GroupDescriptions>
    <PropertyGroupDescription PropertyName="IsCompany" />
  </CollectionViewSource.GroupDescriptions>
  <CollectionViewSource.SortDescriptions>
    <!-- 
    Sort descending by IsCompany so that the ' True' values appear first,
    which means that companies will always be listed before people.
    -->
    <scm:SortDescription PropertyName="IsCompany" Direction="Descending" />
    <scm:SortDescription PropertyName="DisplayName" Direction="Ascending" />
  </CollectionViewSource.SortDescriptions>
</CollectionViewSource>
The association between a ListViewItem and a CustomerViewModel object is established by the ListView's ItemContainerStyle property. The Style assigned to that property is applied to each ListViewItem, which enables properties on a ListViewItem to be bound to properties on the CustomerViewModel. One important binding in that Style creates a link between the IsSelected property of a ListViewItem and the IsSelected property of a Customer­ViewModel, as seen here:
 
<Style x:Key="CustomerItemStyle" TargetType="{x:Type ListViewItem}">
  <!--   Stretch the content of each cell so that we can 
  right-align text in the Total Sales column.  -->
  <Setter Property="HorizontalContentAlignment" Value="Stretch" />
  <!-- 
  Bind the IsSelected property of a ListViewItem to the 
  IsSelected property of a CustomerViewModel object.
  -->
  <Setter Property="IsSelected" Value="{Binding Path=IsSelected, 
    Mode=TwoWay}" />
</Style>
When a CustomerViewModel is selected or unselected, that causes the sum of all selected customers' total sales to change. The AllCustomersViewModel class is responsible for maintaining that value, so that the ContentPresenter beneath the ListView can display the correct number. Figure 17 shows how AllCustomersViewModel monitors each customer for being selected or unselected and notifies the view that it needs to update the display value.
 
// In AllCustomersViewModel.cs
public double TotalSelectedSales
{
    get
    {
        return this.AllCustomers.Sum(
            custVM => custVM.IsSelected ? custVM.TotalSales : 0.0);
    }
}

void OnCustomerViewModelPropertyChanged(object sender, 
    PropertyChangedEventArgs e)
{
    string IsSelected = "IsSelected";

    // Make sure that the property name we're 
    // referencing is valid.  This is a debugging 
    // technique, and does not execute in a Release build.
    (sender as CustomerViewModel).VerifyPropertyName(IsSelected);

    // When a customer is selected or unselected, we must let the
    // world know that the TotalSelectedSales property has changed,
    // so that it will be queried again for a new value.
    if (e.PropertyName == IsSelected)
        this.OnPropertyChanged("TotalSelectedSales");
}
The UI binds to the TotalSelectedSales property and applies currency (monetary) formatting to the value. The ViewModel object could apply the currency formatting, instead of the view, by returning a String instead of a Double value from the TotalSelectedSales property. The ContentStringFormat property of ContentPresenter was added in the .NET Framework 3.5 SP1, so if you must target an older version of WPF, you will need to apply the currency formatting in code:
 
<!-- In AllCustomersView.xaml -->
<StackPanel Orientation="Horizontal">
  <TextBlock Text="Total selected sales: " />
  <ContentPresenter
    Content="{Binding Path=TotalSelectedSales}"
    ContentStringFormat="c"
  />
</StackPanel>

Wrapping Up
WPF has a lot to offer application developers, and learning to leverage that power requires a mindset shift. The Model-View-ViewModel pattern is a simple and effective set of guidelines for designing and implementing a WPF application. It allows you to create a strong separation between data, behavior, and presentation, making it easier to control the chaos that is software development.
I would like to thank John Gossman for his help with this article.

Josh Smith is passionate about using WPF to create great user experiences. He was awarded the Microsoft MVP title for his work in the WPF community. Josh works for Infragistics in the Experience Design Group. When he is not at a computer, he enjoys playing the piano, reading about history, and exploring New York City with his girlfriend. You can visit Josh's blog atjoshsmithonwpf.wordpress.com .

 
模式
WPF 应用程序使用程序的模型视图 ViewModel 设计模式
Josh Smith
 

本文讨论:
  • 模式和 WPF
  • MVP 模式
  • 为什么 MVVM 最好为 WPF
  • 构建与 MVVM 应用程序
本文涉及以下技术: 
WPF,数据绑定 
代码下载可从 MSDN 代码库 
浏览代码联机
专业的软件应用程序的 开发用户界面 不容易。 它可以是数据、 交互设计、 可视化设计、 连接,多线程处理、 安全性、 国际化、 验证、 单元测试和的 Voodoo 的触摸一个渴融合。 考虑用户界面公开基础系统的和必须满足其用户的不可预测的从句要求,它可以是最易失方面很多应用程序。
还有,可帮助 tame 此不实用的 beast 的常见设计模式,但正确分隔并解决问题的多种很难。在更复杂的模式是,越将快捷方式用于以后的破坏所有以前的努力执行的操作权限的方式。
不总是在设计模式,出现错误。 有时我们使用需要编写大量代码,因为在使用的 UI 平台不出借本身很好地简单模式的复杂的设计模式。 需要将是一个平台,更易于构建使用简单、 time-tested、 开发人员批准的设计模式的 UI 它。 幸运的是,Windows Presentation Foundation (WPF) 提供了完全的。
世界上继续增加的速度采用 WPF 在软件,WPF 社区已开发模式和实践自己生态的系统。 此文章中, 我将讨论一些用于设计和实现客户端应用程序与 WPF 这些最佳方法。 利用 WPF 结合模型-视图-ViewModel (MVVM) 设计模式) 的某些核心功能我将介绍的示例程序演示了如何简单也可以是构建 WPF 应用程序"正确方式"。
本文末尾它将会清除数据模板、 命令、 数据绑定,在资源系统和 MVVM 模式所有结合方式来创建一个简单、 可测试、 功能强大的框架,的任何 WPF 应用程序可以 thrive。 本文演示程序可以作为一个作为其核心体系结构使用 MVVM 实际 WPF 应用程序模板。 单元测试演示解决方案中的显示一组 ViewModel 类中存在的该功能时,测试应用程序的用户界面的功能是多么容易。 深入详细信息之前, 一下为什么应首先使用像 MVVM 模式。

订单与混乱
是不必要的无法在简单"Hello,World !"程序中使用设计模式。 任何 competent 开发人员可以了解几行代码一眼。 但是,随着在程序中的功能的数的增加的代码和移动部件的行数增加相应。 最终,系统和它所包含的重复问题的复杂性鼓励开发人员可以组织方式这样做还会更便于他们代码全世界、 讨论、 扩展,并解决问题。 我们通过将已知的名称应用到在源代码中的特定实体降低复杂系统的认知的混乱。 我们确定名称以通过在系统中考虑其职能角色应用于一段代码。
开发人员经常故意构造一个设计模式相对于让我们可以看到 organically 文本模式根据其代码。是什么不对的方法,但是本文中, 我检查显式使用 MVVM 为 WPF 应用程序的体系结构的好处。某些类别的名称包括从 MVVM 模式如结尾"ViewModel,如果类是视图的抽象的已知条件。 此方法有助于避免认知前面提到的混乱。 相反,您可以令人高兴的是存在是大多数专业软件开发项目中的事件的自然状态的控制混乱的状态 !

模型视图 ViewModel 的演变
ever 自人创建软件用户界面,已为了使更容易的常见设计模式。 是例如 Model-视图-演示者 (MVP) 模式已欣赏各种用户界面编程平台上的普及。 MVP 是模型-视图-控制器模式已为数十年的变体。 如果还不 MVP 模式之前用以下是简化的说明。 在屏幕上看到为视图、 显示的数据是模型,和演示者一起挂钩两个。 视图依赖于要填充模型数据,请对用户输入做出反应,提供输入的验证 (可能通过委派到模型) 和其他此类任务的演示者。 如果您希望了解有关模型查看演示者,我建议您阅读 Jean-Paul Boodhoo 2006 年 8 月设计模式列 .
在 2004,Martin Fowler 发布有关命名模式的文章 演示文稿模型 (PM)。 分开的行为和状态视图,PM 模式与类似 MVP。 值得关注的 PM 模式部分是视图的抽象创建,称为演示文稿模型。一个的视图将,成为只是演示文稿模型的呈现。 在 Fowler 的解释他显示演示文稿模型频繁地更新其视图,以便两个保持与彼此保持同步。 该同步逻辑存在演示文稿模型类中的代码。
2005 中, 当前的 WPF 和 Silverlight 架构师,在 Microsoft,一个的 John Gossman unveiled 在模型-视图-ViewModel (MVVM) 模式 在他的博客。 MVVM 是与 Fowler 的演示文稿模型,这两种模式功能一个视图包含视图的状态和行为的抽象。 fowler 而 Gossman 作为标准化可以利用 WPF 来简化用户界面创建的核心功能引入 MVVM,作为一种创建 UI 的独立于平台的抽象一个的视图引入演示文稿模型。 此种意义上讲,我认为 MVVM 为更多常规 PM 图案,tailor-made WPF 和 Silverlight 平台的一个特例。
Glenn 块的极好文章"中 构建复合应用程序与 WPF 的 prism: 模式 "2008 年 9 月刊中, 他说明为 WPF Microsoft 复合应用程序指导。 ViewModel 从未使用过的术语。 相反,术语演示文稿模型用于描述视图的抽象。 在本文,但是,我将引用 MVVM 和视图的抽象,作为一个 ViewModel 模式。 我发现此术语是 WPF 和 Silverlight 社区中的更多 prevelant。
与 MVP 中的对演示者不同一个 ViewModel 不需要对视图的引用。 视图绑定到一个 ViewModel 这反过来,公开模型对象和其他状态特定于视图中包含的数据的属性中。 视图和 ViewModel 之间绑定是简单构造由于一个 ViewModel 对象被设置为视图的 DataContext。 如果属性值在 ViewModel 更改,这些新值自动传播到通过数据绑定的视图。 当用户单击一个按钮在视图时, 在 ViewModel 的命令将执行执行所请求的操作。 ViewModel,永远不会在视图,执行模型数据所做的所有修改。
在的视图类有模型类存在,不知道该视图的 ViewModel 和模型时不知道。 实际上,模型是完全 oblivious 事实存在 ViewModel 和视图。 这是最松散耦合设计,多种方式支付股利,正如您很快就将看到的。

为什么 WPF 开发人员喜欢 MVVM
一旦开发人员成为熟悉 WPF 和 MVVM,很难区分这两者。 MVVM 是 WPF 开发人员的语言 franca,因为它是适合在 WPF 平台 WPF 为了方便地构建应用程序使用 MVVM 模式 (在其他)。 实际上,Microsoft 使用 MVVM 内部开发 WPF 应用程序,Microsoft Expression Blend,如,核心 WPF 平台时正在建设中。 WPF,如外观不控制模型和数据模板的许多方面使用显示的状态和行为的 MVVM 提升强的分离。
在单个的最重要方面,WPF 使 MVVM 好模式使用的是数据绑定基础结构。 由一个 ViewModel 的视图的绑定属性,您获得二者之间的松散耦合,并完全删除需要一个 ViewModel 直接更新视图中编写代码。 数据绑定系统还支持提供了标准化的方式传输到视图的验证错误的输入的验证。
两个其他功能做这种模式因此可用的是 WPF 的数据模板和资源系统。 数据模板应用于在用户界面中显示的 ViewModel 对象的视图。 可以声明在 XAML 中的模板,并让资源系统自动查找并为您应用这些模板,在运行时。 您可以了解详细有关绑定和我 7 月 2008 文章中的数据模板"数据和 WPF: 使用数据绑定和 WPF 中自定义数据显示 ."
如果未在 WPF 中的命令的支持中,MVVM 模式是得强大。 本文,我将介绍如何在 ViewModel 可以公开一个的视图的命令从而使视图以使用它的功能。 如果您不熟悉控制,我建议您阅读 Brian Noyes 全面文章" 高级 WPF: 了解路由事件和 WPF 中的命令 "摘自 2008 年 9 月刊。
除了在 WPF (和 Silverlight 2) 功能,使一个自然的方式构建应用程序的 MVVM,模式也是受欢迎,因为 ViewModel 类是易于单元测试。 应用程序的交互逻辑居住在一组 ViewModel 类中时, 可以轻松地编写测试它的代码。 在一个的意义上的视图和单元测试两个不同类型类型均 ViewModel 使用者。 为应用程序的 ViewModels 有一套测试的提供忙 / 快速回归测试,有助于降低维护应用程序随着时间的成本。
除了提升自动的回归测试的创建,ViewModel 类的 testability 可以帮助正确设计用户界面,可以很容易地外观。 在设计应用程序时您通常可以决定是否内容应在视图和要编写单元测试可以占用该 ViewModel 通过 imagining ViewModel。 如果您可以在 ViewModel 的编写单元测试,而不创建任何 UI 对象,因为它不有特定的可视元素上的任何依赖项还完全可以外观,ViewModel。
最后的开发人员使用可视化设计器,使用 MVVM 使得更易于创建平滑的设计器 / Developer 工作流。 由于视图是只需一个任意消费者一个 ViewModel,它很容易就翻录出的一个视图和要呈现一个 ViewModel 新视图中的下拉。 此简单步骤允许快速原型和用户界面由设计器的计算。
开发团队可以专注于创建功能强大的 ViewModel 类和设计团队可以集中精力进行用户友好的视图。 连接两个团队的输出可能涉及小超过确保正确绑定存在视图的 XAML 文件中。

演示应用程序
到目前为止我已审阅 MVVM 的历史记录和操作的理论。 我还检查为何如此流行间 WPF 开发人员。 现在,它是以汇总您袖子,并查看模式中的时间。 本文附带演示应用程序使用 MVVM 各种方式。 它提供示例,以帮助置于一个有意义的上下文的概念 fertile 的源。 我是在 Visual Studio SP 2008 1,与 Microsoft.NET Framework 3.5 SP 1 中创建的演示应用程序。 Visual Studio 单元测试系统中,运行单元测试。
应用程序可以包含任意数量的工作"区,"用户可以打开每个通过单击在左侧导航区域中的命令链接。 所有工作区主内容区域 Live 一个 TabControl 中。 通过单击该工作区选项卡项目上的关闭按钮,用户可关闭工作区。 该应用程序有两个可用的工作区:"All Customers"和"新客户"。 运行该应用程序并打开某些工作区后, 在用户界面类似 图 1 
图 1 工作区
"All Customers"工作区的只有一个实例可以一次,但任意数量的"新客户"打开工作区可以打开一次。 如果用户决定创建新客户,用户必须填写 图 2 中的数据项表单。
图 2 新客户数据输入表单
填写数据输入表单具有有效的值,并单击保存按钮中,新客户的名称将显示在选项卡后项目和客户添加到所有客户的列表。 应用程序没有删除或编辑现有客户的支持,但该的功能和许多类似,其他特性很容易通过构建实现现有应用程序体系结构的顶部。 现在,有一个高级别的了解演示应用程序的用途,让我们研究如何设计并实现。

中继命令逻辑
在应用程序中的每个视图具有除外的类的构造函数中调用 InitializeComponent 在标准的样板代码为空的源代码文件。 事实上,可以从项目中删除视图的代码隐藏文件和应用程序仍将编译和正常运行。 尽管视图中的事件处理方法缺乏当用户单击按钮上, 时应用程序的反应并满足用户的请求。 这是因已建立的绑定适用于超链接、 按钮和 MenuItem 控件显示在用户界面中的 Command 属性。 这些绑定确保当用户单击控件上,ICommand 对象公开,ViewModel 将执行。 可以将 Command 对象视为更易于使用从视图在 XAML 中声明的 ViewModel 的功能的适配器。
当一个 ViewModel 公开实例属性的类型 I­command 时,Command 对象将通常使用 ViewModel 对象来获取完成其工作。 一个可能的实现模式是创建在 ViewModel 类中的私有嵌套的类,以便命令有权访问其包含 ViewModel 的私有成员并不会 pollute 命名空间。 该嵌套的类实现该 ICommand 接口,并对包含 ViewModel 对象的引用注入其构造函数。 但是,创建 ICommand 实现对于由一个 ViewModel 提供每个命令的嵌套的类可以 bloat ViewModel 类的大小。 更多代码意味着更高版本的可能的错误。
演示应用程序中 RelayCommand 类解决了这个问题。 RelayCommand 允许您将通过传递给其构造函数的委托的命令的逻辑。 此方法允许简洁、 简洁命令实现 ViewModel 类中。RelayCommand 是中找到的 DelegateCommand 的简化变体, Microsoft 复合应用程序库 .relay­command 类如 图 3 所示。
 
public class RelayCommand : ICommand
{
    #region Fields

    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;        

    #endregion // Fields

    #region Constructors

    public RelayCommand(Action<object> execute)
    : this(execute, null)
    {
    }

    public RelayCommand(Action<object> execute, Predicate<object> canExecute)
    {
        if (execute == null)
            throw new ArgumentNullException("execute");

        _execute = execute;
        _canExecute = canExecute;           
    }
    #endregion // Constructors

    #region ICommand Members

    [DebuggerStepThrough]
    public bool CanExecute(object parameter)
    {
        return _canExecute == null ? true : _canExecute(parameter);
    }

    public event EventHandler CanExecuteChanged
    {
        add { CommandManager.RequerySuggested += value; }
        remove { CommandManager.RequerySuggested -= value; }
    }

    public void Execute(object parameter)
    {
        _execute(parameter);
    }

    #endregion // ICommand Members
}
CanExecuteChanged 事件属于该 ICommand 接口实现具有一些有趣的功能。 该委托 CommandManager.RequerySuggested 事件事件订阅。 这可以确保 WPF 控制基础结构要求所有 RelayCommand 对象如果它们可以执行它要求内置命令时。 以下代码从 CustomerViewModel 类我将探讨详细更高版本,说明如何配置一个 RelayCommand 与 lambda 表达式:
 
RelayCommand _saveCommand;
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null)
        {
            _saveCommand = new RelayCommand(param => this.Save(),
                param => this.CanSave );
        }
        return _saveCommand;
    }
}

ViewModel 类层次结构
大多数 ViewModel 类需要相同的功能。 它们通常需要实现 INotifyPropertyChanged 接口,它们通常需要对一个用户友好的显示名称,并且在工作区的情况下它们需要能够关闭 (这就是从用户界面中删除)。 此问题自然适合以 ViewModel 基类的两个,创建以便新 ViewModel 类可以从基类继承的所有常见的功能。 ViewModel 类窗体继承层次结构 图 4 所示。
图 4 继承层次结构
所有您 ViewModels 需从基类不包括是必要条件。 如果您希望通过一起,撰写许多较小的类,而不是使用继承,获得您的类中的功能的不是问题。 像其他任何设计模式 MVVM 是一个指南,不规则组。

ViewModelBase 类
ViewModelBase 是根类别在层次结构是原因它实现常用的 INotifyPropertyChanged 接口,并具有 DisplayName 属性中。 INotifyPropertyChanged 接口包含名为 PropertyChanged 的事件。只要 ViewModel 对象上的一个属性具有新值,它可以引发通知新值的 WPF 绑定系统 PropertyChanged 事件。 接收的通知,时绑定系统查询属性,并在某些用户界面元素将绑定的属性接收新值。
为了了解 ViewModel 对象的属性已更改的 WPF PropertyChangedEventArgs 类将公开 String 类型的一个 PropertyName 属性。 您必须注意,将正确的属性名称传递到该事件参数 ; 否则,WPF 将得到查询新值不正确的属性。
ViewModelBase 的一个有趣方面是它能够验证一个属性具有给定名称确实存在 ViewModel 对象上。 这是非常有用重构时, 因为更改通过 Visual Studio 2008 重构功能的属性的名称不会更新在源代码中会包含该属性名称的字符串也 (不应它)。 事件参数会导致很难跟踪,因此此很少的功能可以极大的 timesaver 的细微错误,请引发 PropertyChanged 事件与不正确的属性名称。 添加此有用的支持的 ViewModelBase 了代码如 图 5 所示。
 
// In ViewModelBase.cs
public event PropertyChangedEventHandler PropertyChanged;

protected virtual void OnPropertyChanged(string propertyName)
{
    this.VerifyPropertyName(propertyName);

    PropertyChangedEventHandler handler = this.PropertyChanged;
    if (handler != null)
    {
        var e = new PropertyChangedEventArgs(propertyName);
        handler(this, e);
    }
}

[Conditional("DEBUG")]
[DebuggerStepThrough]
public void VerifyPropertyName(string propertyName)
{
    // Verify that the property name matches a real,  
    // public, instance property on this object.
    if (TypeDescriptor.GetProperties(this)[propertyName] == null)
    {
        string msg = "Invalid property name: " + propertyName;

        if (this.ThrowOnInvalidPropertyName)
            throw new Exception(msg);
        else
            Debug.Fail(msg);
    }
}

CommandViewModel 类
最简单的具体 ViewModelBase 子类是 CommandViewModel。 它公开一个名为类型 I­command 的命令属性。 MainWindowViewModel 公开这些对象通过其命令属性的集合。 主窗口的左侧导航区域显示一个链接的每个 CommandViewModel 公开 MainWindowView­model,如"查看所有客户"和"创建新客户"。 当用户单击链接时,从而执行某个这些命令在工作区打开 TabControl 在主窗口中。 command­ViewModel 类定义所示:
 
public class CommandViewModel : ViewModelBase
{
    public CommandViewModel(string displayName, ICommand command)
    {
        if (command == null)
            throw new ArgumentNullException("command");

        base.DisplayName = displayName;
        this.Command = command;
    }

    public ICommand Command { get; private set; }
}
MainWindowResources.xaml 文件中存在的关键是"CommandsTemplate 一个 data­template。MainWindow 使用该模板呈现 CommandViewModels 前面提到的集合。 该模板只呈现为一个 ItemsControl 中的链接的每个 CommandViewModel 对象。 每个超链接的 Command 属性绑定到一个 command­ViewModel 的 Command 属性。 该 XAML 如 图 6 所示。
 
<!-- In MainWindowResources.xaml -->
<!--
This template explains how to render the list of commands on 
the left side in the main window (the 'Control Panel' area).
-->
<DataTemplate x:Key="CommandsTemplate">
  <ItemsControl ItemsSource="{Binding Path=Commands}">
    <ItemsControl.ItemTemplate>
      <DataTemplate>
        <TextBlock Margin="2,6">
          <Hyperlink Command="{Binding Path=Command}">
            <TextBlock Text="{Binding Path=DisplayName}" />
          </Hyperlink>
        </TextBlock>
      </DataTemplate>
    </ItemsControl.ItemTemplate>
  </ItemsControl>
</DataTemplate>

MainWindowViewModel 类
为在类关系图中以前看到,WorkspaceViewModel 类派生自 ViewModelBase,并添加功能关闭。 通过"关闭"我意味着内容删除工作区用户界面在运行时。 三个类派生 WorkspaceViewModel: MainWindowViewModel,AllCustomersViewModel,和 CustomerViewModel。 MainWindowViewModel 的请求以关闭由创建该 MainWindow 和其 ViewModel,如 图 7 所示在应用程序类处理。
 
// In App.xaml.cs
protected override void OnStartup(StartupEventArgs e)
{
    base.OnStartup(e);

    MainWindow window = new MainWindow();

    // Create the ViewModel to which 
    // the main window binds.
    string path = "Data/customers.xml";
    var viewModel = new MainWindowViewModel(path);

    // When the ViewModel asks to be closed, 
    // close the window.
    viewModel.RequestClose += delegate 
    { 
        window.Close(); 
    };

    // Allow all controls in the window to 
    // bind to the ViewModel by setting the 
    // DataContext, which propagates down 
    // the element tree.
    window.DataContext = viewModel;

    window.Show();
}
MainWindow 包含其命令属性绑定到 MainWindowViewModel 的 CloseCommand 属性的菜单项。 当用户单击该菜单项时,应用程序类响应,通过调用窗口的 Close 方法,如下:
 
<!-- In MainWindow.xaml -->
<Menu>
  <MenuItem Header="_File">
    <MenuItem Header="_Exit" Command="{Binding Path=CloseCommand}" />
  </MenuItem>
  <MenuItem Header="_Edit" />
  <MenuItem Header="_Options" />
  <MenuItem Header="_Help" />
</Menu>
MainWindowViewModel 包含的名为工作区的 WorkspaceViewModel 对象的一个可见的集合。在主窗口中包含的 ItemsSource 属性绑定到该集合在 TabControl。 每个选项卡项目都有的 Command 属性绑定到其相应的 WorkspaceViewModel 实例的 CloseCommand 关闭按钮。 配置选项卡的每一项的模板的 abridged 的版本所示的代码。 代码位于 MainWindowResources.xaml,,并在模板说明如何呈现带有关闭按钮的选项卡项:
 
<DataTemplate x:Key="ClosableTabItemTemplate">
  <DockPanel Width="120">
    <Button
      Command="{Binding Path=CloseCommand}"
      Content="X"
      DockPanel.Dock="Right"
      Width="16" Height="16" 
      />
    <ContentPresenter Content="{Binding Path=DisplayName}" />
  </DockPanel>
</DataTemplate>
当用户单击 workspace­ViewModel 的 CloseCommand 执行导致其 request­Close 事件触发的选项卡项目中关闭按钮。 MainWindowViewModel 监视其工作区的 RequestClose 事件,并从请求 Workspaces 集合中删除工作区。 因为 main­window 的 TabControl 有绑定到 WorkspaceViewModels 的可见集合的 ItemsSource 属性,从集合中删除项目会导致从 TabControl 删除相应的工作区。 图 8 显示从 main­WindowViewModel 的逻辑。
 
// In MainWindowViewModel.cs

ObservableCollection<WorkspaceViewModel> _workspaces;

public ObservableCollection<WorkspaceViewModel> Workspaces
{
    get
    {
        if (_workspaces == null)
        {
            _workspaces = new ObservableCollection<WorkspaceViewModel>();
            _workspaces.CollectionChanged += this.OnWorkspacesChanged;
        }
        return _workspaces;
    }
}

void OnWorkspacesChanged(object sender, NotifyCollectionChangedEventArgs e)
{
    if (e.NewItems != null && e.NewItems.Count != 0)
        foreach (WorkspaceViewModel workspace in e.NewItems)
            workspace.RequestClose += this.OnWorkspaceRequestClose;

    if (e.OldItems != null && e.OldItems.Count != 0)
        foreach (WorkspaceViewModel workspace in e.OldItems)
            workspace.RequestClose -= this.OnWorkspaceRequestClose;
}

void OnWorkspaceRequestClose(object sender, EventArgs e)
{
    this.Workspaces.Remove(sender as WorkspaceViewModel);
}
在 UnitTests 项目中 MainWindowViewModelTests.cs 文件将包含验证这一功能正常的测试方法。 轻松使用它可以创建 ViewModel 类的单元测试是该 MVVM 模式一个大销售点,因为它允许进行简单测试的应用程序功能无需编写代码与用户界面。 该测试方法如 图 9 所示。
 
// In MainWindowViewModelTests.cs
[TestMethod]
public void TestCloseAllCustomersWorkspace()
{
    // Create the MainWindowViewModel, but not the MainWindow.
    MainWindowViewModel target = 
        new MainWindowViewModel(Constants.CUSTOMER_DATA_FILE);

    Assert.AreEqual(0, target.Workspaces.Count, "Workspaces isn't empty.");

    // Find the command that opens the "All Customers" workspace.
    CommandViewModel commandVM = 
        target.Commands.First(cvm => cvm.DisplayName == "View all customers");

    // Open the "All Customers" workspace.
    commandVM.Command.Execute(null);
    Assert.AreEqual(1, target.Workspaces.Count, "Did not create viewmodel.");

    // Ensure the correct type of workspace was created.
    var allCustomersVM = target.Workspaces[0] as AllCustomersViewModel;
    Assert.IsNotNull(allCustomersVM, "Wrong viewmodel type created.");

    // Tell the "All Customers" workspace to close.
    allCustomersVM.CloseCommand.Execute(null);
    Assert.AreEqual(0, target.Workspaces.Count, "Did not close viewmodel.");
}

将视图应用于一个 ViewModel
MainWindowViewModel 间接添加并删除与主窗口 tab­control workspace­ViewModel 对象。 利用数据绑定,一个 TabItem 的 Content 属性接收 ViewModelBase 派生对象,以显示。ViewModelBase 不是一个 UI 元素,因此它具有不用于呈现其自身的内在支持。 默认,在 WPF 中非 Visual 对象呈现在 TextBlock 中显示其 ToString 方法调用的结果。 清楚地是不是需要除非您的用户具有可以看到我们 ViewModel 类的类型名称的刻录要求 !
您可以很容易地判断 WPF 如何通过呈现一个 ViewModel 对象键入 DataTemplates。 类型化的 DataTemplate 没有分配给它的 x: Key 值,但它不会将设置为类型类的实例其数据类型属性。如果 WPF 尝试呈现您 ViewModel 对象之一,它将检查,请参阅如果资源系统具有类型化的 DataTemplate 作用域中的数据类型是相同 (或的基类) 您 ViewModel 对象的类型。 如果找到,它将使用该模板呈现引用的选项卡该项的内容属性 ViewModel 对象。
MainWindowResources.xaml 文件具有一个 resource­dictionary。 该词典添加到主窗口的资源层次这意味着它所包含的资源位于窗口的资源作用域。 当选项卡项的内容设置为一个 ViewModel 对象时,此词典中的类型化的 DataTemplate 提供视图 (这就是用户控件) 来呈现其,如 图 10 所示。
 
<!-- 
This resource dictionary is used by the MainWindow. 
-->
<ResourceDictionary
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:vm="clr-namespace:DemoApp.ViewModel"
  xmlns:vw="clr-namespace:DemoApp.View"
  >

  <!-- 
  This template applies an AllCustomersView to an instance 
  of the AllCustomersViewModel class shown in the main window.
  -->
  <DataTemplate DataType="{x:Type vm:AllCustomersViewModel}">
    <vw:AllCustomersView />
  </DataTemplate>

  <!-- 
  This template applies a CustomerView to an instance  
  of the CustomerViewModel class shown in the main window.
  -->
  <DataTemplate DataType="{x:Type vm:CustomerViewModel}">
    <vw:CustomerView />
  </DataTemplate>

 <!-- Other resources omitted for clarity... -->

</ResourceDictionary>
您不必编写任何代码确定该视图以显示一个 ViewModel 对象。 WPF 资源系统将会繁重的工作的所有为您在您释放到重点更重要的事情。 更复杂的情况可能会以编程方式选择视图但在大多数情况下不必要的。

在数据模型和存储库
您已经了解如何加载、 显示,和关闭应用程序外壳程序 ViewModel 对象。 现在,常规管道就地,您可以查看到应用程序的域更具体的实现细节。 深入了解应用程序的两个 Workspaces"All Customers"之前,"新客户"让我们首先检查数据模型和数据访问类。 这些类的设计无关几乎与在 MVVM 模式因为您可以创建一个 ViewModel 类,以适应到内容为 WPF 的友好的几乎任何数据对象。
唯一的模型类演示程序中是客户。 此类有少量的属性表示公司如其名、 上次的用户名和电子邮件地址的客户的信息。 它通过实现年 WPF 点击该街道之前存在的该标准 IDataErrorInfo 接口提供验证信息。 客户类中建议的 MVVM 体系结构中或甚至 WPF 应用程序正在使用它的包含执行任何操作。 类可以轻松地来自旧业务库。
数据必须来自并驻留在某处。 在此应用程序,CustomerRepository 类的实例加载,并存储所有的 Customer 对象。 发生从一个 XML 文件加载客户数据,但外部数据源的类型是不相关。 数据可能来自数据库、 Web 服务、 命名的管道、 磁盘,或偶数运营商 pigeons 上的文件: 只是不重要。 只要无论它来自,在其中,具有.NET 对象的某些数据,MVVM 模式就可以在屏幕上获得该数据。
CustomerRepository 类公开,允许您获取所有可用的客户对象的几个方法添加新客户到存储库,并检查是否客户是否已在存储库中。 由于应用程序不允许用户删除客户,存储库不允许删除客户。 当新的客户将 CustomerAdded 事件激发输入 CustomerRepository,通过 AddCustomer 方法。
清楚地,此应用程序数据模型是非常小与实际的业务应用程序要求什么,但的不是重要。 是了解是如何 ViewModel 类创建使用客户和 CustomerRepository。 请注意该 customer­ViewModel 是客户对象周围的包装。 它提供了一个的客户状态和其他由 customer­view 控件的通过一组属性的状态。 CustomerViewModel 重复的客户的状态 ; 它只是公开它通过委派,如下:
 
public string FirstName
{
    get { return _customer.FirstName; }
    set
    {
        if (value == _customer.FirstName)
            return;
        _customer.FirstName = value;
        base.OnPropertyChanged("FirstName");
    }
}
当用户创建新的客户,并单击中 CustomerView 控件的保存按钮时,在 Customer­ViewModel 与关联的视图将添加新的客户对象在 customer­repository。 导致的 CustomerAdded 事件触发,它允许知道它应到其 AllCustomers 集合中添加新的 Customer­ViewModel 的 AllCustomers­ViewModel。 一种意义上讲 Customer­repository 充当 Customer 对象所处理的各种 ViewModels 之间的同步机制。 可能是一个可能将这视为使用中介设计模式。 我会查看多个工作在即将进行的部分,但现在方式引用到 图 11 中图表方式的高级了解所有代码段结合。
图 11 客户关系

新客户数据输入窗体
当用户单击"创建新客户"链接时,MainWindowViewModel 将添加到其列表的工作区中, 新的 CustomerViewModel 并 CustomerView 控件显示。 用户输入到输入域的有效的值后,以便用户可以保持新的客户信息保存按钮就会进入启用的状态。 没有在普通此处,只是常规数据输入表单与输入验证和保存按钮。
客户该类具有支持,可通过其 IDataErrorInfo 接口实现的内置验证。 将确保验证 (客户具有第一个名称标准格式的电子邮件地址,且,客户为人员,姓氏。 如果客户的 IsCompany 属性返回 True,LastName 属性不能在 (其目的所在的公司没有某一姓名) 具有一个值。 此验证逻辑可能意义从客户对象的角度但它不能满足用户界面的需要。 在用户界面要求在用户选择新的客户是个人或公司。 客户类型选择器最初具有值"(不指定)"。 如何可以在用户界面告诉用户是否为 True 或 False 值只允许客户的将 IsCompany 属性,客户类型是未指定?
假定在您有完全控制整个软件系统,可以更改 IsCompany 属性类型可空 <bool>,这将允许为"未选定"的值。 但是,实际不总是如此简单。 假设您不能更改客户类,因为它来自旧库归您的公司中的不同团队。 如果有是不容易方式保留的"未选中"值因现有的数据库架构? 如果其他应用程序已使用客户类,并依赖一个普通的 Boolean 值: 属性? 再一次遇到一个 ViewModel 提供到该恢复。
图 12 中的,测试方法显示此功能工作方式 CustomerViewModel。 以便客户类型选择器有三个字符串显示,CustomerViewModel 将公开一个 CustomerTypeOptions 属性。 它还公开一个客户类型属性选择器中存储所选的字符串。 设置客户类型时, 它将映射到基础客户对象的 IsCompany 属性布尔值的字符串值。 图 13 显示了两个属性。
 
// In CustomerViewModelTests.cs
[TestMethod]
public void TestCustomerType()
{
    Customer cust = Customer.CreateNewCustomer();
    CustomerRepository repos = new CustomerRepository(
        Constants.CUSTOMER_DATA_FILE);
    CustomerViewModel target = new CustomerViewModel(cust, repos);

    target.CustomerType = "Company"
    Assert.IsTrue(cust.IsCompany, "Should be a company");

    target.CustomerType = "Person";
    Assert.IsFalse(cust.IsCompany, "Should be a person");

    target.CustomerType = "(Not Specified)";
    string error = (target as IDataErrorInfo)["CustomerType"];
    Assert.IsFalse(String.IsNullOrEmpty(error), "Error message should 
        be returned");
}
 
// In CustomerViewModel.cs

public string[] CustomerTypeOptions
{
    get
    {
        if (_customerTypeOptions == null)
        {
            _customerTypeOptions = new string[]
            {
                "(Not Specified)",
                "Person",
                "Company"
            };
        }
        return _customerTypeOptions;
    }
}
public string CustomerType
{
    get { return _customerType; }
    set
    {
        if (value == _customerType || 
            String.IsNullOrEmpty(value))
            return;

        _customerType = value;

        if (_customerType == "Company")
        {
            _customer.IsCompany = true;
        }
        else if (_customerType == "Person")
        {
            _customer.IsCompany = false;
        }

        base.OnPropertyChanged("CustomerType");
        base.OnPropertyChanged("LastName");
    }
}
CustomerView 控件包含 ComboBox 绑定到这些属性,此处看到:
 
<ComboBox 
  ItemsSource="{Binding CustomerTypeOptions}"
  SelectedItem="{Binding CustomerType, ValidatesOnDataErrors=True}"
  />
该组合框中的选定的项目更改时, 数据源的 IDataErrorInfo 接口被查询新值有效。 原因,SelectedItem 属性绑定具有 ValidatesOnDataErrors 设置为 True。 由于数据源是一个 customer­ViewModel 对象,绑定系统验证错误要求该 customer­ViewModel 客户类型属性。 大多数情况下,CustomerViewModel 委托客户对象,它包含验证错误的所有请求。 但是,因为客户有 IsCompany 属性的未选中的状态的没有概念,CustomerViewModel 类必须处理验证 ComboBox 控件中新的选定的项目。 该代码会出现 图 14 中。
 
// In CustomerViewModel.cs
string IDataErrorInfo.this[string propertyName]
{
    get
    {
        string error = null;

        if (propertyName == "CustomerType")
        {
            // The IsCompany property of the Customer class 
            // is Boolean, so it has no concept of being in
            // an "unselected" state. The CustomerViewModel
            // class handles this mapping and validation.
            error = this.ValidateCustomerType();
        }
        else
        {
            error = (_customer as IDataErrorInfo)[propertyName];
        }

        // Dirty the commands registered with CommandManager,
        // such as our Save command, so that they are queried
        // to see if they can execute now.
        CommandManager.InvalidateRequerySuggested();

        return error;
    }
}

string ValidateCustomerType()
{
    if (this.CustomerType == "Company" ||
       this.CustomerType == "Person")
        return null;

    return "Customer type must be selected";
}
此代码的关键的方面是 IDataErrorInfo CustomerViewModel 的实现可以处理 ViewModel 特定属性验证的请求委派客户对象在其他请求。 这允许您使用的模型的类中的验证逻辑和具有用于仅意义 ViewModel 类的属性的附加验证。
能够保存一个 CustomerViewModel 是可用于通过 SaveCommand 属性的视图。 该命令使用 RelayCommand 类之前检查,以允许 CustomerViewModel 决定如果它可以保存本身以及告诉保存其状态时执行。 在此应用程序,保存新的客户只是意味着将其添加到一个 CustomerRepository。 决定新的客户是否已准备好保存需要双方的许可。 客户对象必须是要求是否它是有效的),或未,而在 customer­ViewModel 必须确定它是否有效。 此两部分决定是需要因为 ViewModel 特定属性和检查以前的验证。 图 15 显示 Customer­ViewModel 在保存逻辑。
 
// In CustomerViewModel.cs
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null)
        {
            _saveCommand = new RelayCommand(
                param => this.Save(),
                param => this.CanSave
                );
        }
        return _saveCommand;
    }
}

public void Save()
{
    if (!_customer.IsValid)
        throw new InvalidOperationException("...");

    if (this.IsNewCustomer)
        _customerRepository.AddCustomer(_customer);

    base.OnPropertyChanged("DisplayName");
}

bool IsNewCustomer
{
    get 
    { 
        return !_customerRepository.ContainsCustomer(_customer); 
    }
}

bool CanSave
{
    get 
    { 
        return 
            String.IsNullOrEmpty(this.ValidateCustomerType()) && 
            _customer.IsValid; 
    }
}
将此处的 ViewModel 使得更易于创建视图,可以显示客户对象和等待等 Boolean 类型的值的属性的"未选中"状态。 它还提供能够很容易地判断客户保存其状态。 如果直接对客户对象绑定视图,视图将需要大量代码来完成此工作正常。 设计良好的 MVVM 体系结构中大多数视图的源代码应为空,或者,最,只包含操作控件和该视图中包含资源的代码。 有时也是需要在视图的源代码与如挂接事件 ViewModel 对象交互中编写代码,或者调用方法本来会很难从该 ViewModel 本身调用。

所有客户都查看
演示应用程序还包含工作区,ListView 中显示所有的客户。 在列表客户的分组方式根据指明它们是否公司或个人。 用户可以一次选择一个或多个客户,并在右下角中查看其总销售额的总和。
在用户界面是在 AllCustomersView 为控件它们呈现 AllCustomersViewModel 对象。 每个 ListView­item 代表 CustomerViewModel 对象由 AllCustomerViewModel 对象公开 AllCustomers 集合中。 在上一的节您了解如何在 CustomerViewModel 可以呈现为数据输入窗体,并完全相同的 CustomerViewModel 对象在视为 ListView 中的一个项目的呈现现在。CustomerViewModel 该类具有不知道哪些可视元素显示,这就是这种重用是可能的原因。
AllCustomersView 创建 ListView 中的组。 这是通过绑定到 图 16 像配置一个 collection­ViewSource 的 ListView 的 ItemsSource 完成的。
 
<!-- In AllCustomersView.xaml -->
<CollectionViewSource
  x:Key="CustomerGroups" 
  Source="{Binding Path=AllCustomers}"
  >
  <CollectionViewSource.GroupDescriptions>
    <PropertyGroupDescription PropertyName="IsCompany" />
  </CollectionViewSource.GroupDescriptions>
  <CollectionViewSource.SortDescriptions>
    <!-- 
    Sort descending by IsCompany so that the ' True' values appear first,
    which means that companies will always be listed before people.
    -->
    <scm:SortDescription PropertyName="IsCompany" Direction="Descending" />
    <scm:SortDescription PropertyName="DisplayName" Direction="Ascending" />
  </CollectionViewSource.SortDescriptions>
</CollectionViewSource>
ListView 的 ItemContainerStyle 属性建立一个 ListViewItem 和 CustomerViewModel 对象之间的关联。 样式分配给该属性应用于每个 ListViewItem 使属性在 CustomerViewModel 上绑定到属性的 ListViewItem。 该样式中的一个重要绑定会创建一个 ListViewItem 的 IsSelected 属性和 customer­ViewModel 下面看到的 IsSelected 属性之间的一个链接:
 
<Style x:Key="CustomerItemStyle" TargetType="{x:Type ListViewItem}">
  <!--   Stretch the content of each cell so that we can 
  right-align text in the Total Sales column.  -->
  <Setter Property="HorizontalContentAlignment" Value="Stretch" />
  <!-- 
  Bind the IsSelected property of a ListViewItem to the 
  IsSelected property of a CustomerViewModel object.
  -->
  <Setter Property="IsSelected" Value="{Binding Path=IsSelected, 
    Mode=TwoWay}" />
</Style>
当一个 CustomerViewModel 是选择或取消选择时,将导致更改的所有选定的客户的总销售额的总和。 AllCustomersViewModel 类是负责维护值的以便下 ListView ContentPresenter 可以显示正确的号码。 图 17 显示如何 AllCustomersViewModel 监视每个客户所选择或取消选择和通知需要更新显示值的视图。
 
// In AllCustomersViewModel.cs
public double TotalSelectedSales
{
    get
    {
        return this.AllCustomers.Sum(
            custVM => custVM.IsSelected ? custVM.TotalSales : 0.0);
    }
}

void OnCustomerViewModelPropertyChanged(object sender, 
    PropertyChangedEventArgs e)
{
    string IsSelected = "IsSelected";

    // Make sure that the property name we're 
    // referencing is valid.  This is a debugging 
    // technique, and does not execute in a Release build.
    (sender as CustomerViewModel).VerifyPropertyName(IsSelected);

    // When a customer is selected or unselected, we must let the
    // world know that the TotalSelectedSales property has changed,
    // so that it will be queried again for a new value.
    if (e.PropertyName == IsSelected)
        this.OnPropertyChanged("TotalSelectedSales");
}
在用户界面将绑定到 TotalSelectedSales 属性,并应用货币格式值 (货币)。 ViewModel 对象可以通过从 TotalSelectedSales 属性返回一个 Double 值而不是字符串将货币格式而不是在的视图,应用。 ContentPresenter 的该 ContentStringFormat 属性在.NET Framework 3.5 SP 1 中, 添加时是因此如果您必须为目标旧版本的 WPF,您需要应用货币格式代码中:
 
<!-- In AllCustomersView.xaml -->
<StackPanel Orientation="Horizontal">
  <TextBlock Text="Total selected sales: " />
  <ContentPresenter
    Content="{Binding Path=TotalSelectedSales}"
    ContentStringFormat="c"
  />
</StackPanel>

向上覆盖
WPF 有很多提供应用程序开发,并利用该功能的学习需要思维方式 Shift。 模型-视图-ViewModel 模式是一种简单而有效的组的设计和实现 WPF 应用程序的指南。 它将允许您创建强分隔数据、 行为和以便更易于控制是软件开发的混乱的演示文稿。
我想感谢 John Gossman 有关本文的他帮助。

有关使用 WPF 创建出色用户体验于 Josh Smith 。 他已授予 Microsoft MVP 标题的 WPF 社区中其工时。 Josh 适用 Infragistics 体验设计组中。 当他不在计算机中时,他受播放该的钢琴与他 girlfriend 阅读有关历史记录和浏览纽约城市。 您可以访问在 Josh 的博客 joshsmithonwpf.wordpress.com .
posted @ 2012-07-09 11:08  墨梅  阅读(6111)  评论(0编辑  收藏  举报