wenmo8 发布的文章

在我这个系列中,我主要以我正在开发的云会员管理系统为例进行介绍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();
}

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

在Winform里面,很多控件元素都是标准的,如图标、按钮、工具栏等等,所以一般设计标准的Winform界面比较快捷,但是往往这样的界面相对单调一些,特别在界面控件比较少的情况下,我们往往需要加入一些图片、背景什么来衬托一下,看起来图文并茂一些,本文主要介绍,如何在Winfrom里面利用各种控件的特点,设计一个相对比较美观、图文并茂的Winform界面。

1、界面效果分析

在一些场景里面,如一些进销存的系统里面,我们往往把一些相关的模块处理放在一起,如进货、退货、库存调入、调出、产品、库存、盘点等这些信息,就是一个场景里面的内容,有时候可以把它们放在一起,但是如果单纯的放几个按钮,可能会显得比较单调一些,我们为了丰富界面效果,把界面设计效果如下所示。

这个界面虽说不是特别华丽,但是也是比仅仅放几个标准的按钮来的美观一些,而且按钮之间也设置了一些箭头来指示他们的流程或者从属关系,因此在一些场合,我们更倾向使用这种直观、清晰的界面表达方式,同时也增加了一定的美感。

2、界面的设计过程

在上图的界面里面,设计过程也不麻烦,就是首先确定需要在界面上绘制什么,放置那些按钮,规划好这些后,就可以着手做界面的工作了。

我们分析下上面的界面,就是Winform界面上,放置一个背景图片,然后在界面适当的位置上添加对应的图片或者按钮即可,让对应的图标处理单击事件基本上就可以了,比较简单吧,呵呵。

1)绘制背景图片

为了达到最佳的界面效果,但我们不能再Winform界面上绘画,就只有退而求其次,在绘图工具上绘制一些较好的图形作为界面背景了,如可以使用强大万能的Photoshop,也可以使用Word,或者也可以使用其他绘图工具,能抓到老鼠的就是好猫!

我这里喜欢利用一些有现成图元的绘图工具如EDraw Max等等这些来绘制背景,预先留出放置图标按钮的位置,我们绘制一个背景如下所示。

绘制好这些背景,截图出来待用,记住要图片要预留一定的位置,并且具有一定的画布大小,我们想图片按正常方式设为背景即可,这样我们在窗口放大的时候,不至于直接看完图片了,截取出来的图片,左上角预留一定的位置即可。

2)设置窗体背景本增加图片

在我们预先新建的窗体上面,设置它的BackgroundImage背景图片,如下图所示。

上面效果是增加了图片按钮的效果,开始的话,是没有图片按钮的,这些是后来增加上去空白位置的,图标按钮,一般为了好看的效果,传统的Winform可以采用PictureBox,基于DevExpress的,则可以利用LabelControl标签控件的来处理,设置它的图片即可达到很好的效果的。

LabelControl几个设置如下所示。

还有需要设置ImageAlignToText的属性设置为TopCenter,这样图标才能很好的居中显示。

上面的图片是64×64的,一般可以在网站上根据关键字找一些自己喜欢的图标,下面几个图标搜索的网站如下所示。

http://www.iconfinder.com/

http://www.iconpng.com/

http://www.iconlet.com/

http://findicons.com/

http://www.iconseeker.com/

http://www.iconarchive.com/

从上面总能找到一些合适、免费的各种文件类型的图标,我们把它放到合适的场景上就非常棒的了。

背景及图片按钮设置好后,我们就可以处理它的单击事件,可以弹出对应的窗体了,有两种弹出窗体方式,一种是模式对话框,如下代码所示。

        private void btnPurchase_Click(objectsender, EventArgs e)
{
FrmPurchase dlg
= newFrmPurchase();
dlg.ShowDialog();
}

另一种是,打开放在多文档界面里面显示,如下代码所示。

        private void btnStock_Click(objectsender, EventArgs e)
{
ChildWinManagement.LoadMdiForm(Portal.gc.MainDialog,
typeof(FrmStock));
}
private void btnProduct_Click(objectsender, EventArgs e)
{
ChildWinManagement.LoadMdiForm(Portal.gc.MainDialog,
typeof(FrmProduct));
}

最后我们可以得到整体的界面体验效果如下所示。

以上就是图形化界面的设计点滴思路,希望对大家设计界面有所帮助。

在很多业务数据处理的场合,整条数据链的数据完整性是非常重要的,因为我们在系统里面,往往需要同时更新或者写入一些数据,如果其中任何一环处理错误,都应该逐条滚回,这种原子性的确保就是通过事务来进行的,本文介绍的这个事务处理,适用于我的所有开发框架,如Winform开发框架、混合式开发框架、Web框架等,本文主要介绍基于我的会员系统的一些事务处理案例,对事务的使用进行介绍和代码讲解。

由于上面介绍的这些框架都是基于业务逻辑层BLL层之上的,如复杂一点的混合式框架,在BLL层之上还有一个WCF服务层、或者Web API的数据提供层,因此为了适应多种框架的适用性,我们建议把业务规则封装在BLL层,这样各种应用框架使用的时候,代码就不用修改很多,而且业务逻辑统一,也很方便理解。

1、框架的事务支持

上面我提到的几个框架,在底层我们主要是采用了微软的Enterprise Library的数据库访问模块,因此它能够很好抽象各种数据库的事务,以适应各种不同数据库的事务处理。使用微软的Enterprise Library模块,可以很好支持SQLSever、Oracle、Mysql、Access、SQLite等数据库。

开发框架,常见的分层模式,可以分为UI层、BLL层、DAL层、IDAL层、Entity层、公用类库层等等

框架的基类我们封装了大量的通用性处理函数,包括数据访问层、业务逻辑层的基类,所有的基类函数基本上都带有一个
DbTransaction trans = null
的定义,就是我们可以采用事务,也可以默认不采用事务,是一个可选性的事务参数。

如数据访问类的部分接口定义如下所示。

    /// <summary>
    ///数据访问层的接口/// </summary>
    public interface IBaseDAL<T> whereT : BaseEntity
{
/// <summary> ///插入指定对象到数据库中/// </summary> /// <param name="obj">指定的对象</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回True</returns> bool Insert(T obj, DbTransaction trans = null);/// <summary> ///根据指定对象的ID,从数据库中删除指定对象/// </summary> /// <param name="key">指定对象的ID</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> bool Delete(object key, DbTransaction trans = null);/// <summary> ///更新对象属性到数据库中/// </summary> /// <param name="obj">指定的对象</param> /// <param name="primaryKeyValue">主键的值</param> /// <param name="trans">事务对象</param> /// <returns>执行成功返回<c>true</c>,否则为<c>false</c></returns> bool Update(T obj, object primaryKeyValue, DbTransaction trans = null);/// <summary> ///查询数据库,检查是否存在指定ID的对象/// </summary> /// <param name="key">对象的ID值</param> /// <param name="trans">事务对象</param> /// <returns>存在则返回指定的对象,否则返回Null</returns> T FindByID(object key, DbTransaction trans = null);

.....................
//其他操作 }

数据访问接口和数据访问类的实现图示如下所示。

因此在最高级的抽象基类AbstractBaseDAL的数据访问层里面,都有大量关于事务的接口可以使用,同样在BLL层也一样,里面大多数就是基于对AbstractBaseDAL的包装处理,同样预留了
DbTransaction trans = null
的参数方便我们进行事务性的处理。

同样,在BLL层的基类BaseBLL里面的接口,也同样包含了这样的事务参数的,因此在框架里面整合事务是一件非常方便的事情,因为所有接口都预留了使用事务的可能,方便我们整合各个方法。

2、框架事务接口的使用

在框架的DAL层和BLL层,我们都定义了很多公用的、带有事务参数的接口,如果我们在业务处理里面,使用事务的话,那么也是很方便的事情。如在

事务的处理,除了包含对数据库的更新修改需要使用外,如果在事务的处理中,查询数据等接口也要一并采用事务处理,否则就容易发生锁住堵塞的情况,因此,如果使用事务,那么事务参数是全程需要传入来进行数据的检索、修改等操作的。

事务的标准使用过程,一般是先创建事务对象,然后在各个接口里面使用事务处理,如下代码示例所示。

            DbTransaction trans = base.CreateTransaction();if (trans != null)
{
string sql =.............;base.SqlExecute(sql, trans);

sql
=.............;base.SqlExecute(sql, trans);try{
trans.Commit();
result
= true;
}
catch{
trans.Rollback();
throw;
}
}

如果我们定义的处理接口,只是其中事务处理的一个环节,那么我们就需要在接口定义的时候,预留有
DbTransaction trans = null
的参数,示例代码如下所示。

        /// <summary>
        ///添加积分/// </summary>
        /// <param name="id">会员ID</param>
        /// <param name="points">积分(正数为加,负数为减)</param>
        /// <returns></returns>
        public bool AddPoints(string id, decimal points, DbTransaction trans = null)
{
string sql = string.Format("Update {0} Set TotalPoints = TotalPoints + {1} Where ID='{2}'", tableName, points, id);return SqlExecute(sql, trans) > 0;
}
/// <summary> ///添加消费金额/// </summary> /// <param name="id">会员ID</param> /// <param name="amount">消费金额(正数为加,负数为减)</param> /// <returns></returns> public bool AddConsumption(string id, decimal amount, DbTransaction trans = null)
{
//消费的时候,同时修改累计消费金额和最后到店时间 string sql = string.Format("Update {0} Set TotalConsumption = TotalConsumption + {1}, LastConsumptionDate=getdate() Where ID='{2}'", tableName, amount, id);return SqlExecute(sql, trans) > 0;
}

在BLL层,我们同样可以对这些接口预留事务接口,对数据访问的DAL 层进行封装的。

        /// <summary>
        ///添加积分/// </summary>
        /// <param name="id">会员ID</param>
        /// <param name="points">积分(正数为加,负数为减)</param>
        /// <returns></returns>
        public bool AddPoints(string id, decimal points, DbTransaction trans = null)
{
IMember dal
= baseDal asIMember;returndal.AddPoints(id, points, trans);
}
/// <summary> ///添加消费金额/// </summary> /// <param name="id">会员ID</param> /// <param name="amount">消费金额(正数为加,负数为减)</param> /// <returns></returns> public bool AddConsumption(string id, decimal amount, DbTransaction trans = null)
{
IMember dal
= baseDal asIMember;returndal.AddConsumption(id, amount, trans);
}

预留这样的事务参数,对于我们在整合各个处理过程有很大的帮助,否则任何一环没有事务参数,处理就可能出现问题,这也是为什么框架的基类参数,都提供了一个事务参数的可选参数,就是这个道理。

如整合上面积分和金额的处理过程,就是会员系统里面必要的一环,而且这些数据需要连贯在一起进行修改,不能再中间任何一个环节发生数据部分丢失或者不成功的操作,因此我们必须使用事务处理过程。

例如,我在会员消费的过程处理中,其中包含了对消费商品主表、明细表的记录,还包括对库存的调整、还有会员消费、积分赠送等等细粒度的处理,因此我们在事务里面可以按照下面的方式进行处理。

                DbTransaction trans = base.CreateTransaction();if (trans != null)
{
try{bool success = BLLFactory<MemberConsumption>.Instance.Insert(info, trans);if(success)
{
//保存消费明细记录 foreach (ConsumptionDetailInfo detail indetailList)
{
BLLFactory
<ConsumptionDetail>.Instance.Insert(detail, trans);//减少对应库存 string note = "会员消费";double quantity = (-1) *detail.Quantity;
BLLFactory
<Stock>.Instance.ModifyQuantity(info.Corp_ID, info.Shop_ID, info.Creator, detail.ProductNo, quantity, note, trans);
}
//增加消费金额 BLLFactory<Member>.Instance.AddConsumption(info.Member_ID, info.Amount, trans);//添加积分(消费一元积一分) BLLFactory<Member>.Instance.AddPoints(info.Member_ID, info.Amount, trans);//减少余额 decimal subBalance = (-1) *info.Amount;
BLLFactory
<Member>.Instance.AddBalance(info.Member_ID, subBalance, trans);
}

trans.Commit();
result.Success
= true;
}
catch(Exception ex)
{
LogTextHelper.Error(ex);
trans.Rollback();
result.ErrorMessage
=ex.Message;
}
}

上面的方法就是一个完整的事务处理过程,就是在会员消费的情况下发生的,如果我们需要考虑多种应用框架的封装,如Web、Web API、Winform、WCF等方式的调用,那么我们就把它放到了业务逻辑层BLL层进行封装,如下就是BLL层的方法。

        /// <summary>
        ///保存消费记录,同时修改库存/// </summary>
        /// <param name="info">消费主记录</param>
        /// <param name="detailList">消费明细列表</param>
        /// <returns></returns>
        public CommonResult SaveConsumption(MemberConsumptionInfo info, List<ConsumptionDetailInfo> detailList)

这样我们在Web API的调用的时候,就可以不在使用这个特定的
DbTransaction trans = null
参数了,因此上面的方法体就是一个最小的操作单元了。

3、混合框架中Web API的封装和调用

在我的混合式开发框架基础上,我们服务提供可以是传统Winform、WCF,以及WebAPI的方式,框架的效果图如下所示。

对于Web API模式,我们对BLL业务逻辑层进行了封装,它的APIController的方法也就是说如下所示。

        /// <summary>
        ///保存消费记录,同时修改库存/// </summary>
        /// <param name="info">消费主记录</param>
        /// <param name="detailList">消费明细列表</param>
        /// <returns></returns>
[HttpPost]public CommonResult SaveConsumption(JObject param, string token, string signature, string timestamp, string nonce, stringappid)
{
//如果用户签名检查不通过,则抛出MyApiException异常。 base.CheckTokenAndSignatrue(token, signature, timestamp, nonce, appid);dynamic obj =param;if (obj != null)
{
MemberConsumptionInfo info
=obj.info;
List
<ConsumptionDetailInfo> detailList =obj.detailList;return BLLFactory<MemberConsumption>.Instance.SaveConsumption(info, detailList);
}
else{throw new MyApiException("传递参数错误");
}
}

这里面的方法有很多参数,第一个JObject param是一个动态对象的定义参数,具体可以参考《
Web API接口设计经验总结
》里面介绍的“动态对象的接口定义”节点了解。

由于这个处理过程是对数据进行了修改等重要的处理,因此参数需要增加签名数据,以及Token身份的标识,而且整个接口是公布在HTTPS协议的基础上,因此 接口的安全性是得到了非常好的保证。

对于在混合框架中,访问Web API的接口安全性方面,可以参考我前面的文章《
Web API应用架构在Winform混合框架中的应用(1)
》的“Web API访问的安全性考虑”节点了解。整个框架里面,最有保证、最方便的、适应最广泛应用的就是基于Web API的方式接入了,它不仅可以在桌面程序进行处理,也可以在移动端(包括APP和微信公众号等),使用Web API的接口进行数据的获取和提交。WCF方式虽然功能强大,但是相对显得笨重一些,而且数据安全性,需要服务端和客户端采用X509证书进行通讯,对移动端则是很难的一件事情。

言归正传,上面的事务处理,在它的基础上Web API接口进行了封装,我们调用Web API就不需要进行事务的参数传递了,因为它已经是一个操作的整体了,要么成功,要么全部失败滚回即可。

为了适应在混合框架的Winform里面进行调用,我们还是需要对刚才的Web API接口进行了客户端的封装,给Web API传递对应的参数(通过POST方式提交JSON参数给Web API接口),具体的代码如下所示。

        public CommonResult SaveConsumption(MemberConsumptionInfo info, List<ConsumptionDetailInfo>detailList)
{
var action = "SaveConsumption";var postData = new{
info
=info,
detailList
=detailList
}.ToJson();
string url =GetPostUrlWithToken(action);return JsonHelper<CommonResult>.ConvertJson(url, postData);
}

这个过程就是封装了对Web API的调用,并通过JSON数据返回的方式,把他们转换为对应的结果对象,这里的结果是一个通用的结果集对象CommonResult 。

这样我们在Winform的客户端里面就有了统一的调用方式了,非常方便简洁,代码如下所示。

                //获取消费明细
                List<ConsumptionDetailInfo> detailList =GetConsumptionDetail();//保存消费明细,以及在后台利用事务处理各种关系的修改
                CommonResult result = CallerFactory<IMemberConsumptionService>.Instance.SaveConsumption(info, detailList);

最后来一个基于Web API的云会员管理系统的界面作为佐证,这个界面就是使用上面的事务实现多种数据关系的处理的。

开发框架中使用事务处理的文章介绍:

1)
Winform开发框架里面使用事务操作的原理及介绍

2)
Winform开发框架之通用数据导入导出操作的事务性操作完善

3)
使用事务操作SQLite数据批量插入,提高数据批量写入速度,源码讲解

关于Web API的知识和框架使用方面的文章,可以参考下面系列:

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

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

Web API接口设计经验总结

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

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

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