【技术】ccFlow工作流UI外壳定制攻略(下)

攻壳机动队

本篇及其上篇,是延伸《攻壳机动队》所述“灵与壳”思想为暗线,配以ccFlow前端外壳开发过程作明线成文。以《攻壳》人物巴特为第一视角,讲述了如何在缺少直接文档的情况下解读代码完成任务。

无ccFlow技术背景的读者可直接检索“巴特”相关内容查看暗线情节。
——“塔酱你看,”巴特转而对思考战车塔奇克马解释起来…

目录

(续上篇)

  • 目标(A)已办、归档、申请,三个一览型画面
  • (A-2)新模块——定制解决
    • 【定制模块】“归档”UC第二部分:渲染记录一览表(单一Table,带GroupBy)
  • 目标(B)带查询分页的一览画面
  • (B-1)参考模块——概念集——关键机制解析
    • 【参考模块】“查询”UC第一部分:获得流程记录集(用QueryObject,带查询+分页)
    • 【参考模块】“查询”UC第二部分:渲染记录一览表(带分页)
  • (B-2)新模块——定制解决
    • 【定制模块】“归档”UC第一部分:获得流程记录集(用QueryObject,带查询+GroupBy+分页)
    • 【定制模块】“归档”UC第二部分:渲染记录一览表(带GroupBy+分页)
    • 惊变!
  • 发布
  • 后记

上篇中,巴特以SDK方式实现了BP.WF.DB_GenerCompleted(),完成了三种生命周期下流程记录的获取;以BP.En.Entity实体类的ORM映射机制结尾)

【可选分支剧情】对ORM实现方式的官方确认

EnMap.get()中实现ORM映射需要自己挨个对每个字段映射做Add…因为ccFlow毕竟03年就出了,当时.Net好像刚发布1.1,NHibernate 1.0、官方Entity Framework、都得是两年后的事了。”巴特慨叹了一下技术史。

官方QQ中对自实现ORM的确认,该分支中可与Boss级人物对话

【定制模块】“归档”UC第二部分:渲染记录一览表(单一Table,带GroupBy)

因为我们新建的BP.WF.DB_GenerCompleted()返回的也是DataTable型数据集,实现渲染的代码可以参照“我的待办”EmpWorks.ascxBindList()(“我的已办”Runing.ascx亦可)。

【通关要点】

  • “归档”Completed.ascx中需做GroupBy分组的列更少,只需FlowName(流程名称)StarterName(发起人)NodeName(节点名称)PRI(优先级)不再有意义)
  • 简化掉为了GroupBy而做的GroupBy列数+1次遍历,最多也只需要两次(不必拼接string groupVals再拆分成string[]…集合类ArrayList足矣;官方外壳AppDemo的开发人员估计并非核心主力)

※ ※ ※ ※ ※ ※ ※ ※

如果仅止于此还是挺轻松的,尽管经历了冗余型防壁英语拼写混淆术,不过BP.WF.Dev2Interface单兵武器站毕竟使用简便,选对关系数据表/视图、理解实体类ORM映射机制后、自己打造一把也并不复杂,最后也仅仅是仿造一下一览型UC的渲染代码。

巴特又依样画葫芦把“我的申请”UI外壳也按同等方式定制好确认没有新的问题后,就发信通知户草准备进行第一轮同行评审了。

目标(B)带查询分页的一览画面

户草接入:“老大,这‘审核者’我三年前也只是分担了应用层的工作,通过管理UI编辑了几套流程和申请表单,没做过开发啊。”

“只有你了,莫推辞。”巴特说到,“多一个人听过,还可以给定制方法、踩过的坑多一份记忆。万一我也不在了呢。”

户草心里刚犯嘀咕,“记下开发日志不就好了么,兄弟也有正经任务在身…”听到后半句却嘴唇蠕动了下没说出什么,

“看你那边山桂花又开了啊。兄弟我却还蹲在新港下水道里监听LS动向呢,咳~”

因为定制目标A的相关脉络并不复杂,Review其实并没花费太多时间,逐一确认了对现有功能不会造成影响,户草还记起了几个上一版本开发时的限制条件。

随后两人就商讨出了下一步的改善迭代目标:参照查询画面,为记录数多的一览画面补充查询栏和分页栏 (比如“我的归档”,高层审核者的记录数已达近千条)

【道具】参考模块:查询UC控件 (一览UC,带查询栏+分页栏)
位于~WF\Rpt\UC\Search.ascx.cs
被荒卷课长指摘的主要对象。未能按流程的处理状态划分影响到使用,尤其每次还需先选定一种流程、并只能对该种流程做一览查询。
该外壳控件的优点是,适合记录数多的情况(带分页栏),可按工作部门和关键字做筛选(带查询栏),关键字覆盖到表单数据级别。

(B-1)参考模块——概念集——关键机制解析

“当初技术选型定案ccFlow作‘审核者’灵魂的果然并不是少佐…”关闭与户草的连线后,巴特感觉释然的同时却又有了几分无力感,“在技术已经进化到义体生化的时代,人文层面的进化却仍会因为种种原因踯躅难行——对资源控制权和相关衍生利益的争夺从未由自律或法律有效遏制过,除了当整个系统处于成住坏灭的的关键节点时,又有多少个体会如蜜蜂般对整体的存亡齐心协力,而不是以自身、乃至自身并不合理的“利”为第一优先级?并势必扩展到其所能投靠的最大能动单位——派系社团(Faction)的级别。”

玩家注意:此处纯粹为本作剧情冲突设置需要,不映射其他实体。
(如与其他游戏雷同,欢迎共同作分享)

“塔酱你看,”巴特转而对思考战车塔奇克马解释起来,“就好比这BP.WF.Dev2Interface组件接口,当UI外壳生成器是位于服务器端、且能接受.Net组件嵌入的情况下才是友好的,可对于一些响应式(Reactive)的Web外壳、移动端,并不是由服务器端渲染生成的,它们需要的是能够解耦、响应HTTP请求返回诸如JSON结果集的诸如REST方式的接口;仍停留在以组件为接口的架构就无法满足,

“小到一个框架接口的设计者,其职责适性(Competency)都需要对前端后端0.5-1年内的技术趋势有了解,方能使得项目、作为一个整体、更具竞争力;大到一个行政机构的责任人,他的‘特长’,如果却仍只停留在凭借‘灵魂弱点接口’来调动…的话…”

(大段对白可按A键跳过)

巴特最后一条对白是:“群体系统,其实就仍相当于被一个‘无意识的Ghost’操控着。个体很难往这Ghost中注入它们原本就欠缺的。塔酱,分布式AI既不会对自己的躯壳体验有过度执着,又没有‘影响力边界’导致的信息传递衰减效应,甚至无惧死亡,不知由你们AI构成的群体,将来的Ghost,会呈现出何种特性。” 塔奇克马:“……#@$%?……”

【参考模块】“查询”UC第一部分:获得流程记录集(用QueryObject,带查询+分页)

针对目标(B)的需求(为记录数多的UI外壳补充查询栏和分页栏),位于~WF\Rpt\UC\Search.ascx的查询UC控件是最合适的参照对象。

查询UC控件也继承自BP.Web.UC.UCBase3类,可从Page_Load()开始解读。

protected void Page_Load(object sender, EventArgs e)
{
    //1.初始化查询工具条
    this.RptNo = this.Request.QueryString["RptNo"];
    MapRpt currMapRpt = new MapRpt(this.RptNo);
    Entity en = this.HisEns.GetNewEntity;
    Flow fl = new Flow(this.currMapRpt.FK_Flow);
    this.ToolBar1.InitToolbarOfMapRpt(fl, currMapRpt, this.RptNo, en, 1);

    //2.渲染一览表的第一页
    this.SetDGData(1);
}

【概念】
在“查询”UC控件中又将出现不少初次遇见的概念,

  • RptNo:报表编号,即被指定的流程的NDxxxMyRpt编号,如"ND72MyRpt",定制改装难点之一(提示:同时也是EnsName(实体名),因为流程成为了可以被ORM映射实例化的实体)
  • MapRpt:与之前出现过的BP.En.Map类似,专为流程报表用,#无需#关注,仅用作工具条初始化、且可被省略
  • HisEns:His代表并非自身(此处即“查询”控件)、而是外部实体(此处是被指定为查询对象的流程)或枚举类型,Ens代表BP.En.Entities集合型实体,在初次get时会被按照RptNo实例化:BP.DA.ClassFactory.GetEns(this.RptNo)
  • Flow:即流程类BP.WF.Flow,当报表编号为"ND72MyRpt"时、所对应的流程也就是“072:预算外攻壳更换申请”

this.SetDGData()中的后半段渲染、与“我的待办”画面中的this.BindList()类似(只多了分页处理),留待第二部分解读。

this.ToolBar1.InitToolbarOfMapRpt(...)这段,就引出了本次任务中唯一一个通用控件CCFlow.WF.Rpt.UC.ToolBar

【道具】参考模块:工具条UC控件(级别:Component)
位于~WF\Rpt\UC\ToolBar.ascx.cs
Component级UC控件,无法独自构成画面。主要由一个关键字输入框和一个工作部门的下拉框组成。
继承自BP.Web.UC.UCBase3类,但真正的启动点并不在Page_Load(),而位于InitByMapV2()GetnQueryObject(),分别负责根据Map映射信息初始化一个工具条、以及生成一个QueryObject查询体。

首先来看ToolBar.InitByMapV2()的调查原委和结果,

【通关要点】解读ToolBar(1):InitByMapV2()
Search.Page_Load()中调用的是ToolBar.InitToolbarOfMapRpt(Flow, MapRpt, string rptNo, Entity, int pageIdx),但该方法第一个调用的就是ToolBar.InitByMapV2(Entity.EnMap, 1, rptNo),后续会加一些我们并不需要的权限、年月日、发起者 ,因此只需观察InitByMapV2(Entity.EnMap, pageIdx, rptNo)

  • 【工具】UserRegedit用户注册表类
    ccFlow后台使用Key-Value型数据表Sys_UserRegedit维护用户动态数据,相关ORM映射的实体类就是UserRegedit
    按Key值初始化UserRegedit实例,就能读取/保存关联Value值。
  • 第一个疑问:工具条的初始化为何需要RptNo
    调查目标是为了能把ToolBar放到“我的归档”里用,而“我的归档”UI外壳并不像“查询”UI那样需要指定某种申请流程后才能进入,RptNo是否还有意义、能否替代也就成为关键。
    最终发现,RptNo只是被当做EnsName(实体名)在使用,用来从一个UserRegedit注册表项读取工具条中查询控件的默认值(保存则发生在最近一次按下查询按钮时)。
    InitByMapV2()BP.En.Map参数,其实也依赖于由RptNo初始化得到的Entities(通过BP.DA.ClassFactory.GetEns(this.RptNo)),
    BP.En.Map中包括了IsShowSearchKeyDTSearchWayAttrsOfSearchSearchAttrs等参数,除了名字雷同颇费疑猜外(参见混淆术)、相关代码更是大占篇幅。最终结论却是,#无需#关注,以SearchAttrs为例,对特定的流程、会自带如FK_DeptWFState这样的外键/枚举型检索属性,依此初始化查询控件,但对于一般通用的查询工具条,只需关键字(输入框)加工作部署(下拉框)就够。
  • 第二个疑问:“我的归档”这样的通用型一览UI外壳、该用怎样的EnsName
    这种外壳只需处理一种实体类、注册表不会混淆,无需EnsName来区分,但考虑到后面要用到的QueryObject,可使用:FlowDatas(FlowData的集合型)。

接着对ToolBar.GetnQueryObject()的解读,将揭开此次任务的中型可组装武器QueryObject()的幕布:

【武器】查询体QueryObject(级别:Craftable Elite Item)
位于BP.En.QueryObject
尽管同样是需以.Net组件方式接驳、也存在响应式客户端难与之动态通信的问题,QueryObject毕竟表现出了远胜于BP.WF.Dev2Interface的灵活与强大。
其中SQL属性的get方法是隐蔽而关键的存在、会在每次取值时动态拼接出SQL文(尽管其set方法其实做着+=的事会令以为this.SQL = "..."本该是赋值的人吃上一惊),其特点在于有着丰富的包括AddPara()(添加查询参数)AddWhere()(添加查询条件)addAnd(),addOr()(添加逻辑符)addOrderBy()(添加排序)在内的大大小小组合方法,并能自动基于外键/枚举型字段做JOIN关联,以及屏蔽了不同数据库语法差异的复杂性,相当于一架可自定制组装而获得强大威力的中型武器,胜任于精准命中目标集。

  • 输入:所需查询对象的实体类Entity及其集合类Entities(用来蚀刻SQL文FROM字段),加上各种查询条件的组合(WHERE字段、ORDER BY字段)
  • 输出DoQuery()方法执行查询后,结果将被存放在Entities集合中,除了包含实体类所对应的数据表的所有字段外,遇到外键/枚举型字段、还会自动关联成字符串型字段(如FK_Dept会被包装成FK_DeptText)输出(对应于SQL文SELECT字段)

*注意:Entities维护着查询的结果,QueryObject只负责查询的行为(所能查询的Entity的结构、在初始化时已固定)

具体来看ToolBar.GetnQueryObject(ens, en)的调查原委和结果,

【通关要点】解读ToolBar(2):GetnQueryObject(ens, en)(最终调用InitQueryObjectByEns(...)

  • 在“查询”控件SetDGData(int pageIdx)方法的前半段,会根据基于RptNo(即EnsName)获得的Entities以及Entitiy,初始化一个QueryObject
    QueryObject qo = this.ToolBar1.GetnQueryObject(ens, en);
    

该特制QueryObject结合ToolBar中较复杂的查询控件的取值、来组合查询条件(例如关键字输入框TB_Key的值,会用来对Entity类的每个文本字段都做匹配检索,包括外键/枚举型字段也会被关联到所对应的文本信息来组合进匹配检索条件)

  • 第一个疑问:如何在翻页时包含查询条件?(包括关键字、部门)
    分页信息是属于SearchUC控件的,ToolBar构建好QueryObject之后Search会将分页信息传递进去:qo.DoQuery(en.PK, SystemConfig.PageSize, pageIdx),每页记录数、当前第几页,都被作为参数传入;进而查看QueryObject可知(需穿越过一片对应多种数据库的switch..case..分叉路)会落实到GenerPKsByTableWithPara()方法上、从包含了pageIdx个页块的连续记录集中取走最后那一块。“pageIdx个页块”只是通过SQL文Top字段划取,与查询条件相互独立。
    当按下查询按钮时,ToolBar查询控件组中的状态被保存(到UserRegedit中),并自动调用SetDGData(1);当点击分页控件换页时,SetDGData()中传入相应的pageIdxQueryObject则仍是根据之前保存的ToolBar查询控件状态构建,从而保留了查询条件。
  • 第二个疑问:如何重用到“我的归档”UC控件中?现在的BP.WF.Dev2Interface.DB_GenerCompleted()接口并不带查询关键字,如何作为参数嵌入?
    先说结论:无法嵌入。否则就要自己组装一整套查询条件了(相当于QueryObject所做的),不如就直接改用更精准称手的QueryObject,当然这就需要满足QueryObject所需的输入条件(包括RptNo(即EnsName),后述)。
  • 第三个疑问:如何解决查询排序?
    “查询”UC控件会将Entity实体类传递给ToolBar,而不同实体会有不同的排序需求(草薙有一段定制业务代码就是在此处),通常则是根据en.pk主键来做qo.addOrderBy(..)qo.addOrderByDesc(..),这在查询工具条ToolBar中做还是在一览画面中做其实并无区别。

至此从“查询”UC控件取经“如何结合工具条UC控件(中的查询条件)获得流程记录集”便告一段落了。

在此过程中会遇到几处草薙少佐之前定制过的地方,巴特都会一一细心查看猜测原委;攻略叙述中省略了解读中蜿蜒曲折低效的部分,节省了脑细胞。对于爱自己探索的玩家来说,则可能会遭遇到如下几处“冗余型防壁”

【概念】冗余型防壁
增加对功能而言非必要的复杂性,使得破解者必须增加解读工作量的一种防御技术。

  • 实例(小型冗余):ToolBar.InitToolbarOfMapRpt(Flow, MapRpt, string rptNo, Entity, int pageIdx) 中带有的pageIdx参数,给人以当前分页数也会影响到工具条初始化的认识,可一直等深入解读到ToolBar.InitMapV2(bool isShowKey, DTSearchWay, AttrsOfSearch, AttrSearchs, Attrs, int pageIdx, UserRegedit)、最终发现该参数并未被用到。
  • 实例(中型冗余):ToolBar.InitQueryObjectByEns(..)方法(ToolBar.GetnQueryObject()的核心)中的#region区块注释表示,会依次对“关键字”、“普通属性”、“外键”进行处理,后两者对应的代号分别是AttrsOfSearchAttrSearchs,并会在查询代码中多次出现。
    且不说AttrsOfSearchAttrSearchs的命名是如此的令人眼晕(其真实含义其实竟然是“非外键型查询用属性”与“外键型查询用属性”),代码又颇为难读,关键在于你会在后期调试时发现、就连查询画面中、这两个集合也往往都是空。
  • 实例(局部冗余):QueryObject.DoQuery(..)方法中(没错,即使在这款完成度较高的可组装武器的输出部件中),会发现一段代码用到了10进制高精度计算加字符串分割,来完成%取余…
decimal pageCountD = decimal.Parse(recordConut.ToString()) 
                     / decimal.Parse(pageSize.ToString()); //页面个数
string[] strs = pageCountD.ToString("0.0000").Split('.');
if (int.Parse(strs[1]) > 0)
   pageNum = int.Parse(strs[0]) + 1;
else
   pageNum = int.Parse(strs[0]);

“塔—酱,你确定这些真没有一种是攻型防壁?…”(注:攻型防壁会沿着攻击者链路反击其电子脑)巴特抬起左手微微按住两侧太阳穴,尚未被电子化的大脑中似乎都感受到了干扰入侵。

“还是帮我蒸上一壶日晒耶加吧?先不加蜜。”

【参考模块】“查询”UC第二部分:渲染记录一览表(带分页)

当咖啡香味逸出时,巴特已经进入冥想入静约10分钟了,这一数千年前的技术能显著提高此类脑力型任务的效率。

在入静中,脑海中浮现的是草薙在“审核者”的赛博空间中搏击的光影,她一定也是穿越过了这重重其实本无必要的障碍后才完成了初代定制需求的——在实权个体决定了技术选型之后。一个系统只要不是处于“坏灭”的生命周期,还有着耐受性,灌入稍微一丁点的“不合理”也就并不会显得有问题——随后对部分个体致命的问题开始浮现,再随后这样的问题日渐增多起来——要直等到有力的外来竞争他者出现、或大量感受到问题的内在质疑者涌现、才会警醒到系统设计的不合理性。然而在那之前,“少数派”基于自身感知而给出的意见,是不具备实质影响力的。

Search.ascxToolBarQueryObjectEntities/Entity 等元素与相互间关联线如蒙太奇般在大脑中快速回闪过数遍后,一时关闭了的义眼再次泛起了朦朦灰光。

耶加雪菲的浓郁果酸伴随着星点花香,在出定后格外敏锐的味蕾上唤醒了多样的层次,巴特继续解读起与“渲染”相关的实现。

带着问题解读Search.SetDGData(pageIdx)中后半段的渲染部分会更有效。是否还记得目标(A)时参考的“我的待办”UC的渲染部分?唯一的特色就是分组GroupBy,我们要将它沿用到有分页的页面中,却会产生问题,

【通关要点】解读Search.SetDGData(pageIdx)的渲染部分

  • (重要度:低)在使用QueryObject执行qo.DoQuery(en.PK, pageSize, pageIdx)后,首先会有一段基于检索关键字、将查询结果(流程记录集)中与关键字相匹配的部位标记成红色(表示“被命中”)的代码。
  • 查询结果本身并不在QueryObject中,qo只是个可组装查询体,结果被存放在创建qo时作为参数传递给ToolBar.GetnQueryObject(..)Entities ens
  • 此后,草薙定制过的版本会转向另一个独立的BindEnsWithCondition(),而原始版本则集中在BindEns()中完成真正的渲染“绑定”(Bind)
  • 第一个疑问:分页切换如何影响到渲染?
    分页是早在qo.DoQuery(..)时就控制了返回流程记录集时的起始点记录数BindEns()中只需对Entities ens作遍历、逐条渲染en流程记录就行,与分页概念相隔离。
  • 第二个疑问:分页切换是否能与分组GroupBy结合?
    BindEns()中可见,表头部分是根据实体Entity所具有的字段动态生成的,而并未涉及分组切换
    一旦要分组就暴露了一个问题:“待办”UC中的分组是对查询结果记录集做的,而分页之后、每页的记录集将并不是全集,对着子集做分组还有何意义?“归档”UC中如果要加入查询、分页、并保留分组,分组就得提前到查询阶段做——也就是在QueryObject上实现。
    • 其实可以看到,“待办”UC中那种只对结果记录集做的分组,完全可以放到前端/客户端去完成,切换分组甚至可以离线进行。
(B-2)新模块——定制解决

【定制模块】“归档”UC第一部分:获得流程记录集(用QueryObject,带查询+GroupBy+分页)

“要素分析完备,终于可以动手改造了。”巴特从座椅上直起了身活动了下义体,椅背上还挂着那件驼绒风衣。

“巴特,不要忘了,当你向网络连接的时候,我一定会在你的身边。......我走了。”——上次LS案件尾声、草薙寄宿的傀儡少女就是在说完这段话后“死去”的,披着巴特为她贴心穿上的风衣。

“审核者”开发环境连于公安部内网。解读“查询”UC后得到的可组装查询体QueryObject完整地加载到了右侧的“我的装备”屏中。距离“我的归档”UC的二段改造(从普通一览型改为带查询分页)已只有一步之遥。

首先仍是获得记录集的部分。之前采用的是SDK模式、调用BP.WF.Dev2Interface.DB_GenerCompleted()这把半自动武器来命中,现在可以改用QueryObject查询体了。为此我们定制了一个自己的ToolBarSimple查询工具栏UC。

【通关要点】二段定制ToolBarSimple(1):InitByMapV2()

  • RptNo(即EnsName)问题的解决
    作为简化的ToolBar,将无需负责查询“任意实体Entities”(通常指流程实体),只需查询FlowDatas集合即可(映射于数据视图V_FlowData、是所有流程的基本字段部分的并集,参见上篇)
    因此可固定为"FlowDatas",在使用ClassFactory.GetEns()实例化后传递给ToolBarSimple
    _ensName = "FlowDatas";
    _ens = BP.DA.ClassFactory.GetEns(_ensName);
    _en = _ens.GetNewEntity;
    this.ToolBar1.InitByMapV2(_en.EnMap, 1, _ensName);
    
  • 读取UserRegedit用户注册表(恢复查询控件状态)等机制均予以保留
  • 增加一个“工作部署”查询控件
    草薙当时定制的“查询”画面中已经添加了“工作部署”控件(DDL下拉框),直接加在ToolBar共通部件中了; 予以保留即可:
    DDL ddl = new DDL();
    ddl.ID = "DDL_KAB_DEPT";
    DataTable dt = DBAccess.RunSQLReturnTable(sqlQuery);
    ......  //遍历dt.Rows,调用ddl.Items.Add()添加下拉框元素
    this.AddDDL(ddl);
    

接着是用初始化好的ToolBarSimple来构建QueryObject——Completed原本通过DB_GenerCompleted()实现的逻辑、 都将通过配置这一可定制查询体获得精确命中,

【通关要点】二段定制ToolBarSimple(2):GetnQueryObject(ens, en)

  • 传递进来的ensen将同样是基于"FlowDatas"实例化得到的,因而搭建QueryObject时首先就会基于FlowData的文本字段(如FK_Flow(流程ID)FK_Dept(部门ID)FlowStarter(发起者ID))逐一对查询关键字拼接出LIKE SQL铭文;然后包括对迷之取名的AttrsOfSearchAttrSearchs属性集的处理(#无需#深究、对FlowDatas而言实际并未用到)。
  • 工作部署(部门)的查询匹配实现
    基于关键字输入框的取值做SQL铭文拼接时其实跳开了FK_Dept字段,因为部门将与独立的DDL_下拉框控件的取值做匹配。
    参照关键字取值的做法,我们首先从DDL_KAB_DEPT取得用户在UI外壳上选中的部门取值(编号),然后还记得我们说过的QueryObject允许精确的定制么?AddWhere(attr, op, val)就是一种,一般来说,op取值'=''LIKE'就足以满足多数匹配需求;公安部的部署编码有一套较复杂的规则,因此需通过封装一个AddWhereDeptStartsWith(deptCode)来解决。
  • 分页同时包含查询条件:分页其实是要等到Search UC控件调用QueryObject.DoQuery()时实现(最终用到那个叫GenerPKsByTableWithPara()的方法),ToolBar.GetnQueryObject()构建时并未包含相关逻辑。
    查询条件的保存与读取则由SearchUC控件分别在查询按钮按下与画面加载时进行(分别调用ToolBar.SaveSearchState()ToolBar.InitByMapV2()实现),因此这一步只需确保相关代码保留在ToolBarSimple中即可。
  • 排序
    官方版本构建的QueryObject是仅当Entity实体类(所映射的数据表)主键为'No''OID'(即'WorkID')时才依此排序的(迷之原因)。至少需修改成逆序、改用addOrderByDesc()

等到ToolBarSimple构建好QueryObject,“我的归档”UC需对它做出进一步的配置,因为至此只是配置了工具栏中关键字部门的组合查询条件。

【通关要点】二段定制 CompletedQueryObject的定制配置

  • 增加“我的归档”Completed画面特有的逻辑
    BP.WF.Dev2Interface.GenerCompleted()将不再用,需将相关查询命中逻辑改装到QueryObject上,十分轻便:
    //将“我的归档”的查询逻辑配置到QueryObject上
    qo.addAnd();
    qo.AddWhere("FlowEmps", "LIKE", String.Format("'%@{0},%'", WebUser.No)); 
    qo.addAnd();
    qo.AddWhere("WFState", "=", ((int)WFState.Complete));
    if (!String.IsNullOrEmpty(_fk_flow))
    {
       qo.addAnd();
       qo.AddWhere("FK_Flow", "=", _fk_flow);
    }
    
  • 分页的查询阶段实现
    SQL铭文中实现GroupBy分组是用GROUP BY么?(玩家可以动手实验一下)
    实验过之后来看正解:要对哪个字段做分组,就以它作为第一顺位的ORDER BY
    原版的QueryObject缺少这个接口(addOrderBy()只能追加排序字段、不能插到第一顺位),感谢开源作坊,你可以自己在这个可组装武器上开一个Socket槽**并自己实现。

【定制模块】“归档”UC第二部分:渲染记录一览表(带GroupBy+分页)

巴特瞄了一眼,看见塔奇克马正把许多动物形状的曲奇酥底在托盘上摆成了一排,就好像被新改装的QueryObject命中到的记录集一样、静静等待着渲染。

“什么才是群体灵魂的归宿?”巴特不由再次想到这点,“除了个别经历不同者,多数的灵魂都逃不脱遵从当时当地沉积下来的优势文化、而被一种‘集体无意识’所渲染。就如同这些饼干,稳定是第一性。在这种有如泥淖的境况下想让个体Ghost保持“随波逐流”的方式获得“进化”无疑极具挑战,即便等到‘不合理’的恶果积累到爆发矛盾、矛盾的表面影响触动到了群体灵魂并有幸变革成功,都仍可能因为多数个体对矛盾的人文根源长期理解不足(否则早凝聚成群体灵魂的一部分了)而在矛盾缓解后再次退化,埋下再次进入‘坏灭’的因。

“美国,清教徒,在故国英吉利也是难以摆脱‘多数派’在群体灵魂上的压制优势。方法,可能就只有‘新大陆’。如此说来,…Ghost往赛博世界上的‘迁徙’,会不会是又一次的‘五月花’、‘阿贝拉’?” 想到这一层时,巴特未义体化躯壳上的立毛肌都瞬间牵动了起来。

“还是先把最后一点问题清理掉,塔酱,晚餐请帮我订一份煎烤羊排焗蜗牛配橙味土豆泥和香槟。”

【通关要点】二段定制Completed.SetDGData(pageIdx)的渲染部分

  • 表头将固定为申请流程的共通字段:标题,流程名称,发起人,部门,发起日期,结束日期。而不是像“查询”UC那样根据指定流程类型的报表字段渲染。
  • 工作部署(部门)名称的渲染【重要!】
    之前版本的改造中,是将V_FlowData视图扩展成带三个名称字段的V_FlowDataPlus来实现流程名称、发起人名称等的渲染。现在又多出一个“部门名称”,但你会发现甚至都不需要再为此添加新字段,
    因为如前所述QueryObjectSQL属性会在get时自动将外键所关联的文字型字段都包括进SELECT中,这样FK_Dept外键就会被关联到Port_Dept表中的Name字段并以FK_DeptText作为别名。
    可以跟踪到QueryObjectSQL属性形如:
    " SELECT TOP 30 ISNULL(V_FlowDataPlus.FID,0) FID, V_FlowDataPlus.FK_Dept, 
    Port_Dept_FK_Dept.Name AS FK_DeptText, V_FlowDataPlus.FK_Flow, V_FlowDataPlus.FK_NY, 
    Pub_NY_FK_NY.Name AS FK_NYText, V_FlowDataPlus.FlowStarter, 
    Port_Emp_FlowStarter.Name AS FlowStarterText,......,
    ISNULL(V_FlowDataPlus.WFState,0) WFState, CASE V_FlowDataPlus.WFState WHEN 0 THEN '空白' 
    WHEN 1 THEN '草稿' WHEN 2 THEN ... END WFStateTEXT 
    FROM V_FlowDataPlus 
    LEFT JOIN Port_Dept AS Port_Dept_FK_Dept ON V_FlowDataPlus.FK_Dept=Port_Dept_FK_Dept.No 
    LEFT JOIN Pub_NY AS Pub_NY_FK_NY ON V_FlowDataPlus.FK_NY=Pub_NY_FK_NY.No 
    LEFT JOIN Port_Emp AS Port_Emp_FlowStarter ON V_FlowDataPlus.FlowStarter=Port_Emp_FlowStarter.No 
    WHERE (1=1) AND ( ( LEFT(V_FlowDataPlus.FK_Dept, LEN('100')) = '100' ) ......
    AND ( V_FlowDataPlus.WFState =@WFState) ) 
    ORDER BY V_FlowDataPlus.OID DESC "
    
  • 分组GroupBy:因为分组已经在查询阶段实现,渲染时只需在分组字段的内容有变化时插入分组标识行即可。

惊变!

渲染用的代码如之前一样并不复杂,只是这一次,在运行之后你会发现QueryObject.DoQuery()爆出异常:

Column 'FlowName' does not belong to table otb.

FlowName怎么可能找不到?otb表格又是个什么鬼?

这一抛异常不打紧,却牵扯出隐藏于流程引擎内部的一个——惊天秘密!(笑)

WARNING:前方含精致剧透,大幅降低游戏乐趣,硬核玩家慎入!

已临近尾声,就提前放出巴特最终会绘制完成的UC外壳结构图、再对新剧情做讲解吧(职业玩家应该能认出这是鲁棒性分析法),

Robust分析图

此图从左侧User看起,User访问Completed UC外壳后、如前所述、首先就会调用BP.DA.ClassFactory.GetEns(ensName)来创建一个Entitites实例,完成后,接着才是使用ToolBar搭建QueryObject、定制QueryObject获得结果记录集等事;而这次遭遇到的异常,其实就位于ClassFactory这一环节。

WARNING撤除:切换回巴特的视角……

“原本认为依葫芦画瓢调用ClassFactory即可,无需修改到这种核心类的内部,没想到其中还另有乾坤。” 巴特追踪探访到Entity实例的制造厂内部后叹道。

ensName参数中带"."时,ClassFactory.GetEns()就会判断其为一个带NameSpace的完美类名、而利用反射机制获得与之相匹配且为Entities子类的类的实例。但是,当ensName中并不带"."时(瑕疵类名),其实并不会走之前的路线,因此"FlowDatas"并不能加载起BP.WF.FlowDatas的实例,也就无法完成ORM映射。

长话短说的话,巴特尝试过在"."判断柱向右拐、把ensName改成完整的"BP.WF.FlowDatas"却遭遇到了更多麻烦(略),反而沿着不带"."的行进路线较简单地就找到了解决Puzzle的宝箧。

Sys_MapDataSys_MapAttr就是这一对宝箧。玩家需要记得,如果只改置一个的话仍会抛异常。

首先打开Sys_MapData,珠光宝气扑面而来,什么ND1002 飞行部-成员填写ND1007 航空安全部-部门经理审核、大量的Demo_xxx、还有造型奇异的gysjhcyxqdjb 工业设计和创意需求登记表,让人一看就知道找对了地方,于是我们只需在此插入FlowDatas 通用流程记录PTable字段刻上数据视图名"V_FlowDataPlus"即可(其他字段都仿造着写影响不大)。完成了这一步,BP.Sys.MapData(参照Robust分析图中上偏右位置)就能在ClassFactory提出创建请求时找到GEEntities胚胎与数据表间的ORM映射。

草薙遗留下的开发环境里Sys_MapData中已有了FlowDatas的记录(只是关联于V_FlowData视图),因此到这一步其实是能通过的。

随后还需打开Sys_MapAttr,在这里更精密地设置对哪些属性做ORM映射。与Sys_MapData相比这个宝箧的构造要复杂得多。

【道具】关键数据视图:Sys_MapAttr
Sys_MapData中设置好Entities型实体类名、与关联的数据表名之后,就需要到Sys_MapAttr中进一步设置需做ORM映射的属性了。否则缺省只会有OIDRDT这两个共通字段。

关于各主要字段该如何填写:
提示:可在管理器中通过“列属性”确认字段的含义(参见上篇)

  • MyPK:会成为需做ORM映射的属性记录的主键。有多少个属性需要映射(如FK_FlowFK_Dept)就需新建少条记录,并需避免与其他记录之间重名;很容易从已有的记录观察出其命名规范
  • FK_MapData:这就是Sys_MapDataNo主键的值了,e.g."FlowDatas"
  • KeyOfEn:需做ORM映射的属性名,e.g.FK_FlowFK_Dept
  • UIContralType[sic]:该字段若被渲染时适合用何种控件。枚举型,取值可参照BP.En.UIContralType:e.g.TB=0, DDL=1, CheckBox=2, RadioBtn=3
  • MyDataType:字段的数据类型。枚举型,取值可参照BP.DA.DataType:e.g.AppString=1, AppInt=2, AppBoolean=4
  • LGType:老公(误)逻辑类型。枚举型,取值可参照BP.En.FieldTypes:e.g.Normal=0, Enum=1, FK=2
    • 只有LGType=2(FK)的、才会被推导成FieldType.FK 重要,总会有些属性你在Sys_MapAttr的现有记录中找不到类似参照的(比如FK_Flow),如果LGType字段填错,会直接导致该属性不会被自动外键关联成FK_xxText(相关代码位于MapAttr.HisAttr.get
  • UIBindKey:关联的集合型实体类名称,对该属性需做外键/枚举型关联时才填,e.g.FK_Dept的话、对应于BP.Port.Depts实体类、并进而会ORM映射到BP_Port_Dept表格
  • UIRefKey, UIRefKeyText:外键/枚举型关联表格中的主键字段、及字符型名称字段,当符合默认规则时可省略。e.g.FK_Dept的话、在BP_Port_Dept表格中分别存在No主键字段和Name名称字段,于是可省略不填
  • EditType:按特定控件渲染后是否允许编辑。枚举型,取值可参照BP.DA.EditType:e.g.Edit=0, UnDel=1, Readonly=2

FlowDatas而言,需要逐一按照上述规则填入Sys_MapAttr表格的属性如下:

FID, FK_Dept, FK_Flow, FK_NY, FlowEmps, FlowEnderRDT, FlowEndNode, FlowStarter, FlowStartRDT, OID, RDT, Title, WFState

将这些代表着不同侧面的属性逐一安顿好,会是一段平静而愉悦的过程。

你进而会发现,在上篇中扩展出的V_FlowDataPlus整个都不需要了,因为我们其实已不需在视图级别扩展出FlowName之类,而是可自己调试出FK_FlowText等来使用!

Sys_MapDataSys_MapAttr都真正填妥后,ClassFactory的内部机制(如Robust图右上角所示)就能正确实例化"FlowDatas"了。

那之前异常中的“otb表格”又是出自何处呢?

//BP.DA.DBAccess.RunSQLReturnTable_200705_SQL(string sql, Paras paras)
try
{
    DataTable oratb = new DataTable("otb");
    ada.Fill(oratb);
    ......
}
catch (Exception ex)
{
    ......
}

RunSQLReturnTable_200705_SQL()RunSQLReturnTable()针对MSSQL派宗的实现版本,而RunSQLReturnTable()会在QueryObject.DoQueryCommon()时被调用(DoQueryCommon()是对DoQuery()这条线路做的折叠简化),这就是异常中爆出那么个奇特三文字的缘由了(为何MSSQL对应版中存在疑似Ora*派宗的"oratb"仍是官方秘辛之一)。

“‘五月花’…普利茅斯…” 在系统重新构建、二段改造后的“我的归档”外壳终于顺利显示的时候,巴特的思绪却已再次折回到了“Ghost迁徙新大陆”的思绪中(该是为续作留下的伏笔吧)。

发布

外壳定制完成的“审判者”,将被发布到测试场完成质检后正式升级。尤其此次修改涉及到Ghost部分,虽说主要只是做了看似有益的“加法”,仍需多角度测试其影响。

“你的名字是?”——“吴刚”。这个内部ID巴特已经用了很久,却少有人知道其含义。

“受试目标将被发布至综合测试场,请确认已完成同行评审与基本用例测试。”(发布后的测定结果将决定玩家的经验增长值)

“确认。”

从思考战车上下来走向白色木屋,巴特的步履显得有些蹒跚,这两天的工作量都快赶上两个月了,即便对义体化的他来说…此时醺红的夕阳洒落到塔奇克马内部,一幕全息投影被衍射得有些朦胧——隐隐是去年的这个季节,草薙、巴特、户草三人登上八幡山顶的景象。阵阵山风拂过,簌簌金色花雨。

灵魂的强度,毕竟是有限的;除非真能出现“义灵”化的技术。可是,那时的你,还是么。

凛冬将至。

通关!心情不错,可以给称手的HHKB手柄做一次纳米护理了~

彩蛋

地下室昏黄的灯光下,巴特在一张只有他义眼才能看清的复合石墨烯便签上蚀刻着:“赛博世界注定将迎来更多人类‘清教徒’的Ghost,建立起新的‘云巅之城’[1]。在傀儡师接引下舍弃外壳、以及穿越艰险莫测的赛博海会成为天然屏障。随后,也许是许久之后…‘无处不是外壳’。”

后记

最近中途接手一套基于ccFlow的申请管理系统的UI改造,因缺少直接文档、而依靠追踪解剖相关代码完成任务。特将脑部Tracer自动记(Y)录(Y)下的“攻壳定制攻略”整理公开。

本攻略【上篇】讲解了前端开发所需的基本概念、编码规则、数据视图,运用BP.WF.Dev2Interface接口中的方法(SDK模式)完成了流程查询,重点引出了ORM映射。【下篇】基于对下载包中CCFlow\WF\Rpt\UC下“查询”控件的解读、改造了“已完成”流程一览画面,重点用到QueryObject(配套ToolBar),并深入涉及定制Sys_MapDataSys_MapAttr视图来控制ORM映射。

p.s. 《攻壳机动队》的真人版电影计划3月就要上映了,其中武力值颜值双高、又对生命本质有深度思索的女主草薙素子,将由傀儡师斯嘉丽·约翰逊来操控演绎(《超体》中演绎“露西”,《复联》中演绎黑寡妇 ,16年“双十一”晚会中令狐冲还将召唤出斯嘉丽作为了压轴戏码)。


——— 生死去来,棚头傀儡,一线断时,落落磊磊 ———

“Ghost/タマシイ”若断联时,精致与否的UI外壳,都将曲终落幕,再次褪回徒然的本相。


  1. “云巅之城”,仿照清教徒在新大陆尝试建立的“山巅之城”(由约翰·温斯罗普在开往美洲的“阿贝拉”号船上宣讲,并在其12次历任马萨诸塞湾总督期间践行),以清教徒为人文根基的扬基邦、后发展成为美国彻底摆脱宗主国统治、建立起自己的独立宪制的最关键力量。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,117评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,963评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,897评论 0 240
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,805评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,208评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,535评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,797评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,493评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,215评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,477评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,988评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,325评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,971评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,055评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,807评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,544评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,455评论 2 266

推荐阅读更多精彩内容