2023年2月

在Winform开发中,客户体验是个很好的参考性指标,如果一个功能使用的时候感觉很流畅,说明我们的程序执行效率还不错,但是随着数据的真多,原先可能流程的地方可能会变得比较卡,这时候就需要追本索源,找到症结并进行处理。但是,如果我们对Winform开发有一定的经验积累,有些耗时缓慢的界面处理一开始还是比较容易避免,本文介绍一些在开发过程中的一些界面性能上的优化处理,希望对大家有帮助。

1、案例说明

在我开发的CRM系统中,系统的一些用户如经办人员,来自权限管理系统,因此在开发的时候,并没有模拟太多的用户,因此,开始的设计处理是通过下拉列表列出相关的用户即可,如下所示。

最开始的考虑是让用户能够快速选择所需要的经办人员,默认是当前用户,这种设计开始不会有任何问题,因为数据少,测试起来非常流畅。

插入用户的界面部分是这样处理的,由于这个经办人是很多界面地方用到的,所以把它封装为一个界面控件,需要的地方拖过去使用即可,和普通的文本框一样的使用方法。

上面的控件初始化的时候,应该列出一些用户信息,如下所示。

                List<UserInfo> userList =BLLFactory<User>.Instance.GetAll();foreach (SimpleUserInfo info inuserList)
{
this.txtOperator.Properties.Items.Add(newCListItem(info.FullName, info.ID.ToString()));
}

2、界面效率优化

1)优化一

上面的代码粗看没有什么问题,但是我们知道,下拉列表为了提高效率,一般有一个BeginUpdate,EndUpdate的方法用来实现批量录入。为了提高速度,这点我们需要利用上,然后代码就修改为下面的处理方式。

this.txtOperator.Properties.BeginUpdate();
List
<UserInfo> userList = BLLFactory<User>.Instance.GetAll();foreach (SimpleUserInfo info inuserList)
{
this.txtOperator.Properties.Items.Add(newCListItem(info.FullName, info.ID.ToString()));
}
this.txtOperator.Properties.EndUpdate();

在数据不算很多的时候,感觉速度比原来却是
快了一些

2)优化二


是发现我的权限系统用户数据增加到几百个的人员的时候,速度就有点卡了。问题出现在哪里?

由于权限管理系统中用户表是一个较为庞大的表,如下所示。

每次获取用户的时候,我们调用了下面的代码。

BLLFactory<User>.Instance.GetAll()

这个操作是把用户的全部信息字段都获取一次,速度肯定比较慢了,那么我们来改进一下,因为我们这里控件只需要绑定一些简单的用户名,用户ID,用户姓名等基础字段,我们来简化一个对象用来实现数据的获取,如下所示。

于是我在权限管理系统定义了一个简单的用户对象,称为SimpleUserInfo,它只是包含了几个基本的字段即可,这样绑定代码修改如下。

                this.txtOperator.Properties.BeginUpdate();this.txtOperator.Properties.Items.Clear();
List
<SimpleUserInfo> userList = BLLFactory<User>.Instance.GetSimpleUsers();foreach (SimpleUserInfo info inuserList)
{
this.txtOperator.Properties.Items.Add(newCListItem(info.FullName, info.ID.ToString()));
}
this.txtOperator.Properties.EndUpdate();

好了,速度很快了,一眨眼功夫,几百个用户都列出来了,非常高兴。

3)优化三

上面确实感觉速度飞快了,几百个用户瞬间加载,解决了速度的问题,但是带来了另外一个问题,几百个用户,我们通过列表选择用户是否太费眼力了呢,一个个找,没有查找过滤?好费劲!

好既然速度提上来了,我们改进一下用户体验的效果吧,好像记得DevExpress有一个查找的下拉列表叫做SearchLookupEdit,就用它了。

先另外定义一个基于SearchLookupEdit的界面控件,如下所示。

设置它里面列表需要显示的字段,如下所示。

绑定代码如下所示。

        private void OperatorSelectControl_Load(objectsender, EventArgs e)
{
if (!this.DesignMode)
{
txtOperator.Properties.ValueMember
= "ID";
txtOperator.Properties.DisplayMember
= "FullName";
txtOperator.Properties.DataSource
= BLLFactory<User>.Instance.GetSimpleUsers();
}
}

上面的设计和代码绑定弄完成后,我们来看看具体的效果了。

通过弹出的GridView里面展示用户信息,并能支持查询搜索,选择用户速度快了很多,同时也提高用户体验。

4)优化四

说完了优化三,看到优化四,是不是心里说道,不是吧,还有更好的?

当然,我们知道Winform里面很多时候,数据可能都会被反复使用,如经办人员,一般来说,很多界面可能都需要,那么每次都需要从数据库里面取出几百个用户,效率上感觉还是不太好,特别如果是在我的混合式框架中用到的WCF的分布式获取数据方式,也就是说数据不再本地,需要通过网络方式获取,那么就会产生带宽的问题,效率可能无法保证。

好了,说到这里,就是指我想把这些常用到,不会经常变化的数据
缓存
起来,供下一步继续使用。

我们看看获取用户数据的地方修改为下面的方法,关于MemoryCacheHelper的定义及说明,可以参考我上一篇介绍WInform里面使用缓存的随笔《
Winform里面的缓存使用
》。

        /// <summary>
        ///获取用户全部简单对象信息,并放到缓存里面/// </summary>
        /// <returns></returns>
        public static List<SimpleUserInfo>GetSimpleUsers()
{
System.Reflection.MethodBase method
=System.Reflection.MethodBase.GetCurrentMethod();string key = string.Format("{0}-{1}", method.DeclaringType.FullName, method.Name);return MemoryCacheHelper.GetCacheItem<List<SimpleUserInfo>>(key,delegate() { return BLLFactory<User>.Instance.GetSimpleUsers(); },new TimeSpan(0, 10, 0));//10分钟过期 }

好了,使用缓存获取数据,我们只需要稍微调整下控件的获取数据代码即可,如下所示。

        private void OperatorSelectControl_Load(objectsender, EventArgs e)
{
if (!this.DesignMode)
{
txtOperator.Properties.ValueMember
= "ID";
txtOperator.Properties.DisplayMember
= "FullName";
txtOperator.Properties.DataSource
=SecurityHelper.GetSimpleUsers();
}
}

以上经验就是在我的《
CRM客户关系管理系统
》里面,对于经办人这样的用户选择界面所做的一步步精益求精的优化尝试和历程,希望给大家在做同样操作的时候常考。

如果有兴趣,可以进一步了解这个系统的各种界面设计效果。

在邮件处理的方面,Lumisoft.NET可以说是非常不错的一个选择,我在前面几篇文章中都介绍过这个组件。

基于Lumisoft.NET组件开发碰到乱码等一些问题的解决


基于Lumisoft.NET组件的POP3邮件接收和删除操作

基于Lumisoft.NET组件和.NET API实现邮件发送功能的对比

我的博客内容几乎全部来源我的项目开发或者研究工作,因此对于这个组件的使用还是有一定的说服力,这个组件封装了很多底层的一些操作,对于我们操作邮件的发送、接收的处理,更加方便高效。本文主要介绍该组件一个小的应用,基于Lumisoft.NET组件的SMTP账号登陆检测。

做某种工作,一般都有一个背景知识,从背景里面,我们可以大致了解这个工作的应用场景和具体目的,这个基于SMTP账号登陆检测的功能来源我对一批邮件账号进行交替邮件发送的时候需要,我手上有一批免费邮箱的SMTP账号,可以用于邮件的发送,但是我不确信是否所有的账号密码都是可以登录使用的,因此我需要对它们进行检测,只有正确的账号密码,我才用来批量发送邮件。这种检测我希望是通过自动化进行实现,不要人工一个个登录,在偶尔的一次Lumisoft.NET组件的使用过程中,我发现它发送邮件前,都会进行账号密码的登录操作,因此如果有成功的回应,那么就算达到目的了。

Lumisoft组件里面,SMTP_Client的对象有一个Auth方法(或者Authenticate方法),并且调用后,对象本身有一个IsAuthenticated的bool属性,这样就可以达到我们的操作目的了。

具体对SMTP_Client的SMTP账号登陆检测代码如下所示。

        private bool CheckLogin(string smtp, string username, stringpassword)
{
bool result = false;using (SMTP_Client client = newSMTP_Client())
{
try{if (this.txtUseSSL.Checked)
{
client.Connect(smtp, WellKnownPorts.SMTP_SSL,
true);
}
else{
client.Connect(smtp, WellKnownPorts.SMTP,
false);
}

client.EhloHelo(smtp);
var authhh = newAUTH_SASL_Client_Plain(username, password);
client.Auth(authhh);
//client.Authenticate(username, password); result=client.IsAuthenticated;
}
catch(Exception ex)
{
LogHelper.Error(ex);
}
}
returnresult;
}

为了更有效实现账号的检测,我们使用了背景线程进行处理,以达到更好的界面体验。

        privateBackgroundWorker worker;publicFrmEditMailConfig()
{
InitializeComponent();

worker
= newBackgroundWorker();
worker.DoWork
+= newDoWorkEventHandler(worker_DoWork);
worker.RunWorkerCompleted
+= newRunWorkerCompletedEventHandler(worker_RunWorkerCompleted);
}

然后在实现账号检测的事件里面对背景线程进行调用,具体操作代码如下所示。

        void worker_DoWork(objectsender, DoWorkEventArgs e)
{
string smtp = this.txtSmtpServer.Text.Trim();string username = this.txtLoginId.Text.Trim();string password = this.txtPassword.Text.Trim();bool success =CheckLogin(smtp, username, password);
e.Result
=success;
}

这个线程处理后,会返回一个结果,然后我们在线程完成的操作里面对他们进行处理,即可实现把结果展示到界面上了。

        void worker_RunWorkerCompleted(objectsender, RunWorkerCompletedEventArgs e)
{
string username = this.txtLoginId.Text.Trim();bool success =Convert.ToBoolean(e.Result);if(success)
{
string message = string.Format("[{0}] 测试成功", username);
MessageDxUtil.ShowTips(message);
}
else{string message = string.Format("[{0}] 测试失败,可能密码不正确或未开通SMTP", username);
MessageDxUtil.ShowWarning(message);
}
}

当然,这个只是对一个账号密码进行SMTP登录检测,如果需要批量检测,把思路变化一下,遍历需要检测的账号密码,对账号处理完成后,把结果显示到界面上即可。

大约半年前,一个客户希望给他开发一个代收代发的功能模块,只需要一个后台的服务进行自动收取和发送邮件,因此我结合了我自己的定时服务程序,增加了一个基于Lumisoft组件的邮件发送和收取插件模块,整合了一个邮件的代收代发的自动化服务程序。做完这些程序的开发,也撰写了好几篇的技术随笔文章,感觉也有一定的成就感。

基于Lumisoft的技术随笔如下:

基于Lumisoft.NET组件的SMTP账号登陆检测

基于Lumisoft.NET组件开发碰到乱码等一些问题的解决


基于Lumisoft.NET组件的POP3邮件接收和删除操作

基于Lumisoft.NET组件和.NET API实现邮件发送功能的对比

基于定时服务的模块开发技术随笔

Winform开发框架之通用定时服务管理

Winform开发框架之通用定时服务管理2---如何开发定时服务应用

一、邮件代收代发的服务模块

言归正传,本篇随笔主要介绍在我原来的代收代发的服务模块基础上增加相关的处理界面,首先先来了解这个应用的主要应用场景:在一些企业内部,可能有很多员工需要在一个应用平台上统一处理邮件的发送和接收工作,而不用为获取某个账号,到不同的邮件平台上进行邮件的处理。这样做有几个好处,一是使员工能够集中、统一处理邮件的相关操作,二可以把相关的邮件数据留在我们的业务平台上,三是能够管理多个账号的邮件收发等等。

为了实现邮件的即使收发,我们必须实现数据的处理延时问题,如果处理的太慢,就不能达到较好的效果,如果处理的太快---那就最好不过了,但是会耗费过多的硬件资源。

因此我使用了基于WIndows服务的操作,企业的服务器那是相当可以的,处理瓶颈不存在问题,也尽可能把间隔的发送、接受操作设置的更加及时一些。

邮件代收代发的应用程序,
具有下面几个特点:

1)通
过Windows服务驻留在服务器系统中,提供更及时、更高效的响应服务。

2)通过SMTP协议发送邮件和POP3协议获取邮件,支持大多数邮箱的邮件
收发操作。

3)邮件收取和发送采用多线程进行处理,更好利用服务器资源。

该邮件代收代发服务,通过寄宿在Windows服务上,可以更好利用Windows的资源,软件提供一个基于Winform的程序主界面进行服务的安装、卸载、系统配置信息管理等相关的管理工作。

定时服务的管理界面如下所示。

二、邮件代收代发的界面设计

为了实现以上的邮件收发和删除的服务操作,我们需要设计几个业务场景:

1)配置邮件账号信息

2)待发送的邮件存储,发送后邮件存储到发送历史里面。

3)指定接收账号的配置信息

4)接收的邮件存储

5)发送和接收的邮件的附件存储

下面分别对这几个模块的界面进行一个大致的介绍。

1、配置邮件账号信息

关于邮件账号的SMTP账号检测实现,可以参考我上一篇随笔《
基于Lumisoft.NET组件的SMTP账号登陆检测

对于多账号,我们需要用一个列表进行展示,方便操作管理,列表界面如下所示。

2、待发送的邮件存储

待发送的邮件,一般来说每封邮件内容可以不同,也可以设计为批量发送,邮件引用不同的内容即可,看具体的需要。

如果是前者,可以设计界面如下所示。

如果是用于营销推广方面,邮件的内容可能很多相同,如果每次发送都存储一样的邮件正文,则需要很大的存储空间,我们可以设计为只存储邮件明细的ID,把邮件标题和内容抽离进行独立维护。

邮件内容独立保存,在需要给多个客户发送邮件的时候,则选择邮件即可,邮件内容和发送邮件分离的界面如下所示。

上面两种应用场景,分别是不同业务的代表。

另外,邮件的内容编辑,可以选用基于富文本的RichEdit控件,关于DevExpress里面的RichEdit控件的使用,可以参考我之前的随笔文章:《
DevExpress控件使用之RichEditControl的使用

邮件内容编辑也可以选用一种开源的
Zeta HTML Edit Control,一个基于Winform的HTML编辑控件,具体可以参考我前面的随笔介绍:《
分享一个Winform里面的HTML编辑控件Zeta HTML Edit Control,汉化附源码

整个系统的界面场景还涉及到邮件发送界面设计、邮件接收账号配置、邮件接收列表的展示等方面,由于篇幅原因,本文就不再介绍,如果园友对文章比较支持,将会在下一篇继续介绍相关的界面设计思路及心得。

需要继续了解的园友,也可以到我的官方网站(http://www.iqidi.com)上,下载客户关系管理系统进行这方面的设计界面了解。

3、Winform开发框架介绍

Winform开发框架详细了解可以访问:
http://www.iqidi.com/Framework/info.html

我在做Winform界面的时候,一般都是统一化处理,界面顶部放置一些字段条件供查询,下面就是分页查询列表,展示相关的数据。但有时候碰到一些表字段内容分类比较多,有一些特别重要,如果放在一个树形列表来进行快速分类查询,用户体验应该更好。本篇随笔主要介绍如何快速实现树形列表和分页查询整合的WInform程序界面。

1、标准WInform列表界面

标准的查询条件+列表数据展示的WInform界面如下所示。

这个界面主要就是通过代码生成工具(
Database2Sharp
)进行初期的Winform界面生成即可。要了解具体如何生成标准的列表界面和数据编辑界面,查看下面两篇操作介绍随笔即可。


利用代码生成工具Database2Sharp设计数据编辑界面
》、《
代码生成工具之Winform查询列表界面生成
》。

2、树形列表和分页查询整合的Winform程序界面

以上的界面有时候感觉不够友好,正如文章开头说到,我需要在左边放置一些重要的数据分类进行查询,这样能够提高用户体验效果,最终希望的界面效果如下所示。

为了实现这种效果,我们需要进行几部操作。

1)在标准列表界面上增加窗口分割控件(如DevExpress的是SplitContainerControl控件)

传统的Winform界面可以使用SplitContainer控件

在现有已生成界面的基础上,把查询部分和列表部分的控件拖动小一点,然后把上述分隔控件拖动到界面后,在右边面板放入已有的查询和分页控件部分的内容,中间状态的列表界面效果如下所示。

然后在左边放入一个GroupControl控件,并加入树形控件TreeView,这样我们调整后的设计界面效果如下所示。

首先我们需要在代码里面绑定树的初始化代码,生成需要快速查询的内容,示意代码如下所示。主要逻辑思路就是,从数据字典中检索相关的分类,然后绑定一些查询条件,方便后面的处理。

        private voidInitTree()
{
base.LoginUserInfo = Cache.Instance["LoginUserInfo"] asLoginUserInfo;this.treeView1.BeginUpdate();this.treeView1.Nodes.Clear();//添加一个未分类和全部客户的组别 TreeNode topNode = new TreeNode("所有记录", 0, 0);this.treeView1.Nodes.Add(topNode);

TreeNode CategoryNode
= new TreeNode("客户活动类别", 2, 2);this.treeView1.Nodes.Add(CategoryNode);
AddDictData(CategoryNode,
0, "Category");

TreeNode OrderYearNode
= new TreeNode("记录年度", 8, 8);this.treeView1.Nodes.Add(OrderYearNode);
List
<string> yearList = BLLFactory<Maintenace>.Instance.GetYearList();foreach (string year inyearList)
{
TreeNode subNode
= new TreeNode(year, 0, 0);
subNode.Tag
=year;
OrderYearNode.Nodes.Add(subNode);
}
this.treeView1.ExpandAll();this.treeView1.EndUpdate();
}

为了处理树形列表的节点的单击事件,我们可以在其AfterSelect事件进行处理,示意代码如下所示。主要逻辑就是根据及节点和条件的不同,进行不同的处理。

        string treeConditionSql = "";private void treeView1_AfterSelect(objectsender, TreeViewEventArgs e)
{
if (e.Node != null && e.Node.Tag != null)
{
if (e.Node.FullPath.Contains("记录年度"))
{
int year =Convert.ToInt32(e.Node.Tag.ToString());
SearchCondition condition
= newSearchCondition();
condition.AddCondition(
"StartTime", Convert.ToDateTime(string.Format("{0}-01-01", year)), SqlOperator.MoreThanOrEqual);
condition.AddCondition(
"StartTime", Convert.ToDateTime(string.Format("{0}-01-01", year + 1)), SqlOperator.LessThan);
treeConditionSql
= condition.BuildConditionSql().Replace("Where", "");
BindData();
}
else{
treeConditionSql
=e.Node.Tag.ToString();
BindData();
}
}
else{
treeConditionSql
= "";
BindData();
}
}

上面的代码,我们定义了一个局部变量treeConditionSql 用来存储树列表单击后的条件,触发单击事件后,我们最终还是传回给标准列表界面已有的查询操作--BindData函数进行处理。

BindData里面最主要的操作就是构造查询条件,构造条件的语句如下所示,通过SearchCondition对象处理进行使用多数据库的兼容处理。

        /// <summary>
        ///根据查询条件构造查询语句/// </summary> 
        private stringGetConditionSql()
{
//如果存在高级查询对象信息,则使用高级查询条件,否则使用主表条件查询 SearchCondition condition =advanceCondition;if (condition == null)
{
condition
= newSearchCondition();
condition.AddCondition(
"Category", this.txtCategory.Text.Trim(), SqlOperator.Like);
condition.AddCondition(
"Title", this.txtTitle.Text.Trim(), SqlOperator.Like);
condition.AddDateCondition(
"StartTime", this.txtStartTime1, this.txtStartTime2); //日期类型 condition.AddCondition("Contact", this.txtContact.Text.Trim(), SqlOperator.Like);
condition.AddCondition(
"Place", this.txtPlace.Text.Trim(), SqlOperator.Like);
}
string where = condition.BuildConditionSql().Replace("Where", "");//如果是单击节点得到的条件,则使用树列表的,否则使用查询条件的 if (!string.IsNullOrEmpty(treeConditionSql))
{
where =treeConditionSql;
}
return where;
}

最终绑定数据的函数BindData的逻辑代码如下所示。

        /// <summary>
        ///绑定列表数据/// </summary>
        private voidBindData()
{
//entity this.winGridViewPager1.DisplayColumns = "Customer_ID,HandNo,Category,Title,Content,StartTime,EndTime,Contact,ContactPhone,ContactMobile,Place,PlaceAddress,PlacePhone,Note,Editor,EditTime";this.winGridViewPager1.ColumnNameAlias = BLLFactory<Activity>.Instance.GetColumnNameAlias();//字段列显示名称转义 string where =GetConditionSql();
List
<ActivityInfo> list = BLLFactory<Activity>.Instance.FindWithPager(where, this.winGridViewPager1.PagerInfo);this.winGridViewPager1.DataSource = new WHC.Pager.WinControl.SortableBindingList<ActivityInfo>(list);this.winGridViewPager1.PrintTitle = "客户活动管理报表";
}

这样我们就完成了树形列表和分页查询整合一起的数据查询处理逻辑,从而实现我们说需要的结果,这样的界面在某种程度上,给我们提供更多的方便,更好的体验。

一讲到工作流,很多人第一反应就是这个东西很深奥,有时候又觉得离我们较为遥远,确实完善的工作流设计很多方面,而正是由于需要兼顾很多方面,一般通用的工作流都难做到尽善尽美。微软也提供了几个版本的WF框架支持,也有一些厂家是基于这个框架基础上开发的工作流应用。

以前由于项目的需要,参与过一些工作流的项目开发,其中有些是基于我简易工作流的原理上进行拓展的,包括一个广州市各区县使用的行业审批业务平台,由于基于自己的流程处理,界面设计、流程流转等方面可以很好符合客户需求,定制的弹性较好,缺点是不够通用,也需要编写表单部分代码。

后面由于业务的需要,工作流方面的业务逐渐显得迫切,公司是想采用一个较为通用工作流框架来组织目前的业务,因此找了广州一家做工作流的公司,购买了他们的产品,虽然号称完全通过后台配置,零代码实现工作流业务表单的处理,但是由于客户对表单的设计要求比较多,有时候需要结合一些外部的数据接口,流程处理方面也有着进一步的需要,这样可能就打破了他们原来的格局,导致无论在表单设计、流程配置等方面,都需要购买他们工程师的现场服务,来进一步完善整个项目的内容,导致整个项目进展缓慢,遭遇水土不服的处境。

因此感觉,一个工作流模块,号称再强大,如果不能很好结合项目应用,即使零代码的功能配置,也可能使你处于尴尬的境况之中,因为通过配置,可能在代码里面平常很容易实现的表单功能,要通过零代码配置,花费的时间更多更难掌握,因为零代码是有代价的,需要您很好利用他们的API,他们的业务对象,有时候还需要很曲折的摸索参数,而这一切可能就是非常致命的弱点。

1、简易工作流的设计模型

在没有第三方工作流模块的情况下,简易工作流就是利用数据库和业务对象之间的协作关系,构建的一个半模块化的流程引擎,它能通过整合到项目代码中进行更好的融合以便实现工作流的相关功能。

首先我们知道,我们在Office里面创建任何文档,都有一个模板的概念,这样我们方便利用一些现成的数据和布局,工作流也一样,有一个流程模板的概念,如下所示。

然后每个流程模板,本身会预定义了一系列的处理流程,以便在流程实例里面进行不同的处理,因此流程模板还包含了多个流程步骤对象,他们的关系构成如下。

每个流程实例,除了他们自己的流程数据和字段信息外,它本身还有一个表单设计的问题,如费用审批,可能包含填写的费用清单数据等,所以流程实例还应该包含了流程的业务表单对象。

这样他们构成了一个完整的流程业务对象关系,如下所示。

2、流程审批的操作

对于一个流程处理操作,我们知道一般有审批通过、拒绝、退回到某步骤、转发到内部阅读、阅读,以及包括起草者能撤销表单呢等操作,当然如果还有一些具体的业务,可能还会有一些流程的处理才操作,不过基本上也可以归结为上面几种,只是他们每步处理的数据内容不同而已。因此审批的操作步骤分类如下所示。

这些操作我们都可以通过一些界面操作的封装实现,因为他们基本上都是通用的,我们传入一些流程ID等相关标识后,就能交给这些标准的操作界面完成了。

如审批界面如下所示,里面包含了通过、拒绝,跳回到某步骤,增加步骤等功能集合。

上面的界面是审批过程中,对于某一个流程处理人员实现的操作,而有时候,我们可能需要针对多个人进行某个步骤的处理,如传递给内部人员进行分阅操作,那么就应该选定多个人员进行处理,大概的处理界面效果如下所示。

当然,若申请人的申请单填写错误,需要撤销的话,那么也应该有这个操作,撤销表单后,就可以重新填写表单,然后再次提交进行流程。

3、流程审批的表单处理

在表单的动态设计和显示方面,一直没有好的思路,因此我觉得把流程模块作为半模块化即可,把部分界面通过代码编写方式进行整合,因此把表单的填写,表单查看做到用户控件里面,然后在界面里面引用即可。

如下面的表单填写操作界面如下所示,对不同的流程表单,在项目中增加一个表单的填写界面和保存操作即可。

查看并处理的表单操作,我们可以先做一个查看表单信息的界面,然后整合流程的处理工具栏,组合成一个查看、处理操作一体化的流程操作。

当然,如果是能够有动态设计表单,然后进行无缝整合当然更加完美,不过这样的操作,界面设计上也不会很麻烦,一般普通的开发人员都能胜任,因此,对于其他流程表单,依葫芦画瓢就可以完成不同的表单了。

而且里面的工具栏代码都是可以操作的,虽然可能集成了不同业务的处理方式,但是我们还是动态进行处理,处理代码如下所示。

        private voidInitToolBar()
{
//如果流程是可以撤销,且表单状态为处理中,那么可以“撤销”操作可用 bool mayCancel = BLLFactory<AppApply>.Instance.IsApplyMayCancel(ApplyId, base.LoginUserInfo.ID);this.btnCancel.ToVisibility(mayCancel);//可退回重新编辑 bool mayBackToEdit = BLLFactory<AppApply>.Instance.IsApplyMayBackEdit(ApplyId, base.LoginUserInfo.ID);this.btnEdit.ToVisibility(mayBackToEdit);//判断是否需要显示阅办状态 bool isReadStatus = BLLFactory<ApplyRead>.Instance.IsReadSatus(ApplyId, base.LoginUserInfo.ID);this.btnRead.ToVisibility(isReadStatus);//如果不是当前审批人隐藏审批按钮 bool canDeal = BLLFactory<BLL.ApplyUser>.Instance.IsCheckPermission(ApplyId, base.LoginUserInfo.ID);if(canDeal)
{
ApplyFlowInfo flowInfo
= BLLFactory<ApplyFlow>.Instance.GetFirstUnHandledFlow(ApplyId);if (flowInfo != null)
{
string procTypeName = BLLFactory<AppProc>.Instance.GetProcType(flowInfo.ProcType);
BarButtonItem button
= newBarButtonItem();
button.Caption
=procTypeName;
button.Name
=procTypeName;
button.Tag
= flowInfo;//绑定流程内容 button.ImageIndex = 3;
button.LargeImageIndex
= 3;
button.PaintStyle
=BarItemPaintStyle.CaptionGlyph;
button.ItemClick
+= newItemClickEventHandler(button_ItemClick);this.bar1.AddItem(button);
}
}
}

上面对于流程步骤的处理,就交给一个独立的按钮事件进行判断处理即可,根据不同的业务步骤名称进行不同的处理,这样就能够进行很好的控制处理。