直接切入正题,这是我09年到11年左右业余时间编写的项目,最初的想法很简单,做一个能拖拖拽拽就直接生成应用程序的工具,不用写代码,把能想到的业务操作全部封装起来,通过配置的方式把这些业务操作组织起来运行。
项目的核心功能已经基本实现,但12年之后我基本停止了这方面的开发,现在翻出来在这里写出来想和大家交流一下。
下文我主要从技术角度进行一些分解,由于时间比较长,加上当时技术力有限,不足之处还请指正,谢谢。
项目使用了 .Net Framework 3.5 开发,分为两大块: IDE 和 运行时(解析器)
IDE中开发的项目在打包后生成 zip 格式的包,解析器通过读取 zip 包实时解析运行,有点类似中间语言的概念,但我这里生成的 zip 包中主要以 xml 文件为主,通过 xml 文件对项目的 UI,业务,数据结构 进行描述。
到此可以看出,运行时本身并不一定是 .Net 或 WinForm 的,而是可以使用任何平台或语言实现,只要读取 zip 文件和 xml 文件并解析即可。
事实上我自己实现的默认运行时也不是 WinForm,而是用了 Silverlight。
再简单说说 IDE 的设计思路,几个主要的设计目标如下:
1.像 Visual Studio 一样
有可视化的环境,拖拖拽拽界面就出来了。
功能模块全部独立,解耦,以插件的形式存在于主程序(宿主)中。
2.不要写代码,业务通过界面,向导进行配置
拖一个按钮上去,想要单击时做一件事情,就先把按钮拖上去,然后设置这个按钮的事件序列,配置对应的事件。
3.把事件这个概念抽象并封装起来
如“保存数据”这个事件,配置好数据的来源,如窗体上的数据,或系统数据,再配置好要保存的目标,某种数据实体,即可,这个事件被添加到某个事件序列,如按钮的单击事件序列中,项目被运行时解析时,就会按钮这个逻辑执行。
4.对数据操作要有一定的自由度
除了基本的向导式配置以外,要能满足特殊需求,比如支持自定义 sql 语句。但是自定义 sql 语句怎样与数据源,目标交互呢?我设计了一种简单的表达方法,如 UPDATE FROM [User] SET [Name] = {FormElement.txtName} WHERE [Id]={System.UserId}
5.对数据库数据表的操作怎样交互
就是将其抽象为“数据实体”,数据实体也在 IDE 中由用户自己定义,定义的过程类似于 SqlServer,定义好数据实体以后,在 IDE 中进行设计时,通过数据实体来抽象对数据库、表的操作,在打包项目时,可以根据定义的数据实体,生成多种数据库,如 SqlServer,Mysql 等。
6.资源文件的管理
在项目中必然要引用到外部资源,这部分外部资源,怎样引入,管理,打包呢?我在 IDE 中设计了独立的资源管理器,在 IDE 中设计 UI 时,通过资源管理器引用资源,打包时,将资源打包到 zip 文件中。
7.打包前的静态编译检查
类似于我们在 Visual Studio 中写程序,编译时如果有错误就会出现警告或错误提示。在这个 IDE 中,也必须有同样的功能。当引用的数据实体被删除,数据项不存在,引用的资源文件不存在,以及事件配置中一些问题出现时,能够实时,并在打包项目时指出这些错误的具体位置。
8.支持嵌入脚本
能够在事件序列中添加自定义脚本,支持在运行时动态解析或者调用某种脚本语言。 此功能有所设计,但并未开发 。
9.支持插件
此处插件支持指的是 IDE 层面能够支持插件,类似 Visaul Studio 或者 Eclipse 的插件机制,我当时使用的是 .NET 管线技术(很冷门), 实现了相关DEMO,但是没有集成到IDE中。
目前IDE完整支持多国语言,所有文本均使用了资源,但是我没有直接使用资源文件,而是将其强类型化了,具体实现方式下文详述。
在设计开发这个 IDE 的早期,我并没有给自己设定如此详细的目标,现在写其实更多的是回顾和总结。
在这个项目中,大量使用了 GDI+ 绘图,说复杂,给你调用的接口也就那么多,说简单,用 GDI+ 自己写一个功能完备的 WinForm 控件,分分钟教你重新做人。在这个项目中,几乎所有的界面元素都是我自己用 GDI+ 绘制的,使用的第三方控件不多。 后面我会写一些这方面的感想。
下面罗列一些技术难点和主要功能点,有些细节可能没有试着做过都不会意识到那是个问题。
就是控制状态栏上按钮可用不可用,可见不可见,绝大多数时候这不是个问题,但是作为一个 IDE,工具栏上的各种按钮非常多,且按钮的状态和当前设计器中的选中元素个数,选中元素类型,甚至选中元素自身的状态等等相关,还有些按钮是特定控件或元素提供的,怎样统一控制这些按钮的状态?
黄色背景部分是动态挂载上去的,状态的控制在后文中说明。
此处右键菜单指的是窗体设计器中的右键菜单,在窗体设计器中,右键菜单比较复杂,不同控件的右键菜单有所不同,有共通的项目,有特殊的项目,以及状态是不是灰掉可能和控件本身的某些因素有关,但右键菜单本身是不可能通过处于设计状态的控件自身提供的,所以此处如何把控件特有的菜单项挂载上去,又怎样控制它们的状态?注意设计器本身和用来设计的控件是解耦合的。
注意这个例子,右键 DataGrid 产生的右键菜单中存在“添加列” 和“编辑列”两个特殊的项目。
此处当我选中 DataGrid 时,属性网格下方也会出现这两个项目,这里先提一个关键概念,叫做“谓词”,这是一个 DesignSurface 中的概念,后文再详述。
此处实际上我实现了一个独立的菜单(包括工具栏项目)的管理器,并非直接创建 MenuItem 之类的实例去使用,这个管理器也是独立于业务进行设计的,对菜单项的各种状态,行为都进行了抽象与封装。在管理器层面统一调度这些菜单项,通过一定的机制使菜单项的状态与业务状态关联起来,不允许外部代码直接修改菜单项的状态,整套机制本身,与菜单项在UI层面的实现也是无关解耦的,最终生成可见菜单项时,才会生成特定的控件,如 MenuItem,也可以换成其它任何菜单项控件,不影响管理器的功能与逻辑。
我记得当时我研究了几个IDE的设计细节,包括 Visual Studio,应该都采用了类似的机制,好吧我承认是我研究之后借鉴了它们的机制。
这个问题我放在首位,是我意识到这个问题在大型软件中,真的是个很大的问题,我现在参与开发的一款电气化CAD软件中,就存在这个问题,但是他们早期并未意识到这个问题,也谈不上能很好的解决,几千个菜单项,工具栏按钮项,直接硬编码,对他们的状态控制也谈不上成体系,就是粗暴的硬编码,现在的维护,修改,调整都异常痛苦。
最初我是自己用 GDI+ 写了一个简单的设计器模型,支持拖拽,绘制,动态对齐等等功能,但是越往后越复杂,比如绘制一个 DataGrid,你不能光是一个框框,你要自己去绘制它的列,列头,如果要绘制一个图片框,你就要自己去绘制它的图片内容,要考虑图片的缩放方式等等细节,如果要一条道走到黑完全自己实现,成本将非常高昂。
先看看早期直接使用 GDI+ 实现的效果:
下面是直接使用微软 DesignSurface 效果:
和 Visual Studio 效果一样,不过这里需要注意的是 DesignSurface 仅仅也只是提供了基本的窗体设计能力(图中右侧部分),比我上面GDI+自己写的功能多不了多少,但是不用自己绘制控件的外观,其它辅助功能都是需要自行开发的。
左侧的属性列表是自行开发的,.Net Framework 中确实提供了 PropertyGrid 控件,但是对于高阶开发此处并不适用,有很多制限,下文详述。
工具箱本身是独立实现的,不依赖其所处的窗体设计器,同时它自身所承载的控件,也是动态载入的,后期允许第三方插件挂载控件到工具箱中。
这个地方需要注意的不多,一个是动态载入控件,另一个就是在和窗体设计器交互的时候,比如我拖一个控件到设计器上,这里是需要对接 DesignSurface 的。
.Net Framework 中提供了 PropertyGrid 控件,可以实现对对象实例的属性编辑功能,但是难于扩展与自定义,我此处需要个性化定制的地方比较多,所以选择自己实现一个。
主要实现了以下功能
1)对于单个对象实例,列出它的属性(Property,下同),以及属性的值,如果属性值与默认值不同,能够粗体显示。
2)对于特殊的属性,提供对应的扩展编辑器,如颜色属性,在点击后应该提供一个颜色选择器。且这些扩展编辑器,是与属性网格本身解偶的。
3)如果同时设置了多个不同类型(Type)的对象实例,例如在窗体设计器中框选了多个控件,这个场景就复杂一些了;首先得到这些对象实例的类型(Type),抽取共通的属性,属性网格中仅显示共通属性,对于某个属性的值,如果所有对象实例的值是相同的,则显示,如果有所不同,则留空不显示。在设置了某个属性的值之后,能够将新值设置到这些对象实例中。
这里可以用的上“引擎”二字,因为确实比较复杂,我们先将这个问题简化,可以简单理解为对“对象”属性变化的跟踪,可以撤销这些变化,也可以重做这些变化,可以任意步骤的操作。
涉及到的问题和知识点很多,在 IDE 里对象状态的变化又被抽象为具体的“操作”,以及这些操作又要和设计器进行联动,有一定难度。
UI上的效果是直接使用 GDI+ 自定义的一个列表,并不是很复杂,其它能够直观看的界面UI不多,主要是代码了。
上文中提到,要将常用的操作(事件)都封装起来,通过配置的方式来运行,大方向好像并不复杂,但是,怎么做呢?首先事件本身的抽象要独立,要与窗体设计解耦合,其次“事件”的定义应该允许由第三方插件扩展,甚至“触发时机”也应该允许由第三方插件进行扩展。以一个最简单的按钮为例:
看上去和普通编程中的事件机制没什么区别,是的,我们要做的是对其基本机制进行抽象化。例如:
1)触发时机应该与事件寄主解耦,甚至允许第三方插件挂载触发时机。
2)事件序列应该与触发时机解耦,事件序列中的事件定义,应该与以上机制解耦,甚至允许第三方插件扩展。
看看项目中实现的效果:
我们就以“为窗体元素加载数据”这个事件为例,看看现在的事件编辑器大概是什么模样。
这个事件支持“关联数据实体方式”和“执行 SQL 方式”。
切换到数据实体界面中,选一个数据实体,然后设置相关的数据项。
这里就可以配置事件在执行时,从哪里取得数据,我们指定了从 用户 这个数据实体中选择数据,同时指定了一个条件,就是 用户的 Id 要等于 指定文本框中的值。
除了使用界面元素中的值作为条件,还可以使用系统数据,如:
对于选择特定的用户,比如这个 Id 怎样获取呢?只要在加载数据时,把 Id 绑定到一个隐藏的文本框中就可以了,加载数据时,可以读取它的值。
然后切换到载入界面
在载入界面中,指定我的数据取出来以后,加载到界面的哪些元素中。
我们上文提到,希望对数据的操作有一定的自由度,那么在事件编辑器中,就允许直接定义 sql 语句,或者说 sql 语句的模板。
切换到 sql 界面后,首先可以通过 获取 sql 按钮自动根据前面的配置生成 sql,然后在此基础上进行调整,修改。
在 sql 编辑器中,可以通过 {Provider.Source} 的方式访问数据。
支持语法着色,支持智能提示。
目前实现了两种 Provider,FormElement (窗体元素)和 System (系统),在智能提示中支持递进的提示。
所谓递进的提示是输入“{”之后自动给出 Provider,选择后进一步自动给出 Source 列表。
也可以在 “{Provider” 后输入“.” 则自动给出 Source 列表。
智能提示用起来简单方便,看起来也很简单,貌似只是一个 Popup ,实则是一个不小的坑,这个功能困惑了我很久,记得当时到处找大神请教,除了高谈阔论的就是直接告诉我不知道,有个人也研究过 SharpDeveloper,告诉我这个问题深了,后来我又去翻 SharpDeveloper 的源代码,参考了它的实现,完成之后还是相当有成就感的。
对于事件序列的编辑,有两种方法,一种是在设计器中双击控件之后打开的事件序列编辑器
另一种方法是在窗体设计器中提供了以树形方式展示的事件序列,可以直接拖动改变事件的触发时机,或其在事件序列中的位置。
事件在解析器中执行时,是按钮它所处事件序列中的顺序进行执行的。
目前实现的事件大概有十几个,基本的应用程序操作,数据交互等。
不再一一详细说明,因为事件本事是在解耦的情况下独立实现的,IDE并不依赖他们,所以未来扩展也很容易,可以说IDE和解析器是核心引擎,而这些事件定义,只是系统中的“业务”部分。
集合编辑器,就真的只是用来编辑对象集合的,支持对集合中对象实例的编辑,以及集合中元素顺序的调整,并且在与窗体设计器解耦合的基础上,与窗体设计器联动,能够从窗体设计器中的元素取得对象集合,同时与撤销/重做引擎对接,在编辑的过程中,提供撤销/重做的支持。
这个编辑器完成之后复用性比较强,在窗体设计器中有很多地方需要对集合进行编辑,行定义,列定义,元素定义之类。
一个典型的使用场景是在窗体设计器中,对 DataGrid 的列进行编辑。
在窗体设计器中,能够对当前窗体中的各项设置,包含的事件进行有效性的检查。例如我在某个事件中设置了加载数据到 TextBox1 ,后来我删除了这个 TextBox1 ,那么就必须给出提示。
此处的主要难点应该在于解耦合,各种解耦合。
Visual Studio 自带的资源文件编辑器使用起来不是很方便,比如多国语言,是分开在多个资源文件编辑器窗口中编辑的,没有一一对应的显示语言文本,另外直接使用资源文件,使用的是通过 String 做参数的弱类型方式进行调用的,不能做静态编译时检查,也无法保证多语言相关编码的质量。所以这里我没有直接使用资源文件机制,而是进行了二次开发,我专门开发一个资源文件编辑器,提供一个一体化的界面同时编辑多国语言资源文件,使资源key同时和多个资源文件对应起来,同时支持导出excel,交给翻译翻译之后直接导回来,然后解析资源文件中的资源,生成一个统一的 ILanguage 接口,和不同的语言实现,如 class Chinese:ILanguage,class English:ILanguage,调用时,直接使用接口进行强类型调用,即将 Resource.GetString("buttonText") 变换为 _language.buttonText。
另一方面,实现了一种将界面文本绑定到资源的机制,这一点在 WPF 下非常方便,在 WinForm 下就要自己动手了。通过特定字符串标记资源key,在运行时自动扫描窗体或其它容器控件,通过解析这些字符串自动查找对应的资源,将其替换。
目前几乎所有的开发平台都提供了比较友好的用户输入验证方案,在 WinForm 下也有,不过并不是很完善,使用起来限制比较多,功能也有限,不是很顺手。
我自己开发了一套用于 WinForm 的用户界面数据验证功能。举个最简单的例子,我给文本框设置一个不允许空的属性,或者设置一个正则表达式,在我调用验证方法时,就能够对它进行有效性验证。方案非常简单,只是要花点心思把它实现好,各种控件都要支持,要解耦合,验证器要支持多种不同验证机制,验证结果如何向用户反馈等等。
这套验证机制也同样实现在了运行时(解析器)当中。
验证结果的反馈并不一定要用 MessageBox,可以很容易的改进为其它更友好的形式。
在 WinForm 下实现了一套分解的非常细致的模块化开发框架,对软件的功能进行层层解耦,宿主程序与功能模块完全无关,而在功能的设计上,IDE中的功能也实现了完全解耦,上文也多处提到了,窗体设计器与被设计的控件包解耦,事件机制与具体的事件定义解耦等等。
其它功能点主要是指:数据实体定义,主菜单定义,枚举定义,资源管理,以及其它小功能等,下文先做个简单展示,暂不再做详细的说明。
欢迎界面是内嵌了一个 HTML 页面,只是和 C# 代码有简单交互,例如单击链接会调用 C# 方法,并传入参数。
使用 HTML 的一个原因是希望欢迎界面比较漂亮,但是在 WinForm 下实现一个漂亮的,交互性强,维护性强的欢迎界面有一定难度,很浪费时间。
此处注意一个细节, URL 地址不是一个磁盘文件地址,而是一个自定义协议和路径的地址,这个需要自己实现,但是很简单。
年代久远,CSS 和图片可能遗失了,不太好看,见谅。
这块相对比较简单,没有复杂功能。
发布项目时可以根据数据实体定义自动生成数据库。
也可以针对指定的数据实体生成脚本。
界面很简单,生成数据库时,根据枚举定义向枚举表插入枚举数据。
但是有一个小细节是它和窗体设计器是有对接的,它是一个数据源的 Provider,可以在设计器中把控件中的值绑定到枚举。
定义要生成的软件的主菜单和工具栏,运行时解析之后,根据自己的实现方式生成,可以是 Ribbon 的,也可以是传统的,或者其它方式。
菜单或工具栏项目支持事件,可以挂载事件序列。
实现一个资源管理器,目前只实现了对图片资源的管理。
这里有点看点的是,我当时没有找到我觉得不错缩略图控件,于是只好自己实现一个。和 Windows 资源管理器功能一致,没有需要特殊说明的地方,只是自己从头实现一个不能说很难,但是真的很麻烦。借这个地方简单讲一下这个缩略浏览器的实现,可能有些朋友对 GDI+ 不是很了解。
对于在 WinForm 下使用 GDI+ 绘制界面(自己实现一个控件),是比较原始的,想像一下给你一张白纸,和一些简单的绘图接口,画线,画圆,画矩形,其实没别的了。画一个圆角矩形?自己计算坐标系,通过画弧线和画直线画一个。显示一些文本?自己进行字体字号测量坐标系换算,如果涉及到文本换行,超长用省略号代替,都是比较麻烦的。
你要自己在 逻辑上 把握控件的不同状态,如选中,非选中,鼠标滑过等等,和WPF下预定义的状态组不同,WinForm 下这些状态是你要自己去把握的,状态切换时,你要自己根据状态进行重绘……
在绘制界面时,你只有一个从0,0开始的二维的平面坐标系,和它的尺寸。实现这样一个缩略图浏览器,缩略图的排版,布局,一行显示多少个,什么时候换行,选中非选中,鼠标框选,滚动条滚动都需要自己实现,包括鼠标框选时的框,也是需要自己用 GDI+ 绘制的,然后自己计算坐标系,判断哪些项目应该处于被选中状态。
这个控件的代码接近3000行。现在回过头去看,只觉得注释不够详细。
软件中大部分自己实现的控件,都采用了类似的架构进行设计:控件本身,布局管理器,呈现器,呈现器实现,主题。
这里目前并不复杂,生成数据库根据数据实体定义生成即可,生成项目目前我直接使用了项目文件,因为我目前的项目文件格式就是 zip 包,内含 xml 文件。
附带产出是实现了一个简单的比较通用的向导功能。
从上面的截图中能够看到我使用了相当多的自定义控件,或经过美化的 WinForm 自带控件,典型的几个除了上面的缩略图控件,还有IDE上面的主菜单(效果参考了 Paint.NET), DataGrid,重新实现的 ComboBox 等等,居然一步一步形成了一个自己的控件包。可惜技术更新换代日新月异,现在也基本用不上了。
解析器部分我是用 Silverlight 实现了一个,核心实现了,业务没有实现完整,可能是我现在的机器 Silverlight 版本有问题还是怎么回事没有运行起来,也不想去调试了,
并不复杂,只是解析 zip 包,xml文件,生成界面,事件处发时解析事件序列中的事件即可。
最后,如果你现在要做客户端软件,选择 WPF 吧,生产性非常高,功能非常完善与强大,如果你担心性能问题,我想说现在已经2015年了,不是2005年,如果你在开发中遇到了性能问题或其它问题,先从自身找原因。
业程序员永远从自身找问题,业余程序员从平台从语言找问题。
最后想写一点点个人的感想与反思。我在开发这个软件的过程中,犯了许多的错误,这些错误未必是技术上的,但都是严重错误。
首当其冲:闭门造车。活在自己的技术宅世界里,从来不去想,也不愿意去想这个东西有多少实际价值,谁会去用它,到了后来,我明明潜意识里知道这个软件没有太大市场价值,就是不愿意去想这个问题,一门心思去开发。我记得那两年我随身带的手机里,记满了关于这个软件的想法和一些问题的实现思路。我在路上想到某个解决方案或者有什么想法,就立马掏出手机记录,怕回去就会忘记。有一个冬天住的地方没有空调,非常冷,写代码一直写一直写,两个手冻青了,就自己用热水瓶子捂捂继续写,那段时间每到周末就特别高兴,因为有2个完整的工作日可以利用了,不用单纯靠晚上的时间去写,很长一段时间我每天晚上我都只能睡四五个小时,个别时候还没睡着,天就蒙蒙亮了。但是这么辛苦的意义是什么呢?我当时没有认真的思考。
第二:目标非常的不明确。看上去有目标,我要做一个什么什么样的东西,但是太宏大,太宽泛,太遥远。没有认真的考量我要的到底是个什么,所以完全没有详细的计划,里程碑什么都没有。
第三:缺少与外界沟通。这个沟通除了技术,更多的是市场对技术的需求到底是什么,如果回到当初,我会抽自己一巴掌,你睁开眼睛,走出去看看别人的世界好吗!做技术不是高新技术行业,更多的是服务业,我们需要利用手中的技术去为他人服务,这是技术存在的意义。
第四:不懂快速迭代,最小可用集。这个概念现在应该大家都知道了,在当年好像并不是很流行,也可能和我长期做企业级开发有关系,项目周期都非常长。做产品如果不懂得快速迭代是非常危险的,最小可用集就是只要达到最低可用的限度,就立马拿出去见人,当然范围可以是有限的。根据 真正 用户的反馈,快速调整。这个说起来简单,技术人员做起来很容易失控,我做这款软件,就是花了大量的时间精力去研究技术方面的问题,以至于在某些方面挖的非常深,但是这个东西和我的 大目标 其实没有必然关系,不管技术好不好,差一点就差一点,先做出东西来,先用起来,功能先实现,业务先转起来,论证了这东西基本的可行性,再通过迭代去优化。
第五:做东西尽量不要藏着掖着。没意义,藏着掖着无非怕别人剽窃了自己的创意,想法,这个先不论创意想法有多大实际价值,就算你的想法真的很厉害,如果你没有其它门槛,别人看了就会抄去,那你这个东西一定是会出问题的。门槛这么低,怎么就你想到了?别算别人确实一时没想到,你没有其它门槛,被抄了是迟早的事情,藏着掖着没有用。做了东西就要勇敢拿出来和人交流,正面的就吸纳,负面的就多反思自己。
博主正在留意南京的 .Net 相关高级职位
精通 .Net 平台下的 WPF,WinForm 开发,基于 .Net 架构的服务端开发,对 Web 开发亦有相当能力
江苏电信10000号前技术经理,现任某外资企业Team Leader
简历: http://zkebao.com/r/resume.html