Using the MVVM pattern in WPF applications

The MVVM pattern is a technology that existed a long time ago. Recently, due to a project, I needed to use WPF technology. Therefore, I re-opened a program from the past and reviewed the technology of that year.

MVVM pattern

MVVM actually involves three parts, Model, View and ViewModel. The relationship between the three is shown in the figure below.

In a three-part relationship, the content and operations displayed by the view are completely dependent on the ViewModel.

The Model is the heart of the application and represents the largest and most important business asset, as it records all complex business entities, their relationships, and their functionality.

Above the Model is the ViewModel. The two main goals of ViewModel are: to make the Model easily usable by WPF/XAML View; to separate the Model from the View and to encapsulate the Model. These goals are of course very good, but due to some practical reasons, they are sometimes not achieved.

The ViewModel you build knows at a high level how users will interact with your application. However, the ViewModel knows nothing about the View, which is an important part of the MVVM design pattern. This enables interaction designers and graphic designers to create beautiful, effective UIs based on ViewModels, while working closely with developers to design appropriate ViewModels to support their work. In addition, the separation of View and ViewModel also makes ViewModel more conducive to unit testing and reuse.

Since changes to the view model affect the state of the view, we need to use two important technologies: observable objects and command patterns.

Observable objects

Observable objects require that when the state of an object changes, they need to be able to actively notify all observers. WPF involves two important interfaces, INotifyPropertyChanged and INotifyCollectionChanged, which are used to indicate that the state of a single object has changed, and A collection has changed.

The definition of the INotifyPropertyChanged interface is as follows:

namespace System.ComponentModel
{
    // Summary: 
    // Notify the client that a property value has changed.
    public interface INotifyPropertyChanged
    {
        // Summary: 
        // Occurs when a property value is changed.
        event PropertyChangedEventHandler PropertyChanged;
    }
}

This is an interface. Usually we will define a base class that implements this interface for ease of use. In the following implementation, all observers are notified through events.

namespace MVVM.Framework
{
    //Implement the notification of the observer topic
    public class BaseObservableObject : INotifyPropertyChanged
    {
        // event
        public event PropertyChangedEventHandler PropertyChanged;

        // Standard method of triggering events
        protected void OnPropertyChanged(string propertyName)
        {
            // If not registered, it will be null
            if (PropertyChanged != null)
            {
                var e = new PropertyChangedEventArgs(propertyName);
                PropertyChanged(this, e);
            }
        }
    }
}

The definition of INotifyCollectionChanged is as follows. The system has provided a generic implementation ObservableCollection, defined in the namespace System.Collections.ObjectModel, which we can use directly.

namespace System.Collections.Specialized
{
    // Summary: 
    // Notify listeners of dynamic changes, such as when items are added or removed, or when the entire list is refreshed.
    [TypeForwardedFrom("WindowsBase, Version=3.0.0.0, Culture=Neutral, PublicKeyToken=31bf3856ad364e35")]
    public interface INotifyCollectionChanged
    {
        // Summary: 
        //Occurs when the collection changes.
        event NotifyCollectionChangedEventHandler CollectionChanged;
    }
}

Command mode

For the command mode, the most important thing is that we encapsulate each command as an object. The interface involved here is ICommand, which is defined as follows:

namespace System.Windows.Input
{
    // Summary: 
    // define a command
    [TypeConverter("System.Windows.Input.CommandConverter, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
    [TypeForwardedFrom("PresentationCore, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")]
    [ValueSerializer("System.Windows.Input.CommandValueSerializer, PresentationFramework, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, Custom=null")]
    public interface ICommand
    {
        // Summary: 
        // Occurs when a change occurs that affects whether the command should be executed.
        event EventHandler CanExecuteChanged;

        // Summary: 
        // Define the method used to determine whether this command can be executed in its current state.
        //
        // Parameters:
        // parameter:
        //Data used by this command. This object can be set to null if this command does not require passing data.
        //
        //Return results:
        // true if this command can be executed; false otherwise.
        bool CanExecute(object parameter);
        //
        // Summary: 
        // Define the method to be called when this command is called.
        //
        // Parameters:
        // parameter:
        //Data used by this command. This object can be set to null if this command does not require passing data.
        void Execute(object parameter);
    }
}

The command not only contains the execution method, but also includes the method used to determine whether it can be executed, and the event when the execution changes. This allows us to dynamically notify the display of the view when the data status changes. Changes occur at the same time. For example, when there is no data, the modify button is unavailable. When data already exists, the modify button enters the available state, etc.

Usually we will implement this interface. We provide two methods ourselves to implement the operations that need to be performed and the conditions to determine whether they can be performed. There are two commissions involved here.

The Predicate delegate represents a method that returns a bool value. This is a generic delegate. We can pass a parameter in as a condition for judgment.

namespace System
{
    // Summary: 
    // Represents a method that defines a set of conditions and determines whether a specified object meets these conditions.
    //
    // Parameters:
    //obj:
    // The object to be compared according to the conditions defined in the method represented by this delegate.
    //
    //Type parameters:
    //T:
    // The type of object to compare.
    //
    //Return results:
    // true if obj meets the conditions defined in the method represented by this delegate; false otherwise.
    public delegate bool Predicate<in T>(T obj);
}

The Action delegate represents a method without a return value. When we operate in View, what we usually need to change is the state of ViewModel, and we do not need to return the results to View.

namespace System
{
    // Summary: 
    // Encapsulate a method that has only one parameter and does not return a value.
    //
    // Parameters:
    //obj:
    // Parameters of the method encapsulated by this delegate.
    //
    //Type parameters:
    //T:
    //The parameter type of the method encapsulated by this delegate.
    public delegate void Action<in T>(T obj);
}

In this way, our default command implementation becomes the following form. The DelegaeCommand constructor receives two methods, one is the actual encapsulated operation, and the other is used to determine whether it is available.

In addition, an additional UpdateCanExecuteState method is provided, which is automatically called after each execution of the processing method to update the available status.

namespace MVVM.Framework
{
    /// <summary>
    /// Implement command support
    /// </summary>
    public class DelegateCommand : ICommand
    {
        // Conditions for executability
        private readonly Predicate<Object> canExecuteMethod;
        
        //The actual operation performed, indicating a method with an object as a parameter
        private readonly Action<Object> executeActionMethod;

        // Constructor
        // When creating a command object, provide a delegate for the actual execution method
        // Determine whether the commission is enabled
        public DelegateCommand(
            Predicate<Object> canExecute,
            Action<object> executeAction
            )
        {
            canExecuteMethod = canExecute;
            executeActionMethod = executeAction;
        }

        #region ICommand Members

        public event EventHandler CanExecuteChanged;

        // Check if it can be executed
        public bool CanExecute(object parameter)
        {
            var handlers = canExecuteMethod;

            if (handlers != null)
            {
                return handlers(parameter);
            }

            return true;
        }

        // perform operations
        public void Execute(object parameter)
        {
            // Check if an actual method delegate is provided
            var handlers = executeActionMethod;

            if (handlers != null)
            {
                handlers(parameter);
            }

            // After execution, update the status of whether it can be executed
            UpdateCanExecuteState();
        }

        #endregion

        public void UpdateCanExecuteState()
        {
            var handlers = CanExecuteChanged;

            if (handlers != null)
            {
                handlers(this, new EventArgs());
            }
        }
    }
}

The first part is here first, and we will continue later.