企业级自定义表单引擎解决方案(八)表单模型管理

  这段时间陆续收到一些小伙伴的信息,对流程引擎和自定义表单比较感兴趣,内心还是比较欣喜的。多数人还是对elsa实现的流程引擎比较感兴趣,要源码,这部分内容原本是有打算把源码开源出来的,但后来发现elsa的版本升级到了2.0之后,与之前的代码相差比较远,要重构的话,前后端需要改很多东西,elsa1.x的流程流转核心部分代码设计得还是比较巧妙,能满足各种审批业务的变化需求,自己对核心部分的代码做了一些扩展,所以暂时没有升级的打算。

  自定义表单部分的文章还是继续往下面写吧,这部分文章都是偏设计方面的,真正想做低代码软件架构方面的设计开发多少都有一些益处,整体工作流+自定义表单再整合前后端框架从前后设计技术研究及代码实现差不多前后花了一年左右的时间,当然都是工作之余的时间写的。

  之前介绍的自定义表单中的视图定义为单一功能的封装,比如列表视图(定义普通查询区域,高级查询区域,列表操作按钮区域,行操作按钮区域,分页控件区域,列显示区域等)或者表单视图(封装表单行列定义,表单验证等)等,都是具体某个特定功能的实现。而这里介绍的表单模型,则把它定义为一个容器,容器里面会进一步定义行列,容器里面可以包含容器或者表单,每一个页面会定义唯一一个最外层的表单容器,我们可以把它看作根容器,这样就整体形成了一棵树,根节点就是最外层的表单定义,树的节点可以是子表单、视图、表单行、表单列、视图行、视图列、视图控件等,整体就可以构造出一树庞大的树。

  自定义表单最终会转换为一棵树,树的话就会有树的特性,树上的每一个节点,都可以构造一个唯一的Code和PId,自定义表单中的树节点还会扩展出它属于哪个视图或者哪个表单的属性,那么这里就是引申出子表单子视图,父表单父视图的概念。有了树模型的定义,那个后面绝大多数内容都是围绕树模型来实现的,前端在渲染界面的时候,根据树节点一层一层的渲染界面,渲染界面的同时,将每个节点的Code和PId,节点属于哪个表单或者视图都会赋值到每个树节点控件中,有了树模型的定义,那么规则引擎就有了理论支撑,界面中的任何一个事件,都可以定义规则来实现自定义的逻辑(比如点击列表视图的行编辑按钮,弹出编辑人员子表单,则大致的规则引擎执行逻辑为:找到列表视图特定行编辑按钮所在的列表视图,在列表视图中找到编辑人员子表单,把当前行的Id字段取出来作为参数,用模态对话框弹出子表单,用Id字段执行后端方法获取单条人员数据,将人员数据绑定到人员表单中)。

  表单模型没有具体的功能,它的作用是一个容器,它充当视图与视图之间交互的桥梁的作用,当然是通过规则引擎来串联起来的,另外表单也是页面的入口与缓存的存储数据的入口。

表单的数据库设计:

 设计说明:  

表单模型拆分为表单主表、表单项、表单行、表单列,关系都为1:n。常见的表单项只有一个,但像Tab布局或者有先后步骤的Step布局则会有多个。
表单主表关键字段说明:

  • Version(版本):每一次修改表单的任何信息(包括关联的数据),都会重新生成一个版本号,浏览器存储表单信息,每打开一个页面,会将本地表单版本和视图版本传递到服务端比较版本号,如果版本号发生变化,重新请求表单数据(一般系统交互后,视图及表单定义信息很少会发生变化)。
  • FormType(表单类型):分为常规表单、Tab表单、Div表单等,前端根据此类型找到实现定义好的控件渲染。
  • PropertySettings(表单属性):存储前端的一些样式,前端渲染时,读取属性并应用到控件中,一般需要结合具体使用的前端框架设置。
  • RelationInfos(关联信息):表单可能会关联其他表单或视图,比如弹窗,行存储的视图等,这个字段数据库不存储,通过动态计算出来放入缓存中。
  • Rules(规则):定义表单的规则,将规则信息冗余序列化存储到此字段,规则有改动时,会同时更新此字段(表单会冗余存储比较多的内容,这里的规则为一类,主要是为了以最快的速度读取表单相关数据,只需要表单Id访问一张表即可获取所有的数据)。
  • WrapInfos(表单包装器):前端在渲染视图时,如果有包装器,会用包装器包装视图之后再渲染,常见为弹出框的功能封装。
  • FormItems(表单项内容):将表单项、表单行、表单列全部读取出来序列化冗余存储到此字段,同样是为了读取效率。
  • IsTemplate(是否为模版):将一些典型的业务定义为模版,同样存储在表单中。

表单项、表单行:

  • 对应物理结构的划分,字段比较好理解。

表单列:

  表单列可以存储单个控件、子表单、子视图等

  • ColType(列类型):可以是控件、视图或者表单
  • PropertySettings(列属性):存储前端的一些样式,前端渲染时,读取属性并应用到控件中,一般需要结合具体使用的前端框架设置。
  • ComponentName和ControlSettings(控件名称和控件设置):如果列类型是控件,则为控件名称与控件属性,前端找到对应的控件渲染。
  • ObjId(对象Id):表单或者视图Id,前端渲染时,根据此字段找到具体的表单或者视图。
  • WrapConfigs(包装定义):显示到表单中的子表单或者子视图的渲染封装(表单和视图可以用到任何需要的地方,相当于在用的地方再次进行样式封装,比如用Box样式再次封装子表单)。

缓存设计简单介绍:

  自定义表单是典型的修改非常少,访问非常平凡的,系统的每一个功能都需要读取自定义表单的定义信息。为了使自定义表单不影响性能,这里采用了双重缓存设计,浏览器每访问一个页面,都会将表单和视图的定义信息存储到浏览器本地数据库中(IndexDb),应用程序后端将表单和视图的定义信息全部放到应用程序内存中,且将表单或视图的相关信息以字段冗余的方式存储到特定字段中,任何信息的改变都会重新生成新的版本号并清空内存中的缓存,前端请求页面只,会带上浏览器本地存储的表单和关联子表单子视图版本号与服务器版本号对比,版本号不同时,刷新浏览器缓存数据,再渲染页面。分布式部署中就存储缓存一致的问题,后面单独写文章来整体讲解缓存这块的实现。

表单模版:

  自定义表单本来就是要解放繁琐的低效编码问题,但是要把一个表单配置出来,还是会花费比较多的时间,且需要对这套表单引擎比较熟悉,配置同样比较繁琐且低效,那么我们同样可以采用自定义表单的思路,将常见的业务封装为模版,(比如对单一表单进行的常规列表和表单操作,也就是最常见的CRUD操作。或者一对多表单,列表展示主表数据,点开一条件主表数据,对话框显示主表数据及子表列表,对子表列表进行操作等),只需要动态渲染不同的地方,那么就能够实现只需要设置几个简单的参数,就能够自动的生成自定义表单出来,这里的不同地方无非就是Object对象(Object就定义了不同的字段,在渲染字段的地方全部替换为新的Object的字段),标题内容等少数不同的地方。
  模版的实现思路大致为:根据模版Id找到表单模型相关的所有表单和视图,将关联的所用数据表数据读取到内存中,包括规则、控件、视图行、表单项、表单列等,再对Id进行Map映射(新建一个字典对象,读取所有Guid字段的地方,新建映射,Key存储老的Guid,Value存储新建的Guid),将所有数据Guid字段替换为将建的Guid值,将Object对象相关的数据全部删除,用将的Object字段重新生成数据,不同的字段类型设置默认的样式,再将所有内容存储到数据库。
  随着表单引擎的使用,可以定义更多的表单模版,那么表单引擎的功能将越来越丰富也越来越容易使用。
  部分核心部分代码(可下载源码查看):

private Dictionary<Guid, Guid> idMapes;
private void CalculateId(Guid? oldId)
        {
            if (!oldId.HasValue)
            {
                return;
            }
            if (!idMapes.ContainsKey(oldId.Value))
            {
                if (oldId.Value == Guid.Empty)
                {
                    idMapes.Add(oldId.Value, Guid.Empty);
                }
                else
                {
                    idMapes.Add(oldId.Value, Guid.NewGuid());
                }
            }
        }

public async Task CreateFormFromTemplate(Guid formId, string applicationCode, string objectNameMap, string descriptionMap, string strExcludeCreateFields, string category, int itemRowColCount = 2)
{
  ......

// 查询数据库数据
spriteForms = await spriteCommonRepository.GetCommonList<SpriteForm>("SpriteForms", queryIdFormWhereModels);
                spriteViews = await spriteCommonRepository.GetCommonList<SpriteView>("SpriteViews", queryIdViewWhereModels);
formControls = await spriteCommonRepository.GetCommonList<Control>("Controls", queryBusinessFormWhereModels);
                viewControls = await spriteCommonRepository.GetCommonList<Control>("Controls", queryBusinessViewWhereModels);
                formSpriteRules = await spriteCommonRepository.GetCommonList<SpriteRule>("SpriteRules", queryBusinessFormWhereModels);
                viewSpriteRules = await spriteCommonRepository.GetCommonList<SpriteRule>("SpriteRules", queryBusinessViewWhereModels);
                formRuleActions = await spriteCommonRepository.GetCommonList<RuleAction>("RuleActions", queryBusinessFormWhereModels);

// 替换Id
foreach (var spriteForm in spriteForms)
            {
                CalculateId(spriteForm.Id);
                CalculateId(spriteForm.Version);

                spriteForm.Id = idMapes[spriteForm.Id];
                spriteForm.Version = idMapes[spriteForm.Version];
                spriteForm.ApplicationCode = applicationCode;
                spriteForm.Name = ReplaceName(dictObjectNames, dictDescriptions, spriteForm.Name);
                spriteForm.Description = ReplaceName(dictObjectNames, dictDescriptions, spriteForm.Description);
                spriteForm.Category = category;
                spriteForm.IsTemplate = false;
            }
            foreach (var spriteView in spriteViews)
            {
                CalculateId(spriteView.Id);
                CalculateId(spriteView.Version);

                spriteView.Id = idMapes[spriteView.Id];
                spriteView.Version = idMapes[spriteView.Version];
                spriteView.ApplicationCode = applicationCode;
                spriteView.Name = ReplaceName(dictObjectNames, dictDescriptions, spriteView.Name);
                spriteView.Description = ReplaceName(dictObjectNames, dictDescriptions, spriteView.Description);
                spriteView.Category = category;
            }

// 替换Object数据


  ......  
}

  

  感觉还是没有把这块内容描述得特别清楚,很多设计思想用文字还是有点难表单出来!

  自己做这些不知道有没有意义,最近处于半离职状态,很想把这块内容应用到实际业务系统,再深入耕耘下去,但是又不善于推销自己,也有很多无奈,最近为了生活,不得不从头学习QT。

开源地址:https://gitee.com/kuangqifu/sprite

体验地址:http://47.108.141.193:8031(首次加载可能有点慢,用的阿里云最差的服务器)

自定义表单文章地址:https://www.cnblogs.com/spritekuang/

流程引擎文章地址:https://www.cnblogs.com/spritekuang/category/834975.html(采用WWF开发,已过时,已改用Elsa实现,https://www.cnblogs.com/spritekuang/p/14970992.html )

Github地址:https://github.com/kuangqifu/CK.Sprite.Job

原文地址:https://www.cnblogs.com/spritekuang/p/15721275.html