最近开始重构一个稍嫌古老的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 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 }
- 15楼nele
- 不错,支持
- 14楼xmj112288
- 可以试试WPF,里面是数据双向绑定用起来非常方便
- Re: xuanbg
- @xmj112288,引用可以试试WPF,里面是数据双向绑定用起来非常方便,对!WinForm就这点不好!!!,,还好现在委托可以这样写:,View.UserNameInput.EditValueChanged += (sender, args) =gt; _Account = View.UserNameInput.Text;,还是比较方便的说。
- 13楼amazonove
- MVC里的M和MVVM里的M是一个东西吗
- Re: xuanbg
- @amazonove,据我的理解,两者不一样,因为职责不同。不用纠结于那个名称,我们只看其职责就好了。
- 12楼躺zhe也中槍
- 支持,最近也重构之前的指纹系统。
- 11楼gqzhao
- winform市场仍然很有市场,支持楼主。
- 10楼Keanuo
- 个人感觉winform怎么弄也是假的mvvm,而且开发效率不高。
- Re: xuanbg
- @Keanuo,还好吧,没感觉比开发WebAPP更费事,控件什么的布局整齐一些根本不费什么事,也不需要什么特别的技能,效果虽然没有web炫酷,但总体还是很清爽怡人的。,,比起不套用任何模式直接在窗体里面写代码,这样折腾一下还是不少好处的。从目前看来,优化业务逻辑简直太简单了有木有!,,俗话说的好:生命不息,折腾不止。。。。。
- 9楼codesnippet.info
- VM直接可以控制V,这个不能接受
- Re: xuanbg
- @codesnippet.info,引用VM直接可以控制V,这个不能接受,在这一点上,和MVVM模式没有任何区别啊,都是数据双向绑定而已。
- 8楼坦荡
- 支持支持
- 7楼牛腩
- 支持支持
- 6楼gc_Joey
- wpf mvvm 不好么,这么看着好别捏
- Re: xuanbg
- @gc_Joey,引用wpf mvvm 不好么,这么看着好别捏,这个模式是针对WinForm设计的,如果是WPF,自然就MVVM了。
- 5楼myjece
- 感觉太别扭了,为了模式而模式
- Re: xuanbg
- @myjece,并没有哦,这样做最大的好处是可以实现单元测试。而且各部分职责分明对于开发和维护来说,都是很好的基础。
- 4楼Soar、毅
- 找找MVP相关的文章吧。
- 3楼信息化建设
- winform本来就支持双向的数据绑定,再加上绑定强类型的对象(ORM),ORM本身要支持属性变更通知功能,以上说的功能基本就都有了。
- Re: xuanbg
- @信息化建设,引用winform本来就支持双向的数据绑定,再加上绑定强类型的对象(ORM),ORM本身要支持属性变更通知功能,以上说的功能基本就都有了。,WinForm控件中的DataBindings.Add()实现的双向绑定在C#提供了『nameof』这个关键词后用起来才不会有问题。不然属性名称一改就异常了。这个功能在我看来是有缺陷的,要小心使用。,,另外,我还是认为用委托来自己实现双向绑定比较灵活,特别是显示的数据经过拼接、裁剪、修饰或运算的情况下。
- 2楼吴瑞祥
- 就是mvvm...
- Re: xuanbg
- @吴瑞祥,和MVVM还是有本质不同的,在MVVM模式中,数据是要经过Model的,但在这里,Controller只负责业务逻辑,和数据一点边都不沾。而且Controller是由View来驱动而非ViewModel。
- 1楼Pete-Jones
- 要有Demo源码就更好了。
- Re: xuanbg
- @Pete-Jones,我会在重构完成之后发布源码,现在还太乱了点,就不放出来了。主要还是和大家一起探讨这种新的模式的优势和问题。