Web前端技術的發展使得MVVM模式相比WPF時代更加流行,然而老舊的WinForms仍然占據桌面開發技術很大一部分,現在SOD框架提供了WinForms 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,文本框控制項顯示用戶名,一個列表框控制項顯示已經有用戶列表,三個按鈕分別用來向列表添加,修改和刪除數據。
對於數據控制項,可以在此窗體設計器界面,打開“工具箱”,在“常規”選項卡裡面,選擇上下文菜單“選擇項”,瀏覽到packages\PDF.NET.SOD.WinForm.Extensions.5.5.5.1020\lib目錄,選擇“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 }; } }View Code
用戶模型類會使用用戶實體類,它也很簡單,只有一個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); } } }View Code
添加Nuget包引用
對於整個解決方案,我們都需要添加 PDF.NET Core 包,但是對於我們的WinForms 主程式,需要額外添加2個相關的包,一個SOD WinForm擴展和一個SOD Access 擴展,下麵是解決方案安裝的全部包示意圖:
運行解決方案
經過上面的過程,我們添加了視圖元素,設置好了視圖元素的數據綁定,創建了模型和視圖模型對象,一個簡單的MVVM示常式序就好了,下麵是運行效果圖:
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群討論,或者捐助本框架,請移步框架官網:
感謝你選擇SOD框架,相信它能夠為你的開髮帶來很大的便利!
SOD開發團隊
深藍醫生
2016.11.13
------------PS---------------
感謝SOD開發團隊的 @廣州-銀古 同學,他已經及時將SOD框架的 nuget包更新到了最新版本,沒有前面說的 nuget包問題了。