65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2016年3月22日

Ms-PL

5分钟阅读

viewsIcon

38776

downloadIcon

929

基于 Prism 库和 Modern UI for WPF (MUI) 工具包的插件架构原型。

引言

不久前,一位同事告诉我他需要解决的一个问题。一位客户要求他开发一个桌面应用程序,该应用程序根据运行软件的企业办公室的不同而呈现不同的功能集。一方面,我回想起了过去一个项目中的解决方案。另一方面,有一个用于创建具有现代外观和感觉的 WPF 应用程序的开源项目,我关注它已经有几年了,因为我认为它确实非常棒。

我想知道是否可以使用 Prism 和用于创建插件架构的开源 MUI 库来解决这个问题,并提出了一个原型解决方案,我在这里呈现它。

WPF Dynamic Modules Demo

动态模块是一个基于 Prism LibraryModern 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" 文件夹中移除,主菜单将如下所示:

Module one is removed

或者,如果 ModuleTwo 被移除,主菜单将如下所示:

Module two is removed

显然,如果 "modules" 文件夹中没有任何模块,则只能访问静态通用选项。

Both modules are removed

结论

Prism Library 提供了一套用于创建模块化 WPF 应用程序的资源。Modern UI for WPF (MUI) 工具包提供了一套用于为 WPF 应用程序创建漂亮且外观良好的 UI 的资源。本文介绍了一种将这两种世界结合起来创建插件架构的方法。一个相关的议题是授权,即根据用户名或角色动态加载模块,使用户只能访问应用程序的授权区域或功能 ,这超出了原型项目的范围。

当然,还有其他方法甚至更好的方法来实现这种结合,所以请随时发表评论并留下您的想法、建议和意见。我们欢迎!

相关链接

您可能会在以下地方找到补充信息:

*目前,无需在视图的代码隐藏中 "实现" IView 接口。

历史

  • 2016 年 3 月 22 日:版本 1.0 - 初始文章提交
© . All rights reserved.