wenmo8 发布的文章

在我之前介绍的混合式开发框架中,其界面是基于Winform的实现方式,后台使用Web API、WCF服务以及直接连接数据库的几种方式混合式接入,在Web项目中我们也可以采用这种方式实现混合式的接入方式,虽然Web API或者WCF方式的调用,相对直接连接数据库方式,响应效率上略差一些,不过扩展性强,也可以调动更多的设备接入,包括移动应用接入,网站接入,Winfrom客户端接入,这样可以使得服务逻辑相对独立,负责提供接口即可。这种方式中最有代表性的就是当前Web API的广泛应用,促进了各个接入端的快速开发和独立维护,极大提高了并行开发的速度和效率。在企业中,我们可以合理规范好各种业务服务的Web API接口,各个应用接入端可以独立开发,也可以交给外包团队进行开发即可。

1、Winform混合式接入方式回顾

从一开始,我们的Web API 的设计目的就是为了给各种不同的应用进行接入的,例如需要接入Winform客户端、APP程序、网站程序、以及微信应用等等,由于Web API层作为一个公共的接口层,我们就很好保证了各个界面应用层的数据一致性。

上图介绍了各种应用在Web API的接口层之上,一般情况下,我们这层的接口都是提供标准的各种接口,以及对身份的认证处理等等,在Web API层更多考虑的业务范畴的相关接口,而在各个界面层,考虑的是如何对Web API进行进一步的封装,以方便调用,虽然Winform和Web调用Web API的机制有所不同,不过我们还是可以对Web API的客户端封装层进行重用的。

在Winfrom界面调用混合式接入的接口方式,它的示意图如下所示,主要的思路是通过一个统一的门面层Facade接口层进行服务提供,以及客户端调用的封装处理接口。

随着Web API层的广泛使用,这种方式带来了非常大的灵活性,通过在框架层面对各个层的基类进行封装,可以大大简化所需的编码,以及提供统一、丰富的基础接口供调用。

由于Winform调用Web API的时候,客户端对Web API层进行了一个简单的包装,这种方式可以简化对Web API接口的使用,只需要通过调用封装类,并传入相关的参数就可以获得序列化后的对象(包括基础对象和自定义类对象)。

这种封装的方式,由于对基类的统一实现,以及提供对URL地址、参数的组装等处理,非常利于Winform界面后台代码进行调用 ,加快Winfom界面功能的开发。例如我们从进一步细化的架构图上,可以看到整体各个层的一些基类(绿色部分)。

在基于Web API层的构建上,我们提供了Web API服务层的提供了BaseApiController和BusinessController<B, T> 的Web API控制器基类对常规的业务处理进行封装;在Web API服务调用层上,我们提供了BaseApiService<T>的基类进行封装常规接口;

同时提供IBaseService<T>的Facade门面层的统一接口,以及CallerFactory<T>的调用方式供Winform后台代码进行接口调用。

这种在Web API的基础上进行接口的封装,可以极大简化接口的调用,同时也可以提供给Web端的后台控制器使用,非常便于使用,下面就介绍在Web项目中进行混合式接入的实现过程。

2、Web混合式接入介绍

参照Winform混合式接入的方式,我们也可以利用这种方式应用于Web框架上,具体的分层关系如下所示。

上图整合了两种非常常用的接入方式:Web API服务接入、直接连接数据库的接入,一种具有非常强大的特性,一种具有快速的访问效率,各有其应用场景,我们在不同的业务环境进行配置,使其适应我们实际的应用即可,一般情况下,我们建议采用Web API方式进行构建整个业务系统的生态链。

Web API的接口调用,可以通过两种方式进行,一种是采用纯JS框架,类似AngularJS的方式,通过其控制器进行相关接口的调用;还有一种方式,采用Asp.NET的MVC方式,前端界面通过JS调用后端的控制器实现数据处理,具体逻辑有后端逻辑控制器进行Web API的处理,我们这里采用后者,以实现较为弹性的处理。

相对Winform来说,Web上的混合方式接入相对复杂一些,虽然Winform的界面类似Web的MVC中的视图HTML代码,Winform后台逻辑代码类似视图的控制器对象,但是确实麻烦一些,相当于我们还需要在Web界面的后台控制器Controller上在封装下相关的处理接口。

在整个基于混合式接入方式的Web 项目中,对于Web API接口的使用,整个项目的结构如下所示。

有了这些图示的说明,我们应该对整体有一个大概的了解,对于进一步的细节问题,我们可能依然不是太清楚,需要以具体的项目代码工程进行介绍。

1)对于数据库层

我们可以考虑的是多种数据库的接入支持,如SQLServer、Oracle、SQLite、Access,或者PostgreSQL的支持,这些都是基于关系型数据库的支持,具有很好的可替代性和标准一致性。

它们可以通过遵循统一的SQL或者部分自定义的SQL语句进行,或者通过存储过程实现,均可以实现相应的功能。

对于数据库不同的支持方案,我这里采用了Enterprise Library的数据库访问组件进行一致性的支持,这样可以降低各个不同数据库模型的处理,统一使用这种组件访问方式,实现不同数据库的访问。

2)对于业务逻辑层

业务逻辑层,是有几个不同的层进行综合的使用。如项目中的核心层如下所示,包括了业务逻辑层BLL、数据访问层DAL(不同的实现层)、数据访问接口层IDAL、以及传递数据的Entity实体层。

这些模块,在各个层上都有标准的基类用来实现对接口或者功能的封装处理。

如BLL层的继承关系如下

    /// <summary>
    ///基于BootStrap的图标/// </summary>
    public class BootstrapIcon : BaseBLL<BootstrapIconInfo>

如IDAL层的继承关系如下

    /// <summary>
    ///基于BootStrap的图标/// </summary>
    public interface IBootstrapIcon : IBaseDAL<BootstrapIconInfo>

基于Oracle的数据访问层在DALOracle里面,我们看到起继承关系如下。

    /// <summary>
    ///基于BootStrap的图标/// </summary>
    public class BootstrapIcon : BaseDALOracle<BootstrapIconInfo>, IBootstrapIcon

实体层继承关系如下所示。

    /// <summary>
    ///基于BootStrap的图标/// </summary>
    public class BootstrapIconInfo : BaseEntity

这些模块,由于有了基类的封装处理,多数逻辑不用再重写代码,关于它们具体的内容,可以参考之前的开发框架介绍文章了解,这里不再赘述,主要用来介绍其他模块层的继承关系。

3)对于Web API服务层

Web API如果业务模块比较多,可以参考我上篇随笔《
Web API项目中使用Area对业务进行分类管理
》使用Area区域对业务进行分类管理,一般情况下,我们为每个Web API的接口类提供了基类的管理,和我们其他模块的做法一样。

    /// <summary>
    ///所有接口基类/// </summary>
[ExceptionHandling]public class BaseApiController : ApiController

以及

    /// <summary>
    ///本控制器基类专门为访问数据业务对象而设的基类/// </summary>
    /// <typeparam name="B">业务对象类型</typeparam>
    /// <typeparam name="T">实体类类型</typeparam>
    public class BusinessController<B, T>: BaseApiControllerwhere B : class
        where T : WHC.Framework.ControlUtil.BaseEntity, new()

这样,基本的增删改查等常规接口,我们就可以在基类里面直接调用业务逻辑类实现数据的处理,具体的业务子类这不需要重写这些接口实现了。

        /// <summary>
        ///查询数据库,检查是否存在指定ID的对象/// </summary>
        /// <param name="id">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
[HttpGet]public virtual T FindByID(string id, stringtoken)
{
//如果用户token检查不通过,则抛出MyApiException异常。//检查用户是否有权限,否则抛出MyDenyAccessException异常 base.CheckAuthorized(AuthorizeKey.ViewKey, token);

T info
=baseBLL.FindByID(id);returninfo;
}

对于HttpGet和HttpPost的约定,我们对于常规的获取数据,使用前者,如果对数据发生修改,或者需要复杂类型的参数,使用POST方式处理。

子类的继承关系如下所示

    /// <summary>
    ///权限系统中用户信息管理控制器/// </summary>
    public class UserController : BusinessController<User, UserInfo>

这样这个UserController就具有了基类的一切功能,只需要实现一些特定的接口处理即可。

例如我们可以定义一个新的Web API接口,如下所示。

        /// <summary>
        ///通过用户名称获取用户对象/// </summary>
        /// <param name="userName">用户名称</param>
        /// <param name="token">访问令牌</param>
        /// <returns></returns>
[HttpGet]public UserInfo GetUserByName(string userName, stringtoken)
{
//令牌检查,不通过则抛出异常 CheckResult checkResult =CheckToken(token);return BLLFactory<User>.Instance.GetUserByName(userName);
}

这样对于Web API架构来说,控制器的整个继承关系大概如下所示。

如果使用Area区域来对业务模块进行分类,那么整个Web API项目的结构如下所示,各个业务区域分开,有利于对业务模块代码的维护,其中BaseApiController和BusinessController则是对常规Web API接口的封装处理。

4)对于Web API封装层

为了实现Winform混合式框架和Web混合式框架的共同使用Web API服务的封装层,那么我们需要独立一个Web API封装层,也就是***Caller层,包含了直接访问数据库方式、Web API服务接口访问方式,或者加上WCF服务访问方式等的封装层。

这个层的目的是动态读取Web API 接口的URL地址,以及封装对Web API接口访问的繁琐细节,是调用者能够简单、快速的访问Web API接口。

整个Web API封装层的架构,就是基于Facade接口层进行不同的适配,如直接访问数据库方式、Web API服务访问方式的适配处理,以便在客户端调用的时候,自动从不同的接口实现实例化对象,从不同方式来获取所需要的接口数据。

对于用户User对象来说,我们来举一个例子来说明Caller层之间的继承关系。

在Facade层的接口定义如下所示。

    public interface IUserService : IBaseService<UserInfo>

在WebAPI的Caller层实现类代码如下所示。

    /// <summary>
    ///基于WebAPI方式的Facade接口实现类/// </summary>
    public class UserCaller : BaseApiService<UserInfo>, IUserService

对于直接连接方式,实现类的代码如下所示。

    /// <summary>
    ///基于传统Winform方式,直接访问本地数据库的Facade接口实现类/// </summary>
    public class UserCaller : BaseLocalService<UserInfo>, IUserService

这样我们整理下它们关系如下图所示。

对于不同的业务模块,我们基于对应不同的Facade层接口实现不同的Caller层,这样即使有很多项目模块,我们单独维护起来也方便很多,在Winform客户端或者Web端调用Caller层的时候,需要引入对应的Caller层项目,以及业务核心层Core。

例如我们需要在使用的时候,同时引入Core层和Caller层,如下是项目中的部分引用关系。

5)对于Web 界面层

这个Web界面层,主要就是消费Facade层接口实现,用来获取数据展示在界面上的,我们界面上通过HTML + JS Ajax的方式,实现从MVC控制器接口获取数据,那么我们为了方便,依旧在控制器层进行抽象,以便对常规的方法抽到基类里面,这样子类代码就不用重复了。

这样的改变,对于我们已有的MVC项目来说,视图处理代码不需要任何改变,只需要控制器对数据访问的处理调整即可,从而实现MVC普通方式获取数据的界面层,顺利转换到基于Web API +直接访问数据库两者合一的混合式方式上。

原先直接访问数据库的MVC视图控制器的设计,基本上类似于Web  API 中控制器的设计过程,如下所示。

而对于MVC的Web界面层,以混合式方式来访问数据,我们需要引入一个新的控制器来实现适配处理。

这样构建出来的继承关系图,和上面Web的MVC控制器类似。

不同的是,里面调用的任何访问数据的方法,从原来BLLFactory<T>到CallerFactorry<T>的转换了,这样就实现了从简单的直接访问数据库方式,切换到混合式访问数据的方式,在Web框架里面,可以配置为直接访问数据库,也可以配置为通过Web API方式访问数据,非常方便。

例如继承关系类的代码如下所示。

    /// <summary>
    ///基于混合访问方式的用户信息控制器类/// </summary>
    public class UserController : ApiBusinessController<IUserService, UserInfo>

其中对于Web 界面端的控制器,使用混合式访问方式的后台控制器代码如下所示。

        /// <summary>
        ///根据角色获取对应的用户/// </summary>
        /// <param name="roleid">角色ID</param>
        /// <returns></returns>
        public ActionResult GetUsersByRole(stringroleid)
{
ActionResult result
= Content("");if (!string.IsNullOrEmpty(roleid) &&ValidateUtil.IsValidInt(roleid))
{
List
<UserInfo> roleList = CallerFactory<IUserService>.Instance.GetUsersByRole(Convert.ToInt32(roleid));
result
=ToJsonContent(roleList);
}
returnresult;
}

也就是从传统的BLLFactory<User>转换为了CallerFactory<IUserService>,整体性的接口变化很小,很容易过渡到混合式方式的访问。

在Web界面端的视图里面,我们基本上就是根据HTML + Ajax的Javascript方式实现数据的交互处理的,包括显示数据,提交修改等等操作。

同样我们可以通过JS的函数进行抽象,把基本的处理函数,放到一个类库里面,方便界面层使用,然后引入JS文件即可。

@*脚本引用放在此处可以实现自定义函数自动提示*@<scriptsrc="~/Scripts/CommonUtil.js"></script>

如下面所示,是调用JS自定义函数实现列表数据的绑定操作。

            $("#Dept_ID").on("change", function(e) {var deptid = $("#Dept_ID").val();
BindSelect(
"PID", "/User/GetUserDictJson?deptId="+deptid);
});

或者删除的JS代码如下所示

                    var postData ={ ids: ids };
$.post(
"/User/ConfirmDeleteByIds", postData, function(json) {var data =$.parseJSON(json);if(data.Success) {
showTips(
"删除选定的记录成功");
Refresh();
//刷新页面数据 }else{
showTips(data.ErrorMessage);
}
});

以及对一些JS列表树,以及下拉列表,都可以采用JS函数实现快速的处理,如下所示。

                var treeUrl = '/Function/GetFunctionJsTreeJsonByUser?userId=' +info.ID;
bindJsTree(
"jstree_function", treeUrl);

$(
'#lbxRoles').empty();
$.getJSON(
"/Role/GetRolesByUser?r=" + Math.random() + "&userid=" + info.ID, function(json) {
$.each(json,
function(i, item) {
$(
'#lbxRoles').append('<option value="' + item.ID + '">' + item.Name + '</option>');
});
});

以上就是我从整个基于混合式访问的Web项目进行讲解介绍,贯穿了整个数据传输的路线和调用路线,当然其中还有很多细节方面有待细讲,以及需要一些比较巧妙的整合封装处理,整个目的就是希望借助混合式的访问思路,实现多种数据接入方式的适配整合,以及最大程度简化子类代码的编写,并且通过利用代码生成工具对整体框架的各个层代码的生成,我们关心的重点转移到如何实现不同业务的接口上来,从而使得我们能够快速开发复杂的应用,而且又能合理维护好各个项目的代码。

一句话总结整个开发:简单、统一、高效。

在我们开发很多项目中,数据访问都是必不可少的,有的需要访问Oracle、SQLServer、Mysql这些常规的数据库,也有可能访问SQLite、Access,或者一些我们可能不常用的PostgreSQL、IBM DB2、或者国产达梦数据库等等,这些数据库的共同特点是关系型数据库,基本上开发的模型都差不多,不过如果我们基于ADO.NET的基础上进行开发的话,那么各种数据库都有自己不同的数据库操作对象,微软企业库Enterprise Library是基于这些不同数据库的操作做的抽象模型,适合多数据库的支持项目。本文介绍基于微软企业库Enterprise Library 4.1的基础进行的多种数据库的处理。

1、企业库Enterprise Library版本的选择

在选择Enterprise Library版本的时候,我一直都是相对谨慎,因为我们开发的项目涉及很多不同的系统,有的需要XP的支持、有的需要Win7的支持或者Win10等等,需要考虑不同系统之家的兼容问题,由于微软企业库中的数据库访问模块相对比较稳定,因此也基本沿用使用稳定的版本,虽然目前Enterprise Library版本为6.0,但是之前一直在项目中使用的是3.1,这个版本可以在.NET 2.0的项目上运行,而且扩展类库也比较不错,因此一直保留着。

随着框架版本的升级,在XP上最高可以运行.NET 4.0的版本,因此可以考虑使用Enterprise Library 4.1或者5.0的版本(Enterprise Library 6.0版本需要.NET 4.5的支持,无法再XP上运行),相对来说,Enterprise Library4.1的扩展类库支持非常不错(
http://entlibcontrib.codeplex.com/releases/view/38988
),支持了SQLServer、DB2、MySql、ODP.NET(Oracle)、PostgreSQL、SQLite、SqlEx等数据库,而Enterprise Library 5.0版本扩展类库的支持还没有完整提供,需要自己处理。

因此综合上面的原因,我们为了照顾XP、Win7/Win8/Win10等不同系统的兼容性,可以从目前的Enterprise Library 3.1升级到Enterprise Library 4.1,这样可以利用较好的扩展类库的支持(支持支持了SQLServer、DB2、MySql、ODP.NET(Oracle)、PostgreSQL、SQLite、SqlEx等数据库),也相对提高下该数据访问模块的版本。

在我以前绘制的多数据库支持里面调整一下,把我们采用微软企业库后,支持的数据库作为数据层,示意图如下所示。

这样基本上常规的关系型数据库我们都支持了,我们需要开发任何数据库应用,都是统一数据模型,开发起来方便很多了,同时也方便在不同数据库管理系统中进行配置切换。

2、采用微软企业库进行架构设计

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

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

针对数据访问层,我们需要设计好对应的继承关系,以便使得我们的基类能够封装大多数的操作,并给子类相对的弹性处理空间,如对于客户Customer的对象,数据接口层和数据访问实现层的关系如下所示。

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

关于这个架构,我在前面很多文章都有阐述,如果我们还需要扩展一些特殊的数据库支持,可以参考随笔《
基于Enterprise Library的Winform开发框架实现支持国产达梦数据库的扩展操作
》进行一些扩展定制的操作。

3、基于Enterprise Library的多种数据库支持处理

前面我们提到,使用微软企业库Enterprise Library的好处就是可以统一编程模型,实现对多种数据库的兼容处理,而微软企业库Enterprise Library最大的特点是基于配置项实现多种数据库的处理,通过对使用不同的配置项,就可以迅速切换到对应的数据库上来,代码不需要修改。

对于一般的企业库配置处理,我们增加配置项如下所示。

  <configSections>
    <sectionname="dataConfiguration"type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data"/>
  </configSections>

然后为不同的数据库添加不同的连接字符串

对于默认支持的SQLServer数据库,它的连接字符串如下所示。

<?xml version="1.0"?>
<configuration>
  <configSections>
    <sectionname="dataConfiguration"type="Microsoft.Practices.EnterpriseLibrary.Data.Configuration.DatabaseSettings, Microsoft.Practices.EnterpriseLibrary.Data"/>
  </configSections>
  <connectionStrings>
    <!--SQLServer数据库的连接字符串-->
    <addname="sqlserver"providerName="System.Data.SqlClient"connectionString="Persist Security Info=False;Data Source=(local);Initial Catalog=WinFramework;Integrated Security=SSPI"/>
  </connectionStrings>
  <dataConfigurationdefaultDatabase="sqlserver">
  </dataConfiguration>
</configuration>

不过对于一些扩展支持的数据库,我们还需要添加一些映射处理,如对于MySQL的支持,我们需要添加连接字符串:

    <!--MySQL数据库的连接字符串-->
    <addname="mysql"providerName="MySql.Data.MySqlClient"connectionString="Server=localhost;Database=WinFramework;Uid=root;Pwd=123456;"/>

还需要添加ProviderMappings的支持,如下所示的XML。

  <dataConfigurationdefaultDatabase="mysql">
    <providerMappings>
      <adddatabaseType="EntLibContrib.Data.MySql.MySqlDatabase, EntLibContrib.Data.MySql"name="MySql.Data.MySqlClient" />
    </providerMappings>
  </dataConfiguration>

下面我列出所有不同数据库的连接字符串以及映射关系的一个完整版本,供参考。

前面我们提到了,基于配置实现不同数据库的统一处理,我们为了测试不同数据库的连接,我们可以使用下面的简单案例代码来获取数据进行展示。

其实现的代码如下所示。

我们看到,上面的代码没有针对具体的数据库,因此也是非常通用的处理,我们可以直接获取数据并展示出来,我上面案例在SQLServer、Oracle、PostgreSQL、MySQL、SQLite、Access、IBM DB2数据库均测试通过。

具体开发项目的时候,不同数据库有一些不同的处理,如分页操作、获取指定记录的处理等等,这些我们就需要发挥上面提到的数据库基类的功能了,通过基类功能的封装,我们可以除了可以使用所有数据库的共性外,还可以使用它的一些特定处理操作,这样我们就可以充分利用各种不同数据库的特点,但是又统一到一个开发模型上来,降低了各种不同数据库之间开发的成本,同时也减少不同数据库之间的迁移难度,提高代码的可阅读性和可扩展性。

上面关于数据库访问模块的框架构建,已经在我众多的Winform项目、Web开发项目,以及一些后台服务项目上运行良好,并使用了多年,为我们开发各种不同数据库,或者升级到不同数据库版本的处理工作上立下了汗马功劳。

这几天一直在研究TX Text Control的使用,由于这方面的资料相对比较少,主要靠下载版本的案例代码进行研究,以及官方的一些博客案例进行学习,使用总结了一些心得,特将其总结出来,供大家分享学习。本篇随笔主要介绍TX Text Control V20的相关使用心得。

1、TX Text Control控件介绍

TX Text Control是一款功能类似于 MS Word 的文字处理控件,包括文档创建、编辑、打印、邮件合并、格式转换、拆分合并、导入导出、批量生成等功能。广泛应用于企业文档管理,网站内容发布,电子病历中病案模板创建、病历书写、修改历史、连续打印、病案归档等功能的实现。

这个控件主要的功能就是可以作为Word以及其他文档的编辑器使用,虽然展示WORD内容的控件也有一些,如我们可以利用DevExpress里面的RTF文档编辑器来实现,同样运行的很好,结合Aspose.Word后台的文档处理,我们可以做到类似报表的数据生成,而且可以把生成后的文档进行显示、编辑等操作处理。

TX Text Control虽然作为文档编辑各方面都表现不错,不过其MailMerge邮件合并功能还是经常使用的一个功能,就是把我们的数据和文档模板来一个合并,然后显示最终的文档内容,这种可以用来做一些类似发票、邮件、员工信息等的数据处理和显示,MailMerge邮件合并可以绑定主从表的数据,能够符合大多数的要求。

我本来想用它做一个类似电子病历一样的功能模块,不说在文档里面,我们很难做到一些下拉列表的处理( 官方博客里面有一个简单的案例,不过不好用),一般情况下,如果我们只是做文档展示、数据合并等常规的操作,还是很不错的。

这个控件的功能介绍,可以参考葡萄城里面的网页介绍(
http://www.gcpowertools.com.cn/products/textcontrol_winform_features.htm
),这个控件的相关开发人员使用然后分享经验的文章很少,能在网上搜到的大多数是葡萄城人员对这个控件的Demo代码进行一个简单粘贴说明,没有进一步的深入介绍和应用场景的介绍。虽然葡萄城列举了几个电子病历的公司产品案例,不过这几家公司的电子病历产品是很难下载到,也无从知道真假或者使用情况。

这几天我把这个控件的各种特性做了一些学习,并重新把官网的文档编辑例子进行了全新开发,参考着做了一个完全一样的编辑器版本,也基本上对它的各个属性、方法处理有了一个更加深入的了解。

我们先通过一个软件界面来了解整个软件的一些功能(这个是我仿照官方案例做的一个程序)。

这个控件默认安装后,会带有很多Demo案例,具体可以参考目录C:\Users\Administrator\Documents\TX Text Control 20.0.NET for Windows Forms\Samples\ 进行了解。

2、TX Text Control控件的汉化

这个控件界面默认是英文版本的,控件的相关菜单以及提示都是英文,因此我们需要对资源做一些中文本地化处理才能正确显示。

官方没有提供中文汉化包,只提供一个标准的英文资源,如下所示。

我们需要做的就是将它们进行中文翻译,然后重新编译(使用buildres.bat脚本编译)为中文资源dll。

我们先使用VS编辑工具,把这些英文资源记录转换为英文(这是一个比较繁琐的工作,官方网站上有一些旧版本的中文包可供参考,以及最新的V20软件(编辑器软件)下载下来运行参考)。

我们逐一进行中文处理,可以使用百度、Google的翻译,以及软件界面的参考哦。

以管理员方式运行VS的命令行,然后执行命令进行编译资源即可。

buildres.bat zh-CN

编译成功后,在目录里面,会增加两个资源程序集。

txdocumentserver.resources.dll

txtextcontrol.resources.dll

然后我们把它复制到运行目录下,并放在zh-CN的目录里面即可。有了这些中文化的资源程序集,我们就可以利用它进行对控件的内置菜单提示进行中文化了。

中文化操作和其他常规的做法一样,我们在Main函数里面,添加如下代码即可。

Thread.CurrentThread.CurrentUICulture = new System.Globalization.CultureInfo("zh-CN");
Thread.CurrentThread.CurrentCulture
= new System.Globalization.CultureInfo("zh-CN");

运行程序,我们使用右键菜单,发现里面的资源都已经正常汉化了,其他相关的内置菜单和界面也都可以看到正常汉化。

3、TX Text Control的使用

有了汉化,只是我们正常使用控件的第一步,我们需要在程序里面整合控件,那么就需要对它进行使用,以及对控件的属性、事件进行处理,才能得到最佳的应用效果。

我们在VS工具栏里面加入对应的控件,可以看到有以下相关的控件对象可供使用,一般情况下我们使用TextControl,然后在其基础上创建其他RulerBar、ButtonBar、StatusBar即可,而如果我们需要合并数据(很常用)就需要加入MailMerge控件对象。

添加控件后,我们可以对控件的相关基础的复制、粘贴、剪切等操作可以直接利用控件的API即可实现。

        private void menuEdit_Undo_Click(objectsender, EventArgs e)
{
_textControl.Undo();
}
private void menuEdit_Redo_Click(objectsender, EventArgs e)
{
_textControl.Redo();
}
private void menuEdit_Cut_Click(objectsender, EventArgs e)
{
_textControl.Cut();
}

其中查找、替换对话框也是可以通过API进行调出。

        private void menuEdit_Find_Click(objectsender, EventArgs e)
{
_textControl.Find();
}
private void menuEdit_Replace_Click(objectsender, EventArgs e)
{
_textControl.Replace();
}

利用这些最基础的API是常规的操作。

而利用插入相关的对象,如图片、文本框等,就需要做一些简单的编码,方便把对象加入到TextControl对象里面。

        private void menuInsert_Image_Click(objectsender, EventArgs e)
{
TXTextControl.Image imageNew
= newTXTextControl.Image();
_textControl.Images.Add(imageNew, TXTextControl.HorizontalAlignment.Left,
-1, TXTextControl.ImageInsertionMode.DisplaceText);
}
private void menuInsert_TextFrame_Click(objectsender, EventArgs e)
{
try{//Force Exception if standard version: _textControl.TextFrames.GetItem();
Size sizeTextFrame
= new Size(2268, 2268); //4 x 4 cm TXTextControl.TextFrame textFrameNew= newTXTextControl.TextFrame(sizeTextFrame);
_textControl.TextFrames.Add(textFrameNew, TXTextControl.HorizontalAlignment.Left,
-1, TXTextControl.TextFrameInsertionMode.DisplaceCompleteLines);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message, ProductName);
}
}

这个控件最常见的就是MailMerge进行合并数据的操作了,这个也是我们利用它来处理很多模板化文档的目的。

MailMerge对象合并数据的操作,主要是接受集合对象或者是DataTable对象,所以我们必须将我们的数据转换为这种格式,否则合并数据得不到要的结果。

合并数据的处理方式,最开始就是需要设计好模板,这点很重要,模板的设计还是沿用了常规Word文档域对象的概念,需要添加一些域来做后续数据替换的对象占位符,如下是我测试的一个模板。

这个里面主要是主从表整合的一个模板,我们需要绑定常规的主表记录,也需要绑定明细表的集合记录,不过最后我们都需要把数据对象转换为集合(如DataSet),然后才能绑定到文档对象上去。

在上面的文档里面,你知识看到了域对象,而没有看到一个隐藏的一个集合记录的开始和结束的书签设置。关于书签的作用和如何操作,可以了解我之前的随笔文章《
利用Aspose.Word控件实现Word文档的操作
》、《
利用Aspose.Word控件和Aspose.Cell控件,实现Word文档和Excel文档的模板化导出

书签的作用很重要,否则无法正常解析集合的记录并绑定在WORD界面上的,我们打开书签管理对话框,可以看到上述文档里面有两个位置,书签标记的开始和结束位置。

这样我们设计好模板后,第二步就是通过代码生成相关对象,然后和文档进行合并就可以了。

例如我构建一个主表和一个从表的记录,统一把它们生成一个DataSet对象供使用。

        public staticDataSet CreateDataSet()
{
DataSet ds
= newDataSet();

DataTable dtMain
= DataTableHelper.CreateTable("Company,HandNo,Creator,CreateTime|DateTime");
dtMain.TableName
= "main";
DataRow dr
=dtMain.NewRow();
dr[
"Company"] = "广州爱奇迪软件科技有限公司";
dr[
"HandNo"] = "123456";
dr[
"Creator"] = "伍华聪";
dr[
"CreateTime"] =DateTime.Now;
dtMain.Rows.Add(dr);

DataTable dt
= DataTableHelper.CreateTable("ID,ProductName,Description,Price|decimal,Quantity|int");
dt.TableName
= "ProductInfo";
dr
=dt.NewRow();
dr[
"ID"] = "1";
dr[
"ProductName"] = "海飞丝洗发水";
dr[
"Description"] = "海飞丝洗发水, 550ml";
dr[
"Price"] = 19.8M;
dr[
"Quantity"] = 100;
dt.Rows.Add(dr);

dr
=dt.NewRow();
dr[
"ID"] = "2";
dr[
"ProductName"] = "联想品牌电脑";
dr[
"Description"] = "联想Y700-15ISK-ISE 旗舰版";
dr[
"Price"] =6500M;
dr[
"Quantity"] = 10;
dt.Rows.Add(dr);

dr
=dt.NewRow();
dr[
"ID"] = "3";
dr[
"ProductName"] = "IPhone7 128G";
dr[
"Description"] = "苹果IPhone7, 128G";
dr[
"Price"] =5800M;
dr[
"Quantity"] = 10;
dt.Rows.Add(dr);

ds.Tables.Add(dtMain);
ds.Tables.Add(dt);
returnds;
}

先加载模板文档

if (setting == null)
{
setting
= newTXTextControl.LoadSettings();
setting.ApplicationFieldFormat
=TXTextControl.ApplicationFieldFormat.MSWord;
}
_textControl.Load(Application.StartupPath
+ "\\Template\\template1.docx", TXTextControl.StreamType.WordprocessingML, setting);

整合合并数据

DataSet ds =PurchaseInfoHelper.CreateDataSet();
mailMerge1.MergeBlocks(ds);
mailMerge1.Merge(ds.Tables[
"main"], true);

最后就可以看到我们所需要的结果了。

当然,如果很熟悉Aspose.Word控件的使用,我们其实也可以利用Aspose.Word控件来做后台的数据整合处理,Aspose.Word控件支持很多变量定义,以及更加复杂的处理,如我把原来在框架模块里面的人员信息导出Word功能抽取出来,这个模块原先是利用Aspose.Word来处理数据合并的,我不修改其中的逻辑,只是把合并后的数据展示在TX Text Control即可,如下代码所示。

var saveFile =StaffHelper.GenerateDoc();//加载文档
_textControl.Load(saveFile, StreamType.MSWord);

最后就生成了我们开始介绍的软件界面效果。

这个控件目前使用起来还算不错,不过对于一些数据源的处理方面,以后希望继续增加更多的接口,继续保持观察,希望能将研究的成果用在具体的项目上。

在我们一些和文件处理打交道的系统中,我们往往需要记录下最近使用的文件,这样方便用户快速打开之前浏览或者编辑过的文件,这种在很多软件上很常见,本文主要介绍在Winform界面菜单中实现【最近使用的文件】动态菜单的处理,实现一个较为常用的功能。

在我上篇随笔《
文字处理控件TX Text Control的使用
》介绍的内容中,我针对性的对这个控件的使用做了一个全面的了解,发现其中案例代码总这部分的功能实现【最近使用的文件】挺好,于是把它进行了整理,把整个思路作为一篇随笔进行记录,希望对大家有所帮助。

1、菜单动态加入【最近使用的文件】的介绍

这个功能我们在很多程序上有见过,如在Visual  Studio里面,我们可以看到这个列表的动态处理。

以及在Word工具栏里面,一样有这样的实现

这个功能主要的处理逻辑就是,我们在打开文件、保存文件、或者另存为其他文件的时候,把对应的文件路径进行记录存储,当我们打开这个菜单的时候,把它们进行加载动态生成相关的菜单即可,一旦我们选择其中一个文件,我们就把它们加载到主界面进行展示或者编辑即可。

2、菜单动态加入【最近使用的文件】的实现

1)设计处理过程

首先我们需要在界面里面添加一个 菜单的占位符,方便我们以此为基准,加入对应的动态菜单,如下设计界面所示

剩下的就是代码的处理了,我们刚才提到,我们需要记录文件打开,保存、另存为的几个操作的文件,然后存储起来使用,也就是存储一个文件路径和文件标题列表了。

这个存储我们可以通过系统配置文件的常规处理实现,先在程序项目解决方案里面找到对应的Settigns.settings文件,打开后进行添加记录对象处理,如下所示。

有了这些,那么我们的信息存储就实现了第一步了,需要的就是把它们通过代码进行管理起来。

2)代码实现处理过程

有了上面的设计处理过程,我们有了一个固定的菜单可以使用,有了一个配置对象以及对应的属性可以存储和加载处理,那么剩下的就是通过代码把它们之间的关系联系起来,实现动态文件列表的菜单处理即可。

我们定义一个类,并添加对应的文件数量大小和文件列表的属性,用来记录和配置文件定义的属性内容,以及存储对应的菜单项对象,如下所示。

其中我们需要在文件打开,保存、另存的时候,做一个文件列表的记录处理,因此需要增加一个函数,用来把最近的文件追加到列表的顶端(最近文件列表),以及裁剪多于指定数量的记录,具体操作如下所示。

/// <summary>
///添加新文件路径到顶部列表(在打开、保存、另存为操作中)/// </summary>
public void AddRecentFile(stringfilePath)
{
_fileList.Insert(
0, filePath);//从最后位置开始倒着找,如果找到一致名称,则移除旧记录 for (int i = _fileList.Count - 1; i > 0; i--)
{
for (int j = 0; j < i; j++)
{
if (_fileList[i] ==_fileList[j])
{
_fileList.RemoveAt(i);
break;
}
}
}
//最后,仅保留指定的文件列表数量 for (int bynd = _fileList.Count - 1; bynd > _nMaxFiles - 1; bynd--)
{
_fileList.RemoveAt(bynd);
}

UpdateMenu();
}

动态增加菜单的处理,就是根据这些文件列表进行的菜单项处理,先清空旧的记录,然后添加新纪录,并添加对应给的事件处理即可。

其中增加一个【清空列表】的维护性操作。

当然,文件的打开,我们最好用一个状态记录文件是否编辑过,如果编辑过则应该提示用户是否保存原来的文件。

/// <summary>
///最近文件法的菜单项/// </summary>
void menuItem_Click(objectsender, EventArgs e)
{
if(_bDocumentDirty)
{
var result = MessageBox.Show("需要保存到" + DocumentFileName + "吗?", "提示", MessageBoxButtons.YesNoCancel);if (result ==DialogResult.Yes)
{
FileSave();
}
}

ToolStripMenuItem item
=(ToolStripMenuItem)sender;int pos =item.GetCurrentParent().Items.IndexOf(item);if (pos >= 0 && pos <_fileList.Count)
{
DocumentFileName
=item.Tag.ToString();
FileOpen();
}
}
/// <summary> ///清空最近菜单列表的菜单项/// </summary> void clearListItem_Click(objectsender, EventArgs e)
{
_fileList.Clear();
UpdateMenu();
}

其中的菜单项入口,我们应该在主程序初始化后把对应的菜单项赋值给辅助类即可。

//指定【最近使用的文件】的菜单项,方便对文档列表菜单进行动态创建
_fileHandler.RecentFilesMenu = this.menuFile_RecentFiles;

整个过程在此基本完成了,最后我们看看实际的效果,符合我们的预期。

在我们开发的很多分布式项目里面(如基于WCF服务、Web API服务方式),由于数据提供涉及到数据库的相关操作,如果客户端的并发数量超过一定的数量,那么数据库的请求处理则以爆发式增长,如果数据库服务器无法快速处理这些并发请求,那么将会增加客户端的请求时间,严重者可能导致数据库服务或者应用服务直接瘫痪。缓存方案就是为这个而诞生,随着缓存的引入,可以把数据库的IO耗时操作,转换为内存数据的快速响应操作,或者把整个页面缓存到缓存系统里面。缓存框架在各个平台里面都有很多的实现,基本上多数是采用分布式缓存Redis、Memcached来实现。本系列文章介绍在.NET平台中,使用开源缓存框架CacheManager来实现数据的缓存的整个过程,本篇主要介绍CacheManager的使用和相关的测试。

1、CacheManager的介绍

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

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

使用Nuget为项目添加CacheManager包引用。CacheManager包含了很多的Package. 其中CacheManager.Core是必须的,其它的针对不同缓存平台上有不同的对应Package,整个Nuget包包含下面几个部分的内容。

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

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

在网上充斥着基于某种单独缓存的实现和应用的趋势下,这种更抽象一层,以及提供更高级特性的缓存框架,在提供了统一编程模型的基础上,也实现了非常强大的兼容性,使得我一接触到这个框架,就对它爱不释手。

在GitHub上,缓存框架的前几名,除了这个缓存框架外,也还有一些,不过从文档的丰富程度等各方面来看,这个缓存框架还是非常值得拥有的。

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

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

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

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

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

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

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

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

....

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

2、CacheManager缓存框架的应用

通过上面对CacheManager缓存框架的简单了解,我们大概了解了它应用的一些功能,但是实际上我们如何使用它,我们需要做一些学习和了解,首先我们需要在整个应用框架里面,知道缓存框架所扮演的角色。

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

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

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

在这个数据缓存层里面,我们引入了CacheManager缓存框架,实现分布式的缓存处理,使得我们的缓存数据能够在Redis服务器上实现数据的处理,同时可以在系统重启的时候,不至于丢失数据,能够快速恢复缓存数据。

为了实现对这个CacheManager缓存框架的使用,我们需要先进行一个使用测试,以便了解它的各个方便情况,然后才能广泛应用在我们的数据中间层上。

我们建立一个项目,并在引用的地方打开管理NuGet程序包,然后搜索到CacheManager的相关模块应用,并加入到项目引用里面,此为第一步工作。

我们创建一个客户对象类,用来模拟数据的存储和显示的,如下代码所示。

/// <summary>
///模拟数据存储的客户对象类/// </summary>
public classCustomer
{
private static Customer m_Customer = null;private static ICacheManager<object> manager = null;//初始化列表值 private static List<string> list = new List<string>() { "123", "456", "789"};/// <summary> ///客户对象的单件实例/// </summary> public staticCustomer Instance
{
get{if(m_Customer == null)
{
m_Customer
= newCustomer();
}
if (manager == null)
{
manager
= CacheFactory.Build("getStartedCache", settings =>{
settings.WithSystemRuntimeCacheHandle(
"handleName");
});
}
returnm_Customer;
}
}

这个类先做了一个单例的实现,并初始化缓存Customer类对象,以及缓存管理类ICacheManager<object> manager,这个是我们后面用来操作缓存数据的主要引用对象。

我们编写几个函数,用来实现对数据的获取,数据增加、数据删除的相关操作,并在数据增加、删除的时候,触发缓存的更新,这样我们下次获取数据的时候,就是最新的数据了。

/// <summary>
///获取所有客户信息/// </summary>
/// <returns></returns>
public List<string>GetAll()
{
var value = manager.Get("GetAll") as List<string>;if(value == null)
{
value
= list;//初始化并加入缓存 manager.Add("GetAll", value);

Debug.WriteLine(
"初始化并加入列表");
}
else{
Debug.WriteLine(
"访问缓存获取:{0}", DateTime.Now);
}
returnvalue;
}
/// <summary> ///插入新的记录/// </summary> /// <param name="customer"></param> /// <returns></returns> public bool Insert(stringcustomer)
{
//先获取全部记录,然后加入记录 if (!list.Contains(customer))
{
list.Add(customer);
}
//重新设置缓存 manager.Update("GetAll", v =>list);return true;
}
/// <summary> ///删除指定记录/// </summary> /// <param name="customer"></param> /// <returns></returns> public bool Delete(stringcustomer)
{
if(list.Contains(customer))
{
list.Remove(customer);
}
manager.Update(
"GetAll", v=>list);return true;
}

我们编写一个Winform程序来对这个缓存测试,以方便了解其中的机制。

我们在测试读取的时候,也就是对GetAll进行处理,插入以及删除主要就是为了测试缓存更新的处理。代码如下所示。

private void btnTestSimple_Click(objectsender, EventArgs e)
{
var list =Customer.Instance.GetAll();
Debug.WriteLine(
"客户端获取记录数:{0}", list != null ? list.Count : 0);
}
private void btnInsert_Click(objectsender, EventArgs e)
{
var name = "abc";
Customer.Instance.Insert(name);
Debug.WriteLine(
string.Format("插入记录:{0}", name));
}
private void btnDelete_Click(objectsender, EventArgs e)
{
var name = "abc";
Customer.Instance.Delete(name);
Debug.WriteLine(
string.Format("删除记录:{0}", name));
}

我们跟踪记录,可以看到下面的日志信息。

我们可以看到,其中第一次是缓存没有的情况下进行初始化,初始化的记录数量为3个,然后插入记录后,再次获取数据的时候,缓存更新后的数量就变为4个了。

我们前面介绍了插入记录的后台代码,它同时进行了缓存数据的更新了。

/// <summary>
///插入新的记录/// </summary>
/// <param name="customer"></param>
/// <returns></returns>
public bool Insert(stringcustomer)
{
//先获取全部记录,然后加入记录 if (!list.Contains(customer))
{
list.Add(customer);
}
//重新设置缓存 manager.Update("GetAll", v => list);return true;
}

我们前面介绍的缓存初始化配置的时候,默认是使用内存缓存的,并没有使用分布式缓存的配置,它的初始化代码如下:

manager = CacheFactory.Build("getStartedCache", settings =>{
settings.WithSystemRuntimeCacheHandle(
"handleName");
});

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

引入分布式的Redis缓存实现,我们的配置代码只需要做一定的改变即可,如下所示。

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
)
;
});

其他的使用没有任何变化,我们同时增加一些测试数据方便我们查阅对应的缓存数据。

/// <summary>
///测试加入几个不同的数据/// </summary>
/// <returns></returns>
public voidTestCache()
{
manager.Put(
"string", "abcdefg");
manager.Put(
"int", 2016);
manager.Put(
"decimal", 2016.9M);
manager.Put(
"date", DateTime.Now);
manager.Put(
"object", new UserInfo { ID = "123", Name = "Test", Age = 35});
}
private void btnTestSimple_Click(objectsender, EventArgs e)
{
var list =Customer.Instance.GetAll();
Debug.WriteLine(
"客户端获取记录数:{0}", list != null ? list.Count : 0);//测试加入一些值 Customer.Instance.TestCache();
}

我们其中测试,一切和原来没有什么差异,程序的记录信息正常。

但是我们配置使用了Redis的缓存处理,因此可以使用“Redis Desktop Manager”软件来查看对应的缓存数据的,打开软件我们可以看到对应的缓存记录如下所示。

从上图我们可以查看到,我们添加的所有缓存键值都可以通过这个Redis的客户来进行查看,因为我们缓存里面有基于Redis缓存的实现,同理如果我们配置其他的缓存实现,如MemCache等,那么也可以在对应的管理界面上查看到。

我们完成这些处理后,可以发现缓存数据是可以实现多层缓存的,最为高效的就是内存缓存(也是它的主缓存),它会自动协同好各个分布式缓存的数据版本冲突问题。

引入如Redis的分布式缓存有一个好处,就是我们的数据可以在程序重新启动的时候,如果没有在内存缓存里面找到(没有击中目标),那么会寻找分布式缓存并进行加载,从而即使程序重启,我们之前的缓存数据依旧保存完好。

以上就是我基于对缓存框架的整体了解和其角色扮演做的相关介绍,以及介绍CacheManager的使用和一些场景的说明,通过上面简单案例的研究,我们可以逐步引入到更具实际价值的Web API 框架层面上进行使用,以期把缓存框架发挥其真正强大的价值,同时也为我们各种不同的缓存需要进行更高层次的探索,希望大家继续支持。