完全使用一组 DSL 来操作 Grid 控件
最近尝试了一下将 XtraGrid 的初始化工作封装成内部 DSL,例如一个普通的基础数据的增删改查操作的代码会像下面这样:
public partial class UserForm : XtraForm { private readonly UserRepository UserRepository; private readonly UserService UserService; private readonly GridManager_gridManager; public UserForm(UserRepository UserRepository, UserService UserService) { InitializeComponent(); this.UserRepository = UserRepository; this.UserService = UserService; _gridManager = new GridManager (userGrid); _gridManager.CommonInlineEditable() .Caption("系统用户") .Column("登录名", t => t.LoginName) .Column("用户名", t => t.Text) .Column("工号", t => t.Code) .Column("是否停用", t => t.IsStop) .Column("录入码1", t => t.InputCode1) .Column("录入码2", t => t.InputCode2) .Column("录入码3", t => t.InputCode3) .Column("排序", t => t.OrderField) .Validate( v => v.Varify(t => t.LoginName, spec => spec.NotNull() .Unique(UserRepository.IsLoginNameUnique) .Msg("登录名不能为空或重复。").End()) .Varify(t => t.Text, spec => spec.NotNull().Msg("用户名不能为空。").End()) .Varify(t => t.Code, spec => spec.NotNull() .Unique(UserRepository.IsCodeUnique) .Msg("工号不能为空或重复。").End())) .DataSource(() => UserRepository.GetAllOrderBy(t => t.Text).ToDtoList ()) .ById(id => new UserDto().FromEntity(UserRepository.GetById(id))) .SaveAction(dto => { User newEntity = new User(); dto.AssignToEntity(newEntity); UserService.Save(newEntity); }) .UpdateAction(dto => { User entity = UserRepository.GetFromCache(dto.Id); dto.AssignToEntity(entity); UserService.Update(entity); }) .DeleteAction(id => UserService.Delete(id)); } private void SaveButton_Click(object sender, EventArgs e) { _gridManager.SaveAll(); } private void AddButton_Click(object sender, EventArgs e) { _gridManager.AddNew(); } private void DeleteButton_Click(object sender, EventArgs e) { _gridManager.Delete(); } private void RefreshButton_Click(object sender, EventArgs e) { _gridManager.ReLoadData(); } private void UserForm_Load(object sender, EventArgs e) { _gridManager.ReLoadData(); } }
运行起来像这样:
DevExpress 这套控件功能非常强大,自带的可视化设计器也非常方便。如果控件非常多,或者希望进行复杂的交互,配合 DataLayoutControl 拖拖拽拽就可生成表单窗体。感觉照比宝蓝的 RAD 辉煌时代有过之而无不及。那么为什么还要放弃方便的可视化设计器,费力封装成代码呢?主要考虑到直接设置控件的属性和事件有如下几个缺点:
1. 不便于重构。每个数据列的 FeildName 属性都是将实体或DTO的属性名以字符串的方式赋值,将来如果想改名、移除或移动一个属性、移除/合并/拆分实体,无法直接用重构工具精确地找到所有引用到该属性/实体的控件,只能用字符串查找的方法,低效而且容易有遗漏,这也是项目后期进行实体级别的重构让人望而却步的原因之一。虽然使用DTO能缓解此问题,但终究让人不爽。 而使用类似 Column("登录名", t => t.LoginName) 这样的语句进行配置就不会有这种问题了。
2. 导致大量的重复代码。像基础数据维护这种功能,大部分都是简单的增删改查,往往只使用了 Grid 控件功能的一个子集而且大部分都比较相像。例如上图所示,需要显示查找栏,不需要显示分组栏,要显示Grid的标题栏,自动列宽就可以等等。这些属性一个一个地进行设置的话,也挺费力气。当需要增加一个“部门管理”的时候,感觉跟“用户管理” 差不多,就会把“用户管理”这个 Grid 复制过来,甚至可能把 “用户管理”整个窗体的代码完全复制过来,再进行修改。这其实也是一种重复代码。对于把 DRY(don't repeat yourself) 作为一项基本原则的人来说这是无法忍受的。
3. 代码意图不明显,可读性差。虽然只是简单的增删改查,但是麻雀虽小五脏俱全——需要进行前端验证提升用户体验,验证不通过时要以友好的方式显示错误信息,按ESC键能够撤销修改;修改过的数据行希望以粗体显示,按保存按钮保存之后要恢复成正常字体;保存时要判断是修改还是新增数据;对于引用了其它的实体的属性或者枚举类型的属性要做特殊处理。如果前端验证失败,而后用户又修改成可以通过验证的数据之后直接按了保存按钮,虽然能保存成功,但是如果不做特殊处理的话那个红色的叉叉并不会自动消失。。。要把这些功能和细节全处理好的话往往要在好几个事件里面写代码配合着一起完成。这样单独看某一块代码的话往往不知道它是做什么的。不但新人掌握起来比较费劲,过一段时间我们自己要想读懂都要费一番力气。我们希望能把相关的功能集中起来,并且隐藏实现细节。
所以这次设计这组操作 XtraGrid 的内部 DSL 的目标就是:
1. 可读性第一。不追求读起来像自然语言,但希望相关的内容能在一处,读起来自然流畅,尽量隐藏实现细节。从实际效果来看,由于有 lambda 表达式和一些语法糖可用,虽然有一些杂音和少量实现细节,基本上还挺让人满意。
2. 不使用字符串硬编码。不太地道地借助了 FluentNHibernate 的帮助类库轻松实现,以后再彻底把它的代码偷过来。
3. DSL 实现代价最好小一点,作为一个薄的封装层,并且可以随需演进。出于希望DSL的实现越简单越好的想法,并没有使用大量自己的对象模型,而是使用DSL的API直接操作Grid控件的属性。这样一开始确实挺省事,但是往往出于实现上的困难而把实现代码越搞越乱,这个挺让人纠结。
4. 减少冗余代码。冗余代码还是有一点,例如 t=>t.Code 在 Column 和 Validate 中就出现了 2 次,出于实现上的困难暂时只能这样了。
总之基本还算不错,由于实现代码还很不成熟就先不贴出来了。