Winform开发框架之插件化应用框架实现
支持插件化应用
的开发
框架能给程序带来无穷的生命力,也是目前很多系统、程序追求的重要方向之一,插件化的模块,在遵循一定的接口标准的基础上,可以实现快速集成,也就是所谓的热插拔操作,可以无限对已经开发好系统进行扩展,而且不会影响已有的功能,不在需要的模块,通过修改配置移除即可。
我的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开发框架的相关功能点集合,加上目前框架的“支持插件化框架应用,能快速开发插件、支持动态扩展”的特点,就显得更加丰富完善了。