最近开始重构一个稍嫌古老的C/S项目,原先采用的技术栈是『WinForm』+『WCF』+『EF』。相对于现在铺天盖地的B/S架构来说,看上去似乎和Win95一样古老,很多新入行的,可能就没有见过经典的C/S架构的系统。事实上,作为企业信息管理系统,包括ERP/CRM/SCM等,桌面客户端还是很OK的。
这次重构原定的目标有两个:
1、客户端还是WinForm不变,但使用MVC模式重写;
2、WCF改成WebAPI。
经过2周时间的尝试和探索,重构计划变更为:
1、使用VMVC模式来重构WinForm客户端;
2、用WCF实现伪WebAPI,其本质还是个WCF服务,但实现了RESTful风格的WebAPI。
这次和大家分享我对客户端架构的一些探索,就不展开服务端相关的话题了。那么,什么是VMVC呢?呵呵,这个是我发明的新名称,和MVC的区别在于用ViewModel替换了Model。ViewModel和View之间实现双向数据绑定,View上面的交互产生的操作指令,还是由Controller接收,然后通过对ViewModel的操作,更新View的数据。
简单地说,就是ViewModel负责数据流,View负责显示和接受用户指令,而Controller则居中调度。示意图如下:
由于实现了数据双向绑定,所以在一定程度上简化了数据的存储。只需要执行ViewModel上的Save()方法,就可以将新的数据通过WebAPI存储到数据库了。
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 publ