Suggérer une traduction
 
Suggestions d'autres utilisateurs :

progress indicator
Aucune autre suggestion.
MSDN Magazine > Accueil > Tous les numéros > 2009 > MSDN Magazine Février 2009 >  LE WPF MODEL-VIEW-VIEWMODEL (MVVM) DESIGN MOTIF...
Affichage du contenu :  côte à côteAffichage du contenu : côte à côte
Ce contenu traduit automatiquement peut être modifié par les membres de la communauté. Nous vous invitons à améliorer la traduction en cliquant sur le lien « Modifier » associé aux phrases ci-dessous.
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 in Figure 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 in Figure 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 .

Modèles
WPF applications avec le modèle de conception modèle-vue-ViewModel
Josh Smith

Cet article présente :
  • Modèles et WPF
  • Motif MVP
  • Pourquoi MVVM est préférable pour WPF
  • Création d'une application avec MVVM
Cet article utilise les technologies suivantes :
WPF, liaison de données
Téléchargement de code disponible de la bibliothèque de code MSDN
Parcourir le code en ligne
développement de l'interface utilisateur d'une application de logiciels professionnels n'est pas facile. Il peut être un murky mélange de données, création d'interactions, conception visuelle, connectivité, multithreading, sécurité, Internationalisation, validation, de test et une touche de voodoo. Envisagez qu'une interface utilisateur expose le système sous-jacent et doit satisfaire aux exigences stylistique imprévisibles de ses utilisateurs, il peut être la zone plus volatile de nombreuses applications.
Il existe des modèles de conception courantes permettant pour Apprivoiser ce beast difficile, mais séparées correctement et Adressage la multitude de problèmes peuvent être difficiles. La plus compliquée les modèles sont, la plus probable que raccourcis sera utilisé suite qui undermine tous les efforts précédents pour effectuer les opérations la bonne manière.
Il n'est pas toujours les modèles de conception en panne. Parfois, nous utilisons des modèles de conception complexe, qui nécessitent d'écrire beaucoup de code, car la plate-forme de l'interface utilisateur en cours d'utilisation ne lui-même prêter pas bien à un modèle plus simple. Ce qui est nécessaire est une plate-forme qui permet de facilement créer des interfaces utilisateur à l'aide de modèles de conception simple time-tested, développeurs approuvés. Heureusement, Windows Presentation Foundation (WPF) offre exactement qui.
À mesure que le monde de logiciels à adopter WPF d'un taux croissant, la communauté WPF a été développement son propre écosystème de modèles et méthodes. Dans cet article, je vais vérifier certaines de ces meilleures pratiques pour créer et l'implémentation des applications clientes avec WPF. En exploitant certaines fonctionnalités de base de WPF en association avec le modèle de conception modèle-vue-ViewModel MVVM, J'AI Guide un programme exemple qui illustre la simple possible pour créer une application WPF le moyen idéal.
À la fin de cet article, il sera comment effacer les modèles de données, commandes liaison de données, le système de ressources et le motif MVVM tous les imbrication pour créer une structure simple, tests et robuste sur le WPF toute application peut thrive. Le programme de démonstration qui accompagne cet article peut servir d'un modèle pour une application WPF réelle qui utilise MVVM en tant que son architecture de base. Les tests unité de la solution Démo indiquent combien il est facile pour tester la fonctionnalité d'interface utilisateur d'une application lorsque cette fonctionnalité existe dans un ensemble de classes ViewModel. Avant de plonger dans les détails, examinons pourquoi vous devez utiliser un motif comme MVVM en premier lieu.

Commande contre chaos
Il est inutile et counterproductive Utilisation motifs de conception dans un programme simple « Hello, World! ». Tout développeur compétent peut comprendre quelques lignes de code en un coup de œil. Toutefois, comme le nombre de fonctionnalités dans un programme augmente, le nombre de lignes de code et composants mobiles augmente en conséquence. Finalement, la complexité d'un système et les problèmes périodiques qu'il contient, encourage les développeurs à organiser leur code de telle sorte qu'il est plus facile pour comprendre, discuter, étendre et résoudre les problèmes. Nous diminuer le chaos cognitifs d'un système complexe en appliquant des noms connus à certaines entités dans le code source. Nous déterminer le nom pour appliquer à un élément de code en considérant son rôle fonctionnel dans le système.
Les développeurs structure souvent intentionnellement leur code en fonction pour un modèle de conception, par opposition à laisser les modèles émergent organically. Aucun problème avec l'approche, mais dans cet article, J'AI examiner les avantages du explicitement MVVM en tant que l'architecture d'une application WPF. Les noms de certaines classes incluent des termes connus à partir du motif MVVM, telles que se terminant par « ViewModel » si la classe est une abstraction d'un affichage. Cette approche permet d'éviter le chaos cognitifs mentionné précédemment. Au lieu de cela, vous pouvez Heureusement existe dans un état de contrôle chaos, qui est l'état naturel d'affaires de projets de développement logiciel plus professionnels !

L'évolution de modèle-vue-ViewModel
Ever depuis personnes créer des interfaces utilisateur de logiciel, ont été modèles de conception courantes pour faciliter il. Par exemple, le motif MVP (modèle-vue-présentateur) a apprécié popularité sur différentes plates-formes de programmation l'interface utilisateur. MVP est une variante du modèle modèle-vue-contrôleur, qui a depuis des décennies. Dans le cas où vous n'avez jamais utilisé le motif MVP avant, ici, est une explication simplifiée. Afficher à l'écran est la vue, les données, qu'il affiche sont le modèle et le présentateur intercepte les deux ensemble. L'affichage dépend d'un présentateur pour remplir avec les données du modèle, réagir à l'intervention de l'utilisateur, validation d'entrée (peut-être par délégation au modèle) et des autres tâches. Si vous souhaitez en savoir plus sur la Model View Presenter, je suggère que vous lisez de Paul de Jean Boodhoo Colonne des modèles en août 2006 .
En 2004, Martin Fowler publié un article sur un modèle nommé Modèle de présentation (H). Le motif des comptes fournisseurs est similaire à MVP qu'il sépare une vue de son comportement et l'état. La partie intéressante du motif des comptes fournisseurs est qu'une abstraction d'une vue est créée, appelé le modèle de présentation. Un affichage, devient ensuite, simplement un rendu d'un modèle de présentation. Dans explication du Fowler, il montre que le modèle de présentation met fréquemment à jour la vue, afin que les deux restent synchronisés les uns avec les autres. Cette logique de synchronisation existe en tant que code dans les classes de modèle de présentation.
En 2005, unveiled Gossman John, actuellement une des WPF et Silverlight architectes chez Microsoft, le Modèles Affichage ViewModel MVVM motif sur son blog. MVVM est identique au modèle de présentation du Fowler, les deux modèles de fonctionnalité une abstraction d'un affichage qui contient une vue état et comportement. Fowler introduit le modèle de présentation comme un moyen de création d'une abstraction indépendant de la plate-forme de l'interface utilisateur d'un affichage, tandis que Gossman introduit MVVM comme un moyen standard d'exploiter les fonctionnalités de base de WPF pour simplifier la création d'interfaces utilisateur. En ce sens, je considère MVVM à une spécialisation du modèle de comptes fournisseurs plus générale, tailor-made pour les plates-formes WPF et Silverlight.
Dans l'article excellent du bloc Glenn » Prism : modèles pour la création d'applications composites avec WPF « dans le numéro de septembre 2008, il présente le guide application composite Microsoft pour WPF. Le terme que ViewModel n'est jamais utilisée. Au lieu de cela, le terme présentation modèle est utilisé pour décrire l'abstraction d'un affichage. Tout au long de cet article, cependant, je vous voir le motif que MVVM et l'abstraction d'une vue comme un ViewModel. Je trouve que cette terminologie est beaucoup plus prevelant dans les communautés WPF et Silverlight.
Contrairement au présentateur de MVP, un ViewModel n'a pas besoin une référence à un affichage. L'affichage lie à des propriétés sur un ViewModel qui, à son tour, expose les données contenues dans les objets du modèle et autre état spécifique à l'affichage. Les liaisons entre le mode et ViewModel sont simples construire car un objet ViewModel est défini comme DataContext d'un affichage. En cas de propriété valeurs la modification ViewModel, ces nouvelles valeurs propager automatiquement à l'affichage via la liaison de données. Lorsque l'utilisateur clique sur un bouton dans la vue, une commande dans le ViewModel exécute pour effectuer l'action demandée. Le ViewModel, jamais la vue, effectue toutes les modifications apportées aux données de modèle.
Les classes Affichage n'avez aucune idée que les classes de modèle existent, tandis que le ViewModel et le modèle sont a pas connaissance de l'affichage. En fait, le modèle est complètement oblivious au fait que les ViewModel et Affichage existent. Ceci est une conception très couplée, qui paie les dividendes de nombreuses façons, comme vous le verrez bientôt.

Pourquoi les développeurs de WPF adore MVVM
Une fois qu'un développeur devient à l'aise avec WPF et MVVM, il peut être difficile à distinguer les deux. MVVM est la langue franca des développeurs de WPF car il est parfaitement à la plate-forme WPF, et WPF a été conçu pour faciliter facile à créer des applications utilisant le motif MVVM (entre autres). En fait, Microsoft utilisait MVVM en interne pour développer des applications WPF, tels que Microsoft Expression Blend, alors que la plate-forme WPF principaux était en construction. Aspects de WPF, telles que contrôle moins de présentation modèle et les données modèles, utilisent la séparation forte d'affichage de état et comportement promu en MVVM.
L'aspect plus important unique de WPF rend MVVM un modèle très à utiliser est l'infrastructure de liaison de données. Par propriétés de liaison d'un affichage à un ViewModel, vous obtenez libre associant entre les deux et entièrement supprimer la nécessité d'écrire du code dans un ViewModel directement met à jour une vue. Le système de liaison de données prend également en charge validation d'entrée, qui fournit un moyen standard de transmission des erreurs de validation à un affichage.
Deux autres fonctionnalités de WPF qui que ce motif donc utilisable sont les modèles de données et le système de ressources. Les modèles de données s'appliquent vues aux objets ViewModel affichées dans l'interface utilisateur. Vous pouvez déclarer des modèles dans XAML et laisser le système ressource localiser automatiquement et l'appliquer ces modèles pour vous lors de l'exécution. Vous pouvez plus d'informations sur la liaison et les modèles de données dans mon article de juillet 2008 " Données et WPF : personnaliser l'affichage des données avec liaison de données et WPF ."
Si elle n'étaient pas pour la prise en charge pour les commandes dans WPF, le motif MVVM est beaucoup moins puissant. Dans cet article, j'expliquerai comment un ViewModel peut exposer les commandes pour un affichage, permettant l'affichage pour utiliser sa fonctionnalité. Si vous n'êtes pas familiarisé avec commanding, je recommande que vous reportez-vous article Brian Noyes complet » WPF avancé : présentation routage événements et les commandes dans WPF « à partir de ce problème de septembre 2008.
En outre aux fonctionnalités WPF (et 2 Silverlight) qui rendent MVVM un moyen naturel de structurer une application, le motif est également courant car ViewModel classes sont faciles à test d'unité. Lorsque la logique d'interaction d'une application se trouve dans un ensemble de classes ViewModel, vous pouvez facilement écrire du code qui teste il. Dans un sens affichages et des tests d'unité existe uniquement deux types différents de consommateurs ViewModel. Avoir une suite de tests pour une application ViewModels offre gratuite et rapide régression test, qui contribue à réduire les coûts de maintenance d'une application dans le temps.
Plus à la promotion de la création des tests de régression automatisée, la testability de classes ViewModel peut aider à correctement création d'interfaces utilisateur sont faciles à personnaliser l'apparence. Lorsque vous créez une application, vous pouvez souvent décidez si quelque chose doit être dans la vue ou ViewModel par imagining que vous souhaitez écrire un test d'unité pour consommer la ViewModel. Si vous pouvez écrivez tests d'unité pour la ViewModel sans créer les objets interface utilisateur, vous pouvez également complètement apparence le ViewModel car aucune dépendance sur des éléments visuels spécifiques.
Enfin, pour les développeurs qui travaillent avec les concepteurs visuels, à l'aide de MVVM facilite créer un flux de travail régulier créateur/développeur. La mesure où un affichage est simplement un consommateur arbitraire d'un ViewModel, il est facile de simplement extraction d'un affichage des et directe dans un nouvel affichage pour rendre un ViewModel. Cette étape simple permet de prototypage rapide et l'évaluation des interfaces utilisateur effectuées par les concepteurs.
L'équipe de développement peut se concentrer sur la création de classes ViewModel robustes et l'équipe de conception peut se concentrer sur les vues conviviales. Connectez la sortie de deux équipes peut impliquer peu plus de vérifier que les liaisons corrects existent dans fichier XAML une vue.

L'application de démonstration
À ce stade, ont j'révisé de MVVM l'historique et théorie d'opération. J'AI examiné également pourquoi il est si populaire parmi les développeurs WPF. Il est temps maintenant pour vos manches et afficher le motif de l'action. L'application de démonstration qui accompagne cet article utilise MVVM de diverses façons. Il fournit une source fertile d'exemples pour vous aider à placer les concepts dans un contexte significatif. J'AI créé l'application de démonstration dans le Service Pack Visual Studio 2008 1, par rapport à Microsoft .NET Framework 3.5 Service Pack 1. Les tests d'unité exécutez dans le Visual Studio unité du système test.
L'application peut contenir n'importe quel nombre d'espaces de « travail » chacun l'utilisateur peut ouvrir en cliquant sur un lien de commande dans la zone de navigation à gauche. Tous les espaces de travail direct sur la zone de contenu principale dans un objet TabControl. L'utilisateur peut fermer un espace de travail en cliquant sur le bouton Fermer d'onglet article cet espace de travail. L'application comporte deux espaces de travail disponibles: "All Customers" et « nouveau client ». Après exécution de l'application et l'ouverture de certains espaces de travail, l'interface utilisateur ressemble à la figure 1 .
La figure 1 espaces de travail
Seule une instance de l'espace de travail "All Customers" peut être ouverte à la fois, mais n'importe quel nombre d'espaces de travail « nouveau client » peut être ouvert à la fois. Lorsque l'utilisateur décide de créer un client, elle devez renseigner le formulaire de la saisie de données dans la figure 2 .
La figure 2 nouveau formulaire de saisie des données client
Après avoir renseigné le formulaire de saisie données avec des valeurs valides et cliquez sur le bouton Enregistrer, le nouveau nom du client apparaît dans l'onglet article et ce client est ajouté à la liste de tous les clients. L'application n'est prise en charge de la suppression ou modification d'un client existant, mais cette fonctionnalité et bien d'autres fonctionnalités semblable à, est facile à implémenter en construisant en haut de l'architecture d'application existante. Maintenant que vous avez une connaissance générale de quoi l'application de démonstration, examiner nous allons comment il a été conçu et implémenté.

Relais logique de commande
Chaque mode d'affichage dans l'application comprend un fichier codebehind vide, sauf pour le code réutilisable standard qui appelle InitializeComponent dans constructeur de la classe. En fait, vous pouvez supprimer fichiers codebehind des affichages du projet et l'application serait encore compilé et exécuté correctement. Malgré l'absence de méthodes de gestion des événements dans les affichages, lorsque l'utilisateur clique sur les boutons, l'application répond et répond aux demandes de l'utilisateur. Cela fonctionne en raison des liaisons qui ont été établies sur la propriété Command de contrôles de lien hypertexte, bouton et MenuItem affichée dans l'IU. Les liaisons de vous assurer que lorsque l'utilisateur clique sur les contrôles, objets ICommand exposés par le ViewModel exécuter. Vous pouvez considérer l'objet de commande comme une carte qui facilite le consommer la fonctionnalité d'un ViewModel à partir d'un affichage déclarée dans XAML.
Lorsqu'un ViewModel expose une propriété d'instance de type I­Command, l'objet de commande utilise généralement cet objet ViewModel pour obtenir son travail terminé. Un motif d'implémentation possible consiste à créer une classe privée imbriquée dans la classe ViewModel, afin que la commande a accès aux membres privés de son ViewModel contenant et ne pollute pas l'espace de noms. Cette classe imbriquée implémente l'interface ICommand, et une référence à l'objet ViewModel contenant est injectée dans son constructeur. Toutefois, créer une classe imbriquée qui implémente ICommand pour chaque commande exposée par un ViewModel bloat la taille de la classe ViewModel. Code plus signifie un risque plu de bogues.
Dans l'application de démo, la classe RelayCommand résout ce problème. RelayCommand permet d'injecter la logique de la commande via des délégués transmis à son constructeur. Cette approche permet d'implémentation laconique, concis et commande des classes ViewModel. RelayCommand est une variante simplifiée de le DelegateCommand trouvé dans le Bibliothèque d'applications composite Microsoft . La classe Relay­Command est illustrée 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
}
L'événement CanExecuteChanged, qui fait partie de l'implémentation d'interface ICommand, dispose de fonctionnalités intéressantes. Elle délègue l'abonnement d'événement à l'événement CommandManager.RequerySuggested. Cela garantit que WPF commanding infrastructure demande tout que RelayCommand objets si elles peuvent exécuter chaque fois qu'il demande les commandes intégrées. Le code suivant de la classe de CustomerViewModel, j'examinera détaillée plus tard, montre comment configurer un RelayCommand avec des expressions lambda :
RelayCommand _saveCommand;
public ICommand SaveCommand
{
    get
    {
        if (_saveCommand == null)
        {
            _saveCommand = new RelayCommand(param => this.Save(),
                param => this.CanSave );
        }
        return _saveCommand;
    }
}

Hiérarchie de la classe ViewModel
La plupart des classes ViewModel devez les mêmes fonctionnalités. Ils souhaitent souvent implémenter l'interface INotifyPropertyChanged, ils doivent généralement avoir un nom d'affichage convivial et, dans le cas d'espaces de travail, ils ont besoin la possibilité de fermer (c'est-à-dire être supprimé de l'interface utilisateur). Ce problème naturellement prête aux créations d'une classe de base ViewModel ou deux, façon que nouvelles classes ViewModel peuvent héritent toutes les fonctionnalités courantes d'une classe de base. Les classes ViewModel constituent la hiérarchie d'héritage apparaît dans la figure 4 .
La figure 4 hiérarchie d'héritage
Avoir une classe de base pour toutes vos ViewModels est en aucun cas nécessaire. Si vous préférez bénéficier des fonctionnalités de vos classes par composition nombreuses classes plus petits ensemble, au lieu d'utiliser l'héritage, qui n'est pas un problème. Comme tout autre modèle de conception MVVM est un ensemble d'instructions, pas les règles.

Classe ViewModelBase
ViewModelBase est la classe racine dans la hiérarchie, c'est pourquoi il implémente l'interface INotifyPropertyChanged couramment utilisé et que vous a une propriété DisplayName. L'interface INotifyPropertyChanged contient un événement appelé PropertyChanged. Chaque fois qu'une propriété d'un objet ViewModel est une nouvelle valeur, il peut déclenchent l'événement PropertyChanged pour informer le système de liaison WPF de la nouvelle valeur. Lors de la réception cette notification, le système de liaison interroge la propriété et la propriété liée sur un élément d'interface utilisateur reçoit la nouvelle valeur.
Afin que WPF de savoir quelle propriété de l'objet ViewModel a été modifiée, la classe PropertyChangedEventArgs expose une propriété NomPropriété de type String. Vous devez être veiller à transmettre le nom propriété correcte dans cet argument événement ; sinon, WPF finissent interrogeant la propriété incorrecte pour une nouvelle valeur.
Un aspect intéressant de ViewModelBase est que permet pour vérifier qu'une propriété avec un nom donné existe en fait sur l'objet ViewModel. Cela est très utile lors de la refactorisation, car met changer nom d'une propriété via la fonctionnalité de refactorisation de Visual Studio 2008 n'est pas à jour les chaînes dans votre code source qui se contient nom de cette propriété (ni doit il). Déclencher l'événement PropertyChanged avec un nom de propriété incorrecte dans les événements argument peut entraîner des subtiles bogues qui sont difficiles à dépister, donc cette fonctionnalité peu peut être un timesaver énorme. Le code du ViewModelBase qui ajoute cette prise en charge utile est illustré figure 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);
    }
}

Classe CommandViewModel
La sous-classe ViewModelBase concret plus simple est CommandViewModel. Elle expose une propriété appelée commande de type I­Command. MainWindowViewModel expose une collection de ces objets via la propriété de commandes. La zone de navigation sur le côté gauche de la fenêtre principale affiche un lien pour chaque CommandViewModel exposée par MainWindowView­Model, tels que « afficher tous les clients » et « créer nouveau client ». Lorsque l'utilisateur clique sur un lien, par conséquent l'exécution d'une des ces commandes, un espace de travail s'ouvre dans le TabControl dans la fenêtre principale. La définition de classe Command­ViewModel est illustrée ci-dessous :
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; }
}
Dans le fichier MainWindowResources.xaml il existe un Data­Template dont la clé est « CommandsTemplate ». MainWindow utilise ce modèle pour afficher la collection de CommandViewModels mentionné précédemment. Le modèle affiche simplement chaque objet CommandViewModel en tant que lien dans un ItemsControl. Propriété Command de chaque lien hypertexte est liée à la propriété Command d'un Command­ViewModel. Ce code XAML est illustré 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>

Classe MainWindowViewModel
Comme précédemment affichées dans le diagramme de classes, la classe WorkspaceViewModel dérive de ViewModelBase et ajoute la capacité à fermer. À fermer, je VEUX dire que quelque chose supprime l'espace de travail de l'interface utilisateur au moment de l'exécution. Trois classes dérivent de WorkspaceViewModel : MainWindowViewModel, AllCustomersViewModel et CustomerViewModel. Demande de MainWindowViewModel à fermer est gérée par la classe Application, qui crée la MainWindow et son ViewModel, comme indiqué dans la 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 contient un élément de menu dont la propriété Command est liée à CloseCommand propriété la MainWindowViewModel. Quand l'utilisateur clique sur cet élément de menu, la répond classe Application en appelant méthode fermer la fenêtre, comme suit :
<!-- 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 contient une collection observable d'objets WorkspaceViewModel, appelée espace de travail. La fenêtre principale affiche un TabControl dont la propriété ItemsSource est liée à cette collection. Chaque élément onglet dispose d'un bouton Fermer dont la propriété Command est liée à la CloseCommand de son instance WorkspaceViewModel correspondant. Une version abridged du modèle qui configure chaque élément onglet apparaît dans le code qui suit. Le code se trouve dans MainWindowResources.xaml, et le modèle explique comment afficher un élément onglet avec un bouton Fermer :
<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>
Lorsque l'utilisateur clique sur le bouton Fermer dans un élément onglet, qui du Workspace­ViewModel CloseCommand exécute provoque son événement Request­Close. MainWindowViewModel surveille l'événement RequestClose de ses espaces de travail et supprime l'espace de travail de la collection Workspaces à la demande. Puisque le Main­Window TabControl possède sa propriété ItemsSource liée à la collection observable de WorkspaceViewModels, suppression d'un élément de la collection provoque l'espace de travail correspondant être supprimé de l'objet TabControl. Cette logique de Main­WindowViewModel est illustré 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);
}
Dans le projet UnitTests, le fichier MainWindowViewModelTests.cs contient une méthode de test qui vérifie que cette fonctionnalité fonctionne correctement. La facilité avec laquelle vous pouvez créer tests d'unité pour les classes ViewModel est un point selling énorme du motif MVVM, car elle permet de simple test des fonctionnalités d'application sans écrire de code qui touche l'interface utilisateur. Cette méthode de test est illustrée 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.");
}

Appliquer un affichage à un ViewModel
MainWindowViewModel indirectement ajoute et supprime Workspace­ViewModel objets vers et depuis Tab­Control la fenêtre principale. Par s'appuyer sur la liaison de données, la propriété contenue d'un TabItem reçoit un objet dérivé de ViewModelBase pour afficher. ViewModelBase n'est pas un élément d'interface utilisateur, afin qu'aucune prise en charge inhérente pour Affichage lui-même. Par défaut, dans WPF un objet non visuel est affiché en affichant les résultats d'un appel à la méthode ToString dans un TextBlock. Qui clairement n'est pas ce que vous avez besoin, sauf si vos utilisateurs disposent un le souhait de gravure pour afficher le nom type de notre classes ViewModel !
Vous pouvez facilement déterminer que le rendu d'un objet ViewModel à l'aide tapé WPF DataTemplates. Une ModèleDonnées tapée ne contient pas une valeur de x: Key assignée au, mais il possède sa propriété Type de données définie à une instance de la classe Type. Si WPF tente d'afficher un de vos objets ViewModel, il vérifie pour voir si le système ressource possède une ModèleDonnées tapée dans étendue dont type de données est identique (ou une classe de base de) le type de l'objet ViewModel. Si elle trouve une, il utilise ce modèle pour rendre l'objet ViewModel référencé par la onglet propriété de l'élément contenu.
Le fichier MainWindowResources.xaml a un Resource­Dictionary. Ce dictionnaire est ajouté à ressource hiérarchie la fenêtre principale, ce qui signifie que les ressources qu'il contient sont dans ressource étendue la fenêtre. Lorsque contenu un élément onglet est définie à un objet ViewModel, une ModèleDonnées tapée ce dictionnaire fournit une vue (c'est-à-dire, un contrôle utilisateur) pour afficher, comme illustré 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>
Vous devez écrire du code qui détermine laquelle afficher à afficher pour un objet ViewModel. Le système de ressources WPF ne tous l'essentiel pour vous, vous libérer pour mettre en évidence choses plus importantes. Dans les scénarios plus complexes, il est possible de par programmation sélectionner l'affichage, mais dans la plupart des situations qui n'est pas nécessaire.

Les données modèle et espace de stockage
Vous avez le visualiser comment ViewModel objets sont chargés, affichées et fermées par l'environnement d'application. Maintenant que la plomberie générale est en place, vous pouvez consulter les détails d'implémentation plus spécifiques au domaine de l'application. Avant de récupérer profondeur dans les espaces de deux travail l'application « tous les clients » et client nouveau, tout d'abord examinons le modèle de données et les classes d'accès aux données. La conception de ces classes a quasiment rien à voir avec le motif MVVM, car vous pouvez créer une classe ViewModel pour s'adapter de n'importe quel objet de données en par un nom convivial pour WPF.
La classe de modèle unique dans le programme démonstration est client. Cette classe a un petit nombre de propriétés qui représentent des informations sur un client d'une société, telles que leurs prénom, nom de famille et adresse de messagerie. Il fournit les messages de validation en implémentant l'interface IDataErrorInfo standard, qui existait des années avant WPF accès la rue. La classe clients comporte rien qui suggère qu'il est utilisé dans une architecture MVVM ou même dans une application WPF. La classe peut facilement provenir à une bibliothèque d'entreprise hérité.
Données doivent provenir et résident quelque part. Dans cette application, une instance de la classe CustomerRepository charge et enregistre tous les objets client. Il se trouve pour charger les données client à partir d'un fichier XML, mais le type de source de données externes est sans pertinence. Proviennent les données a une base de données, un service Web, un canal nommé, un fichier sur disque ou même pigeons transporteur : simplement peu. À condition que vous disposiez un objet .NET avec des données, sans tenir compte de d'où il vient, le motif MVVM pouvez obtenir ces données à l'écran.
La classe CustomerRepository expose quelques méthodes qui vous permettent d'obtenir tous les clients objets disponibles, Ajouter nouveau un client dans le référentiel et vérifier si un client est déjà dans le référentiel. Car l'application n'autorise pas l'utilisateur à supprimer un client, l'espace de stockage ne vous autorise pas supprimer un client. L'événement CustomerAdded se déclenche lorsqu'un nouveau client saisit CustomerRepository, via la méthode AddCustomer.
Clairement, modèle de données de cette application est très faible par rapport à ce que nécessitent applications métier réelle, mais qui n'est pas important. Ce qui est important de comprendre est comment les classes ViewModel qu'utiliser des clients et CustomerRepository. Notez que Customer­ViewModel est un wrapper autour d'un objet client. Elle expose l'état d'un client et autre état utilisé par le contrôle Customer­View, via un ensemble de propriétés. CustomerViewModel ne duplique pas l'état d'un client ; elle il expose simplement via la délégation, comme suit :
public string FirstName
{
    get { return _customer.FirstName; }
    set
    {
        if (value == _customer.FirstName)
            return;
        _customer.FirstName = value;
        base.OnPropertyChanged("FirstName");
    }
}
Lorsque l'utilisateur crée un nouveau client et clique sur le bouton Enregistrer dans le contrôle CustomerView, le Customer­ViewModel associée qu'affichage s'ajouter le nouvel objet client à la Customer­Repository. Qui déclenche CustomerAdded événement le référentiel, qui permet la AllCustomers­ViewModel que qu'il doit ajouter un nouveau Customer­ViewModel à sa collection AllCustomers. Dans un sens Customer­Repository agit comme un mécanisme de synchronisation entre les divers ViewModels qui traitent des objets Customer. Peut-être une peut considérer ceci comme utilisant le modèle de conception médiateur. J'examiner plus comment cela fonctionne dans les sections à venir, mais pour l'instant voir sur le diagramme de la figure 11 pour une présentation détaillée de toutes les parties imbrication.
La figure 11 liens client

Nouveau formulaire d'entrée données client
Lorsque l'utilisateur clique sur le lien « Créer nouveau client », MainWindowViewModel ajoute un nouveau CustomerViewModel à sa liste d'espaces de travail et un contrôle CustomerView affiche. Une fois que l'utilisateur tape les valeurs valides dans les champs d'entrée, le bouton Enregistrer insère l'état activé afin que l'utilisateur peut conserver les nouvelles informations client. Rien n'est du ordinaires ici, simplement un formulaire de saisie données standard avec entrée validation et un bouton Enregistrer.
La classe de client a intégré contrôle prend en charge, disponible via son implémentation de l'interface IDataErrorInfo. Que validation garantit le client a un prénom, une adresse de messagerie formé correctement, et, si le client est une personne, un nom de famille. Si IsCompany propriété le client renvoie cette propriété a la valeur true, la propriété de nom ne peut avoir une valeur (L'idée est qu'une société n'a pas un nom de famille). Cette logique de validation peut sens du point de vue l'objet client, mais il ne répond pas aux besoins de l'interface utilisateur. L'interface utilisateur nécessite un utilisateur à déterminer si un nouveau client est une personne ou une société. Le sélecteur de type de client initiale a la valeur « (non spécifié) ». Comment peut l'interface utilisateur savoir l'utilisateur que le type de client est non spécifié si la propriété IsCompany d'un client permet uniquement d'une valeur vrai ou faux ?
En supposant que vous ayez contrôle total sur le système logiciel ensemble, vous pourriez modifier la propriété IsCompany de type Nullable <bool>, permettrait de la valeur « désélectionnée. Toutefois, le monde réel n'est pas toujours simple. Supposons que vous ne pouvez pas modifier la classe clients car il provient d'une bibliothèque hérité appartenant à une autre équipe dans votre société. Que se passe-t-il si il est non facile façon à conserver que « désélectionnés valeur en raison du schéma de base de données existant ? Que se passe-t-il si autres applications déjà utiliser la classe de clients et dépendent de la propriété est une valeur de type Boolean normale ? Une fois encore, avoir un ViewModel est le secours.
La méthode de test dans la figure 12 montre comment cette fonctionnalité fonctionne dans CustomerViewModel. CustomerViewModel expose une propriété CustomerTypeOptions afin que trois chaînes pour afficher le sélecteur de type de client. Elle expose également une propriété CustomerType, qui stocke la chaîne sélectionnée dans le sélecteur. Lorsque CustomerType est défini, il mappe la valeur de type String à une valeur Boolean pour IsCompany propriété l'objet client sous-jacent. la figure 13 illustre les deux propriétés.
// 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");
    }
}
Le contrôle CustomerView contient un contrôle ComboBox liée à ces propriétés, comme indiqué ici :
<ComboBox 
  ItemsSource="{Binding CustomerTypeOptions}"
  SelectedItem="{Binding CustomerType, ValidatesOnDataErrors=True}"
  />
Lorsque l'élément sélectionné dans ce contrôle ComboBox, interface IDataErrorInfo la source de données est interrogée pour vérifier si la nouvelle valeur est valide. Qui se produit car la liaison de propriété SelectedItem a ValidatesOnDataErrors défini à vrai. La mesure où la source de données est un objet Customer­ViewModel, le système de liaison demande qui Customer­ViewModel d'une erreur de la propriété CustomerType de validation. La plupart du temps, CustomerViewModel délègue toutes les demandes d'erreurs de validation à l'objet client qu'il contient. Toutefois, puisque client ne possède aucune notion d'avoir un état non sélectionné pour la propriété IsCompany, la classe CustomerViewModel doit gérer validation de l'élément sélectionné nouvelle dans le contrôle ComboBox. Ce code est visible dans la 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";
}
L'aspect clé de ce code est qu'implémentation de CustomerViewModel de IDataErrorInfo peut gérer les demandes pour la validation propriété ViewModel-spécifiques et déléguer des autres demandes à l'objet client. Cela permet de pouvoir utiliser de logique de validation des classes de modèle et avez validation supplémentaire pour les propriétés qui seulement sur sens sur aux classes ViewModel.
La possibilité d'enregistrer un CustomerViewModel est disponible pour un affichage via la propriété SaveCommand. Cette commande utilise la classe RelayCommand examinée précédemment pour autoriser CustomerViewModel décider si elle peut enregistrer lui-même et que faire lorsque vous dit d'enregistrer son état. Dans cette application, l'enregistrement d'un nouveau client signifie simplement ajouter à un CustomerRepository. Décider si le nouveau client est prêt à enregistrer requiert le consentement de deux parties. L'objet client doit être demandé si elle est valide ou non et du Customer­ViewModel doit décider si elle est valide. Cette décision de deux parties est nécessaire en raison des propriétés ViewModel-spécifiques et validation examinées précédemment. L'enregistrement logique de Customer­ViewModel est illustrée figure 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; 
    }
}
L'utilisation d'un ViewModel ici facilite créer un affichage qui peut afficher un objet client et pour les éléments à un état « désélectionné d'une propriété booléenne. Il permet également savoir facilement le client enregistrer son état. Si l'affichage a été lié directement à un objet client, l'affichage nécessiterait beaucoup de code pour que cela fonctionne correctement. Dans une architecture MVVM bien conçue, le codebehind de la plupart des vues doit être vides ou, au maximum contenir uniquement du code qui manipule les contrôles et ressources contenues dans cette vue. Parfois, il est également nécessaire pour écrire du code dans codebehind une vue qui interagit avec un objet ViewModel, tel qu'un événement de raccordement ou appeler une méthode qui sinon, serait très difficile à appeler à partir de la ViewModel lui-même.

Permet d'afficher tous les clients
L'application de démonstration contient également un espace de travail qui affiche tous les clients dans une liste. Les clients dans la liste sont regroupés en fonction indique s'ils sont une société ou une personne. L'utilisateur peut sélectionner un ou plusieurs clients à la fois et afficher la somme de leurs ventes totales dans le coin inférieur droit.
L'interface utilisateur est le contrôle AllCustomersView, ce qui rend un objet AllCustomersViewModel. Chaque ListView­Item représente un objet CustomerViewModel dans la collection AllCustomers exposée par l'objet AllCustomerViewModel. Dans la section précédente, vous l'avez vu comment une CustomerViewModel peut rendre un formulaire de saisie de données, et maintenant le même objet CustomerViewModel exact s'affiche en tant qu'élément dans une liste. La classe CustomerViewModel n'a aucune idée les éléments visuels affichent, c'est pourquoi cette réutilisation est possible.
AllCustomersView crée les groupes dans la liste. Il réalise cette authentification en liant ItemsSource la liste un Collection­ViewSource configuré comme 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>
L'association entre un ListViewItem et un objet CustomerViewModel est établie par propriété ItemContainerStyle la liste. Le style affecté à ce que la propriété est appliquée à chaque ListViewItem, qui permet les propriétés sur un ListViewItem lié aux propriétés au CustomerViewModel. Une liaison important dans ce style crée un lien entre la propriété IsSelected d'un ListViewItem et la propriété IsSelected d'un Customer­ViewModel, comme indiqué ici :
<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>
Lorsqu'un CustomerViewModel soit activée ou désactivée, qui provoque la somme des ventes total sélectionné tous les clients à modifier. La classe AllCustomersViewModel est responsable de gestion de cette valeur, afin que le ContentPresenter sous la liste peut afficher le nombre correct. figure 17 montre comment AllCustomersViewModel analyse chaque client en cours activée ou désactivée et avertit l'affichage qui il doit mettre à jour la valeur d'affichage.
// 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");
}
L'interface utilisateur lie à la propriété TotalSelectedSales et applique (monétaire) mise en forme à la valeur monétaire. L'objet ViewModel a pu appliquer la mise en forme, au lieu de la vue, en renvoyant une valeur de type String au lieu d'une valeur de type double à partir de la propriété TotalSelectedSales de devise. La propriété ContentStringFormat de ContentPresenter a été ajoutée dans le SP1 .NET Framework 3.5, si vous devez cibler une version antérieure de WPF, vous devez appliquer la mise en forme de code de devise :
<!-- In AllCustomersView.xaml -->
<StackPanel Orientation="Horizontal">
  <TextBlock Text="Total selected sales: " />
  <ContentPresenter
    Content="{Binding Path=TotalSelectedSales}"
    ContentStringFormat="c"
  />
</StackPanel>

Conclusion
WPF a un lot pour offrir aux développeurs d'applications, et formation d'exploiter cette puissance requiert une équipe mindset. Le modèle modèle-vue-ViewModel est un ensemble simple et efficace des instructions pour la conception et l'implémentation d'une application WPF. Elle permet de créer une séparation forte entre données, de comportement et de présentation, ce qui facilite le chaos qui est le développement de logiciels de contrôle.
J'aimerais remercier John Gossman pour son aide pour cet article.

Josh Smith est baisers sur l'utilisation de WPF pour créer des expériences utilisateur très. Il a été Décerné le titre de MVP Microsoft pour son travail de la communauté WPF. Josh fonctionne pour Infragistics dans le groupe Créer expérience. Lorsqu'il n'est pas sur un ordinateur, il aime lire la piano, la lecture sur l'historique et Exploration New York City avec son girlfriend. Vous pouvez visiter blog du Josh à joshsmithonwpf.wordpress.com .

Page view tracker