2023年2月

最近一直致力于Winform开发框架的重构工作,因为发现要维护传统Winform开发框架、WCF开发框架、混合式开发框架,以及相关的模块,包括权限管理、字典管理模块、附件管理、人员管理等一些辅助模块,很多代码都会有重复的部分,优化的框架是想提高效率,减少冗余重复代码,本文总结Winform开发框架重构工作中的一些经验总结,以飨读者,希望能够对大家有一定的参考作用。

1、公用类库的分离处理

我的公用类库是自己开发这么多年的总结、收集和整理,对大多数的类库均进行优化整理过,公用类库的本意是对.NET内置的类库进行包装使用,提高使用效率和减少复杂性,随着开发项目的增多和不断的总结,有时候一些常用的第三方类库包装类也很常见,使用很频繁,经常在各个模块中使用,因此把框架中常用到的类库分为两类,一个是".NET内置类公用类库“,一个是第三方类库的包装类,如我经常用到的Aspose.Cell、Apose.Word、NPOI、Myxls等Office相关类库的包装类,还有Log4Net日志类、Zip压缩类库等等,如下界面是一个截图。

这样虽然在管理上增加多了一个公用类库的程序集,但是这样区分有利于我们对类库的扩展和维护。

2、Winform框架基类的封装和独立

在这次的重构工作中,很大程度上是提取所有框架和模块中用到的各种基类到一个第三方类库包装类里面,然后在框架里面统一使用这个类库,如原来数据访问里面常用到的BaseBLL、BaseDAL、IBaseDAL、AbstractBaseDAL,其中BaseDAL虽然继承了AbstractBaseDAL绝大多数的方法,但是不同的数据库还是有一些小差异的,因此把BaseDAL分为了几个不同数据库版本的BaseDAL,包括Access、MySql、Oracle、SqlServer、Sqlite等数据库的基类。

这样把它们独立出来,不用再每个数据访问的模块都复制一份,而且可以方便统一维护和升级,因为基类和接口一旦增加,所有的业务类都会同时具有增加的功能,非常利于维护扩展。除了数据访问相关的基类,还对WCF服务等相关的基类也进行了抽取,这样在WCF框架或者混合型框架中,就可以直接使用了。

3、在框架中使用封装独立的基类

1)传统Winform框架的对象

把这些框架中常用到的类库独立抽取出来后,整个框架文件就比较简洁很多了,也不用再多个模块经常使用BeyondCompare比较来比较去的,框架业务层和数据访问层的项目截图如下所示,业务层除了有一个BLLFactory类外,其他的类都是常规的业务处理对象,BLLFactory没有提取到公用类库,是因为需要当前执行类的一些信息来构造业务对象和数据访问对象;SqlServer数据访问层则没有任何多余的辅助类库。


其中SqlServer数据访问层的类,类定义的部分代码如下所示。

usingWHC.Pager.Entity;usingWHC.WareHouseMis.Entity;usingWHC.WareHouseMis.IDAL;usingWHC.Framework.Commons;usingWHC.Framework.ControlUtil;usingMicrosoft.Practices.EnterpriseLibrary.Data;namespaceWHC.WareHouseMis.DALSQL
{
/// <summary> ///备件信息数据访问类/// </summary> public class ItemDetail : BaseDALSQL<ItemDetailInfo>, IItemDetail
{
......................

2)混合型框架的对象

在混合型框架对于WCF或者传统Winfrom数据访问中,我在其中定义了一个通用的接口层---Facade层,然后分两种实现方式,其中Facade层的接口项目文件如下所示,其中可以看到,除了CallerFactory类外,其他部分均为接口定义,基类接口已经抽取到独立的类库里面去了。

混合型框架的调用示例代码如下所示,其中和传统的Winform调用BLLFactory一样,这里只需要调用CallerFactory构造类即可,而传入给CallerFactory的是Facade层的接口,工厂会根据配置参数进行相应的对象构造,从而实现基于传统本地的数据库访问或者分布式的WCF数据访问方式的综合处理。

            bool exist = CallerFactory<IItemDetailService>.Instance.CheckExist(this.txtItemNo.Text, ID);if(exist)
{
MessageDxUtil.ShowTips(
"指定的备件编号已经存在,不能重复添加,请修改");return false;
}

ItemDetailInfo info
= CallerFactory<IItemDetailService>.Instance.FindByID(ID);if (info != null)
{

.....................................

其中Facade层的CallerFactory对象,会根据配置信息,寻找并构建相应的实现类,有可能是传统的数据访问类,也可能是WCF的数据访问类,这两种实现类的定义如下所示。

usingWHC.WareHouseMis.Entity;usingWHC.WareHouseMis.BLL;usingWHC.WareHouseMis.Facade;usingWHC.Framework.ControlUtil;usingWHC.Framework.ControlUtil.Facade;namespaceWHC.WareHouseMis.WinformCaller
{
public class ItemDetailCaller : BaseLocalService<ItemDetailInfo>, IItemDetailService
{
private ItemDetail bll = null;public ItemDetailCaller() : base(BLLFactory<ItemDetail>.Instance)
{
bll
= baseBLL asItemDetail;
}
#region IItemDetailService 成员 public List<ItemDetailInfo> FindByBigType(stringbigType)
{
returnbll.FindByBigType(bigType);
}
........................

而对于WCF方式的实现类方式如下所示

usingWHC.WareHouseMis.Entity;usingWHC.WareHouseMis.Facade;usingWHC.Framework.Commons;usingWHC.Framework.ControlUtil.Facade;namespaceWHC.WareHouseMis.ServiceCaller
{
public class ItemDetailCaller : BaseWCFService<ItemDetailInfo>, IItemDetailService
{
public ItemDetailCaller() : base()
{
this.configurationPath =EndPointConfig.WcfConfig;this.endpointConfigurationName =EndPointConfig.ItemDetailService;
}
public List<ItemDetailInfo> FindByBigType(stringbigType)
{
List
<ItemDetailInfo> result = new List<ItemDetailInfo>();

IItemDetailService service
=CreateSubClient();
ICommunicationObject comm
= service asICommunicationObject;
comm.Using(client
=>{
result
=service.FindByBigType(bigType);
});
returnresult;
}
..............

4 、使用引用文件方式代替复制文件方式

在一个框架里面,为了减少程序集的数量和多个引用,可能会有多处需要使用同一个文件,这样就可以使用文件引用的方式,在VS里面添加现有文件的时候,选择

如由于我的某一个框架里面,为了减少程序集的数量,我把很多相关的类库集成在一起,形成一个单一的程序集就具有很多功能,但这样的代码虽然有很多个影子,但是肉身只有一个,方便维护。

这样对于某个文件的多处使用,但是统一维护很方便。

5、代码生成工具Database2Sharp的支持

以上的一些对象继承关系和框架的整体代码,项目工程之间程序集的引用,这些手工操作的话,肯定效率大打折扣,因此代码生成工具的支持是非常必要的工作,本框架系列的最新继承关系,全部能够使用Database2Sharp的完美支持,从而使得我们在日常开发过程中,享受快速、高效、统一的代码生成带来的乐趣。

以上就是我在框架重构中的一些总结,希望能给大家有所启发,有所帮助。最后附上Winform开发框架的一个功能总结图形,Winform开发框架的主要功能概览如下图所示。

最近花了很多时间在重构和进一步提炼Winform开发框架的工作上,加上时不时有一些项目的开发工作,我博客里面介绍Web开发框架的文章比较少,其实以前在单位工作,80%的时间是做Web开发的,很早就形成了自己的一套Web开发框架,但是由于一些个人原因,一直没有来得及好好整理和推广,其实那套Web开发框架对大多数企业信息化还是很有帮助的,包括了整体性的框架布局、样式,以及权限控制、菜单管理、字典管理、工作流审批管理等常见模块,不过总体觉得技术上是采用asp.net的Webform技术,因此现在MVC大行其道,一直想把它彻底改造过来,本文是介绍基于MVC4+EasyUI的最新版Web开发框架的一系列形成过程,期间包括一些MVC4技术,EasyUI技术,JQuery技术,以及一些我对整体框架改造的基础性东西,力求更加稳定、通用、高效、简洁,最少的代码做最多的事情。

1、旧的Web开发框架回顾

在原先的Web框架里面,主要是采用FrameSet的原始方式进行布局,很多内容依靠Javascript类库进行操作,小部分采用了EasyUI的一些特性,总体来说,是比较传统的一种框架模式,这个框架里面我已经集成了用户角色等权限方面的管理和控制、菜单管理、字典管理、业务流程审批管理等模块,因此对开发常规的行业应用有着比较快的开发效率,不过缺点也比较明显,就是在多浏览器支持方面,没有做的很好,框架里面采用的布局、样式及技术等方面不够统一,不够新颖,但即使这样,这套框架也顺利用来开发了几套很大规模的行业应用了,项目的大概布局和样式,如下所以。

在Ajax技术如火如荼的时候,我也常常觉得,应该结合一些新的技术特点,来改造一下我自己的框架,因此,使用了纯粹的AJax技术+Asp.NET的“一般处理程序”,也就是ashx的C#后台处理 + HTML的页面来实现数据的处理和显示操作,于是我调整开发了Web的权限系统作为实验(详细可参考《
Web开发框架之权限管理系统
》一文的介绍,界面效果如下所示。

这种框架的布局其实已经是利用了比较潮的EasyUI界面布局,得到一些老客户的支持和鼓励,不过我觉得如果把框架全部调整为这种HTML+AJax+ASHX处理方式,我觉得还是挺麻烦的,而且逐渐MVC这一技术已经越来越成熟和应用更广泛了,因此整体性使用MVC来处理,可能能够带来更好的效率和减少复杂性。

不过我觉得使用EasyUI来实现这样的布局,以及模块的分页面展示(类似Winform的多文档管理,且可以右键菜单进行多种关闭页面)的方式,对一些企业应用、行业管理系统、单位信息化系统,应该是比较不错的界面展示了,相对于上面第一种的框架界面,这种更具有浏览器兼容性,能够适应多浏览器的展现,这样整个世界就清净很多了。

2、基于MVC+EasyUI的框架效果

因此为了搭建整个MVC+EasyUI等技术结合的Web开发框架,就从我原先开发的Web的通用权限管理系统进行改进,来实现整个开发框架的预演了。首先我利用Winform权管理系统的业务逻辑等底层模块,在这个基础上开发基于MVC的Web项目,也就是界面层以下的逻辑我们暂且不考虑,专注MVC带来的变化。

下面的项目工程文件视图,是我做好的,基于MVC4+EasyUI的最新的Web权限管理系统。

运行系统后,登录界面和主界面如下所示。

我们知道,一般信息管理系统都是需要分页显示,编辑,查看等内容的,这些使用EasyUI来实现效果还是比较好的。下面我以一个登录日志的模块来展示分页处理和弹出对话框,展示信息。

编辑界面效果如下所示。

如果查看信息,则不需要编辑框了,直接用Lable展示即可,如下所示。

这些就是整个改造后的权限系统里面的一些截图界面,权当做一个总体性的概括,不设计技术实现的任何东西,下篇开始,着重介绍MVC项目中设计到的一些技术实现和经验总结,希望大家多多支持。

基于MVC4+EasyUI的Web开发框架的系列文章:

基于MVC4+EasyUI的Web开发框架形成之旅--总体介绍


基于MVC4+EasyUI的Web开发框架形成之旅--MVC控制器的设计

基于MVC4+EasyUI的Web开发框架形成之旅--界面控件的使用

基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用

基于MVC4+EasyUI的Web开发框架形成之旅--框架总体界面介绍

基于MVC4+EasyUI的Web开发框架形成之旅--基类控制器CRUD的操作

基于MVC4+EasyUI的Web开发框架形成之旅--权限控制

基于MVC4+EasyUI的Web开发框架经验总结(1)-利用jQuery Tags Input 插件显示选择记录

基于MVC4+EasyUI的Web开发框架经验总结(2)- 使用EasyUI的树控件构建Web界面

基于MVC4+EasyUI的Web开发框架经验总结(3)- 使用Json实体类构建菜单数据

基于MVC4+EasyUI的Web开发框架经验总结(4)--使用图表控件Highcharts

基于MVC4+EasyUI的Web开发框架经验总结(5)--使用HTML编辑控件CKEditor和CKFinder

基于MVC4+EasyUI的Web开发框架经验总结(6)--在页面中应用下拉列表的处理

基于MVC4+EasyUI的Web开发框架经验总结(7)--实现省份、城市、行政区三者联动

基于MVC4+EasyUI的Web开发框架经验总结(8)--实现Office文档的预览

基于MVC4+EasyUI的Web开发框架经验总结(9)--在Datagrid里面实现外键字段的转义操作

基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出

基于MVC4+EasyUI的Web开发框架经验总结(11)--使用Bundles处理简化页面代码

基于MVC4+EasyUI的Web开发框架经验总结(12)--利用Jquery处理数据交互的几种方式

基于MVC4+EasyUI的Web开发框架经验总结(13)--DataGrid控件实现自动适应宽带高度

基于MVC4+EasyUI的Web开发框架经验总结(14)--自动生成图标样式文件和图标的选择操作

自从上篇《
基于MVC4+EasyUI的Web开发框架形成之旅--总体介绍
》总体性的概括,得到很多同行的关注和支持,不过上一篇主要是介绍一个总体的界面效果和思路,本系列的文章将逐步介绍其中的细节,本文主要介绍整个Web开发框架中的MVC控制器的设计。在设计之初,我就希望尽可能的减少代码,提高编程模型的统一性。因此希望能够以基类继承的方式,和我Winform开发框架一样,尽可能通过基类,而不是子类的重复代码来实现各种通用的操作。

1、登录控制的控制器基类设计

我们知道,一般我们创建一个MVC的控制器,都是基于Controller这样的基类来实现。如下代码所示。

    public classTestController : Controller
{
// //GET: /Test/ publicActionResult Index()
{
returnView();
}

}

在我的Winform开发框架里面,用到了泛型的类型,非常方便实现业务逻辑和数据访问基类的设计,控制器是否也可以这样做的呢?

我们知道,一般的MVC控制器需要验证用户是否已经登陆了,这也是很多常见Web操作前的验证,还有对异常的处理,在MVC的基类,可以一并进行记录(这个非常不错),于是我们先来设计一个验证用户身份是否登陆的基类BaseController

    /// <summary>
    ///所有需要进行登录控制的控制器基类/// </summary>
    public classBaseController : Controller 
{
/// <summary> ///当前登录的用户属性/// </summary> public UserInfo CurrentUserInfo { get; set; }/// <summary> ///重新基类在Action执行之前的事情/// </summary> /// <param name="filterContext">重写方法的参数</param> protected override voidOnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);//得到用户登录的信息 CurrentUserInfo = Session["UserInfo"] asUserInfo;//判断用户是否为空 if (CurrentUserInfo == null)
{
Response.Redirect(
"/Login/Index");
}
}
protected override voidOnException(ExceptionContext filterContext)
{
base.OnException(filterContext);//错误记录 WHC.Framework.Commons.LogTextHelper.Error(filterContext.Exception);//当自定义显示错误 mode = On,显示友好错误页面 if(filterContext.HttpContext.IsCustomErrorEnabled)
{
filterContext.ExceptionHandled
= true;this.View("Error").ExecuteResult(this.ControllerContext);
}
}
........................
}

有了这个基类,我们在主页的Home控制类,就可以使用用户信息对象了进行操作了,而且必须要求客户登陆了。

    public classHomeController : BaseController
{
publicActionResult Index()
{
if (CurrentUserInfo != null)
{
ViewBag.FullName
=CurrentUserInfo.FullName;
ViewBag.Name
=CurrentUserInfo.Name;
}
returnView();
}
................
}

2、数据访问业务基类
控制器
的设计

我在我的Winform开发框架里面,对很多基类都使用泛型进行设计,这样可以传递相应的数据类型到基类里面进行处理,如下面的BLL层的业务对象定义代码如下所示。

namespaceWHC.Security.BLL
{
/// <summary> ///角色信息业务管理类/// </summary> public class Role : BaseBLL<RoleInfo>{

....................
    /// <summary>
    ///业务基类对象/// </summary>
    /// <typeparam name="T">业务对象类型</typeparam>
    public classBaseBLL<T> where T : BaseEntity, new()
{
/// <summary> ///插入指定对象到数据库中/// </summary> /// <param name="obj">指定的对象</param> /// <returns>执行操作是否成功。</returns> public virtual boolInsert(T obj)
{
returnbaseDal.Insert(obj);
}

............

业务对象Role,要求传入RoleInfo给基类处理,这样基类就能定义到都对应的T为具体的RoleInfo类型了。在MVC的控制器是否也可以这样做呢?当然可以,下面是我定义的一个控制器继承关系图。

上面的介绍也已经比较明白了,其实就是在BusinessController<B, T>里面传入了两个参数,定义代码如下所示。

    /// <summary>
    ///本控制器基类专门为访问数据业务对象而设的基类/// </summary>
    /// <typeparam name="B">业务对象类型</typeparam>
    /// <typeparam name="T">实体类类型</typeparam>
    public class BusinessController<B, T>: BaseControllerwhere B : class
        where T : WHC.Framework.ControlUtil.BaseEntity, new()
{
/// <summary> ///插入指定对象到数据库中/// </summary> /// <param name="info">指定的对象</param> /// <returns>执行操作是否成功。</returns> public virtualActionResult Insert(T info)
{
bool result = false;if (info != null)
{
result
=baseBLL.Insert(info);
}
returnContent(result);
}

................

我根据传入的BLL业务对象类型B,对象实体类类型T,那么我们就可以构造对应的baseBLL对象,然后调用其基类接口实现基本的操作,如插入,删除,更新,查找等等,这样的模式就和我的Winform开发框架的理念非常吻合了。

我们以角色控制器来说明,它的定义如下所示,如果不需要实现额外的接口(除了常见的操作),基本上不需要写任何代码了,因为所有很多常见的操作,都已经封装在了基类控制器BusinessController<B, T>里面了。

    /// <summary>
    ///角色业务操作控制器/// </summary>
    public class RoleController : BusinessController<Role, RoleInfo>{public RoleController() : base()
{
}

...............

对于一些需要特殊数据处理的操作,可以增加一些自定义的接口函数,也可以重写基类的一些接口,实现数据的相应处理。如我的菜单界面显示中,需要根据缩进的层级对菜单名称进行缩进,以便更好的展示它们的层级结构,那么我就需要对分页函数进行重写了,如下代码所示是整个菜单Menu类的控制器类代码。

    public class MenuController : BusinessController<Menu, MenuInfo>{public overrideActionResult FindWithPager()
{
string where =GetPagerCondition(); //基类实现
PagerInfo pagerInfo
=GetPagerInfo(); //基类实现
List
<MenuInfo> list = baseBLL.FindWithPager(where, pagerInfo);
list
= CollectionHelper<MenuInfo>.Fill("-1", 0, list, "PID", "ID", "Name");//Json格式的要求{total:22,rows:{}}//构造成Json的格式传递 var result = new { total = pagerInfo.RecordCount, rows =list };returnJsonDate(result);
}
/// <summary> ///用作下拉列表的菜单Json数据/// </summary> /// <returns></returns> publicActionResult GetDictJson()
{
List
<MenuInfo> list =baseBLL.GetAll();
list
= CollectionHelper<MenuInfo>.Fill("-1", 0, list, "PID", "ID", "Name");

List
<CListItem> itemList = new List<CListItem>();foreach (MenuInfo info inlist)
{
itemList.Add(
newCListItem(info.Name, info.ID));
}
returnJson(itemList, JsonRequestBehavior.AllowGet);
}
}

我们来看看一段HTML页面里面,使用javascript脚本调用控制器API来实现数据的绑定的操作,也就是使用示例。

            $.getJSON("/Role/FindById?r=" + Math.random() + "&id=" + id, function (json) {
$("#txtID").val(json.ID);
$("#txtName").val(json.Name);
$("#txtNote").val(json.Note);
});

上面这个很标准的接口FindById是业务基类控制器BusinessController<B, T>里提供的。

当然,BusinessController里面可以类似我Winform开发框架里面基类一样,提供很丰富的操作接口,如返回列表Json集合,增删改查的操作及返回,分页数据的返回,以及一些特殊的操作都可以实现。而这些都不需要子类进行任何实现。

如下面实际案例的用户登陆日志,里面的界面功能还是很丰富的,当他的控制器业务类不需要任何实现,只需要继承基类即可。

    public class LoginLogController : BusinessController<LoginLog, LoginLogInfo>{public LoginLogController() : base()
{
}

}

界面部分代码如下所示。

//实现对DataGird控件的绑定操作
function InitGrid(queryData) {
$('#grid').datagrid({ //定位到Table标签,Table标签的ID是grid
url: '/LoginLog/FindWithPager', //指向后台的Action来获取当前用户的信息的Json格式的数据
title: '用户登陆日志',
//下面的这些属性如果谁不太清楚的话我建议去官方网站去学习
iconCls: 'icon-view',
height: 450,
nowrap: true,
autoRowHeight: false,
striped: true,
collapsible: true,
pagination: true,
rownumbers: true,
//sortName: 'ID', //根据某个字段给easyUI排序
sortOrder: 'asc',
remoteSort: false,
idField: 'ID',
queryParams: queryData, //异步查询的参数
columns: [[
{ field: 'ck', checkbox: true }, //选择
{ title: 'ID', field: 'ID', width: 40, sortable: true }, //主键
{ title: '登录用户ID', field: 'User_ID', width: 80, sortable: true },
{ title: '登录名称', field: 'LoginName', width: 80, sortable: true },
{ title: '真实名称', field: 'FullName', width: 80, sortable: true },
{ title: '日志描述', field: 'Note', width: 100, sortable: true },
{ title: 'IP地址', field: 'IPAddress', width: 100, sortable: true },
{ title: 'Mac地址', field: 'MacAddress', width: 120, sortable: true },
{ title: '系统编号', field: 'SystemType_ID', width: 120, sortable: true },
{ title: '记录日期', field: 'LastUpdated', width: 120, sortable: true },
]],
toolbar: [{
id: 'btnAdd',
text: '添加',
iconCls: 'icon-add',
handler: function () {
ShowAddDialog();//实现添加记录的页面
}
}, '-', {
id: 'btnEdit',
text: '修改',
iconCls: 'icon-edit',
handler: function () {
ShowEditOrViewDialog();//实现修改记录的方法
}
}, '-', {
id: 'btnDelete',
text: '删除',
iconCls: 'icon-remove',
handler: function () {
Delete();//实现直接删除数据的方法
}
}, '-', {
id: 'btnView',
text: '查看',
iconCls: 'icon-table',
handler: function () {
ShowEditOrViewDialog("view");//实现查看记录详细信息的方法
}
}, '-', {
id: 'btnReload',
text: '刷新',
iconCls: 'icon-reload',
handler: function () {
//实现刷新栏目中的数据
$("#grid").datagrid("reload");
}
}]
});

$('#grid').datagrid({
onDblClickRow: function (rowIndex, rowData) {
$('#grid').datagrid('uncheckAll');
$('#grid').datagrid('checkRow', rowIndex);
ShowEditOrViewDialog();
}
});
}

这个就是我的控制器设计的中心思想了,下一篇继续介绍整体的MVC系列的Web开发框架,介绍其中Web界面部分的处理和相关经验,希望大家多多提出宝贵的意见。

基于MVC4+EasyUI的Web开发框架的系列文章:

基于MVC4+EasyUI的Web开发框架形成之旅--总体介绍


基于MVC4+EasyUI的Web开发框架形成之旅--MVC控制器的设计

基于MVC4+EasyUI的Web开发框架形成之旅--界面控件的使用

基于MVC4+EasyUI的Web开发框架形成之旅--附件上传组件uploadify的使用

基于MVC4+EasyUI的Web开发框架形成之旅--框架总体界面介绍

基于MVC4+EasyUI的Web开发框架形成之旅--基类控制器CRUD的操作

基于MVC4+EasyUI的Web开发框架形成之旅--权限控制

基于MVC4+EasyUI的Web开发框架经验总结(1)-利用jQuery Tags Input 插件显示选择记录

基于MVC4+EasyUI的Web开发框架经验总结(2)- 使用EasyUI的树控件构建Web界面

基于MVC4+EasyUI的Web开发框架经验总结(3)- 使用Json实体类构建菜单数据

基于MVC4+EasyUI的Web开发框架经验总结(4)--使用图表控件Highcharts

基于MVC4+EasyUI的Web开发框架经验总结(5)--使用HTML编辑控件CKEditor和CKFinder

基于MVC4+EasyUI的Web开发框架经验总结(6)--在页面中应用下拉列表的处理

基于MVC4+EasyUI的Web开发框架经验总结(7)--实现省份、城市、行政区三者联动

基于MVC4+EasyUI的Web开发框架经验总结(8)--实现Office文档的预览

基于MVC4+EasyUI的Web开发框架经验总结(9)--在Datagrid里面实现外键字段的转义操作

基于MVC4+EasyUI的Web开发框架经验总结(10)--在Web界面上实现数据的导入和导出

基于MVC4+EasyUI的Web开发框架经验总结(11)--使用Bundles处理简化页面代码

基于MVC4+EasyUI的Web开发框架经验总结(12)--利用Jquery处理数据交互的几种方式

基于MVC4+EasyUI的Web开发框架经验总结(13)--DataGrid控件实现自动适应宽带高度

基于MVC4+EasyUI的Web开发框架经验总结(14)--自动生成图标样式文件和图标的选择操作

SQLite数据库作为一般单机版软件的数据库,是非常优秀的,我目前单机版的软件产品线基本上全部替换Access作为优选的数据库了,在开发过程中,有时候需要批量写入数据的情况,发现传统的插入数据模式非常慢,几千条数据的写入或者更新可能需要好几分钟时间,而SqlServer则相同的操作可能几秒即可,有无更好的方法来提高它的响应速度呢?答案是有的,就是采用事务提交,默认SQLite的数据库插入操作,如果没有采用事务的话,它每次写入提交,就会触发一次事务操作,而这样几千条的数据,就会触发几千个事务的操作,这就是时间耗费的根源。本文通过详细代码介绍如何使用事务来提高整个批量插入数据的速度,并以实际的Winform开发框架中的字典管理模块的批量插入功能来进行介绍,通过前后速度的对比,使得事务操作提高响应速度更有说服力。

由于一些项目需要,字典管理模块需要批量录入数据,因此改善了我的《
Winform开发框架
》中的字典管理模块,在字典管理模块增加一个批量添加的功能,如下所示。

对一些诊断的数据录入,一般情况下都可能是几千条的数据,还有可能更多的一些分类字典,那么如果每次都需要等待几分钟或者几十分钟,那么这样的响应体验肯定很差。

为了提高响应速度,我这里使用了事务操作,整个事务操作是基于EnterpriseLibray类库的数据库操作,由于我已经在框架的基类中做了封装,因此我们这里看到整个处理过程即可。

其中MyRegion里面的代码就是遍历每行的数据,构造数据字典对象和排序号,然后调用InsertDictData函数进行数据的录入。其中InsertDictData函数的代码是

        /// <summary>
        ///使用事务参数,插入数据,最后统一提交事务处理/// </summary>
        /// <param name="dictData">字典数据</param>
        /// <param name="seq">排序</param>
        /// <param name="trans">事务对象</param>
        private void InsertDictData(string dictData, stringseq, DbTransaction trans)
{
if (!string.IsNullOrWhiteSpace(dictData))
{
DictDataInfo info
= newDictDataInfo();
info.Editor
=LoginID;
info.LastUpdated
=DateTime.Now;
info.DictType_ID
= this.txtDictType.Tag.ToString();
info.Name
=dictData.Trim();
info.Value
=dictData.Trim();
info.Remark
= this.txtNote.Text.Trim();
info.Seq
=seq;bool succeed = BLLFactory<DictData>.Instance.Insert(info, trans);
}
}

整个插入功能按钮的处理全部代码如下所示。

        private void btnOK_Click(objectsender, EventArgs e)
{
string[] arrayItems = this.txtDictData.Lines;int intSeq = -1;int seqLength = 3;string strSeq = this.txtSeq.Text.Trim();if (int.TryParse(strSeq, outintSeq))
{
seqLength
=strSeq.Length;
}
if (arrayItems != null && arrayItems.Length > 0)
{
DbTransaction trans
= BLLFactory<DictData>.Instance.CreateTransaction();if (trans != null)
{
try{#region MyRegion foreach (string strItem inarrayItems)
{
if (this.radSplit.Checked)
{
if (!string.IsNullOrWhiteSpace(strItem))
{
string[] dataItems = strItem.Split(new char[] { ',', '', ';', '', '/', ''});foreach (string dictData indataItems)
{
#region 保存数据 string seq = "";if (intSeq > 0)
{
seq
= (intSeq++).ToString().PadLeft(seqLength, '0');
}
else{
seq
= string.Format("{0}{1}", strSeq, intSeq++);
}

InsertDictData(dictData, seq, trans);
#endregion}
}
}
else{#region 保存数据 if (!string.IsNullOrWhiteSpace(strItem))
{
string seq = "";if (intSeq > 0)
{
seq
= (intSeq++).ToString().PadLeft(seqLength, '0');
}
else{
seq
= string.Format("{0}{1}", strSeq, intSeq++);
}

InsertDictData(strItem, seq, trans);
}
#endregion}
}
#endregiontrans.Commit();
ProcessDataSaved(
this.btnOK, newEventArgs());
MessageDxUtil.ShowTips(
"保存成功");this.DialogResult =DialogResult.OK;
}
catch(Exception ex)
{
trans.Rollback();
LogTextHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}
}
}
}

上面的批量插入,经过前后的测试,
2千条数据批量插入SQLite数据库,
需要大概3~4分钟左右,如果采用了事务操作,则在1~2秒内写入完成

,速度提高不知道多少倍。如果是操作数据比较多的,强烈建议使用事务进行操作,可以给客户很好的体验效果。

如果嫌上面的代码复杂,可以看下面的讲解代码可能就明白了

        using (DbTransaction dbTrans =conn.BeginTransaction())
{
using (DbCommand cmd =conn.CreateCommand())
{
cmd.CommandText
= "INSERT INTO MyTable(MyValue) VALUES(?)";
DbParameter Field1
=cmd.CreateParameter();
cmd.Parameters.Add(Field1);
for (int n = 0; n < 100000; n++)
{
Field1.Value
= n + 100000;
cmd.ExecuteNonQuery();
}
}
}

上面是一种比较简单原始的事务操作,如果批量插入数据,同样能够起到一样的效果。

在实际的项目开发过程中,有好的控件或者功能模块,我都是想办法尽可能集成到我的WInform开发框架中,这样后面开发项目起来,就可以节省很多研究时间,并能重复使用,非常高效方便。在我很早之前的一篇博客《
在GridControl控件中使用SearchLookUpEdit构建数据快速输入
》就曾经介绍,如何在列表控件中实现数据的快速录入,本文介绍另外一种方式,通过文本输入框的输入选择,可以实现数据的快速录入,原理和之前一篇差不多,不过这次利用DevExpress控件的GridLookupEdit控件封装类进行数据的处理。

1、GridControl集成SearchLookUpEdit的效果展现

首先我们来回顾一下,GridControl列表控件中如何实现数据的快速录入的。一般情况下,在我的框架都是利用弹出窗体进行标准数据的录入,这样的情况比较通用,但是对于一些想实现数据在列表中快速录入,可能就不一定符合他们的使用习惯,客户要求往往是第一位,如果有这样的要求,我们一定要想办法满足。

一个实际的案例就是门诊的时候,医生用药的情况,除了选择其他内容外,主要的就是快速录入药品信息。我们平常去大一点 的医院看病,好像看到的多数操作都是这样。还有一种方式就是销售人员提供的报价单,从产品里面选择信息,然后修改下价格,这些场景都是很适合这样的操作的。下面是一个门诊的例子。

2、在文本输入控件中实现数据的快速录入或选择

1)原有系统控件实现的界面效果

很多时候,我们可能需要在编辑框里面快速录入数据,如果是纯粹的选择列表项目,那么可以选择GridLookupEdit或者SearchLookupEdit控件,这两个控件都是可以实现快速的
选择
的,如下DevExpress例子的界面所示。

GridLookupEdit例子效果:

SearchLookupEdit例子效果:

2)Winform开发框架中封装的GridLookupEdit控件的输入效果

上面两个不同的例子,他们弹出一个新的列表框操作都差不多的,不同的是SearchLookupEdit控件会有一个可以查找的输入框,实现数据的过滤查找。

使用GridLookupEdit控件,可以把输入框的属性
TextEditStyle
修改为
修改为
TextEditStyles.Standard

,如果要封装用来可以允许输入新的内容,记得要处理
ProcessNewValue
函数,默认GridLookupEdit控件不允许输入列表中不存在的内容,也就是只能选择列表的内容。

但是,我们项目很多时候,需要一个可以录入,也可以从列表中选择数据的场景,最好能够结合拼音码实现数据的快速录入,如下界面所示。

当我们输入中文的时候,会自动从列表中进行数据的过滤,如下所示。

当我们输入拼音简码的时候,也可以实现一样的效果,如下所示。

当然,如果列表里面没有相应的数据的时候,我们可以输入新的内容(注意:默认的GridLookupEdit是不允许输入新内容的

这样在内容很多的时候,就可以实现数据的快速选择或者录入新的数据了。

例如我的《医院科室数据管理系统》软件产品里面,就需要从大量的入院诊断或者出院诊断中选择或者输入新的诊断,上面的控件效果就是我所需要的。

当然,以上只是框架功能模块的沧海一栗,很多重要的功能并不在该文中介绍,具体可以了解这个框架设计图,或者查看

Winform开发框架的标签内容


Winform开发框架的主要功能概览如下图所示。

3、在文本输入控件中实现数据的快速录入的代码实现

这个功能模块的重要实现就是要重新封装GridLookupEdit控件,并设置其一些默认属性,还需要处理前面所说过的,处理该对象的ProcessNewValue的事件处理,部分代码如下所示。

    /// <summary>
    ///自定义的GridLookUpEdit控件/// </summary>
    public classCustomGridLookUpEdit : GridLookUpEdit
{
/// <summary> ///是否禁止新增内容/// </summary> [Browsable(true), Description("是否禁止新增内容")]public bool DisableAddNew { get; set; }staticCustomGridLookUpEdit()
{
RepositoryItemCustomGridLookUpEdit.RegisterCustomGridLookUpEdit();
}
public CustomGridLookUpEdit() : base()
{
//初始化一些状态 this.Properties.PopupFilterMode = PopupFilterMode.Contains;//包含即可 this.Properties.ImmediatePopup = true;//是否马上弹出窗体 this.Properties.ValidateOnEnterKey = true;//回车确认 this.Properties.TextEditStyle = DevExpress.XtraEditors.Controls.TextEditStyles.Standard;//文本框可输入 this.Properties.NullText = "";this.Properties.NullValuePrompt = "";this.ProcessNewValue += newDevExpress.XtraEditors.Controls.ProcessNewValueEventHandler(CustomGridLookUpEdit_ProcessNewValue);
}
/// <summary> ///实现在列表没有记录的时候,可以录入一个不存在的记录,类似ComoboEidt功能/// </summary> /// <param name="sender"></param> /// <param name="e"></param> void CustomGridLookUpEdit_ProcessNewValue(objectsender, DevExpress.XtraEditors.Controls.ProcessNewValueEventArgs e)
{
if (!DisableAddNew && !this.DesignMode)
{
string displayName = this.Properties.DisplayMember;string valueName = this.Properties.ValueMember;string display =e.DisplayValue.ToString();

DataTable dtTemp
= this.Properties.DataSource asDataTable;if (dtTemp != null)
{
DataRow[] selectedRows
= dtTemp.Select(string.Format("{0}='{1}'", displayName, display.Replace("'", "")));if (selectedRows == null || selectedRows.Length == 0)
{
DataRow row
=dtTemp.NewRow();
row[displayName]
=display;
row[valueName]
=display;
dtTemp.Rows.Add(row);
dtTemp.AcceptChanges();
}
}

e.Handled
= true;
}
}

在调用的时候,我们需要绑定相应的数据,并且给它增加一个拼音码的列,方便通过拼音码来实现快速检索,使用代码如下所示(这里通过扩展方法的方式实现数据的绑定)。

        /// <summary>
        ///绑定下拉列表控件为指定的数据字典列表/// </summary>
        /// <param name="combo">下拉列表控件</param>
        /// <param name="dictTypeName">数据字典类型名称</param>
        /// <param name="defaultValue">控件默认值</param>
        public static void BindDictItems(this CustomGridLookUpEdit combo, string dictTypeName, stringdefaultValue)
{
string displayName =dictTypeName;const string valueName = "值内容";const string pinyin = "拼音码";
DataTable dt
= DataTableHelper.CreateTable(string.Format("{0},{1},{2}", displayName, valueName, pinyin));

Dictionary
<string, string> dict = BLLFactory<DictData>.Instance.GetDictByDictType(dictTypeName);foreach (string key indict.Keys)
{
DataRow row
=dt.NewRow();
row[displayName]
=key;
row[valueName]
=dict[key];
row[pinyin]
=Pinyin.GetFirstPY(key);
dt.Rows.Add(row);
}

combo.Properties.ValueMember
=valueName;
combo.Properties.DisplayMember
=displayName;
combo.Properties.DataSource
=dt;
combo.Properties.PopulateViewColumns();
combo.Properties.View.Columns[valueName].Visible
= false;
combo.Properties.View.Columns[displayName].Width
= 400;
combo.Properties.View.Columns[pinyin].Width
= 200;
combo.Properties.PopupFormMinSize
= new System.Drawing.Size(600, 0);if (!string.IsNullOrEmpty(defaultValue))
{
combo.EditValue
=defaultValue;
}
}

调用代码只需要调用该方法即可。

txtInDiagnosis.BindDictItems("入院诊断");

以上就是我对于这种利用DevExpress控件实现数据的快速录入和选择操作的功能演示和实际代码逻辑展示,希望对大家有所帮助。