最近開始重構一個稍嫌古老的C/S項目,原先採用的技術棧是『WinForm』+『WCF』+『EF』。相對於如今鋪天蓋地的B/S架構來講,看上去彷佛和Win95同樣古老,不少新入行的,可能就沒有見過經典的C/S架構的系統。事實上,做爲企業信息管理系統,包括ERP/CRM/SCM等,桌面客戶端仍是很OK的。程序員
此次重構原定的目標有兩個:數據庫
一、客戶端仍是WinForm不變,但使用MVC模式重寫;編程
二、WCF改爲WebAPI。api
通過2周時間的嘗試和探索,重構計劃變動爲:服務器
一、使用VMVC模式來重構WinForm客戶端;架構
二、用WCF實現僞WebAPI,其本質仍是個WCF服務,但實現了RESTful風格的WebAPI。ide
此次和你們分享我對客戶端架構的一些探索,就不展開服務端相關的話題了。那麼,什麼是VMVC呢?呵呵,這個是我發明的新名稱,和MVC的區別在於用ViewModel替換了Model。ViewModel和View之間實現雙向數據綁定,View上面的交互產生的操做指令,仍是由Controller接收,而後經過對ViewModel的操做,更新View的數據。單元測試
簡單地說,就是ViewModel負責數據流,View負責顯示和接受用戶指令,而Controller則居中調度。示意圖以下:測試
因爲實現了數據雙向綁定,因此在必定程度上簡化了數據的存儲。只須要執行ViewModel上的Save()方法,就能夠將新的數據經過WebAPI存儲到數據庫了。url
ViewModel的職責很是明確,就是一個數據流引擎!因此基本上都是Load()、Save()、Show()、Refresh()、Close()這些無腦方法,一丁點的業務邏輯都木有。很是適合有必定編程經驗,但不瞭解業務邏輯的程序員編寫。
而View就更簡單了,徹底由VS的窗體設計器生成。UI設計師今後不須要PS了,根據產品原型直接拖控件就OK。
最後,全部的業務邏輯都寫在Controller裏面,這樣就爲自動化測試提供了可能。測試工程師只須要編寫一段測試代碼替代Controller,同時對View的數據進行注入就能夠跑單元測試。
下面是我用於嘗試這種模式的示例,但願可以起到拋磚引玉的做用。
代碼結構:
Controller(部分代碼),經過訂閱View上面的肯定按鈕點擊事件實現用戶操做的委託:
1 /// <summary> 2 /// 修改服務器配置 3 /// </summary> 4 private void ConfigServer() 5 { 6 _SetModel = new SetModel(); 7 8 // 訂閱肯定按鈕點擊事件 9 _SetModel.View.ConfirmButton.Click += SetConfirm_Click; 10 _SetModel.ShowDialog(); 11 } 12 13 /// <summary> 14 /// 點擊肯定按鈕 15 /// </summary> 16 /// <param name="sender"></param> 17 /// <param name="e"></param> 18 private void SetConfirm_Click(object sender, EventArgs e) 19 { 20 if (!_SetModel.Test()) return; 21 22 _SetModel.Save(); 23 _SetModel.Close(); 24 }
ViewModel:
1 using System; 2 using System.Windows.Forms; 3 using Insight.Utils.Client; 4 using Insight.Utils.Common; 5 using Insight.WS.Client.Common.Utils; 6 using Insight.WS.Client.MainApp.Views; 7 8 namespace Insight.WS.Client.MainApp.Models 9 { 10 public class SetModel 11 { 12 public LoginSet View = new LoginSet(); 13 14 private string _Address = Config.BaseAddress(); 15 private string _Port = Config.Port(); 16 private bool _SaveUser = Config.IsSaveUserInfo(); 17 18 /// <summary> 19 /// 構造方法,初始化控件初始值 20 /// 經過訂閱事件實現雙向數據綁定 21 /// </summary> 22 public SetModel() 23 { 24 View.AddressInput.EditValueChanged += AddressChanged; 25 View.AddressInput.Text = _Address; 26 27 View.PortInput.EditValueChanged += PortChanged; 28 View.PortInput.Text = _Port; 29 30 View.SaveUserCheckBox.CheckStateChanged += SaveUserChanged; 31 View.SaveUserCheckBox.Checked = _SaveUser; 32 } 33 34 /// <summary> 35 /// 顯示對話框 36 /// </summary> 37 public void ShowDialog() 38 { 39 View.ShowDialog(); 40 } 41 42 /// <summary> 43 /// 關閉對話框 44 /// </summary> 45 public void Close() 46 { 47 View.DialogResult = DialogResult.OK; 48 View.Close(); 49 } 50 51 /// <summary> 52 /// 測試服務器連通性 53 /// </summary> 54 /// <returns>bool 是否經過連通性測試</returns> 55 public bool Test() 56 { 57 var url = $"http://{_Address}:{_Port}/commonapi/v1.0/test"; 58 var result = new HttpClient(url).Request(Params.Token); 59 if (result.Code != "400") return true; 60 61 Messages.ShowError("請配置正確的服務器地址和端口號!"); 62 return false; 63 } 64 65 /// <summary> 66 /// 保存設置 67 /// </summary> 68 public void Save() 69 { 70 if (!_SaveUser) Config.SaveUserName(string.Empty); 71 72 Config.SaveIsSaveUserInfo(_SaveUser); 73 Config.SaveAddress(_Address, _Port); 74 75 Params.InsightServer = $"http://{_Address}:{_Port}"; 76 } 77 78 /// <summary> 79 /// 服務器地址發生變化 80 /// </summary> 81 /// <param name="sender"></param> 82 /// <param name="e"></param> 83 private void AddressChanged(object sender, EventArgs e) 84 { 85 _Address = View.AddressInput.Text; 86 } 87 88 /// <summary> 89 /// 服務端口發生變化 90 /// </summary> 91 /// <param name="sender"></param> 92 /// <param name="e"></param> 93 private void PortChanged(object sender, EventArgs e) 94 { 95 _Port = View.PortInput.Text; 96 } 97 98 /// <summary> 99 /// 保存用戶帳號選項發生變化 100 /// </summary> 101 /// <param name="sender"></param> 102 /// <param name="e"></param> 103 private void SaveUserChanged(object sender, EventArgs e) 104 { 105 _SaveUser = View.SaveUserCheckBox.Checked; 106 } 107 } 108 }