2023年2月

在前段时间,接触一个很喜欢钉钉并且已在内部场景广泛使用钉钉进行工厂内部管理的客户,如钉钉考勤、日常审批、钉钉投影、钉钉门禁等等方面,才体会到原来钉钉已经已经在企业上可以用的很广泛的,因此回过头来学习研究下钉钉的一些业务范围和其SDK的开发工作。钉钉官方的SDK提供了很多方面的封装,不过相对于Java,.NET版本的一直在变化当中,之前研究钉钉C#版本SDK的时候发现一些问题反映给钉钉开发人员,基本上得不到好的解决和回应,而在使用官方的SDK的时候,有些数据竟然无法正常获取(如角色的信息等),而且官方的SDK使用的时候觉得代码较为臃肿,因此萌生了对钉钉官方SDK进行全面重构的想法。本系列随笔将对整个钉钉SDK涉及的范围进行分析重构,并分享使用过程中的效果和乐趣。

1、钉钉的介绍

钉钉(DingTalk)是阿里巴巴集团专为中国企业打造的免费沟通和协同的多端平台,提供PC版,Web版和手机版,支持手机和电脑间文件互传。 钉钉是阿里集团专为中国企业打造的通讯、协同的免费移动办公平台,帮助企业内部沟通和商务沟通更加高效安全。

manageme_background

2、使用钉钉官方SDK存在的一些问题或不足

一般我们在开发的时候,倾向于使用现有的轮子,而不是重复发明轮子。不过如果轮子确实不适合或者有更好的想法,那就花点功夫也无妨。

在使用原有的钉钉SDK的时候,发现存在以下一些问题。

1)部分SDK由于参数或者其他问题,导致获取到的JSON数据无法序列化为正常的属性,如前段时间的角色列表信息部分(后来修复了这个问题)。

2)使用SDK对象的代码过于臃肿,一些固定化的参数在使用过程中还需要传入,不太必要而且增加了很多调用代码。

3)对JSON序列化的部分,没有采用JSON.NET(Newtonsoft.Json.dll)的标准化方案,而是利用了自定义的JSON解析类,导致整个钉钉SDK的解析过程繁杂很多。

4)对整个钉钉SDK的设计显得过于复杂而不容易修改。

5)其他一些看不惯的原因

为了避免大范围的变化导致整个使用接口也变化,我在重构过程中,尽量还是保留钉钉的使用接口,希望使用者能够无缝对接我重构过的钉钉SDK接口,因此我在极力简化钉钉SDK的设计过程的时候,尽量兼容使用的接口。

而且由于我引入了Json.NET的对象标准序列化和反序列化的处理后,发现代码确实简化了不少,对于重构工作提供了非常的方便。

我们来对比一下原有钉钉SDK接口的使用代码和重构钉钉SDK的使用代码。

            IDingTalkClient client = new DefaultDingTalkClient("https://oapi.dingtalk.com/gettoken");
OapiGettokenRequest request
= newOapiGettokenRequest();
request.Corpid
=corpid;
request.Corpsecret
=corpSecret;
request.SetHttpMethod(
"GET");
OapiGettokenResponse response
=client.Execute(request);return response;

上面的代码就是钉钉标准官方SDK的使用代码,用来获取token信息的一个接口。

其实这个初始化DefaultDingTalkClient,并准备使用 OapiGettokenRequest来获取应答对象的时候,我们可以把这个URL(https://oapi.dingtalk.com/gettoken)封装在请求里面的,不需要使用的时候再去找这个URL,而且对应OapiGettokenRequest 请求的时候,数据提交方式POST或者GET方式也应该确定下来了,不需要用户再去设置较好。

用户参数比较少的情况下,可以使用构造函数传递,减少代码的行数。

然后利用扩展函数的方式,我们还可以进一步减少调用的代码行数的。

我们来看看,我重构代码后的调用过程,简化为两行代码即可:

            var request = newOapiGettokenRequest(corpid, corpSecret);var response = new DefaultDingTalkClient().Execute(request);

使用扩展函数的辅助,我们还可以简化为一行代码,如下所示

var token = new OapiGettokenRequest(corpid, corpSecret).Execute();

对于前面N行代码,变为目前的一行代码,效果是一样的,这个就是我希望的效果:
简单是美

如果对于多个Request的调用,我们也可以重用DingTalkClient对象的,如下代码所示。

var client = new DefaultDingTalkClient();var tokenRequest = newOapiGettokenRequest(corpid, corpSecret);var token =client.Execute(tokenRequest);if (token != null && !token.IsError)
{
string id = "1";var request = newOapiDepartmentListRequest(id);var dept =client.Execute(request, token.AccessToken);
...................

当然,由于请求对象和应答对象,我依旧保留了原来对象的名称,只是采用了基于JSON.NET的方式来重新处理了一下对象的定义。

例如对于Token的请求和应答对象,原来的Token应答对象定义如下所示

    /// <summary>
    ///OapiGettokenResponse./// </summary>
    public classOapiGettokenResponse : DingTalkResponse
{
/// <summary> ///access_token/// </summary> [XmlElement("access_token")]public string AccessToken { get; set; }/// <summary> ///errcode/// </summary> [XmlElement("errcode")]public long Errcode { get; set; }/// <summary> ///errmsg/// </summary> [XmlElement("errmsg")]public string Errmsg { get; set; }/// <summary> ///expires_in/// </summary> [XmlElement("expires_in")]public long ExpiresIn { get; set; }

}

我则使用了基于JSON.NET的标注来替代XmlElement的标注,并简化了部分基类属性。这样Json的属性名称虽然是小写,但是我们转换为对应实体类后,它的属性则可以转换为.NET标准的Pascal方式的属性名称。

    /// <summary>
    ///企业内部开发获取access_token的应答./// </summary>
    public classOapiGettokenResponse : DingTalkResponse
{
/// <summary> ///开放应用的token/// </summary> [JsonProperty(PropertyName ="access_token")]public string AccessToken { get; set; }/// <summary> ///失效时间/// </summary> [JsonProperty(PropertyName ="expires_in")]public long ExpiresIn { get; set; }
}

这样我在重构这些应答类的时候,所需要的只需要进行一定的替换工作即可。

而对于数据请求类,我则在基类里面增加一个IsPost属性来标识是否为POST方式,否则为GET方式的HTTP数据请求方式。

然后根据参数和IsPost的属性,来构建提交的PostData数据。

如我修改原有的BaseDingTalkRequest基类对象代码为下面的代码。

    /// <summary>
    ///基础TOP请求类,存放一些通用的请求参数。/// </summary>
    public abstract class BaseDingTalkRequest<T> : IDingTalkRequest<T> whereT : DingTalkResponse
{
/// <summary> ///构造函数/// </summary> publicBaseDingTalkRequest()
{
this.IsPost = true;
}
/// <summary> ///参数化构造函数/// </summary> /// <param name="serverurl">请求URL</param> /// <param name="isPost">是否为POST方式</param> public BaseDingTalkRequest(string serverurl, boolisPost)
{
this.ServerUrl =serverurl;this.IsPost =isPost;
}
/// <summary> ///提交的数据或者增加的字符串/// </summary> public stringPostData
{
get{string result = "";var dict =GetParameters();if(dict != null)
{
if(IsPost)
{
result
=dict.ToJson();
}
else{//return string.Format("corpid={0}&corpsecret={1}", corpid, corpsecret); foreach (KeyValuePair<string, object> pair indict)
{
if (pair.Value != null)
{
result
+= pair.Key + "=" + pair.Value + "&";
}
}
result
= result.Trim('&');
}
}
returnresult;
}
}
/// <summary> ///是否POST方式(否则为GET方式)/// </summary> public virtual bool IsPost { get; set; }/// <summary> ///连接URL,替代DefaultDingTalkClient的serverUrl/// </summary> public virtual string ServerUrl { get; set; }/// <summary> ///POST获取GET的参数列表/// </summary> /// <returns></returns> public virtual SortedDictionary<string, object> GetParameters() { return null; }

}

而对于请求Token的Request等请求对象,我们继承这个基类即可,如下代码所示。

    /// <summary>
    ///企业内部开发获取Token的请求/// </summary>
    public class OapiGettokenRequest : BaseDingTalkRequest<OapiGettokenResponse>{publicOapiGettokenRequest()
{
this.ServerUrl = "https://oapi.dingtalk.com/gettoken";this.IsPost = false;
}
public OapiGettokenRequest(string corpid, string corpsecret) : this()
{
this.Corpid =corpid;this.Corpsecret =corpsecret;
}
/// <summary> ///企业Id/// </summary> public string Corpid { get; set; }/// <summary> ///企业应用的凭证密钥/// </summary> public string Corpsecret { get; set; }public override SortedDictionary<string, object>GetParameters()
{
SortedDictionary
<string, object> parameters = new SortedDictionary<string, object>();
parameters.Add(
"corpid", this.Corpid);
parameters.Add(
"corpsecret", this.Corpsecret);returnparameters;
}
}

这个请求类,也就确定了请求的URL和数据请求方式(GET、POST),这样在调用的时候,就不用再次指定这些参数了,特别在反复调用的时候,简化了很多。

通过这几个类的定义,我们应该对我重构整个钉钉SDK的思路有所了解了,基本上就是以细节尽量封装、简化使用代码的原则进行全面重构的。

而整体的思路还是基于钉钉官方的SDK基础上进行的。

而对于钉钉SDK的核心类 DefaultDingTalkClient,我们则进行大量的修改重构处理,简化原来的代码(从原来的430行代码简化到90行),而实现功能一样的。

主要的逻辑就是我们使用了JSON.NET的标准化序列化的方式,减少了钉钉SDK的繁杂的序列化处理,而前面使用了PostData、IsPost属性也是简化了请求的处理方式。

        /// <summary>
        ///执行TOP隐私API请求。/// </summary>
        /// <typeparam name="T">领域对象</typeparam>
        /// <param name="request">具体的TOP API请求</param>
        /// <param name="accessToken">用户会话码</param>
        /// <param name="timestamp">请求时间戳</param>
        /// <returns>领域对象</returns>
        public T Execute<T>(IDingTalkRequest<T> request, string accessToken, DateTime timestamp) whereT : DingTalkResponse
{
string url = this.serverUrl;//如果已经设置了,则以Request的为主 if(!string.IsNullOrEmpty(request.ServerUrl))
{
url
=request.ServerUrl;
}
if (!string.IsNullOrEmpty(accessToken))
{
url
+= string.Format("?access_token={0}", accessToken);
}
string content = "";
HttpHelper helper
= newHttpHelper();
helper.ContentType
= "application/json";
content
=helper.GetHtml(url, request.PostData, request.IsPost);

T json
= JsonConvert.DeserializeObject<T>(content);returnjson;
}

3、使用重构的钉钉SDK

1)重构代码封装的调用

为了便于介绍对重构的钉钉SDK的使用情况,我编写了几个功能进行测试接口。

获取Token的操作代码如下所示。

        private void btnGetToken_Click(objectsender, EventArgs e)
{
//获取访问Token var request = newOapiGettokenRequest(corpid, corpSecret);var response = newDefaultDingTalkClient().Execute(request);
Console.WriteLine(response.ToJson());
}

对部门信息及详细信息的处理代码如下所示。

        private void btnDept_Click(objectsender, EventArgs e)
{
var client = newDefaultDingTalkClient();var tokenRequest = newOapiGettokenRequest(corpid, corpSecret);var token =client.Execute(tokenRequest);if (token != null && !token.IsError)
{
Console.WriteLine(
"获取部门信息");string id = "1";var request = newOapiDepartmentListRequest(id);var dept =client.Execute(request, token.AccessToken);if (dept != null && dept.Department != null)
{
Console.WriteLine(dept.Department.ToJson());

Console.WriteLine(
"获取部门详细信息");foreach (var item indept.Department)
{
var getrequest = newOapiDepartmentGetRequest(item.Id.ToString());var info =client.Execute(getrequest, token.AccessToken);if (info != null)
{
Console.WriteLine(
"部门详细信息:{0}", info.ToJson());

Console.WriteLine(
"获取部门用户信息");var userrequest = newOapiUserListRequest(info.Id);var list =client.Execute(userrequest, token.AccessToken);if (list != null)
{
Console.WriteLine(list.ToJson());

Console.WriteLine(
"获取详细用户信息");foreach (var userjson inlist.Userlist)
{
var get = newOapiUserGetRequest(userjson.Userid);var userInfo = client.Execute(get, token.AccessToken);if (userInfo != null)
{
Console.WriteLine(userInfo.ToJson());
}
}
}
}
}
}
}
else{
Console.WriteLine(
"处理出现错误:{0}", token.ErrMsg);
}
}

从上面的代码我们可以看到,对Request请求的处理简化了很多,不用再输入烦人的URL信息,以及是否GET还是POST方式。

获取角色的处理操作如下所示。

        private void btnRole_Click(objectsender, EventArgs e)
{
var client = newDefaultDingTalkClient();var tokenRequest = newOapiGettokenRequest(corpid, corpSecret);var token =client.Execute(tokenRequest);if (token != null && !token.IsError)
{
Console.WriteLine(
"获取角色信息");var request = newOapiRoleListRequest();var result =client.Execute(request, token.AccessToken);if (result != null && result.Result != null && result.Result.List != null)
{
Console.WriteLine(
"角色信息:{0}", result.Result.List.ToJson());foreach (var info inresult.Result.List)
{
Console.WriteLine(
"角色组信息:{0}", info.ToJson());

Console.WriteLine(
"获取角色详细信息");foreach (var roleInfo ininfo.Roles)
{
var roleReq = newOapiRoleGetroleRequest(roleInfo.Id);var detail =client.Execute(roleReq, token.AccessToken);if (detail != null && detail.Role != null)
{
Console.WriteLine(
"角色详细信息:{0}", detail.Role.ToJson());
}
}
}
}
}
}

获取的信息输出在VS的输出窗体里面。

2)使用扩展函数简化代码

从上面的代码来看,我们看到 DefaultDingTalkClient 还是有点臃肿,我们还可以通过扩展函数来对请求进行优化处理。如下代码

                var client = newDefaultDingTalkClient();var tokenRequest = newOapiGettokenRequest(corpid, corpSecret);var token = client.Execute(tokenRequest);

我们通过扩展函数实现的话,那么代码还可以进一步简化,如下所示。

var token = new OapiGettokenRequest(corpid, corpSecret).Execute();

对于扩展函数的封装,我们就是把对应的接口IDingTalkRequest增加扩展函数即可,如下代码所示。

以上就是我对钉钉SDK进行整体化重构的过程,由于我需要把所有的Request和Response两种类型的类转换为我需要的内容,因此需要全部的类进行统一处理,每个Request类我需要参考官方提供的URL、POST/GET方式,同时需要进行JSON.NET的标志替换,以及修改相应的内容,工作量还是不小的,不过为了后期钉钉的整体开发方面,这点付出我觉得应该是值得的。

我对不同业务范围的定Request和Response进行归类,把不同的业务范围放在不同的目录里面,同时保留原来的Request和Response对象的类名称,整个解决方案如下所示。

在开发项目的时候,往往碰到的不同的需求情况,兼容不同类型的数据库是我们项目以不变应万变的举措之一,在底层能够兼容多种数据库会使得我们开发不同类型的项目得心应手,如果配合快速的框架支持,那更是锦上添花的举措。我开发的项目或者框架,采用了微软企业库Enterprise Library的模块,倾向于支持多种数据库,也为我们开发不同类型的项目提供非常方便、快速、统一的处理方式。一般常规的数据库包括MS Server、Oracle、MySQL、PostgreSQL、SQLite、DB2、国产达梦等数据库,本篇随笔主要介绍如何实现从MS SQLServer到Mysql数据库,并为不同数据库类型添加实现底层的解决思路。

1、SQLServer数据库导出到MySQL

如果我们已经基于SQL Server进行了开发,并且具有很多基础的数据库数据了,那么我们可以利用SQL Server导出到MySQL数据库中,这种是我们常见的一种开发方式,毕竟基于SQLServer数据库开发起来会比较快速一些。

SQL Server数据库的管理工具是SQL Server Management Studio;而Mysql数据库的管理工具则推荐使用Navicat,这是一款非常强大好用的管理工具。

首先我们使用Navicat建立自己一个空白的Mysql数据库,用来承载SQL Server 的数据导出需要。

例如我们创建一个和SQL Server数据库同名的MVCWebMisTest数据库,并且设置它的字符集为默认或者UTF8的编码方式,如下所示。

然后利用SQL Server Management Studio进行数据的直接导出,选择【任务】【导出数据】,如下所示。

然后在弹出的数据库里面选择导入的数据源和目标,选择导入的数据源界面如下所示。

然后在目标里面选择Mysql的驱动,填入相应的信息,如下界面所示。

然后在下一步选择需要导出的表信息。

很不幸,这种方式操作数据库的导出操作没有办法成功。

2、从Navicat中导入MS SQLServer数据库数据

既然通过SQL Server Management Studio无法导入数据到Mysql数据库中,那么我们尝试下Mysql的数据库管理工具Navicat,它也有数据传输(导入导出)的功能。

我们在刚才建好的数据库上面右键调出对应的【数据传输】功能,然后设置好数据的源和目标,如下所示。

执行数据传输操作,顺利完成,非常强大和方便,而且没有任何错误。如下界面所示。

检查数据库导入的数据表和数据,没有问题。

3、Mysql数据库之间的传递

那么如果我们需要部署到服务器,就需要把当前的Mysql数据库传递(或者还原)到服务器的MySQL数据库中,一般来讲,我们利用Mysql的Navicat管理工具就可以实现数据导出的sql文件里面,然后在服务器里面使用反向操作即可还原数据库成功的了。

转储SQL文件成功如下界面所示。

而在服务器的上面,我们可以利用Navicat的运行SQL文件即可还原Mysql数据库了。

运行的结果如下所示。

当然有时候也会发现存在一些表依赖引用的情况发生,不知道是不是Navicat版本出来的问题,不过如果有这个问题,只要不是编码的问题,都好解决,打开sql文件把创建表的顺序调整一下就可以了,我就碰到过这种极端的情况,把一些依赖表调整到前面来就可以顺利导入数据了。

4、底层的Mysql数据库支持

底层支持多种数据库,对我们开发不同类型的项目有着非常重要的支撑作用,我们不知道客户具体环境会选择那种数据库,如果我们扩展不同的数据库非常方便和迅速,无疑会给我们提供很好的控制力和信心。

采用了微软企业库Enterprise Library作为我们底层的数据库访问模块后,对于多种数据库的访问操作,就会统一采用这个企业库的数据库访问对象,操作起来非常一致,为了对不同数据库的常规增删改查等一些操作进行进一步的封装,已达到简化代码的目的,因此我们可以为每个不同的数据库定义一个数据访问操作基类,以便实现一些不同数据库差异性的处理,但是它们还是有一个共同的数据访问基类。

采用不同的数据库,我们需要为不同数据库的访问层进行生成处理,如为SQLServer数据的表生成相关的数据访问层DALSQL,里面放置各个表对象的内容,不过由于采用了相关的继承类处理和基于数据库的代码生成,需要调整的代码很少。

我们为了不同的数据库准备了不同的数据库实现,不过也仅仅是特殊的接口调用而已,一般常规的增删改查以及分页等普通接口,全部交由基类接口实现即可。

如对于权限管理系统这个常规框架基础模块,多数据库的支持就非常必要的,它的数据库访问层如下所示。

不同的数据库访问层实现也是相差无几的,一般可以利用代码生成工具进行快速生成后,增加自定义接口实现即可。

而对于不同数据库的支持,直接复制过去,改动一下数据库实现层的命名空间和访问层的基类就可以了,如果有自定义SQL脚本上的差异,可以适当的修改即可。一般从一种数据库支持扩展到另外一种数据库支持,无非就是复制过去,进行调整一下即可,非常方便快速,这个就是整体框架支持的魅力和效率所在。

这样整合多种数据库支持的底层后,整个数据访问的架构设计如下所示。

有了这些底层支持,我们在配置文件选择不同的数据库的时候,选择性的保留其中一种数据库配置信息即可。下面是各种数据库支持的配置信息参考,一般保留一种即可。

我在之前介绍了很多关于Boostrap的框架方面的文章,主要是介绍各种插件的使用居多,不过有时候觉得基于Metronic的Boostrap框架的界面效果不够紧凑,希望对它进行一定的调整,那么我们应该如何进行相应的样式调整呢,其实找到对应的CSS进行处理即可。同时也可以结合Chrome浏览器的开发者模式下的Source进行一定的调整修改,得到效果后进行项目源码修改。

1、原始的界面效果

一般对于框架,我也希望尽可能使用默认的效果样式,毕竟设计师都调整的不错了,不过有时候感觉不好的时候,自己也可以根据需要进行一定的调整,我们首先来看看标准界面下的Portlet界面。

上面是一个标准的界面,包括查询、表格数据展示等功能,我把内容区域分为了这两块,使用Portlet界面进行了分区,整体看来界面还是挺美观的,不过就是觉得绿色横条有点偏大了,我们是否可以调整一下呢?

当然可以了,我们对这个样式进行跟踪,找到对应的CSS样式进行修改即可。

我们从对应的CSS文件里面找到这个portlet-title进行调整就好了。

CSS文件的内容是在文件 metronic/assets/global/css/components-rounded.css 里面的,因此我们找到并修改对应样式即可。

可以对他们进行测试进行查看最终效果,然后确定具体的偏移量和高度是否满足即可。

我们可以通过Chrome浏览器进行直接的修改查看,马上可以看到效果,非常方便

最后看看我们调整后的界面效果吧。

在工作流处理表中,首先我们区分流程模板和流程实例两个部分,这个其实就是类似模板和具体文档的概念,我们一份模板可以创建很多个类似的文档,文档样式结构类似的。同理,流程模板实例为流程实例后,就是具体的一个流程表单信息了,其中流程模板和流程实例表单都包括了各个流程步骤。在流程实例的层次上,我们运行的时候,需要记录一些日志方便跟踪,如流程步骤的处理日志,流程实例表单的处理日志等这些信息。

一旦流程实例根据模板创建后,流程先根据模板初始化后,在处理过程还可以动态增加一些审批步骤,使得我们的处理更加弹性化。如下所示。

我们在系统中动态定义很多业务表单,因此需要动态展示创建表单的入口;另外每种业务表单的创建和查看也需要实现动态的构建,才能更好的实现我们业务流程的处理规则。

1、动态显示流程业务入口

我们在工作流模块中,有一个统一的业务创建入口,方便用户的使用,我们需要创建什么类型的业务表单,从中选择创建接口,是一个便利的入口。

我们实现这个展示会相对比较简单,但是创建业务表单的入口需要动态的处理,是根据用户配置的参数进行动态的处理的。

上述的界面是通过在数据库里面动态获取信息,并创建不同的按钮的,因此可以实现流程入口的动态显示,不需要硬编码带来后期的修改。实现的逻辑就是在右侧内容区域的流布局区域,根据表单信息动态创建按钮,并实现对应的事件响应即可,实现代码如下所示。

        /// <summary>
        ///绑定表单列表的展示/// </summary>
        private voidBindData()
{
//使用流布局,清空 this.flowLayoutPanel1.Controls.Clear();//根据条件获取表单列表,并动态创建按钮 string where =GetConditionSql();
List
<FormInfo> list = BLLFactory<BLL.Form>.Instance.Find(where);int i = 0;foreach (FormInfo info inlist)
{
//在流布局中动态加入按钮 SimpleButton button = CreateButton(info, i++);this.flowLayoutPanel1.Controls.Add(button);
}
}
/// <summary> ///根据流程模板的表单信息,动态创建入口按钮/// </summary> /// <param name="info">模板的表单信息</param> /// <param name="imageIndex">图标</param> /// <returns></returns> private SimpleButton CreateButton(FormInfo info, intimageIndex)
{
//定义按钮,在流布局的图标、位置、偏移空间、字体颜色等 SimpleButton button = newSimpleButton();
button.ImageList
= this.imageCollection1;
button.ImageLocation
=ImageLocation.TopCenter;
button.Padding
= new Padding(10, 10, 10, 10);
button.Size
= new Size(102, 114);
button.Margin
= new Padding(10, 10, 10, 10);
button.ImageIndex
=imageIndex;
button.Font
= new Font("宋体", 9f, FontStyle.Bold);
button.ForeColor
=Color.Blue;
button.Text
=info.FormName;
button.Tag
=info.ID;if (!string.IsNullOrEmpty(info.Remark))
{
button.ToolTip
=info.Remark;
button.ToolTipIconType
=DevExpress.Utils.ToolTipIconType.Information;
}
//所有按钮统一处理事件 button.Click += newEventHandler(button_Click);returnbutton;
}

按钮的处理有一个统一的事件实现新建业务表单的赋值和显示窗体。实现的代码如下所示。

        /// <summary>
        ///单击某个动态生成的按钮,触发的申请表单创建界面/// </summary>
        void button_Click(objectsender, EventArgs e)
{
SimpleButton button
= sender asSimpleButton;if (button != null)
{
//获取模板表单必要的信息 var formId =button.Tag.ToString();var formInfo = BLLFactory<BLL.Form>.Instance.FindByID(formId);if (formInfo != null && !string.IsNullOrEmpty(formInfo.ApplyWin))
{
try{//动态构建创建申请单的界面窗体并赋值 var dlg = Assembly.GetExecutingAssembly().CreateInstance(formInfo.ApplyWin) asFrmAddApply;
dlg.FormID
=button.Tag.ToString();
dlg.ShowDialog();
}
catch(Exception ex)
{
LogHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
else{
MessageDxUtil.ShowTips(button.Text
+ "暂未开通");
}
}
}

2、动态显示和创建业务表单的处理

有了上面动态列表的显示,以及统一的按钮处理,事情就好办很多。

我们刚才也涉及到了业务表单的创建调用,是通过反射处理实现业务表单创建窗口的赋值和显示的。

    //动态构建创建申请单的界面窗体并赋值
    var dlg = Assembly.GetExecutingAssembly().CreateInstance(formInfo.ApplyWin) asFrmAddApply;
dlg.FormID
=button.Tag.ToString();
dlg.ShowDialog();

这其中涉及的配置信息就是我们创建一个业务窗口所需要的参数的,如下数据表所示。

其实通过创建这些业务表,我们在封装继承上也做了很多工作,以极大简化业务表单的处理,以下是业务表单新建、编辑、查看的处理操作,它们已经继承自各自的处理类,因此在反射的时候,统一转换为基类即可实现处理。

首先我们来了解一下业务表单的对应关系,一般创建一个业务流程处理,都需要有一个具体的创建业务表单的界面,以及一个查看处理表单的界面。

为了方便,我们尽可能减少代码编写,我们需要把大多数的逻辑处理放在基类实现,这样我们在新增一个业务表单的时候就可以减少很多代码编写及维护了。

例如对于请假申请的业务表单,它们的窗体定义如下所示。

而查看请假申请的业务表单则是如下。

从上面关系我们可以看到,其中对于工作流业务表单的窗体界面都可以实现标准的处理了,继承自某个基类,然后整合相关的数据处理规则即可。

那么我们提炼业务信息后,可以使用代码生成工具快速生成,这样可以极大提高我们的开发效率。

下面就是使用我们定制的框架代码生成工具 Database2Sharp,就可以极大简化工作流业务表单的生成处理了。

3、查看申请单的处理动态化

在我的待办业务列表里面,就可以看到刚才的表单了,双击可以进行查看,以及相关的审批处理工作。

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

除了上面这些基础的表单处理动作,有时候还会定义多个处理人共同处理的会签步骤,只有全部通过才算通过的处理流程。

会签是指创建一个或多个子流程供相关人员进行审批,等待全部人员完成处理后再次回到主流程上,然后决定是否继续流转到下一个流程步骤上去,一般的申请单的主流程如下所示。

这里设置的会签处理就是其中一个步骤,一旦会签处理步骤发起会签,就会构建多个可供审批的子流程了,如下所示。

针对上面的业务介绍,那么显示申请单的处理就必须处理这些步骤是否可用,或者决定进入哪一个流程步骤的了。

对于审批性质的表单,如下是界面的审批操作

而如果是发起【发起会签】的处理操作,那么则是把相关的投票权发送给处理人进行会签处理。

以上就是工作流表单里面设计到的几个动态处理的业务场景,同时我们通过利用动态的信息处理,可以减少硬编码的可能性,同时增加系统的弹性处理,非常方便,由于相关工作流的基类设计较为合理,因此在代码生成的时候,只需要关注简单的界面展示调整即可,通过这种处理方式,可以在多个层面降低开发工作流界面的复杂度,同时系统又增加了很多可扩展性的处理,如可以动态增加表单、动态增加流程步骤、动态指定不同的业务处理类型等等。

通过这些的介绍,我们就是系统在开发的时候,尽可能提取不变的内容或者规则,从而在实际增量开发的过程中降低开发的时间,减少难度,同时统一处理做法,既可以提高效率,又可以提高稳定性和统一性。

在基于Boostrap的Web开发中,往往需要录入日期内容,基于Boostrap的插件中,关于日期的录入可以使用bootstrap-datepicker这个非常不错的插件,以替代默认的type=date这种不太友好的日期录入控件,本篇介绍的是我在我的Boostrap开发框架中利用bootstrap-datepicker插件实现日期的录入处理。

1、bootstrap-datepicker插件的介绍

关于bootstrap-datepicker插件的相关的项目地址如下所示:

http://www.bootcss.com/p/bootstrap-datetimepicker

https://github.com/uxsolutions/bootstrap-datepicker

使用这个插件,和其他插件使用类似,只需要引入对应的JS文件和CSS样式即可,如下是该插件的引用文件。

<scriptsrc="/js/bootstrap-datepicker.js"></script>
<scriptsrc="/css/bootstrap-datepicker3.min.css"></script>

如果需要使用本地语言,引入语言文件即可。

<scriptsrc="/locales/bootstrap-datepicker.zh-CN.js"></script>

插件的文件目录如下所示

插件的使用界面效果如下所示

我们在MVC框架中,使用插件的时候,往往是把对应的CSS和JS,通过打包的方式进行引入,如在C#代码里面BundleConfig.cs的代码如下所示

CSS文件引入如下代码所示

JS文件类似,如下所示。

而我们在开发Boostrap项目的时候,我们可以使用母版的方式引入对应的JS和CSS文件,在子页面则不需要再关注这些应用了,这些是基于MVC的Boostrap开发常见的处理,在这里就不再赘述了。

2、bootstrap-datepicker插件的项目使用代码

初始化bootstrap-datepicker插件也比较简单,最简单的代码如下所示。

<inputtype="text"id="datetimepicker">
$('#datetimepicker').datetimepicker({
format:
'yyyy-mm-dd hh:ii'});

或者

<scripttype="text/javascript">$(".form_datetime").datetimepicker();</script>

而一般使用的时候,我们需要设置语言,格式,按钮等属性,如下所示

            $('.input-daterange input').each(function() {
$(
this).datepicker({
language:
'zh-CN', //语言 autoclose: true, //选择后自动关闭 clearBtn: true,//清除按钮 format: "yyyy-mm-dd"//日期格式 });
});

在查询数据的界面中,我们一般需要一个区间的时间,如下所示。

而该界面的代码如下所示。

    <divclass="input-group input-daterange">
        <inputid="WHC_PayDate"name="WHC_PayDate"type="text"placeholder="付款日期(起)"data-date-format="yyyy-mm-dd"class="form-control"/>
        <divclass="input-group-addon">~</div>
        <inputid="WHC_PayDate2"name="WHC_PayDate"type="text"placeholder="付款日期(止)"data-date-format="yyyy-mm-dd"class="form-control"/>
    </div>

以及录入界面明细的时候,选择单个日期的界面如下所示。

    $('.input-date input').each(function() {
$(
this).datepicker({
language:
'zh-CN', //语言 autoclose: true, //选择后自动关闭 clearBtn: true,//清除按钮 format: "yyyy-mm-dd"//日期格式 });
});

使用插件的时候,我们不可能为每个input初始化bootstrap-datepicker插件,因此使用了类选择器的处理方式实现所有日期插件的初始化。

插件可选的日期格式:

  • yyyy-mm-dd
  • yyyy-mm-dd hh:ii
  • yyyy-mm-ddThh:ii
  • yyyy-mm-dd hh:ii:ss
  • yyyy-mm-ddThh:ii:ssZ

最后看看整体的界面效果吧。