wenmo8 发布的文章

在前面几篇随笔中,介绍了PostSharp的使用,以及整合MemoryCache,《
在.NET项目中使用PostSharp,实现AOP面向切面编程处理
》、《
在.NET项目中使用PostSharp,使用MemoryCache实现缓存的处理
》参数了对PostSharp的使用,并介绍了MemoryCache的缓存使用,但是缓存框架的世界里面,有很多成熟的缓存框架,如MemoryCache、Redis、Memcached、Couchbase、System.Web.Caching等,这时候我们如果有一个大内总管或者一个吸星大法的武功,把它们融合起来,那么就真的是非常完美的一件事情,这个就是我们CacheManager缓存框架了,这样的灵活性缓存框架并结合了PostSharp横切面对常规代码的简化功能,简直就是好鞍配好马、宝剑赠英雄,整合起来处理缓存真的是如虎添翼。

1、CacheManager缓存框架的回顾

关于这个缓存框架,我在随笔《
.NET缓存框架CacheManager在混合式开发框架中的应用(1)-CacheManager的介绍和使用
》中进行了介绍,读者可以从中了解一下CacheManager缓存框架究竟是一个什么样的东西。

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

CacheManager缓存框架支持Winform和Web等应用开发,以及支持多种流行的缓存实现,如MemoryCache、Redis、Memcached、Couchbase、System.Web.Caching等。

纵观整个缓存框架,它的特定很明显,在支持多种缓存实现外,本身主要是以内存缓存(进程内)为主,其他分布式缓存为辅的多层缓存架构方式,以达到快速命中和处理的机制,它们内部有相关的消息处理,使得即使是分布式缓存,也能够及时实现并发同步的缓存处理。

CacheManager缓存框架在配置方面,支持代码方式的配置、XML配置,以及JSON格式的配置处理,非常方便。

CacheManager缓存框架默认对缓存数据的序列化是采用二进制方式,同时也支持多种自定义序列化的方式,如基于JOSN.NET的JSON序列化或者自定义序列化方式。

CacheManager缓存框架可以对缓存记录的增加、删除、更新等相关事件进行记录。

CacheManager缓存框架的缓存数据是强类型的,可以支持各种常规类型的处理,如Int、String、List类型等各种基础类型,以及可序列号的各种对象及列表对象。

CacheManager缓存框架支持多层的缓存实现,内部良好的机制可以高效、及时的同步好各层缓存的数据。

CacheManager缓存框架支持对各种操作的日志记录。

CacheManager缓存框架在分布式缓存实现中支持对更新的锁定和事务处理,让缓存保持更好的同步处理,内部机制实现版本冲突处理。

CacheManager缓存框架支持两种缓存过期的处理,如绝对时间的过期处理,以及固定时段的过期处理,是我们处理缓存过期更加方便。

....

很多特性基本上覆盖了缓存的常规特性,而且提供的接口基本上也是我们所经常用的Add、Put、Update、Remove等接口,使用起来也非常方便。

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

一般来说,对于单机版本的应用场景,基本上是无需引入这种缓存框架的,因为客户端的并发量很少,而且数据请求也是寥寥可数的,性能方便不会有任何问题。

如果对于分布式的应用系统,如我在很多随笔中介绍到我的《混合式开发框架》、《Web开发框架》,由于数据请求是并发量随着用户增长而增长的,特别对于一些互联网的应用系统,极端情况下某个时间点一下可能就会达到了整个应用并发的峰值。那么这种分布式的系统架构,引入数据缓存来降低IO的并发数,把耗时请求转换为内存的高速请求,可以极大程度的降低系统宕机的风险。

我们以基于常规的Web API层来构建应用框架为例,整个数据缓存层,应该是在Web API层之下、业务实现层之上的一个层,如下所示。

2、整合PostSharp和CacheManager实现多种缓存框架的处理

由于MemoryCache是在单个机器上进行缓存的处理,而且无法进行序列号,电脑宕机后就会全部丢掉缓存内容,由于这个缺点,我们对《
在.NET项目中使用PostSharp,使用MemoryCache实现缓存的处理
》基础上进行进一步的调整,整合CacheManager进行使,从而可以利用缓存弹性化处理以及可序列号的特点。

我们在正常情况下,还是需要使用Redis这个强大的分布式缓存的,关于Redis的安装和使用,请参考我的随笔《
基于C#的MongoDB数据库开发应用(4)--Redis的安装及使用
》。

我们首先定义一个CacheAttribute的Aspect类,用来对缓存的切面处理。

    /// <summary>
    ///方法实现缓存的标识/// </summary>
[Serializable]public classCacheAttribute : MethodInterceptionAspect
{
/// <summary> ///缓存的失效时间设置,默认采用30分钟/// </summary> public int ExpirationPeriod = 30;/// <summary> ///PostSharp的调用处理,实现数据的缓存处理/// </summary> public override voidOnInvoke(MethodInterceptionArgs args)
{
//默认30分钟失效,如果设置过期时间,那么采用设置值 TimeSpan timeSpan = new TimeSpan(0, 0, ExpirationPeriod, 0);var cache =MethodResultCache.GetCache(args.Method, timeSpan);var arguments = args.Arguments.ToList();//args.Arguments.Union(new[] {WindowsIdentity.GetCurrent().Name}).ToList(); var result =cache.GetCachedResult(arguments);if (result != null)
{
args.ReturnValue
=result;return;
}
else{base.OnInvoke(args);//调用后更新缓存 cache.CacheCallResult(args.ReturnValue, arguments);
}
}
}

然后就是进一步处理完善类 MethodResultCache来对缓存数据进行处理了。该类负责构造一个CacheManager管理类来对缓存进行处理,如下代码所示。

初始化缓存管理器的代码如下所示,这里利用了MemoryCache作为快速的内存缓存(主缓存),以及Redis作为序列化存储的缓存容器(从缓存),它们有内在机制进行同步处理。

        /// <summary>
        ///初始化缓存管理器/// </summary>
        private voidInitCacheManager()
{
_cache
= 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)
;
});
}

对缓存结果进行处理的函数如下所示。

        /// <summary>
        ///缓存结果内容/// </summary>
        /// <param name="result">待加入缓存的结果</param>
        /// <param name="arguments">方法的参数集合</param>
        public void CacheCallResult(object result, IEnumerable<object>arguments)
{
var key =GetCacheKey(arguments);
_cache.Remove(key);
var item = new CacheItem<object>(key, result, ExpirationMode.Sliding, _expirationPeriod);
_cache.Add(item);
}

首先就是获取方法参数的键,然后移除对应的缓存,加入新的缓存,并设定缓存的失效时间段即可。

清空缓存的时候,直接调用管理类的Clear方法即可达到目的。

        /// <summary>
        ///清空方法的缓存/// </summary>
        public voidClearCachedResults()
{
_cache.Clear();
}

这样,我们处理好后,在一个业务调用类里面进行设置缓存标志即可,如下代码所示。

        /// <summary>
        ///获取用户全部简单对象信息,并放到缓存里面/// </summary>
        /// <returns></returns>
        [Cache(ExpirationPeriod = 1)]public static List<SimpleUserInfo> GetSimpleUsers(intuserid)
{
Thread.Sleep(
500);//return CallerFactory<IUserService>.Instance.GetSimpleUsers();//模拟从数据库获取数据 List<SimpleUserInfo> list = new List<SimpleUserInfo>();for (int i = 0; i < 10; i++)
{
var info = newSimpleUserInfo();
info.ID
=i;
info.Name
= string.Concat("Name:", i);
info.FullName
= string.Concat("姓名:", i);
list.Add(info);
}
returnlist;
}

为了测试缓存的处理,以及对Redis的支持情况,我编写了一个简单的案例,功能如下所示。

测试代码如下所示。

        //测试缓存
        private void button1_Click(objectsender, EventArgs e)
{
Console.WriteLine(
"测试缓存:");//测试反复调用获取数值的耗时 DateTime start =DateTime.Now;var list = CacheService.GetSimpleUsers(1);int end = (int)DateTime.Now.Subtract(start).TotalMilliseconds;

Console.WriteLine(
"first:" +end);
Console.WriteLine(
"List:" +list.Count);//Second test//检查不同的方法参数,对缓存值的影响 start =DateTime.Now;
list
= CacheService.GetSimpleUsers(2);
end
= (int)DateTime.Now.Subtract(start).TotalMilliseconds;
Console.WriteLine(
"Second:" +end);
Console.WriteLine(
"List2:" +list.Count);
}
//更新缓存 private void button2_Click(objectsender, EventArgs e)
{
Console.WriteLine(
"更新缓存:");//首先获取对应键的缓存值//然后对缓存进行修改//最后重新加入缓存 var key = "CacheManagerAndPostSharp.CacheService.GetSimpleUsers";var item =MethodResultCache.GetCache(key);var argument = new List<object>(){1};var result =item.GetCachedResult(argument);
Console.WriteLine(
"OldResult:" +result.ToJson());

List
<SimpleUserInfo> newList = result as List<SimpleUserInfo>;if(newList != null)
{
newList.Add(
new SimpleUserInfo() { ID = new Random().Next(), Name = RandomChinese.GetRandomChars(2) });
}
item.CacheCallResult(newList, argument);
}
//清空缓存 private void button3_Click(objectsender, EventArgs e)
{
Console.WriteLine(
"清空缓存:");//首先获取对应键的缓存值 var key = "CacheManagerAndPostSharp.CacheService.GetSimpleUsers";var item =MethodResultCache.GetCache(key);var argument = new List<object>(){1};//然后清空方法的所有缓存 item.ClearCachedResults();//最后重新检验缓存值为空 var result =item.GetCachedResult(argument);
Console.WriteLine(
"Result:" + result !=null ? result.ToJson() : "null");
}

测试运行结果如下所示。

测试缓存: 
first:
870List:10Second:502List2:10更新缓存:
OldResult:[
{
"ID":0,"HandNo":null,"Name":"Name:0","Password":null,"FullName":"姓名:0","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":1,"HandNo":null,"Name":"Name:1","Password":null,"FullName":"姓名:1","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":2,"HandNo":null,"Name":"Name:2","Password":null,"FullName":"姓名:2","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":3,"HandNo":null,"Name":"Name:3","Password":null,"FullName":"姓名:3","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":4,"HandNo":null,"Name":"Name:4","Password":null,"FullName":"姓名:4","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":5,"HandNo":null,"Name":"Name:5","Password":null,"FullName":"姓名:5","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":6,"HandNo":null,"Name":"Name:6","Password":null,"FullName":"姓名:6","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":7,"HandNo":null,"Name":"Name:7","Password":null,"FullName":"姓名:7","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":8,"HandNo":null,"Name":"Name:8","Password":null,"FullName":"姓名:8","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null},
{"ID":9,"HandNo":null,"Name":"Name:9","Password":null,"FullName":"姓名:9","MobilePhone":null,"Email":null,"CurrentLoginUserId":null,"Data1":null,"Data2":null,"Data3":null}]

测试缓存:
first:
0List:11Second:0List2:10清空缓存:null

同时我们看到在Redis里面,有相关的记录如下所示。

结合PostSharp和CacheManager,使得我们在使用缓存方面更具有弹性化,可以根据情况通过配置实现使用不同的缓存处理,但是在代码中使用缓存就是只需要声明一下即可,非常方便简洁了。

在前面的记录保存和显示里面,都是采用一个视图的方式进行数据的展示和录入的,如果对于主从表的记录同时录入,那么就需要主从表两个GridView来进行展示和数据录入的了,对于主从表录入相对复杂一些,具体如何操作呢?

这里我们依旧采用分页控件来进行数据的分页及直接录入数据操作,而且增加了主从表数据同时在一个GridControl界面上进行处理。

这样主表记录为字典类型,从表为字典明细项目,得到的数据展示界面效果如下所示。

当然我们可直接在底部进行数据的录入,包括主表记录和从表的明细记录,都可以一气呵成的录入并进行保存处理的,界面效果如下所示。

GridView的主从关系需要设置好集合的映射关系,我们需要通过设置GridLevelNode集合实现主从表关系的处理的。

初始化从表的GridView2和主从表关系的代码如下所示

通过上面的初始化代码,指定了主从表的关系后,我们还需要对绑定的数据源进行一定的处理,才能够在GridControl控件上显示主从表关系的记录。

首先需要定义一个业务对象,用来存储主从关系的记录对象。

然后在BindData绑定数据的时候,代码处理如下即可。

这样就可以得到开始介绍的主从表界面效果了。

数据保存的代码和前面的操作类似,我们需要分别对GridView1和GridView2的数据保存操作进行处理,如下代码所示。

GridView2的字典项目明细保存操作如下所示。

主从表的记录删除这里需要顺带介绍一下,由于主从表公用一个右键菜单的删除操作。

那么处理的时候,我们需要判断是操作从表还是主表记录,对它们要分开处理,然后提示是否操作成功,如果成功,我们可以移除这行即可,避免重新更新数据导致的焦点丢失。

在本篇介绍的Winform界面样式改变及存储操作中,是指基于DevExpress进行界面样式的变化。一般情况下,默认我们会为客户提供多种DevExpress的界面皮肤以供个人喜好选择,默认DevExpress提供40余种皮肤样式,用户可以根据自己的喜好,选择较为美观、得体的皮肤,为了方便,我们对用户的皮肤选择进行记录,并可以动态改变。

1、界面皮肤的选择

Winform开发框架(包括混合式Winform开发框架)皮肤如下界面所示。

在皮肤集合中打开,可以看到很多界面皮肤可供选择

上面初始化的皮肤代码,是通过调用SkinHelper的DevExpress内在辅助类实现的,通过添加一个RibbonGalleryBarItem控件对象,然后添加如下代码实现的

DevExpress.XtraBars.Helpers.SkinHelper.InitSkinGallery(rgbiSkins, true);

以及在Ribbon控件上添加一个小的皮肤下拉按钮,如下代码所示

this.ribbonControl.Toolbar.ItemLinks.Clear();this.ribbonControl.Toolbar.ItemLinks.Add(rgbiSkins);

如果我们固定设置界面一种皮肤,那么调用下面的代码直接设置即可,如下所示。

UserLookAndFeel.Default.SetSkinStyle("Office 2010 Blue");

2、界面皮肤的保存和显示

不过,我们如果希望皮肤可以随意更改并可以存储到配置文件中,那么我们就需要对它进行一定的加工处理。

首先我们编写一个设置皮肤名称的函数,如下所示。

        /// <summary>
        ///设置用户界面的皮肤/// </summary>
        private voidSetSkinStyle()
{
//从配置文件读取SkinName的值,如无则使用标准样式 string skinName = config.AppConfigGet("SkinName");
skinName
= string.IsNullOrEmpty(skinName) ? "Office 2010 Blue": skinName;foreach (GalleryItemGroup group inrgbiSkins.Gallery.Groups)
{
foreach (GalleryItem item ingroup.Items)
{
if (string.Concat(item.Tag) ==skinName)
{
item.Checked
= true;
rgbiSkins.Gallery.MakeVisible(item);
UserLookAndFeel.Default.SetSkinStyle(skinName);
return;
}
item.Checked
= false;
}
}
}

这样我们在皮肤改变后,重新读取配置,并重新设置界面的皮肤即可。

那么我们需要实现RibbonGalleryBarItem控件对象的单击事件GalleryItemClick,如下代码所示。

            rgbiSkins.GalleryItemClick += (object sender, GalleryItemClickEventArgs e) =>{
config.AppConfigSet(
"SkinName", string.Concat(e.Item.Tag));
SetSkinStyle();
};

这样综合起来就是两个函数代码实现了,如下所示

        /// <summary>
        ///初始化皮肤设置/// </summary>
        private voidInitSkinStyle()
{
//初始化皮肤样式名称 DevExpress.XtraBars.Helpers.SkinHelper.InitSkinGallery(rgbiSkins, true);
rgbiSkins.GalleryItemClick
+= (object sender, GalleryItemClickEventArgs e) =>{
config.AppConfigSet(
"SkinName", string.Concat(e.Item.Tag));
SetSkinStyle();
};
this.ribbonControl.Toolbar.ItemLinks.Clear();this.ribbonControl.Toolbar.ItemLinks.Add(rgbiSkins);this.ribbonControl.Minimized = true;//默认最小化//UserLookAndFeel.Default.SetSkinStyle("Office 2010 Blue"); SetSkinStyle();
}
/// <summary> ///设置用户界面的皮肤/// </summary> private voidSetSkinStyle()
{
//从配置文件读取SkinName的值,如无则使用标准样式 string skinName = config.AppConfigGet("SkinName");
skinName
= string.IsNullOrEmpty(skinName) ? "Office 2010 Blue": skinName;foreach (GalleryItemGroup group inrgbiSkins.Gallery.Groups)
{
foreach (GalleryItem item ingroup.Items)
{
if (string.Concat(item.Tag) ==skinName)
{
item.Checked
= true;
rgbiSkins.Gallery.MakeVisible(item);
UserLookAndFeel.Default.SetSkinStyle(skinName);
return;
}
item.Checked
= false;
}
}
}

然后在主界面的初始化代码中进行调用即可,同时记得在App.Config文件中增加一个SkinName的配置项,方便程序进行存储皮肤名称。

这样就可以实现我们皮肤的变化及存储功能了,修改皮肤,并重启程序,系统依旧使用保存好的皮肤,功能符合预期。

这个功能不算复杂,提供代码方便大家进行复制使用。

在有些系统应用里面,我们需要对应用服务器、数据库服务器、文件服务器进行分开,文件路径等信息存储在数据库服务器里面,但文件内容则存储在文件服务器里面,通过使用FTP进行文件的上传下载,从而实现更加高效的IO输出,也分担了服务器的压力。本篇随笔介绍使用公用类库里面的FTPHelper辅助类,实现文件的上传下载等基础操作。

1、FTPHelper辅助类的接口说明

首先我们为了方便辅助类的使用,使用了构造函数进行对象的参数初始化,如下所示。

我们整个FTPHelper辅助类,是使用FTP命令和FTP服务器进行交换,因此需要如果要获取文件或目录信息,那么就需要对这些信息进行转换为合适格式的内容,这里我们定义一个结构体用来承载这些信息。

这样我们可以顺利把FTP服务器的字符串返回(DOS格式或者Unix格式字符串)它们转换为对应的结构体,方便使用了。

整个辅助函数,提供了一些基础的函数来操作FTP文件或者目录,如下所示

        /// <summary>
        ///仅获取文件列表/// </summary>
        /// <returns>获取的文件列表</returns>
        public List<FileStruct>ListFiles()/// <summary>
        ///仅获取目录列表/// </summary>
        /// <returns>获取到的目录列表</returns>
        public List<FileStruct> ListDirectories()    

以及

        /// <summary>
        ///关闭FTP服务器的所有连接/// </summary>
        public voidDisconnect()/// <summary>
        ///连接到FTP服务器/// </summary>
        /// <param name="server">FTP服务器的IP或者主机名</param>
        /// <param name="port">FTP服务器端口</param>
        /// <param name="user">登录用户名</param>
        /// <param name="pass">登录密码</param>
        public void Connect(string server, int port, string user, stringpass)/// <summary>
        ///获取FTP的当前工作目录/// </summary>
        public stringGetWorkingDirectory()/// <summary>
        ///在FTP服务器上改变目录/// </summary>
        public void ChangeDir(stringpath)/// <summary>
        ///在FTP服务器上创建新的目录/// </summary>
        public void MakeDir(stringdir)/// <summary>
        ///在FTP服务器上移除目录/// </summary>
        public void RemoveDir(stringdir)/// <summary>
        ///在FTP上移除一个文件/// </summary>
        public void RemoveFile(stringfilename)/// <summary>
        ///在FTP服务器上重命名一个文件/// </summary>
        /// <param name="oldfilename">旧文件名</param>
        /// <param name="newfilename">新文件名</param>
        public void RenameFile(string oldfilename, stringnewfilename)/// <summary>
        ///打开一个存在的文件(支持续传)上传/// </summary>
        /// <param name="filename">本地上传的文件(包含文件路径)</param>
        /// <param name="remote_filename">存储在FTP上的文件名称</param>
        /// <param name="resume">如果存在,指定是否续传</param>
        public void OpenUpload(string filename, string remote_filename, boolresume)/// <summary>
        ///打开下载一个文件/// </summary>
        /// <param name="remote_filename">FTP上的远端文件名</param>
        /// <param name="local_filename">另存为的文件名称(包含文件路径)</param>
        /// <param name="resume">如果存在,指定是否续传</param>
        public void OpenDownload(string remote_filename, string local_filename, boolresume)/// <summary>
        ///上传文件,循环操作直到文件全部上传完毕/// </summary>
        /// <returns>发送的字节数组大小</returns>
        public longDoUpload()/// <summary>
        ///下载文件,循环操作直到文件全部下载完毕/// </summary>
        /// <returns>收到的字节数组大小</returns>
        public longDoDownload()

2、FTPHelper辅助类的使用

基于上述的接口,我们编写了一个使用Demo,主要用来介绍这个辅助类的各种接口使用。

获取到的测试结果输出如下所示

虽然我们在附件管理里面采用了更为通用的《附件管理模块》,统一上传文件到服务器上进行管理,并提供下载管理等内容,连接如下:


Winform开发框架之附件管理应用
》。其他公用类库的使用说明,具体可以参考文章《
公用类库使用帮助

不过我们开发的时候,为了方便,也可以使用 FTPHelper辅助类进行简单的文件上传下载操作。

本篇随笔介绍在Web微信应用中使用博客园RSS以及Quartz.NET实现博客文章内容的定期推送功能,首先对Quartz.NET进行一个简单的介绍和代码分析,掌握对作业调度的处理,然后对博客园RSS内容的处理如何获取,并结合微信消息的群发接口进行内容的发送,从而构建了一个在Web应用中利用作业调度来进行消息发送的业务模型。

Quartz.NET是一个开源的作业调度框架,非常适合在平时的工作中,定时轮询数据库同步,定时邮件通知,定时处理数据等。 Quartz.NET允许开发人员根据时间间隔(或天)来调度作业。它实现了作业和触发器的多对多关系,还能把多个作业与不同的触发器关联。整合了 Quartz.NET的应用程序可以重用来自不同事件的作业,还可以为一个事件组合多个作业。

1、Quartz.NET的使用

Quartz框架的一些基础概念解释:

Scheduler     作业调度器。

IJob             作业接口,继承并实现Execute, 编写执行的具体作业逻辑。

JobBuilder       根据设置,生成一个详细作业信息(JobDetail)。

TriggerBuilder   根据规则,生产对应的Trigger

官方的使用案例代码如下所示

        private void button1_Click(objectsender, EventArgs e)
{
try{
Common.Logging.LogManager.Adapter
= new Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter { Level =Common.Logging.LogLevel.Info };//Grab the Scheduler instance from the Factory IScheduler scheduler =StdSchedulerFactory.GetDefaultScheduler();//and start it off scheduler.Start();//define the job and tie it to our HelloJob class IJobDetail job = JobBuilder.Create<HelloJob>()
.WithIdentity(
"job1", "group1")
.Build();
//Trigger the job to run now, and then repeat every 10 seconds ITrigger trigger =TriggerBuilder.Create()
.WithIdentity(
"trigger1", "group1")
.StartNow()
.WithSimpleSchedule(x
=>x
.WithIntervalInSeconds(
10)
.RepeatForever())
.Build();
//Tell quartz to schedule the job using our trigger scheduler.ScheduleJob(job, trigger);//some sleep to show what's happening Thread.Sleep(TimeSpan.FromSeconds(60));//and last shut down the scheduler when you are ready to close your program scheduler.Shutdown();
}
catch(SchedulerException se)
{
Console.WriteLine(se);
}

Console.WriteLine(
"Finished");
}

启动定义一个HelloJOb的对象,如下代码所示

    public classHelloJob : IJob
{
public voidExecute(IJobExecutionContext context)
{
Console.WriteLine(
"Greetings from HelloJob!");
}
}

2、Quartz的cron表达式

cron expressions 整体上还是非常容易理解的,只有一点需要注意:"?"号的用法,看下文可以知道“?”可以用在 day of month 和 day of week中,他主要是为了解决如下场景,如:每月的1号的每小时的31分钟,正确的表达式是:* 31 * 1 * ?,而不能是:* 31 * 1 * *,因为这样代表每周的任意一天。


由7段构成:秒 分 时 日 月 星期 年(可选)
"-" :表示范围  MON-WED表示星期一到星期三
"," :表示列举 MON,WEB表示星期一和星期三
"*" :表是“每”,每月,每天,每周,每年等
"/" :表示增量:0/15(处于分钟段里面) 每15分钟,在0分以后开始,3/20 每20分钟,从3分钟以后开始
"?" :只能出现在日,星期段里面,表示不指定具体的值
"L" :只能出现在日,星期段里面,是Last的缩写,一个月的最后一天,一个星期的最后一天(星期六)
"W" :表示工作日,距离给定值最近的工作日
"#" :表示一个月的第几个星期几,例如:"6#3"表示每个月的第三个星期五(1=SUN...6=FRI,7=SAT)

官方cron表达式实例

表达式 代表意义
0 0 12 * * ? 每天中午12点触发
0 15 10 ? * * 每天上午10:15触发
0 15 10 * * ? 每天上午10:15触发
0 15 10 * * ? * 每天上午10:15触发
0 15 10 * * ? 2005 2005年的每天上午10:15触发
0 * 14 * * ? 在每天下午2点到下午2:59期间的每1分钟触发
0 0/5 14 * * ? 在每天下午2点到下午2:55期间的每5分钟触发
0 0/5 14,18 * * ? 在每天下午2点到2:55期间和下午6点到6:55期间的每5分钟触发
0 0-5 14 * * ? 在每天下午2点到下午2:05期间的每1分钟触发
0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44触发
0 15 10 ? * MON-FRI 周一至周五的上午10:15触发
0 15 10 15 * ? 每月15日上午10:15触发
0 15 10 L * ? 每月最后一日的上午10:15触发
0 15 10 L-2 * ? Fire at 10:15am on the 2nd-to-last last day of every month
0 15 10 ? * 6L 每月的最后一个星期五上午10:15触发
0 15 10 ? * 6L Fire at 10:15am on the last Friday of every month
0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一个星期五上午10:15触发
0 15 10 ? * 6#3 每月的第三个星期五上午10:15触发
0 0 12 1/5 * ? Fire at 12pm (noon) every 5 days every month, starting on the first day of the month.
0 11 11 11 11 ? Fire every November 11th at 11:11am.

3、Quartz.NET的应用案例

我曾经在统一接口的Web API后台,使用了这个Quartz.NET来实现站场信息的同步处理,这样可以把其他供应商提供的接口数据,同步到本地,可以加快数据的检索和处理效率。

具体代码如下所示。

首先是在Global.asax的后台代码里面进行同步代码处理。

    public classWebApiApplication : System.Web.HttpApplication
{
IScheduler scheduler
= null;protected voidApplication_Start()
{
GlobalConfiguration.Configuration.EnableCors();
GlobalConfiguration.Configure(WebApiConfig.Register);
//创建执行同步的处理 ISchedulerFactory sf = newStdSchedulerFactory();
scheduler
=sf.GetScheduler();

CalendarTask();
CreateOnceJob();
//启动所有的任务 scheduler.Start();
}
protected void Application_End(objectsender, EventArgs e)
{
if(scheduler != null)
{
scheduler.Shutdown(
true);
}
}
/// <summary> ///创建同步任务/// </summary> private voidCalendarTask()
{
IJobDetail job
= JobBuilder.Create<StationSyncJob>()
.WithIdentity(
"StationSyncJob", "group1")
.Build();
//每天凌晨1点执行一次:0 0 1 * * ? ICronTrigger trigger =(ICronTrigger)TriggerBuilder.Create()
.WithIdentity(
"trigger1", "group1") //"0 34,36,38,40 * * * ?" .WithCronSchedule("0 0 1 * * ?")//"0 0 1 * * ?" .Build();

DateTimeOffset ft
=scheduler.ScheduleJob(job, trigger);
LogTextHelper.Info(
string.Format("您在 {0} 时候创建了Quartz任务", DateTime.Now));
}
private voidCreateOnceJob()
{
IJobDetail onceJob
= JobBuilder.Create<StationSyncJob>()
.WithIdentity(
"onceJob", "group1")
.Build();
//启动的时候运行一次 DateTimeOffset startTime = DateBuilder.NextGivenSecondDate(null, 30);
ISimpleTrigger simpleTrigger
=(ISimpleTrigger)TriggerBuilder.Create()
.WithIdentity(
"simpleOnce", "group1")
.StartAt(startTime)
.Build();
DateTimeOffset ft
=scheduler.ScheduleJob(onceJob, simpleTrigger);
}

}

其中同步站场信息的Job实现如下所示(这里是通过调用第三方接口获取数据,然后把它们保存到本地,这个定时服务设定在每天的一个是时间点上执行,如凌晨1点时刻)。

    /// <summary>
    ///同步站场信息/// </summary>
    public classStationSyncJob : IJob
{
public voidExecute(IJobExecutionContext context)
{
LogTextHelper.Info(
string.Format("您在 {0} 时候调用【同步站场信息】一次", DateTime.Now));

StationDetailResult result
= newStationDetailResult();try{
QueryStationJson json
= new QueryStationJson();//空查询,一次性查询所有 BaseDataAgent agent= newBaseDataAgent();
result
=agent.QueryStationDetail(json);if(result != null &&result.success)
{
foreach(StationDetailJson detail inresult.data)
{
StationInfo info
=detail.ConvertInfo();try{
BLLFactory
<Station>.Instance.InsertIfNew(info);
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
LogTextHelper.Info(info.ToJson());
}
}
}
}
catch(Exception ex)
{
result.errmsg
=ex.Message;
result.success
= false;

LogTextHelper.Error(ex);
}
}
}

4、博客的RSS

原则上我们可以利用任何RSS来源来获取响应的博客内容,这里我以自己博客园的RSS源进行介绍使用,我们每个博客园的账号都有一个如下的连接,提供我们最新的博客列表信息。

打开连接,可以看到它的内容就是最新显示的博客内容,如下所示

处理RSS的内容,我们使用内置的SyndicationFeed对象来处理即可,非常方便。

            string url = "http://feed.cnblogs.com/blog/u/12391/rss";
XmlReader reader
=XmlReader.Create(url);
SyndicationFeed feed
=SyndicationFeed.Load(reader);
reader.Close();

上面代码就是获取到对应的RSS内容,然后把它们转换为XMLReader进行解析即可。

然后可以通过一个遍历的处理就可以获取到其中各个的XML节点内容了,非常方便。

            foreach (SyndicationItem item infeed.Items)
{
var id =item.Id;string subject =item.Title.Text;string summary =item.Summary.Text;
}

5、在微信应用中发送博客内容

通过上面的RSS读取操作,我们可以获得对应的博客内容,如果我们需要每周给客户发送一些内容,那么这些就可以通过上面RSS源进行处理发送了。

关于发送文本消息的处理,可以参考我的随笔文章《
C#开发微信门户及应用(3)--文本消息和图文消息的应答

这里我就直接应用上面的接口对内容进行处理发送,具体接口的逻辑就不再罗列。

        /// <summary>
        ///获取博客园文章(RSS)并发送文本给指定的用户/// </summary>
        private voidGetCnblogsArticles()
{
string url = "http://feed.cnblogs.com/blog/u/12391/rss";
XmlReader reader
=XmlReader.Create(url);
SyndicationFeed feed
=SyndicationFeed.Load(reader);
reader.Close();

ICustomerApi api
= newCustomerApi();foreach (SyndicationItem item infeed.Items)
{
Console.WriteLine(item.ToJson());
var id =item.Id;string subject =item.Title.Text;string summary =item.Summary.Text;var content = string.Format("<a href='{0}'>{1}</a>", id, subject);
CommonResult result
=api.SendText(token, openId, content);
Console.WriteLine(
"发送内容:" + (result.Success ? "成功" : "失败:" +result.ErrorMessage));
}
}

得到的界面效果如下所示。

但是这样的效果还是有点差强人意,我们知道微信里面有图文消息的接口,可以利用图文消息的接口进行发送,则更加美观一些。

调整后的代码如下所示。

        /// <summary>
        ///发送博客图文消息给指定用户/// </summary>
        private voidSendBlogsNews()
{
List
<ArticleEntity> list = new List<ArticleEntity>();string url = "http://feed.cnblogs.com/blog/u/12391/rss";
XmlReader reader
=XmlReader.Create(url);
SyndicationFeed feed
=SyndicationFeed.Load(reader);
reader.Close();
int i = 0;foreach (SyndicationItem item infeed.Items)
{
list.Add(
newArticleEntity
{
Title
=item.Title.Text,
Description
=item.Summary.Text,
PicUrl
= i == 0 ? "http://www.iqidi.com/Content/Images/cnblogs_whc.png" : "http://www.iqidi.com/Content/Images/frame_web.png",
Url
=item.Id
});
if(i >= 8)
{
break;
}
i
++;
}

ICustomerApi customerApi
= newCustomerApi();var result =customerApi.SendNews(token, openId, list);
}

这样就是发送图文消息的代码,需要重新构建一个实体类集合进行发送,得到发送的效果如下所示。

整体的界面效果就是我们需要的效果了,不过如果我们需要使用批量发送给订阅用户的话,那么我们需要使用消息的群发接口,群发的消息接口封装如需了解,可以参考文章《
C#开发微信门户及应用(30)--消息的群发处理和预览功能
》。

整个群发消息的逻辑代码如下所示,主要逻辑就是获取博客文章,并上传文章的图片,接着上传需要群发的图文消息资源,最后调用群发接口进行消息的发送即可。

        private voidBatchSendBlogNews()
{
List
<NewsUploadJson> list = new List<NewsUploadJson>();string url = "http://feed.cnblogs.com/blog/u/12391/rss";
XmlReader reader
=XmlReader.Create(url);
SyndicationFeed feed
=SyndicationFeed.Load(reader);
reader.Close();
//上传图片获取MediaId IMediaApi mediaApi = newMediaApi();var result1 = mediaApi.UploadTempMedia(token, UploadMediaFileType.image, @"E:\我的网站资料\iqidiSoftware\content\images\cnblogs_whc.png");//"http://www.iqidi.com/Content/Images/cnblogs_whc.png"); var result2 = mediaApi.UploadTempMedia(token, UploadMediaFileType.image, @"E:\我的网站资料\iqidiSoftware\content\images\frame_web.png");//"http://www.iqidi.com/Content/Images/frame_web.png"); if (result1 != null && result2 != null)
{
int i = 0;foreach (SyndicationItem item infeed.Items)
{
list.Add(
newNewsUploadJson
{
author
= "伍华聪",
title
=item.Title.Text,
content
=item.Summary.Text,//digest = item.Summary.Text, thumb_media_id = i == 0 ?result1.media_id : result2.media_id,
content_source_url
=item.Id,
});
if (i >= 8)
{
break;
}
i
++;
}
}
if (list.Count > 0)
{
UploadJsonResult resultNews
=mediaApi.UploadNews(token, list);if (resultNews != null)
{
IMassSendApi massApi
= newMassSendApi();var result = massApi.SendByGroup(token, MassMessageType.mpnews, resultNews.media_id, "0", true);
}
else{
Console.WriteLine(
"上传图文消息失败");
}
}
}

群发的消息在微信上看到内容和前面的差不多,不过点击并不会直接跳转链接,而是进去到一个详细内容的页面里面,只有单击阅读原文才进行跳转URL,如下所示。

6.结合Quartz.NET实现博客文章内容的定期推送功能

在Web微信应用中使用博客RSS以及Quartz.NET实现文章内容的定期推送功能,我们需要结合Quartz.NET的作业调度处理、微信接口的内容发送,以及博文RSS内容的获取处理,三者整合进行实现整个功能。

首先我们根据上面的代码,设计好调度的Job内容,如下所示。

然后,在Web应用的Global.asa的后台代码里面,编写代码启动作业调度即可。

而根据前面Corn表达式的说明,我们要每周定时发送一次的的规则,如下所示。

每周星期天凌晨1点实行一次: 0 0 1 ? * L

这样我们最终的Globa.asa后台代码如下所示。

    public classGlobal : HttpApplication
{
private IScheduler scheduler = null;void Application_Start(objectsender, EventArgs e)
{
//在应用程序启动时运行的代码 AreaRegistration.RegisterAllAreas();
RouteConfig.RegisterRoutes(RouteTable.Routes);

BundleConfig.RegisterBundles(BundleTable.Bundles);
//构造调度对象,并创建对应的调度任务 scheduler = StdSchedulerFactory.GetDefaultScheduler();
CalendarTask();
//启动所有的任务 scheduler.Start();
}
protected void Application_End(objectsender, EventArgs e)
{
if (scheduler != null)
{
scheduler.Shutdown(
true);
}
}
private voidCalendarTask()
{
IJobDetail job
= JobBuilder.Create<BlogArticleSendJob>()
.WithIdentity(
"BlogArticleSendJob", "group1")
.Build();
//每周星期天凌晨1点实行一次:0 0 1 ? * L ICronTrigger trigger =(ICronTrigger)TriggerBuilder.Create()
.WithIdentity(
"trigger1", "group1")
.WithCronSchedule(
"0 0 1 ? * L")//0 0 1 ? * L .Build();

DateTimeOffset ft
=scheduler.ScheduleJob(job, trigger);
LogTextHelper.Info(
string.Format("您在 {0} 时候创建了Quartz任务", DateTime.Now));
}

综合上面的思路,我们可以利用Quartz.NET做成更多的数据同步任务调度,另外在微信应用中,我们也可以整合很多组件或者控件,来实现更加弹性化的业务支持,如消息群发、访客汇总,内容同步等处理。

以上就是我的一些组件代码的应用思路,其实我们只要涉猎更广一些,很多东西可以使用拿来主义,经过自己的整合优化,可以为我们服务的更好。