1. 火熱的 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 的前端框架猶如雨後春筍。
2. 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)的雙向綁定,即模型的改變引起視圖內容的改變,而視圖的改變也能夠引起模型的改變。
3. 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 綁定代碼,一個具有雙向綁定功能的程式就好了。
4. mvvm 示例解決方案
4.1解決方案概覽
為了向大家演示 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),請閱讀參考下面步驟提供的信息:
- 打開下面連結:
看到內容章節“3,修改下 app.config 文件的連接配置”;
點擊本節下的連結“2.2.3 擴展數據訪問類配置”。
4.2創建 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 對象,並且需要首先編譯一次程式集。最後,單擊確定,我們就設置好了數據控制項要綁定的信息。
4.3創建 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 模型類的全部定義就完成了。
4.4創建 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);
}
}
}
4.5添加 nuget 包引用
對於整個解決方案,我們都需要添加 pdf.net core 包,但是對於我們的 winforms 主程式,需要額外添加 2 個相關的包,一個 sod winform 擴展和一個 sod access 擴展,下面是解決方案安裝的全部包示意圖:

4.6運行解決方案
經過上面的過程,我們添加了視圖元素,設置好了視圖元素的數據綁定,創建了模型和視圖模型對象,一個簡單的 mvvm 示例程式就好了,下面是運行效果圖:

5. mvvm 模式總結
通過運行此示例,相信你已經體驗了 mvvm 的一些特點,但可能難以表述貼切,正好我跟幾個 wpf 資深專家交流後,他們總結出了 mvvm 的幾個核心特點(賣點):
視圖邏輯(視圖模型)和視圖(視圖元素,樣式)的解除耦合;
視圖和視圖模型或者模型的雙向數據綁定,面向數據驅動視圖而不是視圖驅動數據;
視圖和視圖模型的分離將界面功能全部代碼化,並提供 tdd 可能性。
6. 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 包問題了。
最後附上 sod 庫倉庫地址及截圖信息:
- SOD 库仓库地址:https://github.com/znlgis/sod
