wenmo8 发布的文章

在微信小程序开发中,我们可以根据不同的业务场景,开发不同的业务应用,可以基于自身域名服务接口,也可以基于第三方的域名接口进行处理(如果被禁用除外),本篇随笔介绍使用小程序来实现我博客(http://wuhuacong.cnblogs.com)的文章阅读功能,这个小程序主要用来介绍使用介绍基于Javascript的正则表达式的处理应用,和常规在C#里面使用正则表达式有一些差异,因此可以作为后续使用正则表达式处理业务数据的一个练兵吧。

1、Request接口合法域名配置

一般情况下,我们知道微信的Request请求是需要配置合法的域名的,这种安全性可以是微信拦截有潜在危险的或者不喜欢的域名接口,Request合法域名配置界面如下所示。

一般情况下,我们在上面增加合法域名即可,这样小程序发布后,就可以顺利通过检查并获取数据了,本篇随笔由于想读取博客园个人博客园的文章,因此需要配置博客园的域名,不过很不幸,博客园的域名上了黑名单被禁用了。

如果我们在开发环境,我们可以通过不包含对合法域名的检验处理,不过在开发环境必须取消勾选“不校验”。

2、小程序功能设计

首先我们来看看主体界面的效果图,然后在进行分析具体的功能实现,具体界面效果如下所示。

博客文章列表内容如下所示:

文章详细界面效果如下所示:

这些文章直接都是从博客园页面中获取,并通过Javascript的正则表达式进行提取,然后展示在小程序上的,对于HTML内容的展示我们还是使用了WxParse的这个HTML解析组件,具体功能和使用过程可以参考我之前的随笔《
在微信小程序中使用富文本转化插件wxParse
》进行详细了解。对于Javascript函数的封装,我们还是使用比较方便的Promise进行封装处理,具体知识可以参考我随笔《
在微信小程序的JS脚本中使用Promise来优化函数处理
》进行详细了解。

一般我们也准备把公用方法提取出来,放到工具类Utils/util.js里面,配置统一放到utils/config.js里面,这样方便小程序的模块化处理。

项目的文件目录如下所示。

在Utils/util.js里面,我们封装了wx.request的获取内容方法如下所示。

//封装Request请求方法
function request(url,method,data = {},type='application/json'){
wx.showNavigationBarLoading();
return new Promise((resove,reject) =>{
wx.request({
url: url,
data: data,
header: {
'Content-Type': type},
method: method.toUpperCase(),
//OPTIONS, GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT success: function(res){
wx.hideNavigationBarLoading()
resove(res.data)
},
fail:
function(msg) {
console.log(
'reqest error',msg)
wx.hideNavigationBarLoading()
reject(
'fail')
}
})
})
}

而在Config.js里面,我们主要定义好一些常用的参数,如URL等

在列表页面,我们主要是展示文字标题和日期等信息,而列表是需要滚动翻页的,因此我们使用微信界面组件
scroll-view
来展示,具体界面代码如下所示。

<blockwx:if="{{showLoading}}">
    <viewclass="loading">玩命加载中…</view>
</block>
<blockwx:else>
    <scroll-viewscroll-y="true"style="height: {{windowHeight}}rpx"scroll-top="{{scrollTop}}"bindscroll="scroll"bindscrolltolower="scrolltolower">
        <viewclass="blog">
            <blockwx:for="{{blogs}}"wx:for-index="blogIndex"wx:for-item="blogItem"wx:key="blog">
                <viewdata-id="{{blogItem.id}}"catchtap="viewBlogDetail"class="flex box box-lr item">
                  <viewclass="flex item_left">
                    <view><textclass="title">{{blogItem.title}}</text></view>
                    <view><textclass="sub_title">{{blogItem.date}}</text></view>
                  </view>
                </view> 
            </block>
            <blockwx:if="{{hasMore}}">
                <viewclass="loading-tip">拼命加载中…</view>
            </block>
            <blockwx:else>
                <viewclass="loading-tip">没有更多内容了</view>
            </block>
        </view>
    </scroll-view>
</block>

通过绑定向下滑动的事件 bindscrolltolower="scrolltolower" 我们可以实现列表内容的滚动刷新。

我们通过前面介绍的封装Request方法,可以获取到HTML内容,如下函数所示。

  //获取博客文章列表
  getList:function(start =1) {return new Promise((resolve, reject) =>{var that = this;var data ={};var type = "text/html";var url = config.mainblog_url +start;if(that.data.hasMore) {
app.utils.get(url, data, type).then(res
=> {

通过指定type = "text/html",并且传入对应的起始位置,可以获取到对应页面的内容。

在博客园里面,【我的随笔】里面的标准URL地址为:http://www.cnblogs.com/wuhuacong/p/?page=n,其中 n 是当前的页码。

页面上的页码效果如下所示:

分析页面源码,可以看到页码标签的源码如下所示。

因此我们对HTML源码进行正则表达式的匹配即可获取对应的内容(关键是获取多少页,为后面循环获取文章列表做准备)。

关于正则表达式的测试,建议使用RegExBuilder(http://www.jb51.net/softs/389196.html)进行测试,其实我之前倾向于使用The Regulator 2.0这个很好的程序测试正则表达式,不过这个软件经常性的不能启动使用。

不过后来测试使用RegExBuilder,也觉得非常不错,其中勾选ECMAScript是为了我们在Javascript使用正则表达式的选项,毕竟和在C#里面使用正则标识还是有一些差异,如不支持单行选择,以及一些微小差异。

对于在Javascript中使用正则标识,建议大家温习下下面几篇随笔,有所帮助:


深入浅出的javascript的正则表达式学习教程


使用javascript正则表达式实现遍历html字符串


JavaScript之正则表达式

以及一个常见的坑,在HTML内容匹配的时候,不支持.*这种很普通的模式,这种由于不能选择单行模式导致的,变通的方式是使用[\s\S]来实现匹配所有字符处理。

可以参考文章:
https://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work
了解下。

另外对于Javascript的正则书写,经常看到i,g,m的结束符

修饰符   描述
i (ignore case)
执行对大小写不敏感的匹配。
g (global search)
执行全局匹配(查找所有匹配而非在找到第一个匹配后停止)。
m (multiline)
执行多行匹配。

它的意思你参考文章了解:

https://zhidao.baidu.com/question/620656820875333772.html

Javascript的正则匹配处理,
支持正则表达式的String对象的方法
可以使用
search()方法、
match()方法、
replace()方法、
split()方法、


RegExp对象方法包括:

test()方法、
exec()方法。

具体Javascript的正则表达式使用,可以好好学习下《
深入浅出的javascript的正则表达式学习教程
》,就很清晰了。

例如对于我们这篇小程序,我们获取页码的js代码如下所示。

  var reg = /共(\d{0,})页/g;var pageNum = reg.exec(html)[1];//console.log(pageNum);
that.setData({
end:pageNum,
//设置最大页码 });

在获取每篇随笔文章的标题、URL、日期等信息,我编写了一个正则表达式来匹配,如下所示。

var regContent=/class=\"postTitl2\">.*?href=\"(.*?)\">(.*?)<\/a>.*?class=\"postDesc2\">(.*?)\s*阅读:.*?<\/div>/igm;

正则表达式的内容,在使用前,一定需要在这个工具上测试,测试通过了我们再在代码上使用,减少调试错误的时间。

下面的测试结果如下所示。

获取文章列表信息的小程序js代码如下所示。

在之前介绍的列表展示界面代码里面,我们绑定了单击连接的事件,如下界面所示标注所示。

这个事件就是触发导航到详细界面,如下所示,我们把URL作为id传入到详细的界面里面。

  viewBlogDetail: function(e) {var data =e.currentTarget.dataset;var url =data.id;//console.log(url);
wx.navigateTo({
url:
"../details/details?id=" +data.id
})
},

在文章详细界面展示里面,界面的代码如下所示。

<importsrc="../../utils/wxParse/wxParse.wxml" />
<viewclass="flex box box-lr item">
    <viewclass="flex item_left">
        <view>
            <textclass="title">{{detail.title}}</text>
        </view>
    </view>
</view>
<viewclass="page">
    <viewclass="page__bd">
        <viewclass="weui-article">
            <viewclass="weui-article__p">
                <templateis="wxParse"data="{{wxParseData:article.nodes}}"/>
             </view>
        </view>
    </view>
</view>

这里引入了WxParse作为HTML内容解析的组件,我们在页面代码顶部引入组件代码

<importsrc="../../utils/wxParse/wxParse.wxml" />

具体处理的内容在JS代码里面,我们的JS代码如下所示。

  //获取文章详细内容
getDetail(url) {var that = this;var type = "text/html";
app.utils.get(url, {}, type).then(res
=>{//console.log(res); var html =res;var regContent = /id=\"cb_post_title_url\".*?>([\s\S]*?)<\/a>[\s\S]*?id=\"cnblogs_post_body\">([\s\S]*?)<\/div><div\s*id=\"MySignature\">/igmvarmatchArr;if ((matchArr =regContent.exec(html))) {var detail ={
id: url,
title : matchArr[
1], //titile content : matchArr[2], //content };

that.setData({
detail:detail
});
WxParse.wxParse(
'article', 'html', detail.content, that, 5);
};
});
},

其中的Javascript的正则表达式如下:

var regContent = /id=\"cb_post_title_url\".*?>([\s\S]*?)<\/a>[\s\S]*?id=\"cnblogs_post_body\">([\s\S]*?)<\/div><div\s*id=\"MySignature\">/igm

我们在工具上测试,得到相关的效果后再在代码上使用。

最后就可以获得详细的展示效果了,文章详细界面效果如下所示:

在我们开发系统界面,包括Web和Winform的都一样,主要的界面就是列表展示主界面,编辑查看界面,以及一些辅助性的如导入界面,选择界面等,其中列表展示主界面是综合性的数据展示界面,一般往往需要对记录进行合理的分页,集成各种增删改查的按钮等功能。随着开发项目的需求变化,对数据记录分页展示、排序等功能都是常态的要求,因此在代码生成工具中调整了主列表界面的列表展示插件为Bootstrap-table插件,本篇随笔主要介绍在代码生成工具Database2Sharp中集成对Bootstrap-table插件的分页及排序支持功能。

1、Web界面列表分页处理

1)常规分页方式

最开始的Web界面列表分页,使用较为常规Bootstrap Paginator分页模式,内容生成以HTML组合方式,先设置表头,然后获取分页列表数据,遍历生成相关的HTML数据,增加到页面上,这种方式也是比较高效的处理方式,如我在本系列开始的随笔《
基于Metronic的Bootstrap开发框架经验总结(2)--列表分页处理和插件JSTree的使用
》介绍中一样。有时候为了业务数据的快速查询,也会在左侧放置一个树列表方便查询分类,界面如下所示。

这种方式可控性非常好,而且可以对HTML代码进行完全的控制,非常适合在自定义界面中展示一些数据,如我之前介绍过的图标分页展示界面 一样,完全是自定义的内容展示,图标界面如下所示。

2)Bootstrap-table插件分页

使用常规的分页方式界面可控性非常方便,不过随着不同项目的一些特殊要求,对表头排序的需求也是非常强烈的,上面的分页处理方式无法实现表头的排序功能,因此引入了使用非常广泛的Bootstrap-Table插件,该插件应用很广、功能非常强大,可以通过属性配置实现很细致的功能控制。Bootstrap-table插件提供了非常丰富的属性设置,可以实现查询、分页、排序、复选框、设置显示列、Card View视图、主从表显示、合并列、国际化处理等处理功能,而且该插件同时也提供了一些不错的扩展功能,如移动行、移动列位置等一些特殊的功能。

因此我对这个插件进行了使用研究并进行总结,这个插件的详细使用可以参考我的随笔《
基于Metronic的Bootstrap开发框架经验总结(16)-- 使用插件bootstrap-table实现表格记录的查询、分页、排序等处理
》进行了解。这个插件界面展示也是非常美观的。

这个插件最显著的特点就是完美支持客户端或者服务器的数据列排序处理,单击表头就可以实现排序操作。

2、在代码生成工具Database2Sharp中集成对Bootstrap-table插件的分页及排序支持

我们的代码生成工具Database2Sharp是为了框架开发服务的,不管是Winform还是Web开发,都可以基于数据库的基础上进行框架代码的快速生成,以及界面的代码生成,本次调整的代码生成工具功能,在列表界面代码中增加了对Bootstrap-table插件分页的支持,使得我们开发Bootstrap框架的界面代码更加丰富、快捷。

在代码生成工具Database2Sharp上,我们先使用Enterprise Library代码增量生成主体框架的框架代码。

然后在使用Bootstrap的Web界面代码生成功能,如下可以在工具栏界面中选择。

选择数据库和表后,可以进行界面代码(包括控制器代码、视图界面代码)两部分,其中视图分为两种模式,一种是利用Bootstrap-table插件的分页及排序(index.cshtml),一种是常规的Bootstrap Paginator分页处理(index2.cshtml)。

老客户可以继续使用index2.cshtml的样式,也可以使用最新的Bootstrap-table插件的分页及排序方式(index.cshtml)。

生成的界面分为HTML部分和JS部分,都是比较紧密联系的两部分,我们进行一定的调整即可实现丰富的界面排版。

部分的JS代码(展示分页部分处理)如下所示。

列表数据的显示列,默认是以数据库的字段进行生成,我们在生成后可以进行一定的调整,可以合理显示我们关注的数据。

当然生成的界面代码还有很多其他的JS代码,如编辑、查看的代码和控件对应,导入、导出等代码的处理,都是一并生成的,我们根据需要进行一定的裁剪调整即可完成整个界面的处理了,极大的提高开发效率。

在我们的很多框架或者项目应用中,缓存在一定程度上可以提高程序的响应速度,以及减轻服务器的承载压力,因此在一些地方我们都考虑引入缓存模块,这篇随笔介绍使用开源缓存框架CacheManager来实现数据的缓存,在微信开发框架中,我们有一些常用的处理也需要应用到缓存,因此本随笔以微信框架为例介绍缓存的实际使用,实际上,在我们很多框架中,如混合式开发框架、Web开发框架、Bootstrap开发框架中,这个模块都是通用的。

1、框架的缓存设计

在我们的微信开发框架中,缓存作为数据库和对外接口之间的一个分层,提供数据的缓存响应处理,如下结构所示是Web API层对缓存的架构设计。

在缓存的处理中,我侧重于使用CacheManager,这个缓存框架是一个集大成者,关于CacheManager 的介绍,我们可以回顾下我之前的随笔《
.NET缓存框架CacheManager在混合式开发框架中的应用(1)-CacheManager的介绍和使用
》。

CacheManager是一个以C#语言开发的开源.Net缓存框架抽象层。它不是具体的缓存实现,但它支持多种缓存提供者(如Redis、Memcached等)并提供很多高级特性。
CacheManager 主要的目的使开发者更容易处理各种复杂的缓存场景,使用CacheManager可以实现多层的缓存,让进程内缓存在分布式缓存之前,且仅需几行代码来处理。
CacheManager 不仅仅是一个接口去统一不同缓存提供者的编程模型,它使我们在一个项目里面改变缓存策略变得非常容易,同时也提供更多的特性:如缓存同步、并发更新、序列号、事件处理、性能计算等等,开发人员可以在需要的时候选择这些特性。

CacheManager的GitHub源码地址为:
https://github.com/MichaCo/CacheManager
,如果需要具体的Demo及说明,可以访问其官网:
http://cachemanager.michaco.net

2、在微信框架中整合CacheManager 缓存框架

在使用CacheManager 缓存的时候,我们可以直接使用相关对象进行处理,首先需要定义一个类来进行初始化缓存的设置,然后进行调用,调用的时候可以使用IOC的方式构建对象,如下代码所示创建一个自定义的缓存管理类

    /// <summary>
    ///基于CacheManager的接口处理/// </summary>
    public classCacheManager : ICacheManager
{
/// <summary> ///ICacheManager对象/// </summary> public ICacheManager<object> Manager { get; set; }/// <summary> ///默认构造函数/// </summary> publicCacheManager()
{
//初始化缓存管理器 Manager = CacheFactory.Build("getStartedCache", settings =>{
settings
.WithSystemRuntimeCacheHandle(
"handleName")
.And
.WithRedisConfiguration(
"redis", config =>{
config.WithAllowAdmin()
.WithDatabase(
0)
.WithEndpoint(
"localhost", 6379);
})
.WithMaxRetries(
100)
.WithRetryTimeout(
50)
.WithRedisBackplane(
"redis")
.WithRedisCacheHandle(
"redis", true)
;
});
}
}
}

然后在Autofac的配置文件中配置缓存的相关信息,如下文件所示。

如果直接使用Autofac的构造类来处理,那么调用缓存处理的代码如下所示。

            //通过AutoFac工厂获取对应的接口实现
            var cache = AutoFactory.Instatnce.Container.Resolve<ICacheManager>();if (cache != null)
{
accountInfo
= cache.Manager.Get(key) asAccountInfo;if (accountInfo == null)
{
var value = BLLFactory<Account>.Instance.FindByID(accountId);var item = new CacheItem<object>(key, value, ExpirationMode.Absolute, TimeSpan.FromMinutes(TimeOut_Minutes));
cache.Manager.Put(item);

accountInfo
= cache.Manager.Get(key) asAccountInfo;
}
}

如果为了使用方便,我们还可以对这个辅助类进行进一步的封装,以便对它进行统一的调用处理即可。

    /// <summary>
    ///基于.NET CacheManager的缓存管理,文档参考:http://cachemanager.michaco.net/documentation
    /// </summary>
    public classCacheManagerHelper
{
/// <summary> ///锁定处理变量/// </summary> private static readonly object locker = new object();/// <summary> ///创建一个缓存的键值,并指定响应的时间范围,如果失效,则自动获取对应的值/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="key">对象的键</param> /// <param name="cachePopulate">获取缓存值的操作</param> /// <param name="expiration">失效的时间范围</param> /// <param name="mode">失效类型</param> /// <returns></returns> public static T GetCacheItem<T>(string key, Func<T>cachePopulate, TimeSpan expiration,string region = "_", ExpirationMode mode = ExpirationMode.Sliding) where T :class{
CacheItem
<object> outItem = null;//通过AutoFac工厂获取对应的接口实现 var cache = AutoFactory.Instatnce.Container.Resolve<ICacheManager>();if (cache != null)
{
if (cache.Manager.Get(key, region) == null)
{
lock(locker)
{
if (cache.Manager.Get(key, region) == null)
{
//Add、Put差异,Add只有在空值的情况下执行加入并返回true,Put总会替换并返回True//如果按下面的方式加入,那么会留下历史丢弃的键值: cache.Manager.Put(key, value); var value =cachePopulate();var item = new CacheItem<object>(key, region, value, mode, expiration);
cache.Manager.Put(item);
}
}
}
return cache.Manager.Get(key, region) asT;
}
else{throw new ArgumentNullException("AutoFac配置参数错误,请检查autofac.config是否存在ICacheManager的定义");
}
}
}

不过由于官方已经提供了一个类似上面的代码逻辑的TryGetOrAdd方法,这个方法的定义如下所示。

TryGetOrAdd(String, String, Func<String, String, TCacheValue>, out TCacheValue)

Tries to either retrieve an existing item or add the item to the cache if it does not exist. The
valueFactory will be evaluated only if the item does not exist.

Declaration
boolTryGetOrAdd(stringkey, stringregion, Func<string, string, TCacheValue> valueFactory, outTCacheValue value)
Parameters
Type Name Description
String key

The cache key.

String region

The cache region.

Func
<
String
,
String
, TCacheValue>
valueFactory

The method which creates the value which should be added.

TCacheValue value

The cache value.

Returns
Type Description
Boolean

True
if the operation succeeds,
False
in case there are too many retries or the
valueFactory returns null.

我们根据这个参数的定义,可以进一步简化上面的辅助类代码。

                cache.Manager.TryGetOrAdd(key, region, (_key, _region) =>{var value =cachePopulate();var item = new CacheItem<object>(key, region, value, mode, expiration);returnitem;
},
outoutItem);return outItem as T;

整个类的代码如下所示

    /// <summary>
    ///基于.NET CacheManager的缓存管理,文档参考:http://cachemanager.michaco.net/documentation
    /// </summary>
    public classCacheManagerHelper
{
/// <summary> ///创建一个缓存的键值,并指定响应的时间范围,如果失效,则自动获取对应的值/// </summary> /// <typeparam name="T">对象类型</typeparam> /// <param name="key">对象的键</param> /// <param name="cachePopulate">获取缓存值的操作</param> /// <param name="expiration">失效的时间范围</param> /// <param name="mode">失效类型</param> /// <returns></returns> public static T GetCacheItem<T>(string key, Func<T>cachePopulate, TimeSpan expiration,string region = "_", ExpirationMode mode = ExpirationMode.Sliding) where T :class{
CacheItem
<object> outItem = null;//通过AutoFac工厂获取对应的接口实现 var cache = AutoFactory.Instatnce.Container.Resolve<ICacheManager>();if (cache != null)
{
cache.Manager.TryGetOrAdd(key, region, (_key, _region)
=>{var value =cachePopulate();var item = new CacheItem<object>(key, region, value, mode, expiration);returnitem;
},
outoutItem);return outItem asT;
}
else{throw new ArgumentNullException("AutoFac配置参数错误,请检查autofac.config是否存在ICacheManager的定义");
}
}
}

这样代码就简化了不少,而且不用自己控制读取的线程锁了,下面代码是使用辅助类实现缓存的添加及获取处理。

        /// <summary>
        ///为避免频繁的对数据库检索,提高获取账号信息的速度///我们把账号信息根据ID缓存起来,方便快速使用,提高效率。/// </summary>
        public static AccountInfo GetAccountByID(stringaccountId)
{
AccountInfo accountInfo
= null;#region 使用.NET CacheManager缓存 //正常情况下access_token有效期为7200秒,这里使用缓存设置短于这个时间即可 var key = "GetAccountByID_" +accountId;
accountInfo
= CacheManagerHelper.GetCacheItem<AccountInfo>(key, () =>{return BLLFactory<Account>.Instance.FindByID(accountId);
}, TimeSpan.FromMinutes(TimeOut_Minutes));
returnaccountInfo;
}

通过这样的辅助类封装,我们可以在需要缓存的函数里面,统一使用辅助类对数据进行缓存或者读取缓存的操作。

我们也可以直接使用Autofac构建的缓存管理进行操作,如在小程序里面,我们对用户敏感数据的解密处理函数,如下所示。

        /// <summary>  
        ///根据微信小程序平台提供的解密算法解密数据/// </summary>  
[HttpGet]public SmallAppUserInfo Decrypt(string encryptedData, string iv, stringthirdkey)
{
SmallAppUserInfo userInfo
= null;//通过AutoFac工厂获取对应的接口实现 var cache = AutoFactory.Instatnce.Container.Resolve<ICacheManager>();if (cache != null)
{
//从缓存里面,获取对应的SessionKey var sessionkey =cache.Manager.Get(thirdkey);if (sessionkey != null)
{
//对用户身份加密数据进行解析,获取包含openid等属性的完整对象 IBasicApi api = newBasicApi();
userInfo
=api.Decrypt(encryptedData, iv, sessionkey.ToString());
}
}
returnuserInfo;
}

我们在一般的接口函数开发中,为了安全性,我们都需要对传入的参数进行验证,确保参数按照我们所希望的范围输入,如果在范围之外,如空值,不符合的类型等等,都应该给出异常或错误提示信息。这个参数的验证处理有多种方式,最为简单的方式就是使用条件语句对参数进行判断,这样的判断代码虽然容易理解,但比较臃肿,如果对多个参数、多个条件进行处理,那么代码就非常臃肿难以维护了,本篇随笔通过分析几种不同的参数验证方式,最终采用较为优雅的方式进行处理。

通常会规定类型参数是否允许为空,如果是字符可能有长度限制,如果是整数可能需要判断范围,如果是一些特殊的类型比如电话号码,邮件地址等,可能需要使用正则表达式进行判断。参考随笔《
C# 中参数验证方式的演变
》中文章的介绍,我们对参数的验证方式有几种。

1、常规方式的参数验证

一般我们就是对方法的参数使用条件语句的方式进行判断,如下函数所示。

public bool Register(string name, intage)
{
if (string.IsNullOrEmpty(name))
{
throw new ArgumentException("name should not be empty", "name");
}
if (age < 10 || age > 70)
{
throw new ArgumentException("the age must between 10 and 70","age");
}
//insert into db }

或者

public void Initialize(string name, intid)
{
if (string.IsNullOrEmpty(value))throw new ArgumentException("name");if (id < 0)throw new ArgumentOutOfRangeException("id");//Do some work here. }

如果复杂的参数校验,那么代码就比较臃肿

void TheOldFashionWay(int id, IEnumerable<int>col, 
DayOfWeek day)
{
if (id < 1)
{
throw new ArgumentOutOfRangeException("id",
String.Format(
"id should be greater" + "than 0. The actual value is {0}.", id));
}
if (col == null)
{
throw new ArgumentNullException("col","collection should not be empty");
}
if (col.Count() == 0)
{
throw newArgumentException("collection should not be empty", "col");
}
if (day >= DayOfWeek.Monday &&day<=DayOfWeek.Friday)
{
throw newInvalidEnumArgumentException(
String.Format(
"day should be between" + "Monday and Friday. The actual value" + "is {0}.", day));
}
//Do method work }

有时候为了方便,会把参数校验的方法,做一个通用的辅助类进行处理,如在我的公用类库里面提供了一个:参数验证的通用校验辅助类 ArgumentValidation,使用如下代码所示。

     public classTranContext:IDisposable   
{
private readonly TranSetting setting=null;private IBuilder builder=null;private ILog log=null;private ManuSetting section=null;public eventEndReportEventHandler EndReport;publicTranContext()
{
}
publicTranContext(TranSetting setting)
{
ArgumentValidation.CheckForNullReference (setting,
"TranSetting");this.setting =setting;
}
public TranContext(string key,string askFileName,stringoperation)
{
ArgumentValidation.CheckForEmptyString (key,
"key");
ArgumentValidation.CheckForEmptyString (askFileName,
"askFileName");
ArgumentValidation.CheckForEmptyString (operation,
"operation");
setting
=new TranSetting (this,key,askFileName,operation);
}

但是这样的方式还是不够完美,不够流畅。

2、基于第三方类库的验证方式

在GitHub上有一些验证类库也提供了对参数验证的功能,使用起来比较简便,采用一种流畅的串联写法。如
CuttingEdge.Conditions
等。CuttingEdge.Condition 里面的例子代码我们来看看。

public ICollection GetData(Nullable<int> id, string xml, IEnumerable<int>col)
{
//Check all preconditions: Condition.Requires(id, "id")
.IsNotNull()
//throws ArgumentNullException on failure .IsInRange(1, 999) //ArgumentOutOfRangeException on failure .IsNotEqualTo(128); //throws ArgumentException on failure Condition.Requires(xml,"xml")
.StartsWith(
"<data>") //throws ArgumentException on failure .EndsWith("</data>") //throws ArgumentException on failure .Evaluate(xml.Contains("abc") || xml.Contains("cba")); //arg ex Condition.Requires(col,"col")
.IsNotNull()
//throws ArgumentNullException on failure .IsEmpty() //throws ArgumentException on failure .Evaluate(c => c.Contains(id.Value) || c.Contains(0)); //arg ex//Do some work//Example: Call a method that should not return null object result =BuildResults(xml, col);//Check all postconditions: Condition.Ensures(result, "result")
.IsOfType(
typeof(ICollection)); //throws PostconditionException on failure return(ICollection)result;
}
public static int[] Multiply(int[] left, int[] right)
{
Condition.Requires(left,
"left").IsNotNull();//You can add an optional description to each check Condition.Requires(right, "right")
.IsNotNull()
.HasLength(left.Length,
"left and right should have the same length");//Do multiplication }

这种书写方式比较流畅,而且也提供了比较强大的参数校验方式,除了可以使用其IsNotNull、IsEmpty等内置函数,也可以使用Evaluate这个扩展判断非常好的函数来处理一些自定义的判断,应该说可以满足绝大多数的参数验证要求了,唯一不好的就是需要使用这个第三方类库吧,有时候如需扩展就麻烦一些。而且一般来说我们自己有一些公用类库,如果对参数验证也还需要引入一个类库,还是比较麻烦一些的(个人见解)

3、Code Contract

Code Contracts
是微软研究院开发的一个编程类库,我最早看到是在
C# In Depth
的第二版中,当时.NET 4.0还没有出来,当时是作为一个第三方类库存在的,到了.NET 4.0之后,已经加入到了.NET BCL中,该类存在于System.Diagnostics.Contracts 这个命名空间中。

这个是美其名曰:契约编程

C#代码契约起源于微软开发的一门研究语言Spec#(参见http://mng.bz/4147)。

• 契约工具:包括:ccrewrite(二进制重写器,基于项目的设置确保契约得以贯彻执行)、ccrefgen(它生成契约引用集,为客户端提供契约信息)、cccheck(静态检查器,确保代码能在编译时满足要求,而不是仅仅检查在执行时实际会发生什么)、ccdocgen(它可以为代码中指定的契约生成xml文档)。

• 契约种类:前置条件、后置条件、固定条件、断言和假设、旧式契约。

• 代码契约工具下载及安装:下载地址Http://mng.bz/cn2k。(代码契约工具并不包含在Visual Studio 2010中,但是其核心类型位于mscorlib内。)

• 命名空间:System.Diagnostics.Contracts.Contract

Code Contract 使得.NET 中契约式设计和编程变得更加容易,Contract中的这些静态方法方法包括

  1. Requires:函数入口处必须满足的条件
  2. Ensures:函数出口处必须满足的条件
  3. Invariants:所有成员函数出口处都必须满足的条件
  4. Assertions:在某一点必须满足的条件
  5. Assumptions:在某一点必然满足的条件,用来减少不必要的警告信息

Code Contract 的使用文档您可以从
官网下载
到。为了方便使用Visual Studio开发。我们可以安装一个
Code Contracts for .NET
插件。安装完了之后,点击Visual Studio中的项目属性,可以看到如下丰富的选择项:

Contract和Debug.Assert有些地方相似:

  1. 都提供了运行时支持:这些Contracts都是可以被运行的,并且一旦条件不被满足,会弹出类似Assert的一样的对话框报错,如下:
  2. 都可以在随意的在代码中关闭打开。

但是Contract有更多和更强大的功能:

  1. Contracts的意图更加清晰,通过不同的Requires/Ensures等等调用,代表不同类型的条件,比单纯的Assert更容易理解和进行自动分析
  2. Contracts的位置更加统一,将3种不同条件都放在代码的开始处,而非散见在函数的开头和结尾,便于查找和分析。
  3. 不同的开发人员、不同的小组、不同的公司、不同的库可能都会有自己的Assert,这就大大增加了自动分析的难度,也不利于开发人员编写代码。而Contracts直接被.NET 4.0支持,是统一的。
  4. 它提供了静态分析支持,这个我们可以通过配置面板看到,通过静态分析Contracts,静态分析工具可以比较容易掌握函数的各种有关信息,甚至可以作为Intellisense

Contract中包含了三个工具:

  • ccrewrite, 通过向程序集中些如二进制数据,来支持运行时检测
  • cccheck, 运行时检测
  • ccdoc, 将Contract自动生成XML文档

前置条件的处理,如代码所示。

       /// <summary>
        ///实现“前置条件”的代码契约/// </summary>
        /// <param name="text">Input</param>
        /// <returns>Output</returns>
        public static int CountWhiteSpace(stringtext)
{
//命名空间:using System.Diagnostics.Contracts; Contract.Requires<ArgumentNullException>(text != null, "Paramter:text");//使用了泛型形式的Requires return text.Count(char.IsWhiteSpace);
}

后置条件(postcondition):表示对方法输出的约束:返回值、out或ref参数的值,以及任何被改变的状态。Ensures();

        /// <summary>
        ///实现“后置条件”的代码契约/// </summary>
        /// <param name="text">Input</param>
        /// <returns>Output</returns>
        public static int CountWhiteSpace(stringtext)
{
//命名空间:using System.Diagnostics.Contracts; Contract.Requires<ArgumentNullException>(!string.IsNullOrEmpty(text), "text"); //使用了泛型形式的Requires Contract.Ensures(Contract.Result<int>() > 0); //1.方法在return之前,所有的契约都要在真正执行方法之前(Assert和Assume除外,下面会介绍)。//2.实际上Result<int>()仅仅是编译器知道的”占位符“:在使用的时候工具知道它代表了”我们将得到那个返回值“。 return text.Count(char.IsWhiteSpace);
}
public static bool TryParsePreserveValue(string text, ref intvalue)
{
Contract.Ensures(Contract.Result
<bool>() || Contract.OldValue(value) == Contract.ValueAtReturn(out value)); //此结果表达式是无法证明真伪的。 return int.TryParse(text, out value); //所以此处在编译前就会提示错误信息:Code Contract:ensures unproven: XXXXX }

这个代码契约功能比较强大,不过好像对于简单的参数校验,引入这么一个家伙感觉麻烦,也不见开发人员用的有多广泛,而且还需要提前安装一个工具:
Code Contracts for .NET

因此我也不倾向于使用这个插件的东西,因为代码要交付客户使用,要求客户安装一个插件,并且打开相关的代码契约设置,还是比较麻烦,如果没有打开,也不会告诉客户代码编译出错,只是会在运行的时候不校验方法参数。

4、使用内置的公用类库处理

基于CuttingEdge.Conditions 的方式,其实我们也可以做一个类似这样的流畅性写法的校验处理,而且不需要那么麻烦引入第三方类库。

例如我们在公用类库里面增加一个类库,如下代码所示。

    /// <summary>
    ///参数验证帮助类,使用扩展函数实现/// </summary>
    /// <example>
    ///eg:///ArgumentCheck.Begin().NotNull(sourceArray, "需要操作的数组").NotNull(addArray, "被添加的数组");/// </example>
    public static classArgumentCheck
{
#region Methods /// <summary> ///验证初始化/// <para> ///eg:///ArgumentCheck.Begin().NotNull(sourceArray, "需要操作的数组").NotNull(addArray, "被添加的数组");/// </para> /// <para> ///ArgumentCheck.Begin().NotNullOrEmpty(tableName, "表名").NotNullOrEmpty(primaryKey, "主键");</para> /// <para> ///ArgumentCheck.Begin().CheckLessThan(percent, "百分比", 100, true);</para> /// <para> ///ArgumentCheck.Begin().CheckGreaterThan&lt;int&gt;(pageIndex, "页索引", 0, false).CheckGreaterThan&lt;int&gt;(pageSize, "页大小", 0, false);</para> /// <para> ///ArgumentCheck.Begin().NotNullOrEmpty(filepath, "文件路径").IsFilePath(filepath).NotNullOrEmpty(regexString, "正则表达式");</para> /// <para> ///ArgumentCheck.Begin().NotNullOrEmpty(libFilePath, "非托管DLL路径").IsFilePath(libFilePath).CheckFileExists(libFilePath);</para> /// <para> ///ArgumentCheck.Begin().InRange(brightnessValue, 0, 100, "图片亮度值");</para> /// <para> ///ArgumentCheck.Begin().Check&lt;ArgumentNullException&gt;(() => config.HasFile, "config文件不存在。");</para> /// <para> ///ArgumentCheck.Begin().NotNull(serialPort, "串口").Check&lt;ArgumentException&gt;(() => serialPort.IsOpen, "串口尚未打开!").NotNull(data, "串口发送数据");/// </para> /// </summary> /// <returns>Validation对象</returns> public staticValidation Begin()
{
return null;
}
/// <summary> ///需要验证的正则表达式/// </summary> /// <param name="validation">Validation</param> /// <param name="checkFactory">委托</param> /// <param name="argumentName">参数名称</param> /// <returns>Validation对象</returns> public static Validation Check(this Validation validation, Func<bool> checkFactory, stringargumentName)
{
return Check<ArgumentException>(validation, checkFactory, string.Format(Resource.ParameterCheck_Match2, argumentName));
}
/// <summary> ///自定义参数检查/// </summary> /// <typeparam name="TException">泛型</typeparam> /// <param name="validation">Validation</param> /// <param name="checkedFactory">委托</param> /// <param name="message">自定义错误消息</param> /// <returns>Validation对象</returns> public static Validation Check<TException>(this Validation validation, Func<bool> checkedFactory, stringmessage)whereTException : Exception
{
if(checkedFactory())
{
return validation ?? newValidation()
{
IsValid
= true};
}
else{
TException _exception
= (TException)Activator.CreateInstance(typeof(TException), message);throw_exception;
}
}
......

上面提供了一个常规的检查和泛型类型检查的通用方法,我们如果需要对参数检查,如下代码所示。

ArgumentCheck.Begin().NotNull(sourceArray, "需要操作的数组").NotNull(addArray, "被添加的数组");

而这个NotNull就是我们根据上面的定义方法进行扩展的函数,如下代码所示。

        /// <summary>
        ///验证非空/// </summary>
        /// <param name="validation">Validation</param>
        /// <param name="data">输入项</param>
        /// <param name="argumentName">参数名称</param>
        /// <returns>Validation对象</returns>
        public static Validation NotNull(this Validation validation, object data, stringargumentName)
{
return Check<ArgumentNullException>(validation, () => (data != null), string.Format(Resource.ParameterCheck_NotNull, argumentName));
}

同样道理我们可以扩展更多的自定义检查方法,如引入正则表达式的处理。

ArgumentCheck.Begin().NotNullOrEmpty(libFilePath, "非托管DLL路径").IsFilePath(libFilePath).CheckFileExists(libFilePath);

它的扩展函数如下所示。

        /// <summary>
        ///是否是文件路径/// </summary>
        /// <param name="validation">Validation</param>
        /// <param name="data">路径</param>
        /// <returns>Validation对象</returns>
        public static Validation IsFilePath(this Validation validation, stringdata)
{
return Check<ArgumentException>(validation, () => ValidateUtil.IsFilePath(data), string.Format(Resource.ParameterCheck_IsFilePath, data));
}
/// <summary> ///检查指定路径的文件必须存在,否则抛出<see cref="FileNotFoundException"/>异常。/// </summary> /// <param name="validation">Validation</param> /// <param name="filePath">文件路径</param> /// <exception cref="ArgumentNullException">当文件路径为null时</exception> /// <exception cref="FileNotFoundException">当文件路径不存在时</exception> /// <returns>Validation对象</returns> public static Validation CheckFileExists(this Validation validation, stringfilePath)
{
return Check<FileNotFoundException>(validation, () => File.Exists(filePath), string.Format(Resource.ParameterCheck_FileNotExists, filePath));
}

我们可以根据我们的正则表达式校验,封装更多的函数进行快速使用,如果要自定义的校验,那么就使用基础的Chek函数即可。

测试下代码使用,如下所示。

        /// <summary>
        ///应用程序的主入口点。/// </summary>
[STAThread]static void Main(string[] args)
{
ArgumentCheck.Begin().NotNull(args,
"启动参数");string test = null;
ArgumentCheck.Begin().NotNull(test,
"测试参数").NotEqual(test, "abc", "test");

这个ArgumentCheck作为公用类库的一个类,因此使用起来不需要再次引入第三方类库,也能够实现常规的校验处理,以及可以扩展自定义的参数校验,同时也是支持流式的书写方式,非常方便。

在较早博客随笔里面写过文章《
Winform开发框架之简易工作流设计
》之后,很久没有对工作流部分进行详细的介绍了,本篇继续这个主题,详细介绍其中的设计、实现及效果给大家,这个工作流在好几年前就应用在了市行业审批系统上,经过不断的改造适合更广泛的审批流程处理,从最初的Web上扩展到WInform上,并从WInform框架到混合框架上都实现了不错的处理。

1、工作流模块的表设计分析

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

当然实际的流程实例里面需要记录很多信息,其中流程步骤日志、申请单处理日志等信息是必须要记录的,方便我们跟踪相关的处理记录。因此工作流业务表包含多两个日志记录的表,如下所示。

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

当然,为了更好的处理流程的相关信息,还需要记录流程处理人,流程会签人、流程阅办人,以及常用审批意见等相关辅助表,以便对流程的各个处理信息进行合理处理和展示。

下面是具体表单的查看信息,包含了相关的处理步骤信息,以及相关的流程日志信息。

详细表单查看界面如下所示。

流程日志分为几个部分:申请单处理日志、申请单处理历史信息、申请单系统日志等几个部分

2、工作流步骤处理

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

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

WInform开发框架之工作流系列文章:

Winform开发框架之简易工作流设计


Winform开发框架中工作流模块的表设计分析


Winform开发框架中工作流模块的业务表单开发

Winform开发框架中工作流模块之审批会签操作

Winform开发框架中工作流模块之审批会签操作(2)