“Old Jar Pickles New Vegetables”: SOD MVVM Framework Revitalizes Winforms

“Old Jar Pickles New Vegetables”: SOD MVVM Framework Revitalizes Winforms

The necessity of MVVM technology on WinForms; it turns out that implementing an MVVM framework is not difficult. The key is the two-way binding between Model and View, where changes in the Model cause changes in the View content, and changes in the View can also cause changes in the Model.

Last updated 11/23/2021 9:13 PM
用户1177503
19 min read
Category
Winform
Topic
Winform Control Library
Tags
.NET Winform MVVM Winform Open Source Projects Open Source

1. The Hot MVVM Framework

One of the hottest technologies in recent years is frontend technology, with various frontend frameworks, standards, and design styles emerging one after another. Among the many frontend frameworks, those with MVC and MVVM capabilities have become shining stars. For example, Vue.js, which has a high number of GitHub followers, is a work by a Chinese developer, and its design style and documentation friendliness make it even more appealing to Chinese users. Therefore, I recommended it to my company. My main reason for recommending it is its excellent MVVM functionality—data-oriented rather than focusing on DOM details. Compared to jQuery and similar libraries, it saves more code, is more suitable for backend programmers, and facilitates better collaboration between UI designers and programmers.

Below is the schematic diagram of Vue.js implementing MVVM functionality:

Do the above advantages of the Vue.js framework look familiar? That's right, this is the MVVM technology that was popular in WPF years ago. Compared to WinForms technology, WPF can provide UI designers with more powerful design capabilities, creating more dazzling and better-looking interfaces. However, many Microsoft technologies are often ahead of their time and update quickly. When WPF was newly introduced, WinForms still dominated desktop development, and before it could become popular, the mobile development era arrived. Web-based frontend technologies developed significantly, overshadowing WPF. But the MVVM concepts introduced by WPF have flourished in the web frontend, and now various MVVM-based frontend frameworks are springing up like bamboo shoots after rain.

2. MVVM Needs on WinForms

With the rapid development of web frontend technologies, various cross-platform mobile frontend development technologies based on HTML5 have gradually matured. Many applications have transitioned from traditional C/S to B/S or APP models. The attention on C/S-based frontend technologies like WPF has gradually declined. Therefore, MVVM on WPF is not widely applied. Currently, many legacy or new C/S systems still use WinForms technology for development and maintenance. However, there is no good MVVM framework on WinForms. The UI effects and overall development quality and efficiency of WinForms have not been effectively improved. Transitioning to WPF, a development style with different techniques, is technically difficult. Therefore, if there were an MVVM framework that could work on WinForms, it would undoubtedly be great news for the majority of backend .NET developers.

The author has always been a .NET developer and architect working on the front lines, with extensive involvement in web, desktop, and backend development technologies. I deeply understand the psychology of developers who jokingly call themselves "code farmers"—working hard without time to spend with girlfriends or family. Therefore, I have been summarizing and organizing methods to improve development efficiency and development quality. Over nearly ten years, I have developed and refined a development framework called the SOD framework. Recently, while researching technologies to improve web frontend development, the MVVM concepts of the Vue.js framework once again made me realize the necessity of MVVM technology on WinForms. I discovered that implementing an MVVM framework is actually not difficult; the key is the two-way binding between the Model and the View. That is, changes in the Model cause changes in the View's content, and changes in the View can also cause changes in the Model.

3. SOD WinForms MVVM Implementation Principle

To achieve such changes, the bound party must have property change notification functionality. When the binding party changes, it notifies the bound party to handle it accordingly. In .NET, the interface that implements this notification functionality is:

INotifyPropertyChanged

It is defined in System.dll and has been supported since .NET 2.0. Below is the specific definition of this interface:

namespace System.ComponentModel
{
    // Summary:
    //     Notifies clients that a property value has changed.
    public interface INotifyPropertyChanged
    {
        // Summary:
        //     Occurs when a property value changes.
        event PropertyChangedEventHandler PropertyChanged;
    }
}

The entity base class EntityBase of the SOD framework implements this interface:

public abstract class EntityBase : INotifyPropertyChanged, ICloneable, PWMIS.Common.IEntity
{
    /// <summary>
    /// Property change event
    /// </summary>
    public event PropertyChangedEventHandler PropertyChanged;
    /// <summary>
    /// Trigger property change event
    /// </summary>
    /// <param name="propertyFieldName">Property change event object</param>
    protected virtual void OnPropertyChanged(string propertyFieldName)
    {
        if (this.PropertyChanged != null)
        {
            string currPropName = EntityFieldsCache.Item(this.GetType()).GetPropertyName(propertyFieldName);
            this.PropertyChanged(this, new PropertyChangedEventArgs(currPropName));
        }
    }
    // Other code omitted...
}

Therefore, entity classes of the SOD framework can be directly used as the Model in MVVM, provided to the View as the bound object. So we only need to solve how to implement binding operations for WinForms View elements, and then our WinForms applications can achieve MVVM functionality. In WinForms, controls already basically implement binding functionality, which is through the control's DataBindings. Just add bindings to it, as in the following example:

this.textbox1.DataBindings.Add("Text", userEntity, "Name");

In this way, when the content entered in the text box changes, the value of the userEntity.Name property also changes. If userEntity is an SOD entity class, changing userEntity.Name will also synchronously change the Text property of the text box.

The data controls (WinForms, WebForms) of the SOD framework implement the IDataControl interface, which defines several important properties: LinkObject, LinkProperty:

/// <summary>
/// Data mapping control interface
/// </summary>
public interface IDataControl
{

    /// <summary>
    /// Data associated with database data items
    /// </summary>
    string LinkProperty
    {
        get;
        set;
    }

    /// <summary>
    /// Table name associated with data
    /// </summary>
    string LinkObject
    {
        get;
        set;
    }
    // Other interface method content omitted...

We can use LinkObject to specify the entity class object to bind, and LinkProperty to specify the property of the object to bind. Therefore, the following code can achieve two-way binding between WinForms controls and SOD entity classes:

public void BindDataControls(Control.ControlCollection controls)
{
    var dataControls = MyWinForm.GetIBControls(controls);
    foreach (IDataControl control in dataControls)
    {
        //control.LinkObject here is always "DataContext"
        object dataSource = GetInstanceByMemberName(control.LinkObject);
        if (control is TextBox)
        {
            ((TextBox)control).DataBindings.Add("Text", dataSource, control.LinkProperty);
        }
        if (control is Label)
        {
            ((Label)control).DataBindings.Add("Text", dataSource, control.LinkProperty);
        }
        if (control is ListBox)
        {
            ((ListBox)control).DataBindings.Add("SelectedValue", dataSource, control.LinkProperty, false, DataSourceUpdateMode.OnPropertyChanged);
        }
    }
}

Additionally, we might also need to bind some commands to the View. Implementing this functionality is also relatively simple:

private Dictionary<object, CommandMethod> dictCommand;
public delegate void CommandMethod();

public void BindCommandControls(Control control,CommandMethod command)
{
    if (control is Button)
    {
        dictCommand.Add(control, command);
        ((Button)control).Click += (sender, e) => {
            dictCommand[sender]();
        };
    }
}

After this process, we only need to write a few lines of code in the form's load event:

SubmitedUsersViewModel DataContext{get;set;}

private void Form1_Load(object sender, EventArgs e)
{
    base.BindDataControls(this.Controls);
    base.BindCommandControls(this.button1, DataContext.SubmitCurrentUsers);
    base.BindCommandControls(this.button2, DataContext.UpdateUser);
    base.BindCommandControls(this.button3, DataContext.RemoveUser);
}

In the code above, we first define a ViewModel object DataContext. In the BindDataControls method, it is used as the object bound to the View controls. The CurrentUser property's Name property within it is bound to the text box control, so CurrentUser.Name is bound as a composite property. For the label control and list box control, the process is similar, as shown in the diagram below:

In this way, by simply setting data properties on the View and writing a small amount of code-behind binding code, a program with two-way binding functionality is ready.

4. MVVM Example Solution

4.1 Solution Overview

To demonstrate the SOD framework's support for MVVM, we build a simple solution consisting of three project assemblies, corresponding to the three major parts of MVVM:

  1. WinFormMvvm: WinForm example program main program, assembly where View classes reside
  2. WinFormMvvm.Model: Model class assembly
  3. WinFormMvvm.ViewModel: ViewModel assembly

The built solution diagram is as follows:

Note: This solution was created using SOD Ver 5.5.5.1019, as this is the currently available SOD version on nuget. The latest SOD framework has included the MvvmForm.cs file of the WinFormMvvm project into the framework.

The program specifies the database for additional testing in App.config. The database type is Access. The default connection string may require Office 2007 or above.

Below is the content of App.config:

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <connectionStrings>
    <add name ="default" connectionString ="Provider=Microsoft.ACE.OLEDB.12.0;Jet OLEDB:Engine Type=6;Data Source=testdb.accdb" providerName="Access"/>
  </connectionStrings>
    <startup>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
    </startup>
  <runtime>
    <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
      <dependentAssembly>
        <assemblyIdentity name="PWMIS.Core" publicKeyToken="17ba13a12b9fd814" culture="neutral" />
        <bindingRedirect oldVersion="0.0.0.0-5.5.5.1019" newVersion="5.5.5.1019" />
      </dependentAssembly>
    </assemblyBinding>
  </runtime>
</configuration>

If you need support for a lower version of Access database, or want to switch to another database (e.g., SqlServer), please refer to the following steps:

  1. Open the link: http://pwmis.codeplex.com/

  2. See the section "3. Modify the connection configuration in App.config";

  3. Click the link under this section: "2.2.3 Extended data access class configuration".

4.2 Creating the MVVM WinForm View

This is a simple WinForm with three SOD "data controls", including: a label control displaying the user ID, a text box control displaying the user name, a list box control displaying existing users, and three buttons for adding, modifying, and deleting data from the list.

For data controls, you can open the "Toolbox" in the form designer, select the "General" tab, choose the context menu "Choose Items", browse to the packages\PDF.NET.SOD.WinForm.Extensions.5.5.5.1020\lib directory, select "Pwmis.Windows.dll", and you will see the SOD data controls. Then drag them onto the form.

Note: We do not directly set click events for these three button controls; instead, we bind commands. For example, for the add button, we bind the command (a method of the ViewModel) as follows:

base.BindCommandControls(this.button1, DataContext.SubmitCurrentUsers);

This binds the click event of the add user button to the DataContext's SubmitCurrentUsers method.

For the data control binding, only one line of code is needed:

base.BindDataControls(this.Controls);

As mentioned earlier, this method traverses all data controls within the first parameter's controls, finds the LinkObject and LinkProperty properties, and implements the binding between data controls and the ViewModel object. Here, it binds to the properties of the CurrentUser object in the DataContext.

Click the "..." button next to the LinkProperty property of a data control in the property browser, and the following "Data Control Property Selector" form will appear:

Since the object we want to bind here is the DataContext object of the current form, we need to browse and select the main assembly. In the property name column, all properties and sub-properties of this object will be displayed. Note that if the DataContext object does not appear in the list, check whether the Form has declared the DataContext object, and compile the assembly first. Finally, click OK, and we have set the binding information for the data control.

4.3 Creating the MVVM Model

Our model is very simple: it is responsible for creating new users, loading existing users, adding, modifying, or deleting users, and these operations are all database-related, i.e., common CRUD operations. Since this is an example with minimal logic, we can look directly at the code:

public class UserModel
{
    private static int index = 0;
    private LocalDbContext context;
    public UserModel()
    {
        context = new LocalDbContext();
    }

    public List<UserEntity> GetAllUsers()
    {
        var list= OQL.From<UserEntity>().ToList(context.CurrentDataBase);
        int max = list.Max(p => p.ID);
        index = ++max;
        return list;
    }
    public void UpdateUser(UserEntity user)
    {
        int count= context.Update<UserEntity>(user);
    }

    public void AddUsers(IList<UserEntity> users)
    {
        int count = context.AddList(users);
    }

    public void SubmitUser(UserEntity user)
    {
        int count = context.Add(user);
    }

    public void RemoveUser(UserEntity user)
    {
        int count = context.Remove(user);
    }

    public UserEntity CreateNewUser(string userName="NoName")
    {
        return new UserEntity()
        {
              ID= ++index,
              Name =userName
        };
    }
}

The user model class uses the user entity class, which is also very simple, having only an ID property and a Name property. The details are as follows:

public class UserEntity:EntityBase
{
    public UserEntity()
    {
        TableName = "Tb_User";
        PrimaryKeys.Add("UserID");
    }
    public int ID {
        get { return getProperty<int>("UserID"); }
        set { setProperty("UserID", value); }
    }

    public string Name
    {
        get { return getProperty<string>("UserName"); }
        set { setProperty("UserName", value); }
    }

}

Although this user entity class is very simple, it can be directly provided to the View as a model binding element because SOD entity classes implement the "property change notification" interface, as detailed earlier.

Next is the data context that operates on this user entity class. The user model class demonstrates how to use it, but its definition is simple:

class LocalDbContext : DbContext
{
    public LocalDbContext()
        : base("default")
    {
        //local is the connection string name
    }

    protected override bool CheckAllTableExists()
    {
        // Create user table
        CheckTableExists<UserEntity>();
        return true;
    }
}

Thus, the complete definition of a simple MVVM model class is complete.

4.4 Creating the MVVM ViewModel

The ViewModel is an abstraction of the View. It encapsulates the main view processing logic. Unlike the Presenter in MVP, the ViewModel does not contain detailed abstractions of view elements, such as an abstract list control. Instead, it encapsulates the data that the View might use and may include calls to the backend MVVM model objects.

In this example, the user ViewModel's functionality is also simple: it provides the user list needed by the View and responds to the View's commands for adding, modifying, and deleting users. The detailed code is as follows:

public class SubmitedUsersViewModel
{

    private UserModel model = new UserModel();
    public BindingList<UserEntity> Users { get; private set; }
    public UserEntity CurrentUser { get; private set; }


    UserEntity _selectUser;
    /// <summary>
    /// Currently selected user; if set, it will set the current user
    /// </summary>
    public UserEntity SelectedUser {
        get { return _selectUser; }
        set {
            _selectUser = value;
            this.CurrentUser.ID = value.ID;
            this.CurrentUser.Name = value.Name;
        }

    }

    int _selectedUserID;
    public int SelectedUserID
    {
        get { return _selectedUserID; }
        set {
            _selectedUserID = value;
            var obj = this.Users.FirstOrDefault(p=>p.ID==value);
            if (obj != null)
            {
                this.CurrentUser.ID = obj.ID;
                this.CurrentUser.Name = obj.Name;
                _selectUser = this.CurrentUser;
            }

        }
    }

    public SubmitedUsersViewModel()
    {
        var data = model.GetAllUsers();
        Users = new BindingList<UserEntity>(data);
        CurrentUser = new UserEntity();

    }

    public void UpdateUser()
    {
        var obj = this.Users.FirstOrDefault(p => p.ID == this.CurrentUser.ID);
        if (obj != null)
        {
            obj.Name = this.CurrentUser.Name;
            // After update, must call ResetBindings method, otherwise one row of data may be lost on the control
            this.Users.ResetBindings();

            model.UpdateUser(obj);
        }

    }
    public void UpdateUser(int id,string name)
    {
        var obj = this.Users.FirstOrDefault(p => p.ID == id);
        if (obj != null)
        {
            obj.Name = name;
            // After update, must call ResetBindings method, otherwise one row of data may be lost on the control
            this.Users.ResetBindings();

            model.UpdateUser(obj);
        }
    }

    public void SubmitUsers(UserEntity user)
    {
        //UserEntity newUser = new UserEntity();
        //newUser.ID = user.ID;
        //newUser.Name = user.Name;
        //Users.Add(newUser);
        if (!Users.Contains(user))
        {
            Users.Add(user);
            model.SubmitUser(user);
        }
    }
    public void SubmitCurrentUsers()
    {
        UserEntity newUser = model.CreateNewUser(CurrentUser.Name);
        SubmitUsers(newUser);
        CurrentUser.ID = newUser.ID;
    }

    public void RemoveUser()
    {
        if (SelectedUser == null)
        {

            return;
        }
        var obj = this.Users.FirstOrDefault(p => p.ID == SelectedUser.ID);
        if (obj != null)
        {
            this.Users.Remove(obj);
            // After update, must call ResetBindings method, otherwise one row of data may be lost on the control
            this.Users.ResetBindings();

            model.RemoveUser(obj);
        }
    }
}

4.5 Adding NuGet Package References

For the entire solution, we need to add the PDF.NET Core package. However, for the WinForms main program, we need to add two additional related packages: one SOD WinForm extension and one SOD Access extension. The diagram below shows all packages installed in the solution:

4.6 Running the Solution

After the above steps, we have added view elements, set up data bindings for the view elements, and created the model and ViewModel objects. A simple MVVM example program is now ready. Below is the runtime screenshot:

5. MVVM Pattern Summary

By running this example, you have likely experienced some characteristics of MVVM, but it may be difficult to articulate them precisely. After consulting with several WPF senior experts, they summarized the core features (selling points) of MVVM as follows:

  1. Decoupling of view logic (ViewModel) from the View (view elements, styles);
  2. Two-way data binding between the View and the ViewModel or Model, data-driven view rather than view-driven data;
  3. Separation of the View and ViewModel turns all interface functionality into code and provides the possibility of TDD.

6. SOD WinForms MVVM Support

Starting from SOD framework version 5.6.0.1111, released on "Singles' Day," you can get direct WinForms MVVM support in versions after this. If you are using an earlier version, you will need to do a bit more work like in this example, but this will not affect your existing SOD-supported solutions.

This example solution will be available for direct download on the framework's open-source site http://pwmis.codeplex.com, and the source code has been fully submitted. You can view detailed code instructions at the following address:

http://pwmis.codeplex.com/SourceControl/latest#SOD/Example/WinFormMvvm/WinFormMvvm/Readme.txt

For more information, to join the community QQ group for discussion, or to donate to this framework, please visit the framework's official website:

http://www.pwmis.com/sqlmap

Thank you for choosing the SOD framework. We believe it will bring great convenience to your development!

SOD Development Team

Deep Blue Doctor

2016.11.13

------------PS---------------

Thanks to @Guangzhou-Yingu from the SOD development team, who has promptly updated the SOD framework's nuget package to the latest version, resolving the nuget package issues mentioned earlier.

Finally, attached are the SOD repository address and screenshot information:

Keep Exploring

Related Reading

More Articles
Same category / Same tag 2/29/2024

Data Display Can Also Be Done Like This in Winform

In the process of developing Winform, data display functionality is often required. Previously, the gridcontrol control was commonly used. Today, through an example, I would like to introduce how to use the table component from Ant Design Blazor for data display in a Winform Blazor Hybrid application.

Continue Reading
Same category / Same tag 2/29/2024

Can the Winform interface also look good?

A few days ago, I introduced using Blazor Hybrid in Winform, and mentioned that with the Blazor UI, our Winform programs can be designed to look better. Next, I will illustrate with an example of drawing in Winform Blazor Hybrid, hoping it helps you.

Continue Reading