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 at joshsmithonwpf.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 .


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM