1. 程式人生 > 其它 >“老壇泡新菜”:SOD MVVM框架,讓WinForms煥發新春

“老壇泡新菜”:SOD MVVM框架,讓WinForms煥發新春

火熱的MVVM框架

最近幾年最熱門的技術之一就是前端技術了,各種前端框架,前端標準和前端設計風格層出不窮,而在眾多前端框架中具有MVC,MVVM功能的框架成為耀眼新星,比如GitHub關注度很高的Vue.js ,由於是國人作品,其設計風格和文件友好度對國人而言更勝一籌,因此我也將它推薦到公司採用,其中我推薦都理由就是它非常優秀的MVVM功能,面向資料而不是面向DOM細節相比jQuery等更加節省程式碼,更符合後端程式設計師的胃口,也更有利於UI設計人員跟程式設計師都分工配合。

下面是Vue.js實現MVVM功能的原理圖:

前面說的Vue.js框架這些優點的是否很眼熟?沒錯,這就是早些年流行於WPF的MVVM技術,相比WinForms技術,WPF可以提供給UI設計人員更加強大的設計能力,做出更炫更好看的介面。只不過MS的很多技術總是很超前技術更新很快,WPF新推出的時候WinForms還佔據桌面開發主要領域,隨後還沒有火起來移動開發時代已經來臨,基於Web的前端技術大大發展,從而風頭蓋過了WPF,但是WPF引入的MVVM思想卻在Web前端得到了發揚光大,現在各種基於MVVM的前端框架猶如雨後春筍。

WinForms上的MVVM需求

Web前端技術的大力發展,各種跨平臺的基於HTML5的移動前端開發技術逐漸成熟,各種應用逐步由傳統的C/S 轉換到 B/S ,APP模式,基於C/S模式的前端技術比如WPF的關注度逐漸下降,因此WPF上的MVVM並不是應用得很廣,目前很多遺留的或者新的 C/S系統仍然採用WinForms技術開發維護,然而WinForms 上卻沒有良好的MVVM框架,WinForms 的UI效果和整體開發質量,開發效率沒有得到有效提高,要過度到WPF開發這種不同開發風格的技術難度又比較大,所以,如果有一種能夠在 WinForms 上的MVVM框架,無疑是廣大後端.NET程式設計師的福音。

筆者一直是一個奮鬥在一線的.NET開發人員,架構師,對於Web 和桌面,後端開發技術都有廣泛的涉及,深刻理解開發人員自嘲自己為“碼農”的心理的,工作辛苦又沒有時間陪女朋友陪家人,所以我一直總結整理如何提高開發效率,改善開發質量的方法,經過近10年的時間,發展完善了一套開發框架—SOD框架。最近研究改善Web前端開發的技術,Vue.js框架的MVVM思想再一次讓我覺得WinForms上MVVM技術的必要性,發現要實現MVVM框架其實並不難,關鍵在於模型(Model)和檢視(View)的雙向繫結,即模型的改變引起檢視內容的改變,而檢視的改變也能夠引起模型的改變。

SOD WinForms MVVM實現原理

要實現這種改變,對於被繫結方,必須具有屬性改變通知功能,當繫結方改變的時候,通知被繫結方讓它做相應的處理。在.NET中,實現這種通知功能的介面就是:

 INotifyPropertyChanged

它的定義在System.dll 中,早在 .NET 2.0 就已經支援。下面是該介面的具體定義:

namespace System.ComponentModel
{
    // 摘要: 
    //     向客戶端發出某一屬性值已更改的通知。
    public interface INotifyPropertyChanged
    {
        // 摘要: 
        //     在更改屬性值時發生。
        event PropertyChangedEventHandler PropertyChanged;
    }
}

SOD框架的實體類基類 EntityBase 實現了此介面:

public abstract class EntityBase : INotifyPropertyChanged, ICloneable, PWMIS.Common.IEntity
{
/// <summary>
        /// 屬性改變事件
        /// </summary>
        public event PropertyChangedEventHandler PropertyChanged;
        /// <summary>
        /// 觸發屬性改變事件
        /// </summary>
        /// <param name="propertyFieldName">屬性改變事件物件</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));
            }
       }
// 其它程式碼略… … 
  }

所以SOD框架的實體類可以直接用來作為MVVM上的Model提供給View 做為被繫結物件,因此要我們只需要解決WinForms 形式的View 元素如何實現繫結操作,那麼我們的WinForms 應用即可實現MVVM功能了。在WinForms 上,控制元件基本上都已經實現了繫結功能,它就是控制元件的 DataBindings,向它新增繫結即可,例如下面的例子:

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

這樣當文字框架輸入的內容改變後,實體類物件 userEntity.Name 屬性的值也會改變。如果userEntity是SOD實體類,所以userEntity.Name 改變,文字框的Text屬性也會同步改變。

SOD框架的資料控制元件(WinForms,WebForms)都實現了 IDataControl 介面,它定義了幾個重要的屬性 LinkObject,LinkProperty :

/// <summary>
    /// 資料對映控制元件介面
    /// </summary>
    public interface IDataControl
    {
        
        /// <summary>
        /// 與資料庫資料項相關聯的資料
        /// </summary>
        string LinkProperty
        {
            get;
            set;
        }
        
        /// <summary>
        /// 與資料關聯的表名
        /// </summary>
        string LinkObject
        {
            get;
            set;
        }
// 其它介面方法內容略… … 

我們可以使用 LinkObject 來指定要繫結的實體類物件,而LinkProperty 來指定要繫結的物件的屬性,因此可以通過下面的程式碼實現WinForms 控制元件與SOD實體類的雙向繫結:

public void BindDataControls(Control.ControlCollection controls)
        {
            var dataControls = MyWinForm.GetIBControls(controls);
            foreach (IDataControl control in dataControls)
            {
                //control.LinkObject 這裡都是 "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);
                }
            }
        }

另外,我們可能還需要將 一些命令繫結到檢視上,而要實現此功能也比較簡單:

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](); 
                };
            }
        }

經過這樣的過程後,我們僅需要在窗體載入事件上寫下面的幾行程式碼就行了:

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

上面的程式碼中,首先定義了一個檢視模型物件 DataContext,在方法 BindDataControls 裡面作為繫結到檢視控制元件上的物件,它裡面的 CurrentUser屬性的Name屬性繫結到了文字框控制元件上,所以 CurrentUser.Name 是作為複合屬性來繫結的,對於標籤控制元件和列表框控制元件,也是類似的過程,如下圖:

這樣,在檢視上做簡單的資料屬性設定和寫少量的code behind繫結程式碼,一個具有雙向繫結功能的程式就好了。

MVVM示例解決方案

解決方案概覽

為了向大家演示SOD框架對於MVVM的支援,我們搭建一個簡單的解決方案,一共分為三個專案程式集,分別對應MVVM的三大部分:

WinFormMvvm:            WinForm 示例程式主程式,檢視類所在程式集

WinFormMvvm.Model:      模型類程式集

WinFormMvvm.ViewModel:  檢視模型程式集

搭建好的解決方案圖如下:

注意:此解決方案是使用SOD Ver 5.5.5.1019 做的,因為這是目前nuget 上SOD的版本,最新的SOD框架已經把WinFormMvvm專案的 MvvmForm.cs 檔案納入到框架之內了。

 程式在App.config中指定了本次附加測試的資料庫,資料庫型別為 Access,預設的連線字串可能要求Office 2007以上版本支援。

下面是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>

如果你需要更低版本的 Access 資料庫支援,或者換用其它資料庫(比如 SqlServer),請閱讀參考下面步驟提供的資訊:

1,開啟下面連結:

   http://pwmis.codeplex.com/

2,看到內容章節“3,修改下App.config 檔案的連線配置”;

3,點選本節下的連結“2.2.3 擴充套件資料訪問類配置”。

建立MVVM的WinForm檢視

這是一個簡單的WinForm 窗體,有三個SOD“資料控制元件”,包括:一個標籤控制元件顯示使用者的ID,文字框控制元件顯示使用者名稱,一個列表框控制元件顯示已經有使用者列表,三個按鈕分別用來向列表新增,修改和刪除資料。

對於資料控制元件,可以在此窗體設計器介面,開啟“工具箱”,在“常規”選項卡里面,選擇上下文選單“選擇項”,瀏覽到packagesPDF.NET.SOD.WinForm.Extensions.5.5.5.1020lib目錄,選擇“Pwmis.Windows.dll” ,即可看到SOD的資料控制元件,然後拖拽到窗體上即可。

注意我們不會給這三個按鈕控制元件直接設定單擊事件,而是通過命令繫結的形式。例如對應新增按鈕,我們如下繫結命令(檢視模型的一個方法):

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

這會將新增使用者的按鈕控制元件的單擊事件,繫結到DataContext的SubmitCurrentUsers 方法上。

而對於資料控制元件的繫結,只需要下面的一行程式碼:

base.BindDataControls(this.Controls);

前面已經說過,該方法會遍歷方法上第一個引數裡面的所有資料控制元件,找到LinkObject和LinkProperty屬性,實現資料控制元件和檢視模型物件的繫結,這裡繫結的是 DataContext物件的CurrentUser物件的屬性。

單擊屬性瀏覽器中資料控制元件的LinkProperty 屬性旁邊的“…”按鈕,會彈出下面的“資料控制元件屬性選擇器”窗體:

由於這裡我們要繫結的物件是當前窗體的DataContext物件,所以需要瀏覽選擇到主程式集,這樣在屬性名稱一欄,會顯示此物件所有的屬性和子屬性。注意如果DataContext物件沒有出現在列表裡面,需要檢查Form 窗體是否聲明瞭 DataContext物件,並且需要首先編譯一次程式集。最後,單擊確定,我們就設定好了資料控制元件要繫結的資訊。

建立MVVM的模型

我們的模型很簡單,就是負責建立新使用者,載入已有使用者,新增,修改或者刪除使用者,並且這些操作都是針對資料庫的,也就是我們通常的CRUD操作。由於是示例沒有太多邏輯,我們直接看程式碼即可:

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

使用者模型類會使用使用者實體類,它也很簡單,只有一個ID屬性和一個Name屬性,詳細內容如下:

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

    }

該使用者實體類雖然很簡單,卻可以直接提供給檢視作為模型繫結的元素,因為SOD實體類都實現了“屬性修改通知”介面,前面已經詳細說明。

接下來就是操作此使用者實體類的資料上下文了,使用者模型類展示瞭如何使用它,但是它的定義卻很簡單:

    class LocalDbContext : DbContext
    {
        public LocalDbContext()
            : base("default")
        {
            //local 是連線字串名字 
        }

        protected override bool CheckAllTableExists()
        {
            //建立使用者表 
            CheckTableExists<UserEntity>();
            return true;
        }
    }

至此,一個簡單的MVVM模型類的全部定義就完成了。

建立MVVM的檢視模型

檢視模型是對檢視的一個抽象,它封裝了主要的檢視處理邏輯,與MVP的Presenter不同,檢視模型並不會包含詳細檢視元素的抽象,比如一個抽象的列表控制元件,而是對檢視可能用到的資料進行封裝,並且可能包含對後端MVVM的模型物件呼叫。

在本例中,我們的使用者檢視模型的功能也很簡單,就是提供檢視需要的使用者列表和響應檢視的增加,修改,刪除使用者的命令,詳細程式碼如下

public class SubmitedUsersViewModel
    {

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

        
        UserEntity _selectUser;
        /// <summary>
        /// 當前選擇的使用者,如果設定,則會設定當前使用者
        /// </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;
                //更新後必須呼叫 ResetBindings 方法,否則控制元件上的資料會丟失一行
                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;
                //更新後必須呼叫 ResetBindings 方法,否則控制元件上的資料會丟失一行
                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);
                //更新後必須呼叫 ResetBindings 方法,否則控制元件上的資料會丟失一行
                this.Users.ResetBindings();

                model.RemoveUser(obj);
            }
        }
    }

新增Nuget包引用

對於整個解決方案,我們都需要新增 PDF.NET Core 包,但是對於我們的WinForms 主程式,需要額外新增2個相關的包,一個SOD WinForm擴充套件和一個SOD Access 擴充套件,下面是解決方案安裝的全部包示意圖:

執行解決方案

經過上面的過程,我們添加了檢視元素,設定好了檢視元素的資料繫結,建立了模型和檢視模型物件,一個簡單的MVVM示例程式就好了,下面是執行效果圖:

MVVM模式總結

通過執行此示例,相信你已經體驗了MVVM的一些特點,但可能難以表述貼切,正好我跟幾個WPF資深專家交流後,他們總結出了MVVM的幾個核心特點(賣點):

1,檢視邏輯(檢視模型)和檢視(檢視元素,樣式)的解除耦合; 2,檢視和檢視模型或者模型的雙向資料繫結,面向資料驅動檢視而不是檢視驅動資料;

3,檢視和檢視模型的分離將介面功能全部程式碼化,並提供TDD可能性。

SOD WinForms MVVM支援

自SOD框架版本 5.6.0.1111 釋出的這個“光棍節“版本中,您已經可以在此以後的版本中獲得直接的WinForms MVVM支援,如果是之前的版本,那麼需要本示例程式一樣稍微多做一點工作,但這對於你現有的SOD支援的解決方案來說不會造成任何影響。

本示例方案將會放到框架的開源網站 http://pwmis.codeplex.com 上提供直接的下載,並且原始碼已經全部提交,可以通過下面地址檢視詳細的程式碼說明:

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

瞭解更多資訊或者加入社群QQ群討論,或者捐助本框架,請移步框架官網:

http://www.pwmis.com/sqlmap

感謝你選擇SOD框架,相信它能夠為你的開發帶來很大的便利!

SOD開發團隊

深藍醫生

2016.11.13

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

感謝SOD開發團隊的 @廣州-銀古 同學,他已經及時將SOD框架的 nuget包更新到了最新版本,沒有前面說的 nuget包問題了。