介绍 前段时间我的一个同事和我讨论了一个他要解决的问题。一个客户要求他开发一套桌面软件,这个软件在企业的不同部门使用它时展现出不同的功能模块。一方面,我想起了我曾经做过的解决方案中的实现。另一方面有一个能构建出有现代展现风格的WPF应用程序的开源程序,这个开源程序我跟了好几年了,因为我觉得它确实很棒。 我在想是否能把Prism库和那个开源的MUI界面库结合起来做一个插件式架构,于是我就做出了接下来要展现的原型方案。 动态模块(Dynamic Modules)是一个WPF模块化应用的原型样例,基于Prism库和WPF控件库Modern UI(MUI)。这是一个以插件式架构创建metro风格的、WPF界面应用程序的概念的实例。 背景和需求 这篇文章要求读者至少要有基本的WPF、Prism库和Unity依赖注入容器的背景知识。工程需要使用Visual Studio 2015进行编译。
架构 本文提出的插件架构的核心思想是: 把所需的工程模块都放到一个目录中(或者把所有模块都放到一个目录中,在加载时进行过滤)。 动态的从模块文件夹中加载工程模块。 每个模块暴露出一个入口点作为主菜单的选项。 根据加载的模块动态构建主菜单。 主菜单中的第一个选项是固定的,且对于每个用户都是一样的。 一个包含共享服务、存储库、DTO和数据模型定义等信息的核心模块是静态加载的,从解决方案的任何工程都能引用到。
把动态模块拷贝到一个目录中是生成后事件中的一步。这个模块都没有被启动工程引用,并且是通过检查目录中的程序集来动态发现的。这些模块工程的生成后事件如下所示,以保证把生成的程序集拷贝到那个目录中。 xcopy "$(TargetDir)$(TargetFileName)" "$(TargetDir)modules\" /y 解决方案生成到 "..\bin\"目录。 理解代码如果你检出了 MUI 示例工程中的 MainWindow.xaml 文件,你可以看到主菜单是如何静态的生成的: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | < mui:ModernWindow.MenuLinkGroups >
< mui:LinkGroup DisplayName = "Welcome" >
< mui:LinkGroup.Links >
< mui:Link DisplayName = "Introduction" Source = "/Pages/Introduction.xaml" />
</ mui:LinkGroup.Links >
</ mui:LinkGroup >
< mui:LinkGroup DisplayName = "Layout" >
< mui:LinkGroup.Links >
< mui:Link DisplayName = "Wireframe" Source = "/Pages/LayoutWireframe.xaml" />
< mui:Link DisplayName = "Basic" Source = "/Pages/LayoutBasic.xaml" />
< mui:Link DisplayName = "Split" Source = "/Pages/LayoutSplit.xaml" />
< mui:Link DisplayName = "List" Source = "/Pages/LayoutList.xaml" />
< mui:Link DisplayName = "Tab" Source = "/Pages/LayoutTab.xaml" />
</ mui:LinkGroup.Links >
</ mui:LinkGroup >
< mui:LinkGroup DisplayName = "Controls" >
< mui:LinkGroup.Links >
< mui:Link DisplayName = "Styles" Source = "/Pages/ControlsStyles.xaml" />
< mui:Link DisplayName = "Modern controls" Source = "/Pages/ControlsModern.xaml" />
</ mui:LinkGroup.Links >
</ mui:LinkGroup >
...
...
...
</ mui:ModernWindow.MenuLinkGroups >
</ mui:ModernWindow >
|
主窗口的主菜单是 ModernWindow 类的一个依赖属性,属性名是MenuLinkGroups。它返回一个LinkGroupCollection 类的实例,这个类继承自ObservableCollection<LinkGroup>。也就是说,主菜单是若干link group的集合。每个 LinkGroup 代表着菜单中的一个入口。所以,如果每个动态模块都可以导出一个LinkGroup的实例,我们所需要做的就是将其加入到Link group的集合中。 导出方式以接口契约的形式公布出来。 1 2 3 4 | public interface ILinkGroupService
{
LinkGroup GetLinkGroup();
}
|
核心模块中定义ILinkGroupService的接口。这表明如果一个模块要在主菜单中加入一个选项,该模块只需实现返回LinkGroup实例的 GetLinkGroup()方法即可。ILinkGroupService接口的实现以及theGetLinkGroup()方法示例代码如下: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | public class LinkGroupService : ILinkGroupService
{
public LinkGroup GetLinkGroup()
{
LinkGroup linkGroup = new LinkGroup
{
DisplayName = "Module One"
};
linkGroup.Links.Add( new Link
{
DisplayName = "Module One" ,
Source = new Uri($ "/DM.ModuleOne;component/Views/{nameof(MainView)}.xaml" , UriKind.Relative)
});
return linkGroup;
}
}
|
现在我们就可以动态加载这些模块了,为每个模块生成ILinkGroupService 接口的实例,并且在主菜单中插入导出的选项。 下面是Bootstrapper类中ConfigureModuleCatalog()方法的实现代码。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | protected override IModuleCatalog CreateModuleCatalog()
{
return new DirectoryModuleCatalog() { ModulePath = MODULES_PATH };
}
protected override void ConfigureModuleCatalog()
{
var directoryCatalog = (DirectoryModuleCatalog)ModuleCatalog;
directoryCatalog.Initialize();
linkGroupCollection = new LinkGroupCollection();
var typeFilter = new TypeFilter(InterfaceFilter);
foreach ( var module in directoryCatalog.Items)
{
var mi = (ModuleInfo)module;
var asm = Assembly.LoadFrom(mi.Ref);
foreach (Type t in asm.GetTypes())
{
var myInterfaces = t.FindInterfaces(typeFilter, typeof (ILinkGroupService).ToString());
if (myInterfaces.Length > 0)
{
var linkGroupService = (ILinkGroupService)asm.CreateInstance(t.FullName);
var linkGroup = linkGroupService.GetLinkGroup();
linkGroupCollection.Add(linkGroup);
}
}
}
var moduleCatalog = (ModuleCatalog)ModuleCatalog;
moduleCatalog.AddModule( typeof (Core.CoreModule));
}
|
我们首先创建 Bootstrapper.ModuleCatalog作为一个 DirectoryModuleCatalog ,并且初始化模块目录。 然后迭代各动态发现的模块,对于每个模块,查找实现了ILinkGroupService接口的类型。如果这样一个类型找到了,则创建该类的实例并调用它的GetLinkGroup()方法。返回的LinkGroup实例被插入到框架程序(Shell)中的菜单项集合里: 1 2 3 4 5 6 7 8 9 10 11 | protected override DependencyObject CreateShell()
{
Shell shell = Container.Resolve<Shell>();
if (linkGroupCollection != null )
{
shell.AddLinkGroups(linkGroupCollection);
}
return shell;
}
|
框架的AddLinkGroups()方法定义如下: 1 2 3 4 5 6 7 8 9 | public void AddLinkGroups(LinkGroupCollection linkGroupCollection)
{
CreateMenuLinkGroup();
foreach (LinkGroup linkGroup in linkGroupCollection)
{
this .MenuLinkGroups.Add(linkGroup);
}
}
|
CreateMenuLinkGroup()方法创建了主菜单中静态公共的菜单组,每个循环中又动态创建了一个菜单组。这样就大功告成了,伙计。如果某个模块,比如模块1ModuleOne从模块目录中被移除了,主菜单将变成这样: 相应的,如果模块2ModuleTwo被移除了,主菜单将变成这样: 显而易见的如果在模块目录中没有模块,则只会显示出静态公共菜单项。 结论
Prism库提供一组丰富的资源来创建模块化的 WPF应用程序。 WPF 库Modern UI(MUI)提供了丰富资源来创建界面美观的WPF应用程序。这篇文章展示了使用一个两全其美的办法来创建一个插件式架构。另一个相关主题是认证授权,不在此原型系统的讨论范围之内,即根据用户名或其角色来动态加载模块,以限制用户只能访问应用程序中已授权的功能模块。 我相信肯定有其他的甚至更好的方式来做这样的融合,请随时评论并留下的你想法、建议和意见。我们欢迎你评论。 感兴趣的链接 你会在下列链接中找到补充信息: 目前已不需要实现在视图的后端代码中实现IView 接口。
|