Patterns - WPF program development using the MVVM design pattern

illustrate

This article is the author's own translation of the journal Patterns-WPF Apps With The Model-View-ViewModel Design Pattern published by Josh Smith on MSDN . It is purely the author's personal preference, if there is any infringement, please inform the relevant personnel. Moreover, the author's energy, experience and level are limited, and there are many inappropriate translations. I hope everyone can criticize and correct me.
The author of the demo code in the article also annotated the Chinese instructions, and the specific source code files will be updated in this article after the author organizes them.

text

There are many popular design patterns that can be used to tame the "beast" of developing programs, but it is very difficult to reasonably isolate and solve most of them. The more complex the design pattern, the more likely it is to replace a lot of work previously done by using shortcuts.
This is not always the fault of design patterns, sometimes we need to write a lot of code to use complex design patterns because the UI platform used may not be suitable for some simple design patterns. So what is really needed are simple, event-tested, developer-approved design patterns for creating UIs. Fortunately, WPF (Windows Presentation Foundation) is exactly the design pattern needed.
With the increasing usage of WPF in the software world, the WPF community has established its own ecology of development models and application practices. In this article, I'll discuss best practices for designing and implementing client applications in WPF. Combining some features of WPF with the MVVM design pattern, I will use a sample program to explain how to create a WPF application simply and correctly.
The end of the article will clarify how data templates, commands, data binding, resource system and MVVM pattern come together to create a simple, testable, stable framework WPF application. The demonstration program implemented in this article can be used as a WPF application template that actually uses the MVVM design pattern. The use of unit tests in the demo scenario illustrates how easy it is to test an application's user interface after separating the business logic into ViewModel classes. Before diving into the details, let's review why you should consider using the MVVM design pattern in the first place.

Order vs Chaos

There is no need to use a design template in a simple "Hello, World!" project. Any competent developer can read these few lines of code at a glance. However, as the number of features in the program increases, the amount of code increases and the corresponding user controls increase. Ultimately, both repetitive code and complex systems motivate developers to optimize code in a direction that is easier to understand, communicate, extend, and problem-solve. We reduce identification confusion in the system by using standard names (defined by the role the code plays in the system) to name specific entities in the source code.
Developers often deliberately structure their code according to design templates in order to have a hierarchy of implementation methods, and there is nothing wrong with this approach. But in this article, I verified the benefits of using MVVM as a WPF application framework, including using standard terms to define specific class names in MVVM, for example, if a class is an abstract class of View, the class name ends with "ViewModel "end. This method can avoid the recognition confusion problem mentioned earlier. Instead, you can be content with the kind of controllable chaos that pervades most software development projects.

Evolution of MVVM

With the software design of user interaction came into being many well-known design patterns. For example, the MVP (Model-View-Presenter) pattern is very popular in various UI software design platforms. MVP is a variant of the MVC (Model-View-Controller) pattern that has been developed for decades. In case you have never used the MVP pattern, here is a brief introduction. What you see in front of the screen is the View, the data it displays is the Model, and the Presenter connects the two. The View relies on the Presenter to populate the Model with data, react to user input, provide input validation (possibly sent to the Model via delegation), and other similar tasks. If you want to learn more about MVP, I recommend reading Jean-Paul Boodhoo's August 2006 Design Patterns column .
Back in 2004, Martin Fowler published an article about a pattern called PM (Presentation Model). The PM pattern separates View from behavior and state, similar to the MVP pattern. The interesting part of the PM pattern is the creation of an abstract View called the Presentation Model. View also becomes the rendering of Presentation Model. In Fowler's explanation, he explained that the Presentation Model needs to update the View frequently, so as to ensure the synchronization between the two. The synchronous logic code is written in the Presentation Model class.
In 2005, John Gossman (now a WPF and Silverlight architect at Microsoft) published the MVVM pattern on his blog. MVVM is equivalent to the Presentation Model proposed by Fowler. The characteristics of both modes are an abstraction that contains View state and behavior. Fowler introduced the Presentation Model as a view for creating UI platform-independent abstractions, while Gossman introduced MVVM as a standardized way to simplify the core functions of user interface. From this perspective, I think of MVVM as a more general PM pattern tailored specifically for the WPF and Silverlight platforms.
Glenn Block published an excellent article in the September 2008 issue: Prism: Patterns for Building Composite Applications with WPF . He explains Microsoft's Composite Application Guidelines for WPF. The term ViewModel is never used, instead, the term Presentation Model is used to describe the View abstraction. Throughout the text, however, I prefer the MVVM pattern and use the abstract View as the ViewModel. Because I've found the term to be more popular in the WPF and Silverl communities.
Unlike the Presenter in MVP, the ViewModel does not need to implement a reference to the View. The View binds properties to the ViewModel, and conversely, the ViewModel exposes properties that contain the Model object and View-specific state. The construction of the binding between View and ViewModel is very simple, just set the ViewModel object as the View's context (DataContext). If the value of a property in the ViewModel changes, the new value is automatically passed to the View through the binding. When the user clicks the button in the View, the command in the ViewModel will execute the corresponding request. Whether it is ViewModel or View, they perform all changes to Model data.
The View class does not know the existence of the Model class, and the ViewModel and Model know nothing about the View. In fact, Model obviously knows the existence of ViewModel and View. This is a very loosely coupled design, the benefits of which you will see next.

Why WPF Developers Love MVVM

Once a developer is very familiar with WPF and MVVM it becomes difficult to distinguish between the two. MVVM is the lingua franca of WPF developers because it is well suited to the WPF platform, and WPF is designed to make creating applications with MVVM easier (compared to other platforms). In fact, Microsoft also uses MVVM to develop WPF applications internally, such as Microsoft Express Blend, and the core WPF platform is also under construction. MVVM highlights many aspects of WPF, such as loose control model and data template, and a strong separation of state and behavior.
The most important thing that makes MVVM an excellent pattern for WPF is the use of data binding infrastructure. By binding the properties of the View to the ViewModel, the coupling between the two is reduced and the View is completely avoided by writing code in the ViewModel and directly updating the View. The data binding system also supports input validation, which provides a standard way to pass input validation error messages to the View.
Two other things that make this pattern work so well in WPF are the use of data templates and the resource system. Data templates are used in Views that display ViewModel objects in the user interface. You can declare templates in XAML and have the resource system automatically locate and apply those templates at runtime. You can read more about binding and data templates in my July 2008 article Data and WPF: Customize Data Display with Data Binding and WPF .
If WPF didn't support commands, the MVVM pattern wouldn't be that powerful. In this article, I will show how the ViewModel exposes commands to the View, that is, how the View can use the functions of the ViewModel. If you are not very familiar with commands, I recommend you to read a comprehensive article Advanced WPF: Understanding Routed Events and Commands in WPF published by Brain Noyes in September 2008.
In addition to the characteristics of WPF and Silverlight2 that make MVVM a natural way to build applications, the MVVM pattern is also famous because the ViewModel class can be used very easily in unit tests (uint test). When an application's internal logic is stored in a set of ViewModel classes, you can easily write code to test it. In a sense, Views and unit tests are two different types of ViewModel consumers. A suite of testing methods for ViewModels provides free and fast regression testing capabilities for the application, helping to reduce the cost of maintaining the application time.
In addition to improving the ability to create automated regression tests, the testability of the ViewModel class can help developers more easily design the appearance of the user interface. When you design an application, you can usually consider writing a unit test for the ViewModel to determine whether the content is placed in the View or the ViewModel. If you can write a unit test for the ViewModel without creating any UI objects, then you can completely design the appearance of the ViewModel independently of any specific visual elements.
Finally, for those developers who use a visual designer, using MVVM makes it easier to create a smooth designer/developer workflow. Since View can be the user of any ViewModel, it is very simple to replace different Views to render ViewModel. This enables designers to quickly build and evaluate user interactions for applications.
The development team can focus on creating stable ViewModel classes, and the design team can focus on implementing user-friendly Views. Just making sure that the correct bindings are implemented in the View's XAML file will ensure that the output matches between the two teams.

demo application

So far, I've reviewed the history and principles of operation of MVVM, and explained why it's become so popular among WPF developers. Now is the time to roll up your sleeves and get down to the actual work. The demo examples in the article use a variety of ways to apply MVVM. It provides a wealth of sample resources to help turn concepts into reality. I created the demo application in the environment of Visual Studio 2015 SP1 and Microsoft .NET Framework 5.1 SP1. Run unit tests in Visual Studio's unit testing system.
The application contains any number of workspaces (Workspace), and the user opens one of them by clicking the command link in the left navigation bar. All workspaces are stored in the tab control (TabControl) in the main content display area. Users can close the tab by clicking the close button on the workspace tab. The app contains two workspaces available: View All Accounts and Create New Account. After running the application and opening some workspaces, the user interface looks like Figure 1.
Figure 1 Workspace
Only one instance of the "View All Customers" workspace in the application can only be opened once, but any number of "Create New Customers" workspaces can be opened. When a user decides to create a new customer, he needs to fill out the form in Figure 2.
Figure 2 Create a new customer form
After entering valid values ​​in the form and clicking the Save button, the new customer's name will be displayed on the tab and the new customer will be added to the list of all customers. The application does not support deleting and editing existing customers, but this functionality and other similar features can be implemented on top of existing application frameworks. Now that you should have a solid understanding of the demo application, let's examine how it was designed and implemented.

dependent command logic

There is an empty code file behind every View in this application, except for the standard sample code called InitializeComponent that is generated in the constructor. In fact, you can remove the background code of View from the project, and the application can still compile and run normally. Despite the lack of event handling methods in the View, when the user clicks the button, the application can still respond to satisfy the user's request. The reason this is possible is because of the binding mechanism built into the naming properties of hyperlinks, buttons, and menu bars in the user interface. These bindings ensure that when the user clicks on these controls, the ICommand objects exposed by the ViewModel execute. You can think of the command object as an adapter whose function is to make it easier to use the functions of the ViewModel declared in XAML from the View.
When a ViewModel exposes an instance property of type ICommand, the command object uses the ViewModel object to do its work. One possible implementation pattern is to create a private nested class inside the ViewModel class, so that the commands can access the private members of the ViewModel it contains without affecting the namespace. This nested class implements the ICommand interface and declares a reference to the containing ViewModel object in the constructor. However, creating an embedded class implementing ICommand for each command exposed in the ViewModel would increase the size of the ViewModel. And more code means more potential for bugs.
In the demo application, use the RelayCommand class to solve the above problem. RelayCommand allows you to pass a delegate in its constructor to implement the logic of the command. This method can be implemented using concise and clear commands in the ViewModel class. RelayCommand is a simple variant of DelegateCommand in the Microsoft Composite Application Library. RelayCommand is implemented as shown in Figure 3 below.

public class RelayCommand : ICommand
{
    
    
    #region Fields 
    readonly Action<object> _execute;
    readonly Predicate<object> _canExecute;
    #endregion // 字段 
    #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 // 构造函数 
    #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 成员 
}

The CanExecuteChanged event, implemented as part of the Icommand interface, has some interesting properties. It subscribes to the CommandManager.ReuqerySuggested event via a delegate. This ensures that the WPF command framework asks all RelayCommadn objects if they can execute internal commands. The following code is from the CustomerViewModel class, showing how to configure a RelayCommand using lambda expressions, and I will analyze it in depth later.

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 share some common features. Usually need to implement INotifyPropertyChanged interface, need to have a user-friendly display name, in the workspace of this example, also need to close the function of the window. This problem is naturally associated with creating one or two base classes of ViewModel, so that the newly created ViewModel class can inherit the common functions in the base class. The inheritance hierarchy of the ViewModel class form is shown in Figure 4.
Figure 4 Inheritance Hierarchy
It is necessary to create a base class for the ViewModel you create. If you tend to combine many small classes in your classes to implement features, it's fine to not use a hierarchy. Like other design patterns, MVVM is a guideline, not a rule.

ViewModelBase class

ViewModelBase is the root class in the hierarchy, it implements the common INotifyPropertyChanged interface and has a DisplayName property. The INotifyPropertyChanged interface contains an event called PropertyChanged. Whenever a property in the ViewModel object has a new value, it fires the PropertyChanged event to notify WPF's binding system that there is a new value. Once the notification is received, the binding system starts to query this property, and the UI element bound to this property will get the new value.
In order for WPF to know which property in the ViewModel object has changed, the PropertyChangedEventArgs class exposes a PropertyName property of type String. You must be very careful to pass the correct property name into the property parameter, otherwise WPF will query for the wrong property value.
An interesting aspect of ViewModelBase is that it provides functionality to verify that a property with a given name actually exists in the ViewModel object. This feature is very useful when refactoring, because changing the property name in the refactoring feature of Visual Studio 2008 will not update the string containing the property name in your source code. Triggering the PropertyChanged event with an incorrect property name in the event parameter will produce very subtle bugs that are hard to track down, so this validation feature can save a lot of time in troubleshooting. The ViewModelBase code that adds this useful feature is shown in Figure 5 below.

// 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 of type ICommand named Command. MainWindowViewModel exposes a collection of CommandViewModels through its command property. The left navigation bar area of ​​the main window displays a link to each CommandViewModel exposed by the MainWindowViewModel, such as "View All Customers" and "Create New Customer". When the user clicks on one of the links, thereby executing one of the commands, a workspace is opened in the main window tab. The CommandViewModel class is defined as follows:

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; }
}

There is a data template with key "CommandsTemplate" in the MainWindowResources.xaml file. The main window uses this template to render the aforementioned collection of CommandViewModels. This template simply renders the CommandViewModel object as a link in the ItemsControl. The Command property of each hyperlink is bound to the Command property in the CommandViewModel. The XAML display is shown in Figure 6 below.

<!-- 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 you can see from the previous class structure diagram, WorkspaceViewModel is derived from ViewModelBase and adds the function of closing. "Close" refers to removing certain workspaces from the user interface at runtime. WorkspaceViewModel derives three classes: MainWindowViewModel, AllCustomersViewModel and CustomerViewModel. The close request of MainWindowViewModel is handled by the App class that created MainWindow and its ViewModel, as shown in Figure 7 below.

// In App.xaml.cs 
protected override void OnStartup(StartupEventArgs e)
{
    
    
    base.OnStartup(e); MainWindow window = new MainWindow();
    // 创建主窗口绑定的ViewModel
    string path = "Data/customers.xml";
    var viewModel = new MainWindowViewModel(path);
    // 当ViewModel被询求关闭,则关闭窗口 
    viewModel.RequestClose += delegate {
    
     window.Close(); };
    // 通过设置上下文遍历元素树,将窗口中所有的控件绑定到ViewModel
    window.DataContext = viewModel;
    window.Show();
}

MianWindow contains a menu column, and the Command property of the menu is bound to the CloseCommand property of MainWindowViewModel. When the user clicks on this column on the menu, the response of the App class is triggered by calling the Close method of the window, as follows:

<!-- In MainWindow.xaml -->
<Menu>
    <MenuItem Header="_文件">
        <MenuItem Header="_退出" 
                    Command="{Binding Path=CloseCommand}" />
    </MenuItem>
    <MenuItem Header="_编辑" />
    <MenuItem Header="_设置" />
    <MenuItem Header="_帮助" />
</Menu>

MainWindowViewModel contains a visual collection of WorkspaceViewModel objects called Workspaces. The main window contains a tab control whose ItemsSource property is bound to this collection. Each tab has a close button whose Command property is bound to the CloseCommand of the associated WorkspaceViewModel instance. An abbreviated code for a template that configures each tab is shown below. The code is in the MainWindowResource.xaml file, and the template shows how to render a tab 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 on the tab, the WorkspaceViewModel's CloseCommand will execute, triggering its RequestClose event. MainWindowViewModel monitors the RequestClose event of its workspace, and once the event occurs, the corresponding workspace will be removed from the Workspaces collection. Since the ItemsSource property of the main window tab control is bound to this collection of WorkspaceViewModels, removing an item from the collection will cause the corresponding workspace to be removed from the tab control. The logic of MianWindowViewModel is shown in Figure 8 below.

// 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 unit test (UnitTests) project, the MainWindowViewModelTests.cs file contains the test method to verify the normal operation of the function. Being able to easily create unit tests for ViewModel classes is a huge selling point of the MVVM design pattern because it enables simple program testing without writing UI-related code. This test method is shown in Figure 9 below.

// In MainWindowViewModelTests.cs 
[TestMethod]
public void TestCloseAllCustomersWorkspace()
{
    
    
    // 创建的是MainWindowViewModel,不是MainWindow. 
    MainWindowViewModel target = new MainWindowViewModel(Constants.CUSTOMER_DATA_FILE);
    Assert.AreEqual(0, target.Workspaces.Count, "Workspaces isn't empty.");
    // 查找打开“查看所有客户”工作空间的命令. 
    CommandViewModel commandVM = target.Commands.First(cvm => cvm.DisplayName == "View all customers");
    // 打开“查看所有客户”的工作空间. 
    commandVM.Command.Execute(null); Assert.AreEqual(1, target.Workspaces.Count, "Did not create viewmodel.");
    // 确保创建的工作空间的格式正确. 
    var allCustomersVM = target.Workspaces[0] as AllCustomersViewModel;
    Assert.IsNotNull(allCustomersVM, "Wrong viewmodel type created.");
    // 通知“查看所有客户”的工作空间关闭. 
    allCustomersVM.CloseCommand.Execute(null);
    Assert.AreEqual(0, target.Workspaces.Count, "Did not close viewmodel.");
}

Apply View to ViewModel

MainWindowViewModel indirectly implements adding and removing main window tab items on a WorkspaceViewModel object. Relying on data binding, the Content property of the tab receives and displays an object that depends on the ViewModelBase. ViewModelBase is not a UI element, so it does not support its own rendering internally. By default, WPF supports calling the ToString method of a non-visual object in a text block (TextBlock) to render the object. This is obviously not what you need, unless your users are very eager to see the type name of the ViewModel type.
By using typed DataTemplates you can very easily tell WPF how to render a ViewModel object. A typed DataTemplate has no x:Key values ​​assigned to it, but contains a DataType property on instances of the Type class. If WPF tries to render a ViewModel object, it checks to see if there is a typed DataTemplate with the same DataType as the ViewModel (or base class) object in the resource system in use. If it finds such one, it uses this template to render the ViewModel object referenced by the tab's Content property.
The MainWindowResource.xaml file has a resource dictionary (ResourceDictionary). This dictionary is added to the resource structure of the main window, which means that the resources contained in the dictionary are added to the scope of resources used by the window. When a tab's content is set to a ViewModel object, then the typed DataTemplate derived from this dictionary will provide the view (which is a user control) to render it. As shown in Figure 10 below.

<!-- 这个资源字典供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>
    <!-- 其它资源在这里忽略了... -->
</ResourceDictionary>

You don't need to write any code to determine which view to use for the ViewModel object to display. The WPF resource system will do all the heavy lifting for you, so you can focus on more important things. To further complicate the situation, it is possible to select the view programmatically, but this is unnecessary in most cases.

Data Model and Warehouse

You've seen ViewModel objects loaded, shown, and dismissed through the application's window. Now that the infrastructure is complete, you can carefully review the implementation details of the main body of the application. Before diving into the application's two workspaces, View All Customers and New Customer, let's first examine the data model and data access classes. The design of these classes has almost nothing to do with the MVVM design pattern, because you can create any View Model class that works with WPF's data objects.
Customer is the only Model class in the demo program. This class contains a small number of attributes representing company customer information, such as primary name, secondary name, email address. This class also provides validation information by implementing the standard IDataErrorInfo interface. This Customer class has nothing to do with the MVVM architecture or even the WPF application, so this class can come from the old business library.
Data has to come from and reside somewhere. In this application, an instance of the CustomerRepository class is used to load and store all customer objects. This happens when loading customer data from an XML file, but has nothing to do with the type of external data source. Data can come from databases, web services, named channels, files on hard drives or even carrier pigeons: it really doesn't matter. It doesn't matter where the data comes from (the MVVM design pattern can get data from the window), as long as you have data in your .NET objects.
The CustomerRepository class exposes methods that allow you to retrieve valid Customer objects, add a customer to the repository, and check whether the repository already contains the customer. Since the application does not allow users to delete customers, the warehouse does not allow removing customers. The CustomerAdded event is responded to when a customer is added to the CustomerRepository using the AddCustomer method.
Obviously the data model for this application is very small compared to what the actual business application needs, but that doesn't matter. It is important to understand how the ViewModel class uses Customer and CustomerRepository. Notice that the CustomerViewModel is a wrapper around the Customer object. A set of properties are used to expose the state of the Customer and other states used by the CustomerView control. CustomerViewModel does not copy the state of Customer but exposes it through delegation, as follows:

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 in the CustomerView control and clicks the Save button, the CustomerViewModel associated with the View will add a Customer object to the CustomerRepository. This will trigger the CustomerAdded event of the CustomerRepository, which is intended to notify the AllCustomersViewModel to add a new CustomerViewModel to its AllCustomers collection. In a sense, the CustomerRepository acts as a synchronization mechanism for ViewModels that are dealing with various Customer objects. One might see this as using the Intermediary design pattern. In the next chapters I will introduce more about how this mechanism works. But now let's take a deeper look at how these parts fit together by referring to the block diagram in Figure 11.
Figure 11 Customer relationship

New customer data entry form

When the user clicks the "Create New Customer" link, MainWindowViewModel adds a new CustomerViewModel to its workspace list, adding a CustomerView control to display it. After the user has entered a valid value into the input field, the Save button will be enabled so the user can save the new customer's information. Nothing more ordinary than a regular data entry form with input validation and a save button.
The Customer class supports internal validation by implementing the IDataErrorInfo interface. This validation ensures that the customer has a primary name, a well-formed email address, and (if the customer is a person) a secondary name. If the customer's IsCompany property is true, no value is allowed for secondary name (company name does not have a secondary name). This validation logic makes sense from the perspective of the Customer object, but this is not what needs to be presented on the user interface. What the user interface needs is to allow the user to choose whether the newly created customer is a person or a company. The initial value of the Customer type selector is "unselected". If the value of the customer's IsCompany attribute is only allowed to be true or false, how should the user interface tell the user that the user type is not selected?
Assuming you have full control over the entire software system, you can change the IsCompany property to a nullable boolean, which supports unselected values. However, the actual situation is not so simple. Let's say you can't change the Customer class because this class comes from an old library from another team in your company. If there is no easy way to save the "unchecked" value because of the existing data structure? What if other applications already use the Customer class and depend on this property being a normal boolean? Again, it can be solved by using ViewModel.
The test method in Figure 12 shows how this functionality in the CustomerViewModel is used. CustomerViewModel exposes a CustomerTypeOptions property so that the customer type selector can have three strings to display. It also exposes a CustomerType property, which is used to store the string selected in the selector. When CustomerType is set, it maps the string value to the Boolean value of the underlying Customer object's IsCompany property. Figure 13 shows these 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 drop-down list (ComboBox) to bind these two properties as follows:

<ComboBox ItemsSource="{Binding CustomerTypeOptions}" 
              SelectedItem="{Binding CustomerType, ValidatesOnDataErrors=True}" />

When the selected item in the drop-down list changes, the IDataErrorInfo interface of the data source is queried to check whether the new value is valid. This happens because the SelectedItem property binding has ValidatesOnDataErrors set to true. Since the data source is a CustomerViewModel object, the binding system will ask the CustomerViewModel for validation error information on the CustomerType property. For the most part, the CustomerViewModel delegates all false-authentication requests to the Customer object it contains. However, since the IsCompany property in Customer does not have an unchecked state defined, the CustomerViewModel class must implement the task of validating the newly selected item in the drop-down list. The code is as shown in Figure 14.

// In CustomerViewModel.cs 
string IDataErrorInfo.this[string propertyName]
{
    
    
    get
    {
    
    
        string error = null; if (propertyName == "CustomerType")
        {
    
    
            // Customer类的IsCompany属性是一个布尔值,所以它没有“未选中”这  
            //个状态值. 
            //CustomerViewModel类处理这个映射和验证 
            error = this.ValidateCustomerType();
        }
        else {
    
     error = (_customer as IDataErrorInfo)[propertyName]; }
        //将命令注册到CommadManager,例如Save命令,实现对命名是否执行的查询.
        CommandManager.InvalidateRequerySuggested();
        return error;
    }
}
string ValidateCustomerType()
{
    
    
    if (this.CustomerType == "Company" || this.CustomerType == "Person")
        return null;
    return "Customer type must be selected";
}

The key to this code is to use the CustomerViewModel's implementation of IDataErrorInfo to handle validation requests for ViewModel-specific properties and delegate other requests from the Customer object. This allows you to use validation logic in the Model class and have an additional validation property that only makes sense in the ViewModel class.
The View can save a CustoemrViewModel through the SaveCommand property. SaveCommand is used to allow the CustomerViewModel to decide if it can save itself and what to do when told to save its state, before the RelayCommand class checks. In this application, saving a new customer is simply adding it to the CustomerRepository. Determining whether a new customer can be saved depends on two aspects. Ask if the new customer is valid in the Customer object, and decide if it is valid in the CustomerViewModel. Double validation is necessary because what happens first is the checking of ViewModel specific properties and validation. The saving logic of CustomerViewModel is as 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 ViewModel is used here to make it easier to create Views that display Customer objects and events such as boolean properties for the "unchecked" state. It also provides the ability to notify the client to save its state. If the View were directly bound to the Customer object, then the View would require a lot of code to implement these functions. In the standard MVVM architecture, most of the View's background code should be empty, or at most only contain the resources and operation control codes in the View. Sometimes it is also necessary to write behind-the-scenes code for interacting with ViewModel objects in View, such as triggering an event or calling a method, which is usually difficult to call in ViewModel itself.

All Customer View

The demo app includes a workspace that displays a list of all customers. The customers in the list are grouped according to whether an individual or a company is entered. Users can select one or more customers at the same time and view the total sales of these customers in the lower right corner.
The user interface is an AllCustomerView control that renders AllCustomerViewModel objects. Each list item (ListViewItem) represents a CustomerViewModel object in the AllCustomers collection exposed by the AllCustomerViewModel object. In the previous chapters, you saw how the CustomerViewModel was rendered as a data entry form, and now the same CustomerViewModel object is rendered as an item in a list. The CustomerViewModel class knows nothing about the type of visual element it is displayed on, which is why it can be used over and over again.
AllCustomerView creates the groups shown in the list by binding the list's ItemsSource to a CollectionViewSource. The CollectionViewSource configuration is as shown in 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 list items and CustomerViewModel objects is established by the list's ItemContainerStyle property. The style (Style) that allows the list item's property to be bound to the CustomerViewModel property is assigned to each list item's property. An important binding style is to create an association between the list item's IsSelected property and the CustomerViewModel's IsSelected property, as follows:

<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 not selected it will change the total number of selected customers. The AllCustomerViewModel class is responsible for maintaining this value so that the ContentPresenter below the list can display the correct value. Figure 17 shows how the AllCustomerViewModel monitors whether each customer is selected or not and notifies the View to update the displayed 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";
    //确保我们引用的属性是有效的
    //这个一个调试技术,在Release生成中不会执行
    (sender as CustomerViewModel).VerifyPropertyName(IsSelected);
    //当一个客户的选择状态发生变更时,需要让系统知道选中的总数量的属性发生乐变化,
    //以便为了该属性获取一个新的值 
    if (e.PropertyName == IsSelected)
        this.OnPropertyChanged("TotalSelectedSales");
}

The UI binds the TotalSelectedSales property and applies currency formatting to display the value. Apply currency formatting to the ViewModel object (not the View) by returning a string from the TotalSelectedSales property in place of the double value. The ContentStringFormat attribute is added to the ContentPresenter of .NET framework 3.5 SP1, so if it is an old version of WPF application, you need to add the currency format code shown below:

<!-- In AllCustomersView.xaml -->
<StackPanel Orientation="Horizontal">
    <TextBlock Text="Total selected sales: " />
    <ContentPresenter 
        Content="{Binding Path=TotalSelectedSales}" 
        ContentStringFormat="c" />
</StackPanel>

summarize

WPF gives application developers a lot of features. Developers need to change their way of thinking to apply what they have learned, and make greater use of the power of WPF. The MVVM design pattern is a simple and effective guide for designing and implementing WPF applications. It allows you to create development software that is more independent in data, logic, and display, and less prone to chaos.

Guess you like

Origin blog.csdn.net/weixin_37537723/article/details/106916294