2023年2月

支持插件化应用
的开发
框架能给程序带来无穷的生命力,也是目前很多系统、程序追求的重要方向之一,插件化的模块,在遵循一定的接口标准的基础上,可以实现快速集成,也就是所谓的热插拔操作,可以无限对已经开发好系统进行扩展,而且不会影响已有的功能,不在需要的模块,通过修改配置移除即可。
我的Winform开发框架一直以来,来源于多年的项目积累以及客户的反馈,已经具备了众多很好的特性以及相关的模块组合,为了更好拥抱变化,提高基于Winform开发框架基础上开发新系统的效率,以及为框架融入更多好的特性,故此把我的Winform开发框架在原来的基础上进行扩展,实现基于插件化应用

框架特性。

为了引入插件化的应用框架特点,我在上一篇随笔《
Winform开发框架之权限管理系统的改进
》已经对我的通用权限管理系统进行了改进,其中增加了菜单管理模块就是为了做插件化做准备的,我们通过权限管理系统配置好菜单的相关信息,然后在应用框架中动态加载菜单功能即可实现。这个菜单模块,是用来配置基于Web开发框架或者Winform开发框架、WCF开发框架的菜单,通过预先的配置,框架程序的动态加载解析,就能实现插件模块的热插拔功能了。实际插件化框架的菜单配置界面效果如下所示。

最终在Winform开发框架的程序中,实现基于插件化的应用,如下所示。

先来看看我改造Winform开发框架,最终形成的框架界面效果,然后在逐一进行介绍,整个开发框架的实现过程。

1、框架的项目工程规划

为了减少框架整体的复杂性以及提高重用,对插件化的应用框架的项目工程进行了划分,包括“
框架基础界面模块”、“插件应用框架启动模块”、仓库管理系统模块业务逻辑、仓库管理系统模块窗体界面等几个部分。前面两个部分是插件化框架的核心,可以认为是不需要变化的模块,提供所有插件应用动态创建以及使用的框架支撑;后面两个是具体的主业务模块,这里以WInform开发框架中的仓库管理系统作为主业务模块,它本身也是插件应用之一,具体的项目工程结构以及说明如下所示。

项目名称 项目说明
WHC.Framework.BaseUIDx 框架基础界面模块,定义窗体界面基类、通用Excel导入模块、
通用
高级查询模块等
WHC.Framework.StarterDx 插件应用框架启动模块,集成权限登录、动态菜单创建、插件应用动态加载、基础框架功能等
WHC.WareHouseMis 仓库管理系统模块的业务逻辑
WHC.Framework.WareHouseDx 仓库管理系统模块的窗体界面

从上面的表格说明中,我们可以看到“
WHC.Framework.StarterDx
”项目工程,是“插件应用框架启动模块”,它基本上只和权限管理系统模块有关联关系,因为权限系统是框架底层支撑的模块,包括用户登录、菜单管理、权限控制等都需要从权限管理系统中获取数据,具体的主要业务功能如下所示。

2、框架的菜单动态加载

本文第一张图片里面,介绍了菜单的定义信息,其中包括了图标的配置,这些图片为了方便管理,以及插件需要动态添加菜单图标,我把它放置在了程序目录的相对路径下面,如下所示,动态创建菜单的时候,从指定的路径去获取图标并加载即可。

动态加载菜单是指在插件化应用框架启动,用户登录后进入主界面后,在主界面中动态创建相应的菜单(菜单在权限管理系统中进行配置管理),如下代码所示。

其中是RibbonPageHelper为了方便动态创建菜单而创建的辅助类,部分代码如下所示。

    /// <summary>
    ///动态创建RibbonPage和其下面的按钮项目辅助类/// </summary>
    public classRibbonPageHelper
{
privateRibbonControl control;publicMainForm mainForm;public RibbonPageHelper(MainForm mainForm, refRibbonControl control)
{
this.mainForm =mainForm;this.control =control;
}
public voidAddPages()
{
//约定菜单共有3级,第一级为大的类别,第二级为小模块分组,第三级为具体的菜单 List<MenuNodeInfo> menuList = WHC.Security.BLL.BLLFactory<SysMenu>.Instance.GetTree(Portal.gc.SystemType);if (menuList.Count == 0) return;int i = 0;foreach(MenuNodeInfo firstInfo inmenuList)
{
//如果没有菜单的权限,则跳过 if (!Portal.gc.HasFunction(firstInfo.FunctionId)) continue;//添加页面(一级菜单) RibbonPage page = newDevExpress.XtraBars.Ribbon.RibbonPage();
page.Text
=firstInfo.Name;
page.Name
=firstInfo.ID;this.control.Pages.Insert(i++, page);if(firstInfo.Children.Count == 0) continue;foreach(MenuNodeInfo secondInfo infirstInfo.Children)
{
//如果没有菜单的权限,则跳过 if (!Portal.gc.HasFunction(secondInfo.FunctionId)) continue;//添加RibbonPageGroup(二级菜单) RibbonPageGroup group = newRibbonPageGroup();
group.Text
=secondInfo.Name;
group.Name
=secondInfo.ID;
page.Groups.Add(group);
if(secondInfo.Children.Count == 0) continue;foreach (MenuNodeInfo thirdInfo insecondInfo.Children)
{
//如果没有菜单的权限,则跳过 if (!Portal.gc.HasFunction(thirdInfo.FunctionId)) continue;//添加功能按钮(三级菜单) BarButtonItem button = newBarButtonItem();
button.PaintStyle
=BarItemPaintStyle.CaptionGlyph;
button.LargeGlyph
=LoadIcon(thirdInfo.Icon);
button.Glyph
=LoadIcon(thirdInfo.Icon);

button.Name
=thirdInfo.ID;
button.Caption
=thirdInfo.Name;
..................
group.ItemLinks.Add(button);
}
}
}
}
...............

菜单为了方便管理,约定分为3级菜单,三个层级的菜单示意图如下所示。

启动顶部的选项卡级别为第一级,下面的Ribbon分组为第二级,具体的功能菜单(或者按钮)为第三级,以上就是通过菜单数据动态创建的菜单界面图。

3、框架的用户信息和权限控制

基础框架需要传统的登录进行验证,登录成功后,把用户关联的具有的权限下载到本地,然后由系统逻辑统一判断即可。

插件应用框架系统的登录代码和普通的差别不大,登录后把相关信息存储在框架变量中,如下所示。

        private void btLogin_Click(objectsender, EventArgs e)
{
................. try{string ip =NetworkUtil.GetLocalIP();string macAddr =HardwareInfoHelper.GetMacAddress();string loginName = this.cmbzhanhao.Text.Trim();string identity = WHC.Security.BLL.BLLFactory<WHC.Security.BLL.User>.Instance.VerifyUser(loginName, this.tbPass.Text, Portal.gc.SystemType, ip, macAddr);if (!string.IsNullOrEmpty(identity))
{
UserInfo info
= WHC.Security.BLL.BLLFactory<WHC.Security.BLL.User>.Instance.GetUserByName(loginName);if (info != null)
{
#region 获取用户的功能列表List<FunctionInfo> list = WHC.Security.BLL.BLLFactory<WHC.Security.BLL.Function>.Instance.GetFunctionsByUser(info.ID, Portal.gc.SystemType);if (list != null && list.Count > 0)
{
foreach (FunctionInfo functionInfo inlist)
{
if (!Portal.gc.FunctionDict.ContainsKey(functionInfo.ControlID))
{
Portal.gc.FunctionDict.Add(functionInfo.ControlID, functionInfo.ControlID);
}
}
}
#endregionbLogin= true;
Portal.gc.UserInfo
=info;
Portal.gc.LoginUserInfo
=ConvertToLoginUser(info);this.DialogResult =DialogResult.OK;
}
}
else{
MessageDxUtil.ShowTips(
"用户帐号密码不正确");this.tbPass.Text = ""; //设置密码为空 }
}
catch(Exception err)
{
MessageDxUtil.ShowError(err.Message);
}
}

为了使框架记录的权限信息、用户数据、以及系统的一些配置信息能够传递到每个插件应用的窗体中,设计了一个插件应用界面需要实现的接口,放在了BaseUI项目工程中。

namespaceWHC.Framework.BaseUI
{
/// <summary> ///父窗体实现的权限控制接口/// </summary> public interfaceIFunction
{
/// <summary> ///初始化权限控制信息/// </summary> void InitFunction(LoginUserInfo userInfo, Dictionary<string, string>functionDict);/// <summary> ///是否具有访问指定控制ID的权限/// </summary> /// <param name="controlId">功能控制ID</param> /// <returns></returns> bool HasFunction(stringcontrolId);/// <summary> ///登陆用户基础信息/// </summary> LoginUserInfo LoginUserInfo { get; set; }/// <summary> ///登录用户具有的功能字典集合/// </summary> Dictionary<string, string> FunctionDict { get; set; }/// <summary> ///应用程序基础信息/// </summary> AppInfo AppInfo { get; set; }

}
}

然后在BaseUI的项目中,界面基类BaseForm实现这个接口。

namespaceWHC.Framework.BaseUI
{
public partial classBaseForm : DevExpress.XtraEditors.XtraForm, IFunction
{
publicBaseForm()
{
InitializeComponent();
}

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

最后,就是我们如何传递用户信息以及权限信息到窗体本身,传递到窗体作为其本身的变量后,就可以很方便使用这些关键的信息了。

在我们动态加载插件应用的后,我们会创建对应的Form对象,然后转换为IFunction接口,赋予该接口相关的变量属性即可实现用户信息及权限信息的传递,如下代码所示。

               Form tableForm =(Form)Activator.CreateInstance(formType);//如果窗体集成了IFunction接口(第一次创建需要设置)
                IFunction function = tableForm asIFunction;if (function != null)
{
//初始化权限控制信息 function.InitFunction(Portal.gc.LoginUserInfo, Portal.gc.FunctionDict);//记录程序的相关信息 function.AppInfo = newAppInfo(Portal.gc.AppUnit, Portal.gc.AppName, Portal.gc.AppWholeName, Portal.gc.SystemType);
}

4、插件应用的动态加载

上面我们说到,只要是实现基于Form的,我们都可以动态创建方式调用显示插件的界面出来,而如果界面实现了IFucntion的权限控制接口,那么我们就能够传递给它响应的数据,实现更加完善的控制功能。

在第一张关于权限系统的菜单管理图片中,我们看到了有个Winform的窗体类型的字段,里面就是用来动态构造插件的配置信息,我们主要是用来构造插件的窗体,并传递给它相关数据即可,下图是菜单管理里面的 “Winform窗体类型” 信息的具体内容。

但我们完成菜单的动态创建后,菜单按钮的响应事件就是触发动态加载插件的事件。

我们添加菜单的时候,对它的响应事件也做了处理,具体代码如下所示。

                        //添加功能按钮(三级菜单)
                        BarButtonItem button = newBarButtonItem();
.................
button.Caption=thirdInfo.Name;
button.Tag
=thirdInfo.WinformType;
button.ItemClick
+= (sender, e) =>{if (button.Tag != null && !string.IsNullOrEmpty(button.Tag.ToString()))
{
LoadPlugInForm(button.Tag.ToString());
}
else{
MessageDxUtil.ShowTips(button.Caption);
}
};
group.ItemLinks.Add(button);

单击事件的响应处理就是动态构建插件应用的事件,其中就是根据“Winform窗体类型”的数据进行解析的。

                string dllFullPath =Path.Combine(Application.StartupPath, filePath);
Assembly tempAssembly
=System.Reflection.Assembly.LoadFrom(dllFullPath);if (tempAssembly != null)
{
Type objType
=tempAssembly.GetType(type);if (objType != null)
{
LoadMdiForm(
this.mainForm, objType, isShowDialog);
}
}

通过动态创建菜单模块,动态加载插件应用,以及权限控制等管理,我们就能隔离框架本身和插件应用模块之间的耦合性关联,所有后续开发或者别人开发的业务模块,都可以很方便的通过权限管理系统配置数据、自动更新模块更新程序应用的方式,把一个高效、易于扩展、动态管理的系统应用弄得丰富多彩,有声有色。

基于插件化应用框架的Winform开发框架改造,使得今后开发业务系统,只是基于一定的接口协议,开发插件应用即可,整体性的框架本身可以有专门的人员进行维护,提高团队对业务模块的横向切割和快速开发的效率,更好、统一、高效完成企业化应用框架的搭建和使用。

下面的图形是之前Winform开发框架的相关功能点集合,加上目前框架的“支持插件化框架应用,能快速开发插件、支持动态扩展”的特点,就显得更加丰富完善了。

从事Winform开发框架的研究和推广,也做了有几个年头了,从最初的项目雏形到目前各种重要特性的加入完善,是经过了很多项目的总结归纳和升华,有些则是根据客户需要或者应用前景的需要进行的完善,整个Winform开发框架具有很好的通用性和借鉴性,本文从该Winform开发框架进行概括总结,力求把各个重要的特性进行一些详细的说明,使大家了解整个Winform开发框架的面貌和特性。

Winform开发框架总体性概括有:支持插件化应用开发,能够动态配置及加载开发的插件;菜单支持动态配置,可根据用户权限进行动态控制显示;集成各种独立开发好的模块,如权限管理系统、字典管理模块等,自动更新模块等,实现系统的权限控制、字典管理、软件支持远程自动更新等功能;可重复使用的系统基础模块,Winform分页控件和.NET开发公用类库等;整个框架通过与代码生成工具Database2Sharp进行配合,能够一键生成整体性框架代码,Winform系统界面代码,开发更高效。

整个Winform开发框架,提供了很多常见业务系统提供的功能,包括Excel数据导入导出,能快速导入自定义模块的Excel数据,快速导出列表数据;自定义Excel自定义模板报表的生成,生成各种复杂的Excel报表;强大通用的统计图表模块,数据统计更方便;基于多数据的数据查询模块和通用高级查询模块,查询数据更方便;框架提供基于多种数据库(Sqlserver/Oracle/Mysql/Sqlite/Access)的整合,能够手动配置数据库类型,通用模块内置支持多种数据库切换;框架提供了高度封装的数据访问基类,开发代码更少更高效;框架界面基类也进行统一封装,使用更方便,效果更统一;框架提供基础性的支持,包括集成登陆、闪屏、托盘功能,以及为插件模块提供登陆用户信息和系统信息。

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

1、支持插件化应用开发,能够动态配置及加载开发的插件

框架实现基于插件化的应用,如下所示。


支持插件化应用
的开发
框架能给程序带来无穷的生命力,也是目前很多系统、程序追求的重要方向之一,插件化的模块,在遵循一定的接口标准的基础上,可以实现快速集成,也就是所谓的热插拔操作,可以无限对已经开发好系统进行扩展,而且不会影响已有的功能,不再需要的模块,通过修改配置移除即可。

插件化应用也是目前框架和系统开发的主流方向,从开发企业的产品管理角度来看,第一可以把控好已有的框架产品,第二可以把内部的人员分配到不同的业务模块中,让他们遵循一定的接口即可快速开发,从而
提高团队对业务模块的横向切割和快速开发的效率,更好、统一、高效完成企业化应用框架的搭建和使用。

2、菜单支持动态配置,可根据用户权限进行动态控制显示

一般Winform程序中,菜单都是直接写在主程序中的,这样对于修改和维护菜单的相关信息,可能有所不便,而且对于插件化应用,新增菜单是常见的事情,这样就需要对菜单进行动态配置管理了,通过后台菜单的配置和权限的指定,能够实现菜单的动态加载和权限验证。

因此菜单也是权限分配的一部分,为了有效管理菜单资源,我们把菜单放到权限管理系统中进行管理控制。

设定好菜单的图标和Winform窗体类型,就能在Winform框架中动态构建菜单信息了。菜单的图表放置在系统的相对目录下即可。

3、集成各种独立开发好的模块,如权限管理系统、字典管理模块等,自动更新模块等

3.1 权限管理模块

权限的分配和管理,基本上是每个业务系统需要考虑的东西,而这些常用的东西,在整个Winform开发框架中,把它作为一个独立的模块,既方便维护管理,也留有接口很容易进行集成,这样能够在项目中反复进行使用。

权限系统需要考虑的系统用户、组织机构、角色、权限定义及分配、菜单管理、用户登录日志等相关信息。对于每新增一个系统,我们只需要在权限管理系统中增加一个系统类型定义,以及相关的功能、菜单数据即可,非常方便管理。

3.2 字典管理模块

另外,除了权限的管理,字典管理也是常规业务模块的一个总要功能,因此字典管理模块在Winform开发框架中,也是一个独立的模块进行开发管理,在使用的时候进行集成即可。

字典的分类可以按多级进行分类管理,可以拖拉进入新的分类,非常方便。

3.3 自动更新模块

一般情况下,为了有效管理应用程序的发布及更新,我们需要软件支持远程自动更新功能;
在Winform开发框架中再引入一个自动升级更新的通用模块,这个自动升级的通用模块除了具备一般的功能外,可以通过配置程序标题、升级路径方式等方式,实现通用化的升级操作,其他需要升级的程序,拿过来就可以使用,经过项目的整合及优化,已经具备较好的应用前景了。

本通用自动更新模块具有下面几个特点:

1)程序标题可配置,更新路径可配置,适用于各种需要更新的项目整合。

2)支持文件复制或者对压缩包进行解压,适合更多琐碎程序集的整体升级。

3)支持文件更新后自动启动,或者带参数的启动方式。

4、可重复使用的系统基础模块,Winform分页控件和.NET开发公用类库等

4.1 Winform分页控件

为了提高数据的获取及显示效率,一般情况下,我们都需要对数据进行分页获取和显示,在网络化环境,这种方式获取数据特别重要,在Winform里面,没有现成的分页控件可以使用,因此出于这个考虑,把数据显示、数据导出、数据打印等众多功能集成一起,更加方便使用。分页控件完美支持列表数据的分页,可以调整显示列及顺序,是否显示复选框,字段别名,奇偶列的颜色变化,列信息提示和数据复制等等众多实用细致的功能的。

列表的数据绑定及显示代码,通过代码生成工具,可以一键生成所需的界面代码,开发效率飞一般的提升。


4.2 .NET公用类库

俗话说,一个好汉十个帮,众人拾柴火焰高等都说明一个道理,有更多的资源,更丰富的积累,都是助你走向成功,走向顶峰的推动力。就我们开发者而言,其中技巧的积累、资源的积累,就是类似一个个好汉、一根根好柴,是我们能够进行高效开发的保证和推动力。

这些类库是我从事多年软件开发,逐渐提炼和发现的一些闪光点或者好片段,有些是吸收别人的优秀的东西,有些是自己逐步提炼的精华,以前,在网络上看到一些开源的项目,总会先看看其是否有封装良好、功能独立的辅助类库,发现好的辅助类库,总是欣喜若狂好一阵子,学习中逐步积累,研究中逐渐提炼,多年过后,略有小成,终为今天所介绍的辅助类库集合。
这些辅助类库平时也并不是所有的都会用得上,不过一些常用的,几乎各个项目就会用到,类库涉及面非常广,能够为我们开发节省很多时间,并且我们也可以根据自己的需要进行扩充完善,形成自己的类库集合。

博客公用类库在线帮助文档列表:

厚积薄发,丰富的公用类库积累,助你高效进行系统开发(1)----开篇总结
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(2)----常用操作
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(3)----数据库相关操作
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(4)----CSV、Excel、INI文件、独立存储等文件相关
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(5)----热键、多线程、窗体动画冻结等窗体操作
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(6)----全屏截图、图标获取、图片打印、页面预览截屏、图片复杂操作等
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(7)-----声音播放、硬件信息、键盘模拟及钩子、鼠标模拟及钩子等设备相关
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(8)----非对称加密、BASE64加密、MD5等常用加密处理
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(9)----各种常用辅助类
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(10)---各种线程同步的集合类
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(11)---各种线程相关操作类
厚积薄发,丰富的公用类库积累,助你高效进行系统开发(12)--- 网络相关操作辅助类

5、代码生成工具Database2Sharp的整合

整个框架通过与代码生成工具Database2Sharp进行配合,能够一键生成整体性框架代码,Winform系统界面代码,开发更高效。

在整个Winform开发框架中,Database2Sharp生成出来的代码体现了非常完美的整合性,能够无缝接入开发的框架系统中,无论是常规的业务逻辑和数据访问层代码,以及一些列表、编辑界面的Winform界面代码,都能快速生成,稍作调整即可满足业务模块的需要。

Database2Sharp是一个简单点击几次鼠标就能完成一周代码量的代码生成工具,效率惊人、友好体贴,真正的开发好伴侣。提供了对SqlServer 2000、SqlServer 2005、Oracle、Mysql、Access、SQLite的支持;可以生成各种架构代码、Winform界面代码,并且和Winform开发框架完美整合,体现出更高的开发效率。

6、通用的Excel数据导入导出,能快速导入自定义模块的Excel数据,快速导出列表数据

由于一般的业务系统,经常性的数据导入时很正常的业务需求,因为毕竟使用Excel来操作数据也很方便,或者由于系统之间的数据交换需要,我们需要提供一个入口给客户导入所需要的数据。但是导入数据的时候,不同的业务数据对应不同的Excel文件,很难做到统一,但如果是每个业务模型,都创建一个不同的导入界面来操作Excel数据,又会觉得可能某种程度上重复劳动,增加开发及维护成本。

那么有无一种介于两者之间的方法,来实现效率的最优化,并且能够统一利用好一个导入的界面呢,在开发领域,只要能想到的,一般也能做到,由于工作的需要,在我的
Winform开发框架
中引入了一个通用的数据导入模块,来实现这个既是统一,又是变化的业务需求。Winform框架提供的个通用的Excel数据导入导出机制,通过代码生成工具Database2Sharp自动生成的代码,就包含了如何使用这个通用导入模块的相关代码以及该模块的导出数据的代码,我们要做的就是在系统运行起来,导出一些数据作为某个模块的Excel模板即可。下面的功能按钮就是使用代码生成工具自动生成的界面包含的按钮。

通用数据导入功能,包含下面几个方面的内容。

下面就是一个实际生成的功能模块,其导入界面的运行效果。

在最底的状态栏里面,但我们保存数据的时候,会调用后台线程进行数据保存,并显示数据导入的进度状态,由于是采用后台线程处理,不会阻塞当前的界面,在多文档的
Winform开发框架
界面中,可以切换到其他业务界面进行其他处理,不影响整体界面操作。

7、自定义Excel自定义模板报表的生成,生成各种复杂的Excel报表

很多情况下,我们需要生成比较专业的Excel模块,因此自定义模板报表就是一种很好的方案,Winform开发框架提供了多种自定义Excel报表的生成。

使用普通的二维表,虽然能满足大多数的情况,不过在一般的业务中,自定义模板的报表根据贴近实际,符合客户的要求,虽然自定义模板的报表,比普通的二维报表复杂一些,不过利用Apose.Cell控件,并在预设模板中预设变量,可以生成很复杂的报表。

具体的自定义模板报表可以参考下我总结的两篇文章。

使用Aspose.Cell控件实现Excel高难度报表的生成(一)


使用Aspose.Cell控件实现Excel高难度报表的生成(二)

例如生成一个标准的出库单,这个表单有表头信息,列表数据信息,并非一个普通的二维表,而且这种格式比较固定,因此很适合自定义模块报表的生成操作。

其他
设计模板如下所示:

实际生成的报表如下所示:

8、强大通用的统计图表模块,数据统计更方便

统计图表在很多项目都可能用到,集成到框架中,更方便大家对一些图表项目的设计理解以及功能的重用。在一般的传统的框架中,可以采用ZedGraph开源控件或者微软自带的MSChart进行图表设计,DevExpress控件套件有自己的图表控件,这里主要介绍基于DevExpress控件的图表控件进行图表设计。

8.1 普通统计图表

这里指的普通统计图表,只是对表某一项目进行单一的统计,可以从饼状图、柱状图的图表中体现这些项目各自所占的比例和数值,在Winform框架中的普通统计图表模块中,包括了饼状图、柱状图和数据表格,这样更方便对数据进行全面的分析和查看。整个模块是可以重用的,指定字段属性就可以比较合理的展现出不同分类项目的统计效果了,具体效果图如下所示。

上面的统计图表中,还包含了下面两个功能模块,如下所示。

8.2 动态项目统计图表

有时候,对于表里面的数据,可能要对不同类型的内容进行动态的统计,以确定他们各自的比例情况,那么这些动态项目的统计图表就比较合适了,例如,对于病人资料的管理,可能需要统计各种病种所占的比例或者各种职业类型的犯病率,这些不太确定的统计项目,就需要一个能够支持动态项目的统计图表进行支撑,对于本Winform框架,为了较好呈现这个类型报表的意义,我选择了对备件类型所占的比例进行一个统计分析,得到下面的统计图表,如下所示。

上面的图表统计,除了能够根据一些条件进行限定查询范围外,还可以对一些预设的统计字段进行动态选取,然后根据字段里面的各种内容(统计项目)进行统计,这样就可以比较有效的统计出各种类型的数值和比例了。

8.3 多重坐标对比统计图表模块

在Winform框架里面,可以对某一年各月份的出入库数量进行一个分析,得到下面的统计图。

以上数据不多,展现可能不太好看,下面我给出我另一个软件系统的界面,其中对病人的出入院记录进行一个统计对比分析,统计报表如下所示。

9、基于多数据库的数据查询模块和通用高级查询模块,查询数据更方便

在我的Winform开发框架中,使用了一个查询辅助类SearchCondition来实现查询条件的获取和转化,这个辅助类内置了对多种数据库条件的分析处理,因此能够很好生成所需要的数据查询条件,正确高效获取所需的数据进行显示。

        /// <summary>
        ///根据查询条件构造查询语句/// </summary> 
        private stringGetConditionSql()
{
//如果存在高级查询对象信息,则使用高级查询条件,否则使用主表条件查询 SearchCondition condition =advanceCondition;if (condition == null)
{
condition
= newSearchCondition();
condition.AddCondition(
"ItemName", this.txtName.Text, SqlOperator.Like)
.AddCondition(
"ItemBigType", this.txtBigType.Text, SqlOperator.Like)
.AddCondition(
"ItemType", this.txtItemType.Text, SqlOperator.Like)
.AddCondition(
"Specification", this.cmbSpecNumber.Text, SqlOperator.Like)
.AddCondition(
"MapNo", this.txtMapNo.Text, SqlOperator.Like)
.AddCondition(
"Material", this.txtMaterial.Text, SqlOperator.Like)
.AddCondition(
"Source", this.txtSource.Text, SqlOperator.Like)
.AddCondition(
"Note", this.txtNote.Text, SqlOperator.Like)
.AddCondition(
"Manufacture", this.txtManufacture.Text, SqlOperator.Like)
.AddCondition(
"ItemNo", this.txtItemNo.Text, SqlOperator.LikeStartAt)
.AddCondition(
"WareHouse", this.txtWareHouse.Text, SqlOperator.Like)
.AddCondition(
"Dept", this.txtDept.Text, SqlOperator.Like)
.AddCondition(
"UsagePos", this.txtUsagePos.Text, SqlOperator.Like)
.AddCondition(
"StoragePos", this.txtStoragePos.Text, SqlOperator.Like);
}
string where = condition.BuildConditionSql().Replace("Where", "");return where;
}
    /// <summary>
    ///Sql的查询符号/// </summary>
    public enumSqlOperator
{
[Description("Like 模糊查询")]
Like,
[Description("Not LiKE 模糊查询")]
NotLike,
[Description("Like 开始匹配模糊查询,如Like 'ABC%'")]
LikeStartAt,
[Description("= 等于号")]
Equal,
[Description("<> (≠) 不等于号")]
NotEqual,
/// <summary> ///> 大于号/// </summary> [Description("> 大于号")]
MoreThan,
[Description("<小于号")]
LessThan,
[Description("≥大于或等于号")]
MoreThanOrEqual,
[Description("≤ 小于或等于号")]
LessThanOrEqual,
[Description("在某个字符串值中")]
In
}

另外,一个好的数据查询 ,一般有一个强大的高级查询模块,这个模块在很多程序中都很常见,也是给客户扩展查询的一个很好的补充,由于我一直希望我的Winform开发框架能够精益求精,所以做了这个通用高级查询模块,希望对今后我自己所有的项目以及框架本身,都能高效的使用。

在介绍输入条件的时候,我们注意到,查询输入,基本上可以分为几类:其一是常规的文本类型,使用文本框替代即可;其二是下拉列表类型,用户从列表下面选择内容;其三是日期类型,需要用户指定开始日期和结束日期;其四是数字类型,需要用户指定起始和结束的数值。

1)常规的文本类型条件输入界面:

2) 下拉列表类型条件输入界面:

3) 日期类型条件输入界面:

4) 数字类型条件输入界面:

10、框架提供基于多种数据库(Sqlserver/Oracle/Mysql/Sqlite/Access)的整合

虽然我们在实际项目中,一般采用一种数据库进行处理,但是不同的项目,采用的数据库类型可能不同,本Winform开发框架为了方便演示和扩展的需要,内置支持了Sqlserver/Oracle/Mysql/Sqlite/Access,更多的数据库,也可以通过扩展数据库访问基类的方式进行更多数据库的支持。

Winform开发框架里面的所有模块,如用到了数据存储的,如权限管理管理模块、通用数据字典管理模块,均内置支持这几种数据库的整合支持。整个Winform开发框架的数据库访问,能够手动配置数据库类型,对于同一种数据库,也可以把数据存储分开存储,如业务数据存储在一个数据库,权限管理控制存储在另外一个数据库这种方式。

Winform开发框架提供多种数据库支持,数据访问基类依然很精简,因为我们利用的数据库访问模块是EnterpriseLibrary,把数据库抽象化,并且我把所有数据库通用操作放在了一个超级基类上,具体的数据库基类只需要实现变化的部分即可。业务访问类则使用泛型进行封装处理。

因此,Winform开发框架提供了高度封装的数据访问基类,开发代码更少更高效。

11、框架界面基类也进行统一封装,使用更方便,效果更统一

为了更好开发常用界面模块,Winform开发框架把一些公用的界面模块,统一放置在了一个BaseUI的项目中,把其中的通用高级查询、通用数据导入模块、常见处理界面基类,插件接口等模块放在一起,根据易于管理和使用。这样开发的模块,重用很多常规的界面,开发效率更快,使用更方便,效果更统一了。

12、框架提供基础性的支持,包括集成登陆、闪屏、托盘功能,以及为插件模块提供登陆用户信息和系统信息

整个Winform开发框架,是有一个框架启动模块进行集中处理的,系统启动后,用户登录处理后,通过动态加载菜单和插件模块,并在主体框架界面中进行展示,提供权限控制和登录用户信息等方面的框架支持。另外框架支持闪屏图片的动态配置,系统激活热键和托盘缩小灯功能。

由于系统登录后,框架本身存储了用户登录和权限信息,框架动态加载某个模块后,会把用户信息和权限控制信息,注入到模块的界面基类中,因此插件的界面模块只要是继承了BaseUI的界面基类,就能够获取到用户信息和权限控制信息了。

特性总结

Winform框架,本身就是为了能够快速开发一个高效、稳定、美观大方、扩展性强的应用软件系统。因此我在自己十年左右的共享软件开发生涯以及公司项目开发中,不断思考,精雕细琢,对很多重要的特性都进行了归纳和升华,吸收项目中好的闪光点,借鉴一些好的软件开发思路,力求把软件做的更好;在开发效率方面,除了开发一些常规通用的模块、在模块内部又充分考虑继承、重用的规则,还对大幅度提高效率的代码生成工具,根据Winform开发框架的实现思路和特点,进行了完善优化,使得无论在业务代码生成,还是在界面代码生成方面,均能把开发效率发挥到极致,希望整个Winform开发框架能够持续发挥它的魅力和吸引力,为更多的人带来希望,体验开发的乐趣。

导入Excel的操作是非常常见的操作,可以使用Aspose.Cell、APOI、MyXls、OLEDB、Excel VBA等操作Excel文件,从而实现数据的导入,在导入数据的时候,如果是强类型的数据,那么这几种方式好像都表现差不多,正常操作能够导入数据。如果是非强类型的数据,那么就需要特别注意了,一般情况下,导入的DataTable数据列的类型是以第一行内容作为确定列类型的,本文介绍利用Aspose.Cell控件导入Excel非强类型的数据的操作。

什么是强类型的数据呢,就是在Excel表格中,除了第一列名外,他们的数据格式都一样的,如下所示。

如果使用C#导入到Winform界面中,那么数据显示效果如下所示。从这里可以看到,这些数据都是遵循一定的格式,名字为字符串,年龄为数值。

使用OLEDB方式导入Excel数据的代码如下所示。

            try{                

myDs.Tables.Clear();
myDs.Clear();
this.gridControl1.DataSource = null;string connectString = string.Format(connectionStringFormat, this.txtFilePath.Text);string firstSheet =ExcelHelper.GetExcelFirstTableName(connectString);
OleDbConnection cnnxls
= newOleDbConnection(connectString);
OleDbDataAdapter myDa
= new OleDbDataAdapter(string.Format("select * from [{0}]", firstSheet), cnnxls);
myDa.Fill(myDs,
"【导入表】");this.gridControl1.DataSource = myDs.Tables[0];this.gridView1.PopulateColumns();
}
catch(Exception ex)
{
LogHelper.Error(ex);
MessageDxUtil.ShowError(ex.Message);
}

但有时,我们可能会碰到客户数据不一样的地方。如年龄可能输入了“10”,也可能输入“10岁”这样的,常规的导入,一般是以第一个出现的数值而定,如果是字符串,那么后面的数值可能导入就不能正常显示了。例如,
如果是下面的Excel,那么数据Marks列就会以第一行数据为准,后面的那些 “Fail” 字符,将不可识别,在DataTable中的值变为NULL值了。

为了有效录入非强类型的表格数据,我们可以就不能使用常规的操作代码,Aspose.Cell的控件提供了一个API,名为ExportDataTableAsString的函数,这个函数是把所有内容转换为字符串集合类型,这样所有的内容将被保留。

public System.Data.DataTable ExportDataTableAsString (
Int32
firstRow,
Int32
firstColumn,
Int32
totalRows,
Int32
totalColumns,
Boolean
exportColumnName )

Name Description
firstRow The row number of the first cell to export out.
firstColumn The column number of the first cell to export out.
totalRows Number of rows to be imported.
totalColumns Number of columns to be imported.
exportColumnName Indicates whether the data in the first row are exported to the column name of the DataTable

使用Aspose.Cell这个API导入的数据代码如下所示。

bool exportColumnName = true;
Aspose.Cells.Workbook workbook = newAspose.Cells.Workbook();
workbook.Open(filepath);
Aspose.Cells.Worksheet worksheet
= workbook.Worksheets[0];
datatable
= worksheet.Cells.ExportDataTableAsString(iFirstRow, iFirstCol, rowNum + 1, colNum + 1, exportColumnName);

使用该函数导入的数据,因为全部都以字符串格式进行导入,就不会出现,有些解析不了的问题了。在Winform中显示出来的结果显示,正常。

自从在《
Winform开发框架之插件化应用框架实现
》一文中,介绍并总结了Winform开发框架插件化应用框架的实现后,赢得了很多同行和客户的支持,于是把我的WCF开发框架、混合式开发框架都进行了升级,把它们都提升到插件化应用的高度上。本文主要介绍WCF开发框架,如何实现插件化的应用。从我随笔《
基于我的Winform开发框架扩展而成的WCF开发框架
》介绍可以看到,一般的WCF应用,是在客户端添加服务应用的方式,然后使用自动生成的WCF服务客户端代理来访问相应的服务的,这种方式比较方便快捷,但是也增加了客户端界面和WCF服务的耦合性,架构布局如下所示。


为了更好有效利用松耦合的特点,以及插件化应用的特点,我对整个WCF开发框架进行了一个大的调整,以便更好整合及利用好的特点。如下图所示,插件化应用框架的启动模块,它除了依赖基础服务模块中(包括权限管理模块和字典模块),其他的模块如WCF服务模块、主业务插件模块A(主业务模块B)等都是通过配置方式实现对接的,他们之间没有明显的耦合关系。

整个项目工程的布局如下所示,其中BaseUIDx为基础界面类,方便各个插件模块重用而分离出来的。其他模块的功能如上图所示。

主业务插件模块是指各种各种的插件化业务模块,他们本身包含有界面部分、WCF服务调用、以及业务逻辑等内容。

为了更好分离WCF服务的部署和WCF服务逻辑(这也是最佳实践),实现更好的代码控制和重用,WCF项目的架构关系设置成如下所示。

其中WHC.WareHouseMis.WCFLibrary是整个WCF服务的业务逻辑模块,它囊括了统一调用接口Facade层、WCF业务实现WCFLibrary层、BLL业务层、数据接口IDAL层、数据访问实现DAL层、实体类Entity层。

当然,以上的关系不需要手工来做这些繁琐的代码对应关系,只需要设计好表后,使用代码生成工具Database2Sharp一键生成就可以了,其中很多项目关系已经生成好了,增量开发的时候,重新引用下文件关系即可。

而且整个WCF不在使用在界面层直接引用WCF服务的方式,而采用了自定义的客户端信道(允许从自定义的配置文件中加载)方式实现对应WCF服务客户端代理类的创建。

以WCF的调用类ItemDetailCaller为例,使用代码生成工具生成的代码,它已经继承了某个接口IItemDetailService服务基类了,并给它指定了具体的WCF服务节点即可,如下代码所示。

    public class ItemDetailCaller : BaseService<ItemDetailInfo>, IItemDetailService
{
public ItemDetailCaller() : base()
{
this.endpointConfigurationName =EndPointConfig.ItemDetailService;
}

实现接口IItemDetailService的函数很有规律,使用下面类似的代码即可。

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

这样进行了包装后,我们使用WCF服务就好像之前的Winform开发框架使用BLLFactory的方式一样了,WCF服务调用示例代码如下所示。

                    bool succeed = CallerFactory<IItemDetailService>.Instance.Update(info, info.ID.ToString());if(succeed)
{
try{
StockInfo stockInfo
= CallerFactory<IStockService>.Instance.FindByItemNo2(this.txtItemNo.Text, this.txtBelongWareHouse.Tag.ToString());if (stockInfo != null)
{
stockInfo.WareHouse
=txtBelongWareHouse.Text;
CallerFactory
<IStockService>.Instance.Update(stockInfo, stockInfo.ID.ToString());
}
//不管是更新还是新增,如果对应的备件编码在库房没有初始化,初始化之 bool isInit = CallerFactory<IStockService>.Instance.CheckIsInitedWareHouse(this.txtBelongWareHouse.Text, this.txtItemNo.Text);if (!isInit)
{
CallerFactory
<IStockService>.Instance.InitStockQuantity(info, 0, this.txtBelongWareHouse.Text);
}
}
catch(Exception ex)
{
MessageDxUtil.ShowTips(
string.Format("初始化库存为0失败:", ex.Message));
}
return true;
}

在我的《
Winform开发框架
》中,可使用多种数据库作为程序的数据源,除了常规的Oracle数据库、SqlServer、MySql数据库,其中还包括了SQLite数据库、Access数据库,后两个数据库都是在单机版程序中常用到的数据库,各自有着自己的特点,在我的《
Winform开发框架
》的提炼和多个项目的反复使用过程中,对SQLite数据库、Access数据库的一些特点进行了一些总结,以期达到常用常新,避免走弯路的目的。

1、简化数据库的地址,使用|DataDirectory|代替运行目录

可以使用这种
|DataDirectory|的方式,这个是内置的.net目录,可以替代运行时刻的当前目录。

使用这个|DataDirectory|关键字有一点值得注意的地方,就是如果开发Asp.net的Web应用或者WCF服务应用,那么把数据库放到App_Data目录下,这是一个特殊的目录;如果是开发Winform程序,就是把数据库放到运行程序的根目录即可,不需要在创建App_Data了,因为Winform里,这个不是特殊目录,如果你创建了这样的目录,反而是画蛇添足了且不能正常运行。

使用这种方式,那么你构建的数据库连接方式就如下所示。

Access数据库的链接字符串:

connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\ComponentData.mdb;User ID=Admin;Jet OLEDB:Database Password=;"

Sqlite的链接字符串:

connectionString="Data Source=|DataDirectory|\ComponentData.db;Version=3;"

2、SQLite 数据导入的设置

SQLite 数据库管理有很多工具,我用过很多,总还是觉得SQLite Developer最为方便,除了创建新数据库、注册数据库、加密解密、查询修改表等、还支持各种数据库数据的导入操作,不过一般在使用数据的导入的时候,需要注意一些地方,否则可能会出现乱码或者不正确的地方。

SQLite Developer日期设置,在导入数据库前,我们一般需要设置该工具的一些属性。

导入数据后,显示的日期格式就会正常的了。

另外,如果现存的数据库(例如SQLServer数据库)中的字段为ntext(或者nvarchar(max))类型或者Access的memo类型的数据有中文的内容,导入到SQLite里面就会出现乱码现象,这个是一个比较头疼的事情。

如果字段内容不太大的话,那么可以把ntext(或者nvarchar(max))类型的字段修改为nvarchar(4000以下),然后使用SQLite Developer导入到Sqlite数据库里面,那么就正常显示中文。

如果内容比较大,除非使用更好的SQLite管理工具或者使用C#代码编写例子插入数据库内容,暂时还没有更好的办法。

3、SQLite语法内容

在使用SQLite的时候,你会发现它的语法既类似SQLServer,又有点类似MySql,又有些自己的特点。

SQL语法:

Select * From [Patient] where leavedate > date('2012-08-01') order by leavedate Limit 10,50

1)例如上面的语句,其中的[Patient]使用了"[]"作为限定符,默认正常的单词也可以不用,使用它的目的是为了防止和保留字、关键字同名的。

2)上面语句date('2012-08-01')或者datetime('2012-08-01 12:30:00')这样的格式,是为了对字符串进行转换为日期进行比较,如果没有使用这个date或者datetime关键字,那么数据库会实现字符串的对比,如果时间的格式不匹配,如日期里面存储的是2010-8-1这样的内容,而sql语句比较的时候,使用 leavedate >'2010-08-01' 的字符串进行比较就会出现逻辑上不正确了。

3)limit语法。使用limit进行分页比较简单,这个语句类似于MySql的语法,Limit 10,50 的意思是跳过10行,取50行的意思,返回50条记录。,Limit 10这是返回10条记录的意思。

4)在使用参数化语句的时候,SQLite数据库可以使用 @ 符号,也可以使用 $ 符号进行处理。

5)如果插入一条记录,想获取最后一条的返回自增长ID的话,那么使用语句 Select LAST_INSERT_ROWID()就可以获取到了。

4、Access和Sqlite数据库的链接字符串

Access2000/Access2003的链接字符串
格式:

<addname="access2000"providerName="System.Data.OleDb"connectionString="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=|DataDirectory|\WareHouse.mdb;User ID=Admin;Jet OLEDB:Database Password=;" />

Access2007的链接字符串
格式(使用时候还需要安装AccessDatabaseEngine的数据库引擎):

<addname="access2007"providerName="System.Data.OleDb"connectionString="Provider=Microsoft.ACE.OLEDB.12.0;Data Source=|DataDirectory|\Database1.accdb;User ID=Admin;"/>

Sqlite数据库链接字符串:

<addname="sqlite"providerName="System.Data.SQLite"connectionString="Data Source=|DataDirectory|\WareHouse.db;Version=3;" />

Sqlite数据库链接字符串(含加密密码):

<addname="sqlite"providerName="System.Data.SQLite"connectionString="Data Source=|DataDirectory|\WareHouse.db;Version=3;Password=myPassword" />

5、Access的特殊语法

Access大多数的语法和SqlServer的语法一致,不过有一些比较特别一点。

1)参数化语句,Access也支持参数化的语句的,不过他们的顺序必须和参数出现的顺序一致,否则就会出错。

2)使用EnterpriseLibrary类库注意事项,Access参数化设置的时候,需要修改日期类型为OleDbType.Date。

                param[i] = new OleDbParameter("@" +field, val);if (val isDateTime)
{
param[i].OleDbType
= OleDbType.Date;//日期类型特别处理,否则Access数据库访问出错 }

3)获取最后插入的自增长ID,可以通过语句SELECT @@IDENTITY获得。

6、动态数据库字符串加密

我们知道,对于SQLite或者Access这些数据库,链接字符串里面的密码必须是未加密的明文,数据库链接才能被有效识别,而这样就不能很好的保护我们的数据库了。为了实现有效的数据库管理,可以对数据库加密(使用用户看不到数据库密码),而放置链接字符串的时候,我们没有放置密码部分,而在代码里面动态给数据库连接字符串进行加密(硬绑定密码),这种也是一个有效的方法,程序发布后,程序集里面的密码经过混淆加密,一般不可识别。例如我的Winform开发框架采用的动态追加数据库密码字符串的方法如下所示。

        #region 连接字符串加密

        /// <summary>
        ///根据配置数据库配置名称生成Database对象/// </summary>
        /// <returns></returns>
        protected overrideDatabase CreateDatabase()
{
Database db
= null;if (string.IsNullOrEmpty(dbConfigName))
{
db
=DatabaseFactory.CreateDatabase();
}
else{
db
=DatabaseFactory.CreateDatabase(dbConfigName);
}

DbConnectionStringBuilder sb
=db.DbProviderFactory.CreateConnectionStringBuilder();
sb.ConnectionString
=GetConnectionString();
GenericDatabase newDb
= newGenericDatabase(sb.ToString(), db.DbProviderFactory);
db
=newDb;returndb;
}
/// <summary> ///动态改变或者连接字符串/// </summary> /// <returns></returns> protected virtual stringGetConnectionString()
{
string connectionString = "";
DatabaseSettings setting
= ConfigurationManager.GetSection("dataConfiguration") asDatabaseSettings;if(setting != null)
{
string defaultConnection =setting.DefaultDatabase;
connectionString
=ConfigurationManager.ConnectionStrings[defaultConnection].ConnectionString;//尝试加密或者解密 if(!connectionString.EndsWith(";"))
{
connectionString
+= ";";
}
connectionString
+= string.Format("Jet OLEDB:Database Password=mypassword;");
}
returnconnectionString;
}
#endregion

以上就是一些关于SQLite数据库和Access数据库的使用经验总结,希望读者能够有所收获,并能够分享自己的经验总结,非常感谢您的阅读,如果有宝贵的意见可以留言告知。