时间回到2010年,那时候还是熟悉代码生成+基础框架这种模式,基本的开发思路是通过代码生成器生成实体,再生成接口与实现类,最后拖拉控件,写界面数据绑定代码。基本上就是动软代码生成器给出的模式,或是微软的Repository Factory模式的实践,迷恋于微软的Enterprise Libray,这个框架是从Application Block演化而来。我也是算是.NET技术推广以来,第一批学习.NET技术的开发人员。
一直在寻找一种界面与逻辑分离的技术,也没有思路,上面代码生成造成的结果是逻辑代码分布在系统的各个地方,改一个字段或是增加字段都需要重新生成一次,给系统的稳定性带来困扰。用《企业应用架构模式》中的一种模式总结,就是事务脚本(Transaction Script),不过这种模式好理解,也没有复杂的技术堆栈,通过对这种模式的掌握,由.NET学习者变成熟练的.NET代码工人。
第一次看到LLBL Gen Pro,它长成这个样子:
LLBL Gen Pro 2.5/2.6是它发展历史上很经典的一个版本,查询接口稳定成熟,遇到问题了去tinyform上发个帖子,过一会就有专业的人员响应回复。经过大半年的学习,熟悉了这个ORM框架的用法,开始高级一点的定制开发,它的模板编辑器如下面的图所示:
LLBL Gen Pro从3.x开始,把原来二进制的项目文件lgp改成Xml格式的文件llblgenproj。这是一个很重要的变化,
因为数据库属性最终映射的实体属性可以在设计器中修改,所以必须读取LLBL Gen Pro的项目文件才能确定最终映射的属性名称。 我的辅助开发工具中也依赖于llblgenproj项目文件的这个特性,在LLBL Gen Pro 2.x时代这是不可能的。
当时我的同事做了一个基于ORM的代码生成工具,用于生成实体接口与实现代码,解释如下:
数据库表SalesOrder –> 实体SalesOrderEntity -> 接口ISalesOrderManager –> 接口实现SalesOrderManager
后面两个步骤就是需要做的工作,同事设计的工具的原型如下:
有接近3年的时间,我都迷恋于这个工具产生的接口与实现类代码。直到后来有客户不断提出对接口与实现中细节的修改,我慢慢无法忍受用.NET代码写代码生成器,还要编译的苦恼。当时同事们都极力推荐模板生成技术,于是用Code Smith写下了模板代码,一直延续到今天。分享一下Code Smith生成接口的代码:
<%@ CodeTemplate Language="C#" TargetLanguage="C#" Src="" Inherits="" Debug="True" Description="Template description here." %>
<%@ Property Name="EntityPicker" Type="ISL.Extension.EntityPickerProperty" Optional="False" Category="Project" Description="This property uses a custom modal dialog editor." %>
<%@ Property Name="AssemblyFile" Type="System.String" Default="" Optional="False" Category="Project" Description=""
Editor="System.Windows.Forms.Design.FileNameEditor"%>
<%@ Assembly Name="System.Data" %>
<%@ Import Namespace="System.Data" %>
<%@ Assembly Name="ISL.Empower.Extension" %>
<%@ Import Namespace="ISL.Extension" %>
<%@ Import Namespace="System.Collections.Generic" %>
<%@ Assembly Name="SD.LLBLGen.Pro.ORMSupportClasses.NET20" %>
<%@ Import Namespace="SD.LLBLGen.Pro.ORMSupportClasses" %>
<script runat="template">
public string EntityName
{
get
{
return EntityPicker.EntityName;
}
}
public string ShortEntityName
{
get
{
return EntityName.Substring(0,EntityName.Length-6);
}
}
public string FullEntityName
{
get
{
return string.Format("{0}.EntityClasses.{1}", BusinessLogicProjectName, EntityName);
}
}
private string _businessLogicProjectName;
public string BusinessLogicProjectName
{
get
{
if(string.IsNullOrWhiteSpace(_businessLogicProjectName))
_businessLogicProjectName=EntityClassHelper.PrefixProjectName(AssemblyFile);
return _businessLogicProjectName;
}
}
public string EntityParamerList
{
get
{
IEntity2 policy = EntityClassHelper.GetEntityObject(AssemblyFile, EntityPicker.EntityName);
string parm = string.Empty;
List<string> parms=new List<string>();
foreach (IEntityField2 field in policy.PrimaryKeyFields)
{
parm = string.Format("{0} {1}", field.DataType.Name, field.Name);
parms.Add(parm);
}
return string.Join(",", parms.ToArray());
}
}
public string EntityLowercaseName
{
get
{
return EntityPicker.EntityName.Substring(0, 1).ToLower() + EntityPicker.EntityName.Substring(1);
}
}
</script>
using System;
using System.Collections.Generic;
using System.Data;
using System.Text;
using SD.LLBLGen.Pro.ORMSupportClasses;
using <%=BusinessLogicProjectName%>;
using <%=BusinessLogicProjectName%>.FactoryClasses;
using <%=BusinessLogicProjectName%>.EntityClasses;
using <%=BusinessLogicProjectName%>.HelperClasses;
using <%=BusinessLogicProjectName%>.InterfaceClasses;
using <%=BusinessLogicProjectName%>.DatabaseSpecific;
namespace <%=BusinessLogicProjectName%>.InterfaceClasses
{
public interface I<%=ShortEntityName%>Manager
{
<%=EntityName%> Get<%=ShortEntityName%>(Guid sessionId,<%=EntityParamerList %>);
<%=EntityName%> Get<%=ShortEntityName%>(Guid sessionId,<%=EntityParamerList %>,IPrefetchPath2 prefetchPath);
<%=EntityName%> Get<%=ShortEntityName%>(Guid sessionId,<%=EntityParamerList %>,IPrefetchPath2 prefetchPath,ExcludeIncludeFieldsList fieldList);
EntityCollection Get<%=ShortEntityName%>Collection(Guid sessionId,IRelationPredicateBucket filterBucket);
EntityCollection Get<%=ShortEntityName%>Collection(Guid sessionId,IRelationPredicateBucket filterBucket,ISortExpression sortExpression);
EntityCollection Get<%=ShortEntityName%>Collection(Guid sessionId,IRelationPredicateBucket filterBucket,ISortExpression sortExpression, IPrefetchPath2 prefetchPath);
EntityCollection Get<%=ShortEntityName%>Collection(Guid sessionId,IRelationPredicateBucket filterBucket, ISortExpression sortExpression, IPrefetchPath2 prefetchPath, ExcludeIncludeFieldsList fieldList);
<%=EntityName%> Save<%=ShortEntityName%>(Guid sessionId,<%=EntityName%> <%=EntityLowercaseName%>);
<%=EntityName%> Save<%=ShortEntityName%>(Guid sessionId,<%=EntityName%> <%=EntityLowercaseName%> ,EntityCollection entitiesToDelete);
<%=EntityName%> Save<%=ShortEntityName%>(Guid sessionId,<%=EntityName%> <%=EntityLowercaseName%>, EntityCollection entitiesToDelete, string seriesCode);
void Delete<%=ShortEntityName%>(Guid sessionId,<%=EntityName%> <%=EntityLowercaseName%>);
bool Is<%=ShortEntityName%>Exist(Guid sessionId,<%=EntityParamerList %>);
bool Is<%=ShortEntityName%>Exist(Guid sessionId,IRelationPredicateBucket filterBucket);
int Get<%=ShortEntityName%>Count(Guid sessionId,IRelationPredicateBucket filterBucket);
<%=EntityName%> Clone<%=ShortEntityName%>(Guid sessionId,<%=EntityParamerList %>);
void Post<%=ShortEntityName%>(Guid sessionId,<%=EntityParamerList %>);
void Post<%=ShortEntityName%>(Guid sessionId,<%=EntityName%> <%=EntityLowercaseName%>);
}
}
再后来微软推出了T4模板代码生成工具,曾经有一段时间想把Code Smith转换成T4的模板,Code Smith 5.x不支持.NET3.5,一些.NET类库写的扩展方法,Code Smith模板不能用,这是想转成T4代码模板的原因。然而在网上找一个带智能提示,语法高亮的T4模板编辑器相当困难,在国外找到一个也是试用版,国内也没有破解版,再后来就没有完全没有动力去折腾了。Code Smith 6.x完全支持.NET 3.5,一直延续用到今天。
借助于LLBL Gen Pro,再加上以前积累的一些公共代码类库,一套原始的ERP系统成型,参考下面的视图:
这个项目中,抽象出了三个公共基类库,公共方法Common,公共控件WinUI,公共程序Core。后来硬盘丢失,实在找不到这个项目的源代码,不过设计思路与项目的架构已经了然于胸。
到2012年的时候,接触到Infragistics界面控件包,它几乎重写了整个WinForms的控件,提供的属性非常丰富。当时公司购买了这套控件的许可,可查看到控件的所有源代码。不过大部分时间都没有去看源代码,只有遇到不可理解的错误时,才会跟踪进入源代码查看参数传递是否合理正确。
有了实体和支持强类型对象的控件,这两者的结合,深远的影响了后来的程序设计生涯。虽然现有偶尔也会用DataTable,但大面积使用的开发模式仍旧是使用实体+数据绑定。
.NET数据绑定是需要深入学习的另一个领域,有了数据绑定,下面代码可以省略:
//Get value from control
string refNo=txtRefNo.Text;
//set value to control
txtRefNo.Text="SO201507190001";
只需要将实体绑定给BindingSource控件,整个界面上的控件就全都有了值,不用上面的代码逐个赋值。
protected override void BindControls(EntityBase2 entity)
{
base.BindControls(entity);
InventoryMovementEntity inventoryMovement = (InventoryMovementEntity)entity;
inventoryMovementBindingSource.DataSource = inventoryMovement;
}
对于WinForms开发,大量的取值和赋值操作代码都省略了,减少了代码,提高系统可维护性。
基本上到这里,我已经可以独立开发系统,系统的各个部件都可以处理好,我的开发步骤如下:
1 设计数据库表。找过很多case工具以辅助生成SQL Server数据表,最后还是回归SQL Server Management Studio,这是最好用的最简洁的工具,也方便与同事交流。当两个人用的数据库设计工具不同,而发生一些微小的错误或差异时,常常会令人抓狂。
2 LLBL Gen Pro生成实体,设置实体间关系。基本上就是连接到数据库,刷新实体,生成或更新实体文件。
3 生成实体读写的接口与实现类。借用Code Smith模板,效率高
4 拖拉界面,绑定数据源控件。即使没有学过编程,也可以经过短暂的培训快速上手开发界面。
5 给实体增加业务逻辑代码,界面与逻辑分离。这是要手写代码的地方,写业务逻辑,包含计算逻辑与验证逻辑。