2023年2月

最近一直在整合WebAPI、Winform界面、手机短信、微信公众号、企业号等功能,希望把它构建成一个大的应用平台,把我所有的产品线完美连接起来,同时也在探索、攻克更多的技术问题,并抽空写写博客,把相应的技术心得和成果进行一定的介绍,留下开拓的印记。本文主要介绍混合框架整合Web API应用过程中,分析Winform界面如何一步步对Web API的调用处理的。

1、Winform界面的应用方向

在很多场合,分布式采用Web方式构建应用,不过相对Winform来说,Web界面的体验性没有那么好,界面呈现也相对单调 一些,而且涉及到和打印、摄像、读卡等硬件处理的时候,Winform的优势就更加明显了,Winform唯一被诟病的是其分布性的处理和安装发布的问题,分布性可以通过直接利用Web API的方式进行处理,从而逻辑集中在Web API层,而安装发布,则可以通过自动更新的模式进行处理,如目前很多桌面程序,都是自动更新的方式进行迭代更新。

因此Winform可以基于一个Web API的整体性平台,构建很多应用生态链。例如我们常见的微信应用(企业号,公众号,订阅号等)、以及Winform应用、原生APP、Web网站应用等等,如下图所示。

其中是把Web API作为核心层,可以在上面开发我们各种企业业务应用就可以了。

在前面介绍过相应的Web API的封装和调用规则,如下图所示,红色部分为Web API 的调用路线,从Winform客户端开始,经过统一门面结构Facade接口层,对Web API的服务层进行调用,下面这个图从大的方向来阐述了整个调用的路线,不过于调用细节的理解并不很准确,因为涉及到很多内容已经省略了。 下面我们将把整个调用的路线进行完整的阐述说明。

2、Winfrom界面调用WebAPI的过程

在前面的小节里面,我们说到了Winform调用Web API的过程,这个过程可以通过下面这个图示进行讲解。

1)
首先我们在界面一般是通过定义一个Winform窗体,并在其中放置相应的控件来承载信息的,这个和普通的Winform是一样的,例如我们定义一个窗体对象FrmMember,以及FrmEditMember。

2)
在主体界面里面,我们需要调用FrmMember这个窗体,可以通过对话框的方式,或者是多文档的方式进行调用显示。

            FrmEditMember dlg = newFrmEditMember();
dlg.ShowDialog();

或者多文档界面展示

ChildWinManagement.LoadMdiForm(this, typeof(FrmMember));

3)
在界面里面,我们需要调用接口对象(Web API的客户端包装类)进行获取对应的信息,这里使用到了接口工厂CallerFactory<T>这种方式进行调用。

MemberInfo info = CallerFactory<IMemberService>.Instance.FindByID(ID);

4)
上面这个工厂类CallerFactory<T>是负责获取到对应的接口实现类并创建对象,方便我们进行调用处理。它的逻辑主要是通过IMemberService接口所在的程序集(例如WHC.CloudMember.WebApiCaller),然后获取对应接口的实现类,并构建一个这样的接口实例出来使用的。

例如字典模块,混合框架里面,他们的各个模块的实现类是放在程序集里面的,我们的目标就是根据接口的名称,从对应的部分获取相应的Web API接口调用包装类进行使用。

5)
我们构建的Web API接口调用包装类(WebApiCaller里面的内容),为了实现更加方便的调用,我们为它进行了一定的封装,使它在基于泛型的基础上具有基础增删改查、分页等功能的调用处理。

从这个类的定义里面,我们可以看到Web API的调用包装类MemberCaller是继承自BaseApiService<MemberInfo>这样的泛型基类的。这个BaseApiService<MemberInfo>就具有对特定对象的增删改查、分页等基础调用功能了。

例如在基类BaseApiService里面的查找对应对象的接口代码如下所示

        /// <summary>
        ///查询数据库,检查是否存在指定ID的对象(用于字符型主键)/// </summary>
        /// <param name="key">对象的ID值</param>
        /// <returns>存在则返回指定的对象,否则返回Null</returns>
        public virtual T FindByID(stringkey)
{
var action = "FindByID";string url = GetTokenUrl(action) + string.Format("&id={0}", key);return JsonHelper<T>.ConvertJson(url);
}

这里面的逻辑就是构建一个带有token(用户身份标识)的连接字符串和参数字符串,从而获取HTML内容后把它转换为具体对象的处理了。

其中转换的代码就是利用了Newtonsoft.Json的对象的转换,具体代码如下所示。

        /// <summary>
        ///转换Json字符串到具体的对象/// </summary>
        /// <param name="url">返回Json数据的链接地址</param>
        /// <returns></returns>
        public static T ConvertJson(stringurl)
{
HttpHelper helper
= newHttpHelper();
helper.ContentType
= "application/json";string content =helper.GetHtml(url);
VerifyErrorCode(content);

T result
= JsonConvert.DeserializeObject<T>(content);returnresult;
}

6)
用户的访问令牌(Token信息)

当然我们调用这个接口前,我们需要获取到对应的Token(用户令牌)然后才能进行API的调用了。这个Token的机制采用了JWT的令牌生成方式,具有很好的通用性。

例如我使用自己的Web API调试工具,获取到对应的token方式如下所示。下面的1-5的标识就是获取token所需要的签名数据,当然连接还带有几个账号认证所需要的信息了,如账号密码、所在公司等信息。

当然我们也可以使用浏览器进行测试获取Token的信息,只是没有那么方便而已。

系列文章如下所示:

Web API应用架构在Winform混合框架中的应用(1)

Web API应用架构在Winform混合框架中的应用(2)--自定义异常结果的处理

Web API接口设计经验总结

Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解

Web API应用架构在Winform混合框架中的应用(4)--利用代码生成工具快速开发整套应用

Web API应用架构在Winform混合框架中的应用(5)--系统级别字典和公司级别字典并存的处理方式

前面几篇介绍了Web API的基础信息,以及如何基于混合框架的方式在WInform界面里面整合了Web API的接入方式,虽然我们看似调用过程比较复杂,但是基于整个框架的支持和考虑,我们提供了代码生成工具的整合,使得开发整套应用是非常方便和高效的。本文主要介绍如何利用代码生成工具Database2Sharp,如何迅速生成基于Web API的Winform应用。

1、代码生成工具的功能介绍

代码生成工具Database2Sharp,是我为整个开发过程开发的一款核心软件,已经走过了10个年头,随着开发项目的多样化,这个工具也逐渐整合更多的功能,以期提高我们的开发效率。

Database2Sharp能够支持Winform开发框架、WCF开发框架、混合式Winfrom开发框架、Entity Framework实体框架、基于MVC4+EasyUI的Web开发框架、基于Metronic的Bootstrap
的Web
项目等
开发框架的代码生成和整合工作;可以生成各种架构代码、生成Web界面代码、Winform界面代码,导出数据库文档等功能。软件
生成的框架代码具有统一的架构风格和统一调用规则,并在多年的软件开发应用中得到实践验证,具有非常高的生产效率。

该软件的目的在整合、简化各种框架的开发流程,并统一整个开发的继承和结构关系,提高开发效率。

为了生成整个基于Web API的Winform应用,我们选择Enterprise Library的框架生成混合式框架项目进行简单的介绍,希望大家对代码生成有一个初步的了解。

首先选择数据库需要生成代码的表,操作如下所示。

接着我们选择框架的主命名空间,如我们实体类的整个命名空间为WHC.CloudMember.Entity,那么主命名空间就是前面部分WHC.CloudMember了。

最后会让我们确认整个生成过程的,如下所示。

这样单击【完成】按钮后,我们就可以顺利生成我们所需的项目代码了。为了快速开发一个完善的项目,我们一般基于各种不同的框架基础上,增量开发一些不同的业务模块,这样就可以快速整合到我们的项目里面进行使用了。

2、基于WebAPI接入的Winform项目结构

1)框架总体介绍

前面随笔,我介绍了整个混合框架中基于Web API 的Winform界面的整合过程,其中大的方面设计图如下所示。

而对应的从Winform界面调用Web API的过程则如下所示。

但是这两个可能没有项目结构查看的话,对整体的了解可能还是有所欠缺,本文继续以实际项目的结构,也就是我以Web API方式改造的会员管理系统项目结构进行展示,以实践的项目开发过程来阐述整个开发过程,希望读者对这些有更好的了解和更直观的认识。

整个会员管理系统项目结构如下所示。

项目结构的总体说明如下所示。

项目名称 项目说明
WHC.CloudMember.ClientDx 基于Web API的WInform界面项目
WHC.CloudMember.Caller 接口调用封装类模块
CloudMemberApi Web API的服务发布项目
WHC.CloudMember.Core 业务逻辑模块的项目,包含数据访问层、接口、实体类等

2)Web API服务项目介绍

我们在Winform界面里面调用的最终服务就是Web API服务,那么我们需要围绕这个内容进行介绍,首先介绍其中的Web API项目CloudMemberApi,它的详细内容就是包含前面几篇关于Web API随笔介绍的内容,包括服务接口的定义、异常的处理、令牌权限认证及识别、Web API客户端的调用的知识点。

在控制器里面,通过不同的文件来区分不同的业务应用,当然如在必要的时候,也可以考虑引入Area的概念,对不同业务进行归类处理。

在上面的API项目里面,主要的内容就是控制器的生成了,利用代码生成工具的Web API控制器的生成功能,可以快速生成相关的代码。

代码生成后,会生成对应类的继承关系,以及部分接口的实例代码,方便我们进行使用。

3)服务调用包装项目Caller项目

在Caller项目里面,就是为客户端提供一致的调用方式,不管是WCF方式、直接访问数据库方式,还是目前介绍的Web API方式,我们通过Caller项目类的封装处理,就能屏蔽他们之间的差异,使得我们的界面调用实现统一性,并且也方便在各个方式的调用中进行切换。利用在这个项目里面,Web API的Caller层项目结构如下所示。

这个项目里面包含了门面层的接口定义层Facade接口层,由于我们考虑了WCF的接入方式,因此需要增加一些WCF的接口标识,如果仅仅是考虑Web API的接入,则可以不用[ServiceContract]和[OperationContract]的声明内容。

对于Web API的调用,我们一般通过继承基类的方式,可以直接使用基类定义的增删改接口的封装处理,这样可以极大简化我们所需的代码,同时降低代码编写的复杂度,常规的功能我们也不用重复编写了。

同时,我们一般都保留直接调用数据库的包装类项目代码【WinformCaller】,这样我们可以在开发的时候,先着重调试好直接访问数据库的模式,然后在调试基于Web API的方式,这样可以避免很多低层次的错误,同时也可以快速调试跟踪代码,为我们后面的Web API接入排除更多的错误,提高开发效率和代码质量。

4)业务模块核心项目Core项目

在各个框架以及Web API的服务里面,数据库访问的核心逻辑都在Core项目里面的,这个项目其实包含了BLL层、DAL层、IDAL层、Entity层的模块内容,封装了多种数据库的访问方式,默认框架支持SQLServer、Oracle、MySql、SQLite、Access、达梦等数据库的支持,我们在DAL层进行相应的扩展处理即可,一般情况下,我们为了方便,也可以仅仅设置我们所需要的数据访问层即可。如会员管理模块的Core项目截图如下所示。

如之前的架构设计图,介绍如下所示。

4)基于Web API接口的云会员管理系统

我们先来了解下基于Web API接口的云会员管理系统的总体界面效果。整个系统采用的是混合式的Winform结构,但是数据的来源采用基于Web API的提供方式,也就是我们目前常用到的一种数据提供方式。所有的模块都是基于混合式架构的基础上,对各种接口进行了基类封装,结合代码生成工具,可以生成整体的框架代码:包括Core层业务代码;Web API层控制器代码;以及对Web API调用进行封装的Caller层代码;还有就是最为重要的,Winform界面的动态生成,界面的生成可以省却大量的低效率工作,并且可以绑定一致的访问代码,使得我们开发效率有了质的提高,并且能够和整个框架有更好的整合和一致性。

基于这个基础上我们开发了很多业务系统,并且也为客户向相同领域扩展了很多信息管理系统,拥有了丰富的Winform项目建设经验。下面是云会员管理系统的界面截图。

系列文章如下所示:

Web API应用架构在Winform混合框架中的应用(1)

Web API应用架构在Winform混合框架中的应用(2)--自定义异常结果的处理

Web API接口设计经验总结

Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解

Web API应用架构在Winform混合框架中的应用(4)--利用代码生成工具快速开发整套应用

Web API应用架构在Winform混合框架中的应用(5)--系统级别字典和公司级别字典并存的处理方式

在我这个系列中,我主要以我正在开发的云会员管理系统为例进行介绍Web API的应用,由于云会员的数据设计是支持多个商家公司,而每个公司又可以包含多个店铺的,因此一些字典型的数据需要考虑这方面的不同。如对于
证件类型,收费处理状态,民族,职称
等这些固定化的内容,我们可以放到全局字典里面,但是对于一些如会员相关的字典数据,如
产品单位、产品类型
等内容,如果也全部规定为全局的系统字典,那么就缺乏灵活性,这些数据应该可以由各自进行差异化处理。

1、云会员系统的字典数据模型

我们先来了解下基于Web API接口的云会员管理系统的总体界面效果。

由于一般的云会员系统,都是允许用户注册一个公司,然后公司层面开设多个商铺的,如系统的登陆界面如下所示。

因此数据的范围需要考虑的更广,他们的关系如下所示。

而我们原先设计的字典模型如下所示。

而在公司数据这个层次上,我们需要考虑公司层级的数据字典存储,但是我们进一步分析可以看到,虽然数据字典数据是公司层级的,但是数据字典的类型(如证件类型、产品类型等)这些是固定不变的,也就是我们如果存储公司层级的字典数据,那么也只是需要存储对应的字典项目即可。因此我们可以增加多一个和TB_DictData的数据表类似的表进行存储即可,它的数据设计如下所示。

为了方便在系统里面使用同一的字典项目内容,我们创建一了一个统一的字典项目管理模块,也就是系统字典管理界面,如下所示。

2、公司层级的字典数据存储实现

有了上面的设计模型,相信大多数人员都可以想到它的具体实现思路了。

首先我们需要以系统字典数据为参考,如默认就是取系统的字典项目数据,如果公司级别的用户修改或者删除了字典数据内容,那么对应的字典类别的字典项目就应该以修改的为准了。

但是我们不可能为新建公司账户的时候,都为每个公司自动创建一份对应类型的字典数据,那样稍显麻烦,而且一开始就创建也比较麻烦。

先建立一个公司字典的数据管理界面,它和字典数据管理界面一样,不过是存储在另外一个表里面,自动根据当前用户的公司标识进行存储的。

批量添加公司字典的数据如下所示。

一般我们在使用公司层级的字典数据或者系统公共层级的字典数据的时候,都是根据字典类型进行判断的。

因此在公司层级根据字典项目类型获取数据的时候,我们在业务接口底层做了判断,判断如果对应公司的字典项没有数据,则复制一份过去,如果公司层次有对应的数据类型,那么就获取公司层级的字典项目数据即可。

具体的代码逻辑如下所示。

        /// <summary>
        ///根据字典类型名称获取所有该类型的字典列表集合/// </summary>
        /// <param name="dictType">字典类型名称</param>
        /// <param name="corpId">公司ID</param>
        /// <returns></returns>
        public List<CorpDictDataInfo> FindByDictType(string dictTypeName, stringcorpId)
{
ICorpDictData dal
= baseDal asICorpDictData;
List
<CorpDictDataInfo> list =dal.FindByDictType(dictTypeName, corpId);//如果公司字典没有数据,则从系统字典获取 if (list.Count == 0)
{
List
<DictDataInfo> dict = BLLFactory<DictData>.Instance.FindByDictType(dictTypeName);foreach (DictDataInfo info indict)
{
list.Add(
newCorpDictDataInfo(info, corpId));
}
//写入公司字典表,避免下次再去获取 foreach (CorpDictDataInfo info inlist)
{
baseDal.Insert(info);
}
}
returnlist;
}

在Web API的控制器接口,还是和其他的处理一样,增加对应的参数处理即可。

        /// <summary>
        ///根据字典类型名称获取所有该类型的字典列表集合/// </summary>
        /// <param name="dictType">字典类型名称</param>
        /// <param name="corpId">公司ID</param>
        /// <returns></returns>
[HttpGet]public List<CorpDictDataInfo> FindByDictType(string dictTypeName, string corpId, stringtoken)
{
//令牌检查,不通过则抛出异常 CheckResult checkResult =CheckToken(token);return BLLFactory<CorpDictData>.Instance.FindByDictType(dictTypeName, corpId);
}

在Facade层定义字典的对应接口的时候,我们的代码如下所示

        /// <summary>
        ///根据字典类型名称获取所有该类型的字典列表集合/// </summary>
        /// <param name="dictType">字典类型名称</param>
        /// <param name="corpId">公司ID</param>
        /// <returns></returns>
[OperationContract]
List
<CorpDictDataInfo> FindByDictType(string dictTypeName, string corpId);

在基于Web API的封装调用接口,我们的调用封装类如下所示。其中token以及Web API的相关参数处理,在基类模块进行了封装,减少了很多代码的拼接。

     /// </summary>
        /// <param name="dictType">字典类型名称</param>
        /// <param name="corpId">公司ID</param>
        /// <returns></returns>
        public List<CorpDictDataInfo> FindByDictType(string dictTypeName, stringcorpId)
{
var action = "FindByDictType";string url = GetTokenUrl(action) + string.Format("&dictTypeName={0}&corpId={1}", dictTypeName, corpId);

List
<CorpDictDataInfo> result = JsonHelper<List<CorpDictDataInfo>>.ConvertJson(url);returnresult;
}

然后我们在界面上的字典项目下拉列表,则可以通过扩展函数的方式进行绑定。

        /// <summary>
        ///初始化字典列表内容/// </summary>
        private voidInitDictItem()
{
//初始化代码 this.txtProductType.BindDictItemsByCorp("会员产品类型", LoginUserInfo.CompanyId);
}
        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表[如果公司字典记录不存在,则使用系统字典记录,否则使用公司记录]/// </summary>
        /// <param name="combo">下拉列表控件</param>
        /// <param name="dictTypeName">数据字典类型名称</param>
        public static void BindDictItemsByCorp(this ComboBoxEdit combo, string dictTypeName, stringcorpId)
{
BindDictItemsByCorp(combo, dictTypeName, corpId,
null);
}
/// <summary> ///绑定下拉列表控件为指定的数据字典列表[如果公司字典记录不存在,则使用系统字典记录,否则使用公司记录]/// </summary> /// <param name="combo">下拉列表控件</param> /// <param name="dictTypeName">数据字典类型名称</param> /// <param name="defaultValue">控件默认值</param> public static void BindDictItemsByCorp(this ComboBoxEdit combo, string dictTypeName, string corpId, stringdefaultValue)
{
Dictionary
<string, string> dict = CallerFactory<ICorpDictDataService>.Instance.GetDictByDictType(dictTypeName, corpId);
List
<CListItem> itemList = new List<CListItem>();foreach (string key indict.Keys)
{
itemList.Add(
newCListItem(key, dict[key]));
}

BindDictItems(combo, itemList, defaultValue);
}

以上就是一个整体性的思路,并在系统中能够顺利解决问题的做法,希望大家可以借鉴。

系列文章如下所示:

Web API应用架构在Winform混合框架中的应用(1)

Web API应用架构在Winform混合框架中的应用(2)--自定义异常结果的处理

Web API接口设计经验总结

Web API应用架构在Winform混合框架中的应用(3)--Winfrom界面调用WebAPI的过程分解

Web API应用架构在Winform混合框架中的应用(4)--利用代码生成工具快速开发整套应用

Web API应用架构在Winform混合框架中的应用(5)--系统级别字典和公司级别字典并存的处理方式

在一些应用场景中,我们可能需要记录某一天,某个时段的日程安排,那么这个时候就需要引入了DevExpress的日程控件XtraScheduler了,这个控件功能非常强大,提供了很好的界面展现方式,以及很多的事件、属性给我们定制修改,能很好满足我们的日程计划安排的需求,本文全面分析并使用这个控件,希望把其中的经验与大家分享。

1、日程控件的表现效果

整个日程控件,可以分为日视图、周视图、月视图等等,当然还有一些不常用的时间线、甘特图等,本例我们来关注控件的使用以及这几个视图的处理。先来看看他们的界面效果,如下所示。

日视图:

在视图里面,默认可以打开响应的日程事件进行编辑的。

周视图:

月视图:

2、日程控件XtraScheduler的使用

我们在上面展示了这个控件的几个视图的界面,一般情况下的控件使用还是很方便的,也就是直接拖拉SchedulerControl到Winform界面即可,但是我们为了符合我们的使用需求,还是需要设置不少属性或者事件的处理的。

1)几种视图的切换

由于控件,默认也是提供右键菜单,对几种控件视图进行切换的,如下菜单所示。

但是我们也可以通过代码进行切换处理,具体代码很简单,该控件已经进行了很好的封装,直接使用即可。

        private void btnDayView_Click(objectsender, EventArgs e)
{
//需要为日视图类型 this.schedulerControl1.ActiveViewType =SchedulerViewType.Day;
}
private void btnWeekView_Click(objectsender, EventArgs e)
{
//需要为周视图类型 this.schedulerControl1.ActiveViewType =SchedulerViewType.FullWeek;
}
private void btnMonthView_Click(objectsender, EventArgs e)
{
//需要为周视图类型 this.schedulerControl1.ActiveViewType =SchedulerViewType.Month;
}

2)设置禁用编辑、新增等功能处理

该日程控件,可以通过控件属性,对日程记录的新增、编辑、删除等菜单功能进行屏蔽或者开放(默认是开放的)。

通过控件属性的方式,操作如下所示。

当然我们也可以通过代码对这些属性进行设置,如下代码所示。

            SchedulerControl control = this.schedulerControl1;//禁用日程增加、删除、修改、拖拉等操作
            control.OptionsCustomization.AllowAppointmentCreate =DevExpress.XtraScheduler.UsedAppointmentType.None;
control.OptionsCustomization.AllowAppointmentDelete
=DevExpress.XtraScheduler.UsedAppointmentType.None;
control.OptionsCustomization.AllowAppointmentEdit
=DevExpress.XtraScheduler.UsedAppointmentType.None;
control.OptionsCustomization.AllowAppointmentDrag
=DevExpress.XtraScheduler.UsedAppointmentType.None;
control.OptionsCustomization.AllowAppointmentMultiSelect
= false;
control.OptionsRangeControl.AllowChangeActiveView
= false;
control.Views.MonthView.CompressWeekend
= false;
control.OptionsBehavior.ShowRemindersForm
= false;

3)日程控件的头部日期显示处理

默认的日程控件,其日视图、周视图的头部默认显示的是日期,如下所示。

如果需要把它修改为我们想要的头部内容(如加上星期几),那么就需要对这个头部显示进行自定义的处理才可以了。

有两种方式可以实现这个功能, 其一是引入一个自定义类,如下所示。

    public classCustomHeaderCaptionService : HeaderCaptionServiceWrapper
{
publicCustomHeaderCaptionService(IHeaderCaptionService service)
:
base(service)
{
}
public override stringGetDayColumnHeaderCaption(DayHeader header)
{
DateTime date
=header.Interval.Start.Date;return string.Format("{0:M}({1})", date, date.ToString("dddd",new System.Globalization.CultureInfo("zh-cn")));
}
}

然后在控件初始化后,添加对这个处理实现即可。

            //重载头部显示
            IHeaderCaptionService headerCaptionService = (IHeaderCaptionService)control.GetService(typeof(IHeaderCaptionService));if (headerCaptionService != null)
{
CustomHeaderCaptionService customHeaderCaptionService
= newCustomHeaderCaptionService(headerCaptionService);
control.RemoveService(
typeof(IHeaderCaptionService));
control.AddService(
typeof(IHeaderCaptionService), customHeaderCaptionService);
}

或者也可以重载CustomDrawDayHeader事件进行修改处理,如下所示。(推荐采用上面一种)

        private void schedulerControl1_CustomDrawDayHeader(objectsender, CustomDrawObjectEventArgs e)
{
//重绘Header部分,设置日程头部显示格式 SchedulerControl control = this.schedulerControl1;
SchedulerViewType svt
=control.ActiveViewType;if (svt == SchedulerViewType.Day || svt == SchedulerViewType.FullWeek ||svt== SchedulerViewType.Week || svt ==SchedulerViewType.WorkWeek)
{
DayHeader header
= e.ObjectInfo asDayHeader;
DateTime date
=header.Interval.Start;
header.Caption
= string.Format("{0}({1})", date.ToString("MM月d日"), date.ToString("dddd", new System.Globalization.CultureInfo("zh-cn")));
}
}

4)自定义菜单的处理

在日程控件XtraScheduler的使用中,我们也可以获取到控件的菜单对象,并对它进行修改、删除,或者新增自己的菜单事件也是可以的,我们实现事件PopupMenuShowing即可,这个事件在菜单显示前进行处理,如下面所示代码。

        private void schedulerControl1_PopupMenuShowing(objectsender, PopupMenuShowingEventArgs e)
{
//对日程的右键菜单进行修改 SchedulerControl control = this.schedulerControl1;if (e.Menu.Id ==DevExpress.XtraScheduler.SchedulerMenuItemId.DefaultMenu)
{
//隐藏【视图更改为】菜单 SchedulerPopupMenu itemChangeViewTo =e.Menu.GetPopupMenuById(SchedulerMenuItemId.SwitchViewMenu);
itemChangeViewTo.Visible
= false;//删除【新建所有当天事件】菜单 e.Menu.RemoveMenuItem(SchedulerMenuItemId.NewAllDayEvent);//设置【新建定期日程安排】菜单为不可用 e.Menu.DisableMenuItem(SchedulerMenuItemId.NewRecurringAppointment);//改名【新建日程安排】菜单为自定义名称 SchedulerMenuItem item =e.Menu.GetMenuItemById(SchedulerMenuItemId.NewAppointment);if (item != null) item.Caption = "新建一个计划";//创建一个新项,用内置的命令 ISchedulerCommandFactoryService service =(ISchedulerCommandFactoryService)control.GetService(typeof(ISchedulerCommandFactoryService));
SchedulerCommand cmd
= service.CreateCommand(SchedulerCommandId.PrintPreview);//打印预览 SchedulerMenuItemCommandWinAdapter menuItemCommandAdapter = newSchedulerMenuItemCommandWinAdapter(cmd);
DXMenuItem menuItem
=(DXMenuItem)menuItemCommandAdapter.CreateMenuItem(DXMenuItemPriority.Normal);
menuItem.BeginGroup
= true;
e.Menu.Items.Add(menuItem);
//创建一个新的自定义事件菜单 DXMenuItem menuTest = new SchedulerMenuItem("测试菜单");
menuTest.Click
+=menuItem2_Click;
menuTest.BeginGroup
= true;
e.Menu.Items.Add(menuTest);
}

}
void menuItem2_Click(objectsender, EventArgs e)
{
MessageDxUtil.ShowTips(
"测试菜单功能");
}

3、日程控件XtraScheduler的数据绑定

在日程控件里面,我们最重要,最关注的莫过于它的数据绑定及内容显示了,因为只有这样,我们才可以用于实价的应用当中,为用户显示他所需的数据,并存储我们所需要的数据。

在日程控件里面,有相应的引导我们进行这样的处理,还是非常不错的。

数据的绑定,我们需要了解日程控件的默认处理方式,因为它也提供了一些数据字段的信息,我们从控件的对象里面,看到有创建数据库的信息,里面有一些表的字段,我们可以参考来创建我们的数据存储信息,其中就包括了资源Resource的存储,日程事件安排Appointments的存储,如下所示。

根据这个里面的字段信息,我们可以建立自己的数据库模型如下所示。

在数据库里面创建这两个表,并根据这些表对象,使用代码生成工具Database2Sharp进行代码的快速生成,然后复制生成的代码到具体的测试项目里面,生成的代码无需任何修改即可直接使用在具体项目里面,测试项目如下代码结构所示。

如日程资源对象的数据库信息,就会转换为具体的实体类信息,供我们在界面中使用了,这样也符合我的Winform开发框架的实体类绑定规则,提高我们数据的强类型约束。

如资源对象的实体类代码生成如下所示。

   /// <summary>
    ///日程资源/// </summary>
[DataContract]public classAppResourceInfo : BaseEntity
{
/// <summary> ///默认构造函数(需要初始化属性的在此处理)/// </summary> publicAppResourceInfo()
{
this.ID = 0;this.ResourceId = 0;this.Color = 0;this.Image = new byte[] { };
}
#region Property Members[DataMember]public virtual int ID { get; set; }/// <summary> ///资源ID/// </summary> [DataMember]public virtual int ResourceId { get; set; }/// <summary> ///资源名称/// </summary> [DataMember]public virtual string ResourceName { get; set; }/// <summary> ///颜色/// </summary> [DataMember]public virtual int Color { get; set; }/// <summary> ///图形/// </summary> [DataMember]public virtual byte[] Image { get; set; }/// <summary> ///自定义/// </summary> [DataMember]public virtual string CustomField1 { get; set; }#endregion}

有了这些对象,我们还需要做的就是绑定控件和保存控件数据到数据库里面的处理。

但是这里还需要注意一个问题就是,这个日程控件数据是通过字段映射的方式进行数据绑定的,也就是它本身也提供了几个常规字段的信息,因此我们需要把它们的属性和数据库的字段(这里是实体类)的信息进行匹配。

如我们可以通过绑定如下,事项Appointments和Resources的Mappings处理。

        /// <summary>
        ///设置日程控件的字段映射/// </summary>
        /// <param name="control">日程控件</param>
        private voidSetMappings(SchedulerControl control)
{
AppointmentMappingInfo appoint
=control.Storage.Appointments.Mappings;
appoint.AllDay
= "AllDay";
appoint.Description
= "Description";
appoint.End
= "EndDate";
appoint.Label
= "AppLabel";
appoint.Location
= "Location";
appoint.RecurrenceInfo
= "RecurrenceInfo";
appoint.ReminderInfo
= "ReminderInfo";
appoint.ResourceId
= "ResourceId";
appoint.Start
= "StartDate";
appoint.Status
= "Status";
appoint.Subject
= "Subject";
appoint.Type
= "EventType";

ResourceMappingInfo res
=control.Storage.Resources.Mappings;
res.Caption
= "ResourceName";
res.Color
= "Color";
res.Id
= "ResourceId";
res.Image
= "Image";
}

确定控件属性和实体类之间关系后,我们就需要从数据库里面加载信息了。我们在窗体的代码里面增加两个资源对象的集合列表,如下代码所示。

        //日程资源集合和事件列表
        private List<AppResourceInfo> ResourceList = new List<AppResourceInfo>();private List<UserAppointmentInfo> EventList = new List<UserAppointmentInfo>();

然后就是把数据从数据库里面,通过开发框架底层的工厂类进行数据的提取,如下代码所示。

        private void btnLoadData_Click(objectsender, EventArgs e)
{
//从数据库加载日程信息 List<AppResourceInfo> resouceList = BLLFactory<AppResource>.Instance.GetAll();this.schedulerStorage1.Resources.DataSource =resouceList;

List
<UserAppointmentInfo> eventList = BLLFactory<UserAppointment>.Instance.GetAll();this.schedulerStorage1.Appointments.DataSource =eventList;if (resouceList.Count > 0)
{
MessageDxUtil.ShowTips(
"数据加载成功");
}
else{
MessageDxUtil.ShowTips(
"数据库不存在记录");
}
}

而保存数据,我们把对象里面的集合存储到数据库里面即可。

        private void btnSave_Click(objectsender, EventArgs e)
{
int count = BLLFactory<AppResource>.Instance.GetRecordCount();if (count == 0)
{
try{foreach (AppResourceInfo info inResourceList)
{
BLLFactory
<AppResource>.Instance.Insert(info);
}
foreach (UserAppointmentInfo info inEventList)
{
BLLFactory
<UserAppointment>.Instance.Insert(info);
}

MessageDxUtil.ShowTips(
"数据保存成功");
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
else{
MessageDxUtil.ShowTips(
"数据库已存在数据");
}
}

这样,通过代码工具Database2Sharp生成的代码,直接具有数据存储和获取的功能,例子就很容易明白和处理了,在实际的项目中,我们可能还需要存储用户的额外信息,如公司、部门、自定义信息等等,当然也可以通过这样的模式进行快速的开发,从而实现高效、统一、稳定的系统开发过程。

但是,言归正传,我们前面介绍的字段,都是控件里面有的内容,如果是控件里面没有,我们需要增加的自定义属性,那么我们应该如何处理呢,还有默认的日程界面可以修改吗,等等这些也是我们经常会碰到的问题。

首先我们在日程控件界面上,通过连接按钮的方式,创建一个自定义的日程窗体,如下所示

这样我们就可以看到,在项目里面增加了一个日程编辑框了,打开窗体界面,并增加一个自定义的控件内容,最终界面如下所示。

默认的后台代码里面,具有了LoadFormData和SaveFormData两个重载的方法,这里就是留给我们对自定义属性进行处理的方法体了。

我们在其中增加部分自定义属性字段的映射处理即可,如下代码所示。

        /// <summary>
        ///Add your code to obtain a custom field value and fill the editor with data./// </summary>
        public override voidLoadFormData(DevExpress.XtraScheduler.Appointment appointment)
{
//加载自定义属性 txtCustom.Text = (appointment.CustomFields["CustomField1"] == null) ? "" : appointment.CustomFields["CustomField1"].ToString();base.LoadFormData(appointment);
}
/// <summary> ///Add your code to retrieve a value from the editor and set the custom appointment field./// </summary> public override boolSaveFormData(DevExpress.XtraScheduler.Appointment appointment)
{
//保存自定义属性 appointment.CustomFields["CustomField1"] =txtCustom.Text;return base.SaveFormData(appointment);
}

然后我们记得在主体窗体的映射里面,为他们增加对应的字段映射即可,映射代码如下所示。

            AppointmentCustomFieldMappingCollection appointCust =control.Storage.Appointments.CustomFieldMappings;
appointCust.Add(
new AppointmentCustomFieldMapping("CustomField1","CustomField1"));

这样就构成了一个完整的映射信息。

        /// <summary>
        ///设置日程控件的字段映射/// </summary>
        /// <param name="control">日程控件</param>
        private voidSetMappings(SchedulerControl control)
{
AppointmentMappingInfo appoint
=control.Storage.Appointments.Mappings;
appoint.AllDay
= "AllDay";
appoint.Description
= "Description";
appoint.End
= "EndDate";
appoint.Label
= "AppLabel";
appoint.Location
= "Location";
appoint.RecurrenceInfo
= "RecurrenceInfo";
appoint.ReminderInfo
= "ReminderInfo";
appoint.ResourceId
= "ResourceId";
appoint.Start
= "StartDate";
appoint.Status
= "Status";
appoint.Subject
= "Subject";
appoint.Type
= "EventType";

AppointmentCustomFieldMappingCollection appointCust
=control.Storage.Appointments.CustomFieldMappings;
appointCust.Add(
new AppointmentCustomFieldMapping("CustomField1","CustomField1"));

ResourceMappingInfo res
=control.Storage.Resources.Mappings;
res.Caption
= "ResourceName";
res.Color
= "Color";
res.Id
= "ResourceId";
res.Image
= "Image";
}

以上就是我在整合日程控件XtraScheduler的经验总结,其中已经考虑了数据存储和显示,以及快速开发的几个方面,当然我们可以根据这些案例,做出更好的日程应用来了。

在上篇随笔《
在Winform开发中使用日程控件XtraScheduler
》中介绍了DevExpress的XtraScheduler日程控件的各种使用知识点,对于我们来说,日程控件不陌生,如OutLook里面就有日历的模块,但是这个日程控件真的是很复杂的一个控件,需要全面掌握可能需要花费很多的时间去了解,由于是技术研究,我总是希望把它常用的功能剖析的更加彻底一些,前面随笔也介绍了它的存储功能,把它基于实体类的方式存储在数据库里面,不过介绍的还不够,本文继续上面的内容,进行数据存储方面的介绍。

在查阅了大量资料,以及一两天的潜入研究,总算把它的数据存储和相关熟悉有一个比较清晰的了解。

1、数据绑定及加载的处理回顾

在上篇随笔里面,我总体性介绍了这个控件的数据绑定,以及数据是如何保存到数据库里面的,绑定到DevExpress的XtraScheduler日程控件的步骤是需要先设置好映射关系(Mappings),然后绑定数据源即可。

操作代码如下所示。

        /// <summary>
        ///设置日程控件的字段映射/// </summary>
        /// <param name="control">日程控件</param>
        private voidSetMappings(SchedulerControl control)
{
AppointmentMappingInfo appoint
=control.Storage.Appointments.Mappings;
appoint.AllDay
= "AllDay";
appoint.Description
= "Description";
appoint.End
= "EndDate";
appoint.Label
= "AppLabel";
appoint.Location
= "Location";
appoint.RecurrenceInfo
= "RecurrenceInfo";
appoint.ReminderInfo
= "ReminderInfo";
appoint.ResourceId
= "ResourceId";
appoint.Start
= "StartDate";
appoint.Status
= "Status";
appoint.Subject
= "Subject";
appoint.Type
= "EventType";

ResourceMappingInfo res
=control.Storage.Resources.Mappings;
res.Caption
= "ResourceName";
res.Color
= "Color";
res.Id
= "ResourceId";
res.Image
= "Image";
}

然后接着就是绑定Appointment和Resource到对应的数据源里面接口。

            //从数据库加载日程信息
            List<AppResourceInfo> resouceList = BLLFactory<AppResource>.Instance.GetAll();this.schedulerStorage1.Resources.DataSource =resouceList;

List
<UserAppointmentInfo> eventList = BLLFactory<UserAppointment>.Instance.GetAll();this.schedulerStorage1.Appointments.DataSource = eventList;

2、日程数据的增删改处理

但是,上面这样的存储在是实际上是比较少的,也就是我们往往可能会在界面上进行新增或者复制记录,修改记录,或者删除记录等操作,因此需要进一步利用日程控件的完善接口来处理这些操作。

我们在VS的对应控件属性里面可以看到一些关于存储的重要事件,也就是日程的增删改处理事件,如下所示。

上面这几个事件也就是对应在日程控件里面右键菜单对应的增删改操作。

另外日程控件还可以支持拖动修改、拖动复制、删除键删除操作的,这些也是会继续调用上面那些增删改的操作事件的,所以我们就对他们进行完善,我们重点是处理ing类型的事件,如Inserting的事件,在写入日程控件集合之前的处理。

            //写回数据库操作的事件
            control.Storage.AppointmentInserting +=Storage_AppointmentInserting;
control.Storage.AppointmentChanging
+=Storage_AppointmentChanging;
control.Storage.AppointmentDeleting
+= Storage_AppointmentDeleting;

对于修改数据前的处理,我们是让它在顺利写入数据库后,在决定是否更新日程对象的存储集合还是丢弃修改记录,如下所示。

        void Storage_AppointmentChanging(objectsender, PersistentObjectCancelEventArgs e)
{
Appointment apt
= e.Object asAppointment;
UserAppointmentInfo info
=ConvertToAppoint(apt);bool success = BLLFactory<UserAppointment>.Instance.Update(info, apt.Id);
e.Cancel
= !success;
}

注意上面的e.Cancel =true或者false代表是否放弃,上面的代码逻辑就是如果我们顺利写入数据库,那么就可以成功更新到日程控件的存储集合里面,而且就可以在界面看到最新的结果。

有了上面的理解,我们就可以进一步完善在插入前、删除前的代码处理了。

对于删除前的操作,我们可以用的代码如下所示。

        void Storage_AppointmentDeleting(objectsender, PersistentObjectCancelEventArgs e)
{
Appointment apt
= e.Object asAppointment;if (apt != null && apt.Id != null)
{
if (MessageDxUtil.ShowYesNoAndWarning("您确认要删除该记录吗?") ==DialogResult.Yes)
{
bool success = BLLFactory<UserAppointment>.Instance.Delete(apt.Id);
e.Cancel
= !success;
}
}
}

我们使用代码MessageDxUtil.ShowYesNoAndWarning来判断是否继续,如下界面所示。

对于插入的记录,我们需要更加注意,需要写入数据库后,进行本地的存储记录的更新,这样才能合理显示,否则容易发生复制、创建的记录位置总是不对,偏移到其他地方去的。

        void Storage_AppointmentInserting(objectsender, PersistentObjectCancelEventArgs e)
{
Appointment apt
= e.Object asAppointment;
UserAppointmentInfo info
=ConvertToAppoint(apt);bool success = BLLFactory<UserAppointment>.Instance.Insert(info);
e.Cancel
= !success;if(success)
{
LoadData();
}
}

LoadData就是我们从数据库加载日程信息,并绑定到日程控件的存储对象里面,其中需要注意的就是需要使用RefreshData方法,让日程控件的存储对象刷新一下,这样才能够顺利显示我们添加的记录。

            //从数据库加载日程信息
            List<AppResourceInfo> resouceList = BLLFactory<AppResource>.Instance.GetAll();this.schedulerStorage1.Resources.DataSource =resouceList;

List
<UserAppointmentInfo> eventList = BLLFactory<UserAppointment>.Instance.GetAll();this.schedulerStorage1.Appointments.DataSource = eventList;
            this.schedulerControl1.RefreshData();//必须,每次修改需要刷新数据源,否则界面需要重新刷新

3、多人资源的处理

在日程控件里面,支持多人资源的处理,默认是资源只能选择其一,需要多人的话,那么就需要设置下面的属性来显示声明使用多人资源,如下所示。

schedulerControl1.Storage.Appointments.ResourceSharing = true;

使用多人的资源,可以对资源进行复选,它的映射记录就是ResourceIds的了,所以设置映射属性的时候,我们需要判断这个ResourceSharing 属性。

            if(control.ResourceSharing)
{
appoint.ResourceId
= "ResourceIds";
}
else{
appoint.ResourceId
= "ResourceId";
}

其中ResourceId的内容格式如下所示

<ResourceIds>  <ResourceId Type="System.String" Value="1" /><ResourceId Type="System.String" Value="2" />  </ResourceIds>

和ResourceId不同这里的值就是一个XML内容,这个和提醒等内容的存储格式一样,都是基于XML的内容。日程控件涉及到的几种XML的信息获取如下所示。

            //多人资源的信息
            if(apt.ResourceIds != null)
{
AppointmentResourceIdCollectionContextElement item
= newAppointmentResourceIdCollectionContextElement(apt.ResourceIds);
info.ResourceIds
=item.ValueToString();//第二种//AppointmentResourceIdCollectionXmlPersistenceHelper helper = new AppointmentResourceIdCollectionXmlPersistenceHelper(apt.ResourceIds);//info.ResourceIds = helper.ToXml(); }//日程重复信息 if (apt.RecurrenceInfo != null)
{
info.RecurrenceInfo
=apt.RecurrenceInfo.ToXml();
}
//提醒信息 if (apt.Reminder != null)
{
info.ReminderInfo
=ReminderCollectionXmlPersistenceHelper.CreateSaveInstance(apt).ToXml();
}

以上就是我们经常用到的日程控件的处理内容了,希望对大家有所帮助。