“老壇泡新菜”: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群討論,或者捐助本框架,請移步框架官網:
感謝你選擇SOD框架,相信它能夠為你的開發帶來很大的便利!
SOD開發團隊
深藍醫生
2016.11.13
------------PS---------------
感謝SOD開發團隊的 @廣州-銀古 同學,他已經及時將SOD框架的 nuget包更新到了最新版本,沒有前面說的 nuget包問題了。