使用 Prism 和 Modern UI for WPF Toolkit 的动态模块





5.00/5 (8投票s)
基于 Prism 库和 Modern UI for WPF (MUI) 工具包的插件架构原型。
引言
不久前,一位同事告诉我他需要解决的一个问题。一位客户要求他开发一个桌面应用程序,该应用程序根据运行软件的企业办公室的不同而呈现不同的功能集。一方面,我回想起了过去一个项目中的解决方案。另一方面,有一个用于创建具有现代外观和感觉的 WPF 应用程序的开源项目,我关注它已经有几年了,因为我认为它确实非常棒。
我想知道是否可以使用 Prism 和用于创建插件架构的开源 MUI 库来解决这个问题,并提出了一个原型解决方案,我在这里呈现它。
动态模块是一个基于 Prism Library 和 Modern UI for WPF (MUI) 工具包的 WPF 模块化应用程序的示例原型。它是创建具有 Metro 风格的现代 UI WPF 应用程序插件架构的概念证明。
背景和要求
本文假定读者至少对 Windows Presentation Foundation (WPF)、Prism Library 和 Unity IoC 容器有一定的基本了解。编译项目需要 Visual Studio 2015。
架构
提出的插件架构的核心思想是
- 将所需的项目模块放入一个目录中(或者将所有模块放入,并在加载时进行过滤)。
- 动态加载 "modules" 文件夹中的项目模块。
- 每个模块都公开一个主菜单选项的入口点。
- 动态地从加载的模块构建主菜单。
- 主菜单的第一个选项是固定的,对所有用户都是通用的。
- 一个核心模块,包含共享服务、存储库、DTOs、数据模型定义等,是静态加载的。它可以被任何解决方案项目引用。
动态模块在构建的后期会复制到一个目录中。这些模块不被启动项目引用,而是通过检查目录中的程序集来发现。模块项目具有以下构建后期步骤,以便将自身复制到该目录中
xcopy "$(TargetDir)$(TargetFileName)" "$(TargetDir)modules\" /y
解决方案被构建到 "..\bin\" 文件夹中。
理解代码
如果您查看 MUI 演示项目 中 MainWindow.xaml 的源代码,您会看到主菜单是如何静态构建的。
<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>
。也就是说,主菜单是链接组的可观察集合。每个 LinkGroup 代表一个菜单入口点。因此,如果每个动态模块都有一个导出 LinkGroup
实例的方法,我们只需要将其添加到链接组的可观察集合中。这可以通过接口契约来实现。
public interface ILinkGroupService
{
LinkGroup GetLinkGroup();
}
核心模块定义了 ILinkGroupService
接口。它规定,如果一个模块想要在主菜单上添加一个选项,它可以实现 GetLinkGroup()
方法来实现,该方法实际上返回一个 LinkGroup
实例。ILinkGroupService
接口和 GetLinkGroup()
方法的实现将如下所示:
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()
方法实现的逻辑。
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)
{
// We found the type that implements the ILinkGroupService interface
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 时被传递给它。
protected override DependencyObject CreateShell()
{
Shell shell = Container.Resolve<shell>();
if (linkGroupCollection != null)
{
shell.AddLinkGroups(linkGroupCollection);
}
return shell;
}
Shell.AddLinkGroups()
方法定义如下:
public void AddLinkGroups(LinkGroupCollection linkGroupCollection)
{
CreateMenuLinkGroup();
foreach (LinkGroup linkGroup in linkGroupCollection)
{
this.MenuLinkGroups.Add(linkGroup);
}
}
其中 CreateMenuLinkGroup()
方法创建主菜单的 static
通用选项,而 foreach
循环创建动态选项。这就是全部内容。如果某个模块(例如 ModuleOne
)从 "modules" 文件夹中移除,主菜单将如下所示:
或者,如果 ModuleTwo
被移除,主菜单将如下所示:
显然,如果 "modules" 文件夹中没有任何模块,则只能访问静态通用选项。
结论
Prism Library 提供了一套用于创建模块化 WPF 应用程序的资源。Modern UI for WPF (MUI) 工具包提供了一套用于为 WPF 应用程序创建漂亮且外观良好的 UI 的资源。本文介绍了一种将这两种世界结合起来创建插件架构的方法。一个相关的议题是授权,即根据用户名或角色动态加载模块,使用户只能访问应用程序的授权区域或功能 ,这超出了原型项目的范围。
当然,还有其他方法甚至更好的方法来实现这种结合,所以请随时发表评论并留下您的想法、建议和意见。我们欢迎!
相关链接
您可能会在以下地方找到补充信息:
- Modern UI for WPF (MUI) 演示项目
- Modern UI for WPF 模板(Visual Studio 扩展)
- Prism Samples WPF / Modularity With Unity
- 关于 Prism 的 ViewModelLocator 约定*
*目前,无需在视图的代码隐藏中 "实现" IView
接口。
历史
- 2016 年 3 月 22 日:版本 1.0 - 初始文章提交