【源码分享】WPF漂亮界面框架实现原理分析及源码分享

2023-01-01,,,,

1 源码下载
2 OSGi.NET插件应用架构概述
3 漂亮界面框架原理概述
4 漂亮界面框架实现
 4.1 主程序
 4.2 主程序与插件的通讯
  4.2.1 主程序获取插件注册的服务
  4.2.2 插件获取主程序注册的服务
  4.2.3 服务接口
 4.3 权限管理插件的登录窗体
 4.4 界面框架插件
  4.4.1 导航服务
  4.4.2 界面框架扩展实现
 4.5 插件
  4.5.1 插件引用了第三方程序集
  4.5.2 一个程序集如何让所有插件都直接使用
  4.5.3 插件引用了另一个插件的程序集
  4.5.4 插件间的通讯实现
  4.5.5 如何从插件动态的加载类型
5 关于框架的艺术
6 总结

1 源码下载

直接放出源码地址,为了编译源码,需要下载安装OSGi.NET插件框架安装包:http://www.iopenworks.com/。

【1】框架安装包:MuiTreeNavVsPackage.zip(使用方法见上一篇文章:分享一个漂亮WPF界面框架创作过程及其源码)。

【2】框架源代码:MuiTreeNavSource.zip    注意:要正确编译,必须安装Nuget且连接网络,必须提前安装iOpenWorksSDK。

2 OSGi.NET插件应用架构概述

基于OSGi.NET插件框架的应用由以下三个部分构成:

(1)主程序:针对特定应用环境(WPF、Web、WinForm等应用环境),加载启动插件,获取插件入口,运行入口程序。

(2)插件:提供应用功能,实现对其它插件功能扩展并暴露功能扩展点。

(3)插件框架:与特定应用环境无关,实现插件的加载、启动、停止、更新和卸载,实现插件功能组合与扩展。

3 漂亮界面框架原理概述

WPF漂亮界面框架最终展示效果如下图所示。主界面中间区域的左边是导航栏,右边是显示区域,点击导航栏的导航节点后,在内容区域动态显示其内容。此外,还提供了标题栏、状态栏、系统菜单、系统设置等默认功能。

该界面,从功能上看,它由界面框架插件、演示插件、权限管理插件、插件中心插件以及通用功能插件构成,如下所示。

这些插件的功能组合关系如下所示,"应用 = 界面框架插件 + 功能插件(演示/权限管理/插件中心插件)扩展"。界面框架定义了系统主界面风格、可扩展的属性导航栏、可扩展的内容区域等元素构成。

上述的权限管理插件除了提供角色管理/用户管理功能,它还定义了一个登录窗体。主程序exe文件在执行时,首先创建并启动OSGi.NET插件框架,然后通过服务总线获取权限管理插件注册的登录窗体,并显示。此时,程序执行的控制权则完全交由插件。

在权限管理插件的登录界面,登录成功之后,它会显示界面框架插件定义的MainWindow主界面。该主界面则开始来组合插件的功能。下面,我们来看看插件实现的细节。

4 漂亮界面框架实现

4.1 主程序

主程序主要实现:(1)创建启动插件框架;(2)获取入口,并进入入口程序。下面我们来看看这个WPF主程序的入口。

在App.xaml.cs中定义了一个函数StartBundleRuntime,如下所示。

private void StartBundleRuntime()
{
    ……
    // 创建BundleRuntime
    var bundleRuntime = new BundleRuntime();
    // 不启动多版本支持
    bundleRuntime.EnableAssemblyMultipleVersions = false;
    // 监听插件状态变化,更新进度条
    bundleRuntime.Framework.EventManager.AddBundleEventListener(BundleStateChangedHandler, true);
    // 监听框架状态变化
    bundleRuntime.Framework.EventManager.AddFrameworkEventListener(FrameworkStateChangedHandler);
    // 将Application实例添加到全局服务,与插件进行共享
    bundleRuntime.AddService<Application>(this);
    // 启动插件框架
    bundleRuntime.Start();
    // 移除事件监听
    bundleRuntime.Framework.EventManager.RemoveBundleEventListener(BundleStateChangedHandler, true);
    bundleRuntime.Framework.EventManager.RemoveFrameworkEventListener(FrameworkStateChangedHandler);

    Startup += App_Startup;
    Exit += App_Exit;
    _bundleRuntime = bundleRuntime;
}

在主程序中,它使用以下代码来获取入口,这个入口是一个LoginWindow。

private void App_Startup(object sender, StartupEventArgs e)
{
    ……
	// 获取loginWindow实例,并显示该窗口
    var loginWindow = bundleRuntime.GetFirstOrDefaultService<Window>();
    loginWindow.Loaded += (sender2, e2) =>
    {
        loginWindow.Activate();
    };
    loginWindow.Show();
}

4.2 主程序与插件的通讯

OSGi.NET插件框架提供了一个简单的方式来实现主程序与插件间的通讯,即服务。

主程序可以通过插件框架BundleRuntime来注册和获取服务,插件可以通过插件激活器的上下文来注册和获取服务、或者使用BundleRuntime.Instance这个单例来注册与获取服务。也就是说,主程序的BundleRuntime、插件的上下文IBundleContext都是对应相同的服务总线。

服务在这里表述为:服务 = 接口/基类 + 实现类。比如ISayHelloService接口、SayHelloServiceBase基类、SayHelloService实现类。我们可以注册服务为:

AddService<ISayHelloService>(new SayHelloService())

或者

AddService<SayHelloServiceBase>(new SayHelloService())

那么获取服务的方式就是:

Get**Service<ISayHelloService>()

或者

Get**Service<SayHelloServiceBase>()

4.2.1主程序获取插件注册的服务

在该框架,主程序需要获取权限管理插件注册的登录窗体,然后运行,接着将系统控制权转交给插件。这时候,主程序通过以下代码来获取服务。

(1)创建启动插件框架

var bundleRuntime = new BundleRuntime();

bundleRuntime.Start();

(2)获取服务

var loginWindow = bundleRuntime.GetFirstOrDefaultService<Window>();

loginWindow.Show();

权限管理插件在Activator类中,通过以下代码将LoginWindow注册到服务总线。

public class Activator : IBundleActivator
{
    public void Start(IBundleContext context)
    {
        context.AddService<Window>(new LoginWindow());
    }

    public void Stop(IBundleContext context)
    {

    }
}

这里,需要注意的是:主程序只能等插件框架启动起来后,才能够获取插件注册的服务。

4.2.2插件获取主程序注册的服务

主程序可以为插件注册全局的服务,这样所有插件在启动的时候,就可以直接来访问。主程序注册全局服务的代码如下:

var bundleRuntime = new BundleRuntime();

bundleRuntime.AddService<ISayHelloService>();

bundleRuntime.Start();

注意:主程序在BundleRuntime.Start方法调用前注册的服务,插件在启动时即可获取。

这时候,插件可以在激活器中直接获取到该服务了。

public class Activator : IBundleActivator
{
    public void Start(IBundleContext context)
    {
        var sayHelloService = context.GetFirstOrDefaultService<ISayHelloService>();
        sayHelloService.Hell(“Lorry Chen”);
    }

    public void Stop(IBundleContext context)
    {

    }
}

4.2.3 服务接口

在4.2.1小节中,主程序和权限管理插件在处理服务时,使用Window这个类作为服务的契约。这个服务契约是在.NET Framework中直接定义的,因此主程序和插件都可以访问到。如果我们新定义的服务SayHelloService(ISayHelloService接口、SayHelloService服务实现类),那么这时候主程序和插件都需要通过接口ISayHelloService来获取服务,这时候建议将ISayHelloService接口定义到一个外部的程序集,主程序可以引用它,插件也可以依赖它。

4.3 权限管理的登录窗体

基于4.2,我们发现通过服务可以实现主程序和插件之间的通讯。当主程序获取到权限管理注册的登录窗体实例,便获取该窗体并展现它,此后应用系统便交由插件来控制了。

在权限管理插件的登录窗体,它由LoginUserControl.xaml来实现,在该页面的后台代码的登录处理函数中,一旦登录成功,它将创建一个主窗体MainWindow,并且显示该窗体,如下图所示。

在这里,权限管理插件创建了主窗体MainWindow类,这个类实际上是由界面框架插件定义的主窗体。因此,该插件依赖了界面框架插件,并添加了对UIShell.WpfShellPlugin程序集的引用。如下所示。

通过上述的工作,登录窗体在登录成功之后,就可以显示界面框架的主窗体了。

4.4 界面框架插件

应用系统由界面框架插件、服务插件和功能插件构成,它们的组合关系如下所示。

从界面功能上来讲,系统由主界面框架、插件中心插件、权限管理插件、演示插件组成,在其背后还有一些非界面功能插件,比如数据库访问等。

界面框架插件提供了一个可扩展、可组合的界面功能展示。界面框架插件暴露了一个名为UIShell.NavigationService的扩展点,权限管理插件、插件中心插件、其它插件则定义了针对该扩展点的扩展。

界面框架对应的扩展格式如下所示。该格式由名为Node的XML节点组成,Node节点可以嵌套包含子节点。

<Extension Point="UIShell.NavigationService">
  <Node Id="2E3614E0-388D-46E4-88A8-42E7CB3B421F" Name="权限管理"
        Icon="/UIShell.RbacManagementPlugin;component/Assets/Permission.png"
		Order="490">
    <Node Name="角色管理" Permission="RoleManagementPermission"
          Value="UIShell.RbacManagementPlugin.RolePermissionUserControl"
          Icon="/UIShell.RbacManagementPlugin;component/Assets/Role.png" Order="1" />
    <Node Name="用户管理" Permission="UserManagementPermission"
          Value="UIShell.RbacManagementPlugin.UserPermissionUserControl"
          Icon="/UIShell.RbacManagementPlugin;component/Assets/User2.png" Order="2" />
  </Node>
</Extension>

当界面框架插件没有加载任何扩展时,界面是空白的。左边导航栏用于加载插件定义的导航菜单,右边用于加载插件的显示内容。

那么插件中心插件就是由对界面框架插件的扩展及如下功能构成,如下所示。

插件中心插件对界面框架插件的界面扩展是通过如下的Manifest.xml来定义的。

同理,权限管理插件也是对界面框架插件定义了扩展并实现了如下功能。

权限管理插件对界面框架的扩展定义在Manifest.xml中实现,如下所示。

课程管理这个示例插件也是如此。

4.4.1 导航服务

插件对界面框架的扩展的XML由导航服务来进行解析。通俗的讲,该服务实现的是将以下XML节点变更NavigationNode对象。

<Extension Point="UIShell.NavigationService">
  <Node Id="2E3614E0-388D-46E4-88A8-42E7CB3B421F" Name="权限管理"
        Icon="/UIShell.RbacManagementPlugin;component/Assets/Permission.png"
        Order="490">
    <Node Name="角色管理" Permission="RoleManagementPermission"
          Value="UIShell.RbacManagementPlugin.RolePermissionUserControl"
          Icon="/UIShell.RbacManagementPlugin;component/Assets/Role.png" Order="1" />
    <Node Name="用户管理" Permission="UserManagementPermission"
          Value="UIShell.RbacManagementPlugin.UserPermissionUserControl"
          Icon="/UIShell.RbacManagementPlugin;component/Assets/User2.png" Order="2" />
  </Node>
</Extension>

NavigationNode对象如下图所示,它包含子对象。该对象对应于XML节点。我们可以通过INavigationService来获取这些对象集合。INavigationService会默认从名字为"UIShell.NavigationService"的扩展点来创建对象。如果我们使用了类似的导航扩展定义,但使用了不同的扩展点,可以使用INavigationServiceFactory来创建指定扩展点的导航服务。

导航服务还隐藏了针对扩展变更事件的处理。该服务暴露了NavigationChanged事件来通知导航节点变更。

4.4.2 界面框架扩展实现

界面框架首先需要实现一个空的布局,其内容区域为树和空白显示区域。树使用TreeView,空白显示区域的父控件是DockPanel。那么,该框架实现的核心就是将NavigationNode的集合转换成TreeViewNode集合,当点击TreeViewNode时,能够将其对应的用户控件加载。

界面框架的XAML如下所示。

<UserControl x:Class="UIShell.WpfShellPlugin.Pages.Layout"……>
    <Grid Style="{StaticResource ContentRoot}">
        <DockPanel>
            <DockPanel DockPanel.Dock="Bottom" Height="20" ……>
                ……//Status Bars
            </DockPanel>

            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Name="TreeViewColumn"></ColumnDefinition>
                    <ColumnDefinition Width="*"></ColumnDefinition>
                </Grid.ColumnDefinitions>

                <TreeView Grid.Column="0" Grid.Row="0" Name="NavigationTreeView"
                    SelectedItemChanged="NavigationTreeView_SelectedItemChanged" />

                <GridSplitter DragCompleted="GridSplitter_DragCompleted" />

                <TextBlock  Grid.Column="1" Grid.Row="0" Name="LoadingTextBlock"
                    Text="加载中......"  …… Visibility="Hidden"></TextBlock>

                <Grid Grid.Column="1" Grid.Row="0" Name="LayoutDockPanel">
                </Grid>
            </Grid>
        </DockPanel>

        <DockPanel Name="SideBarDockPanel"
Background="{DynamicResource WindowBackground}"
Width="300" HorizontalAlignment="Right" Visibility="Hidden">
            <Border BorderThickness="2" BorderBrush="{DynamicResource Accent}">
                <Grid>
                    <Grid.RowDefinitions>
                        <RowDefinition Height="45" />
                        <RowDefinition Height="*" />
                    </Grid.RowDefinitions>

                    <TextBlock Name="SideBarTitleTextBlock"
Grid.Row="0" Margin="16, 16, 16, 0"
Foreground="{DynamicResource Accent}" FontSize="20" />

                    <DockPanel Grid.Row="1" Margin="16"
                        Name="SideBarDockPanelContent">
                    </DockPanel>
                </Grid>

            </Border>
        </DockPanel>
    </Grid>
</UserControl>

从这些XAML片段,你可以看到,LayoutDockPanel这个名字的控件时用于放置动态加载的插件的控件,加载时机是在NavigationTreeView的SelectedItemChanged事件。另外,该界面框架还实现了SideBarDockPanel,用于支持从侧面动态滑出一个侧边框。

下面我们看看界面框架针对扩展的处理。

接着我们看看ResetNavigation函数的实现。

其实现的核心就是InitializeNavigationTreeView。

该函数就是根据NavigationNode集合,递归创建TreeViewItem。下面我们来看看点击树形导航节点时,如何动态加载显示插件的控件,其核心代码如下。

从插件动态加载类型时,我们使用的是node.Bundle.LoadClass,即获取扩展注册的插件对象,调用该对象的LoadClass方法来加载用户控件,然后将用户控件显示在LayoutDockPanel控件。

不过,当前界面框架还处理一些其它的功能:

(1)当前导航节点的侧边栏,即当切换菜单时,会自动打开/关闭与其关联的侧边栏;

(2)缓存与关闭,即加载用户控件后,会直接缓存,在切换时,会将前一个控件隐藏,接着显示当前控件;只有关闭后,用户控件才从父控件移除掉;

(3)关闭内容区域与导航节点选择的同步,也就是说,关闭当前内容后,会默认显示前一个页面,此时,导航节点的选择也必须同步切换;

(4)相关对象的关系存储。

4.5 插件

下面我将从以下几个方面来谈一下开发插件过程中,需要处理的一些问题。

4.5.1 插件引用了第三方程序集

在主界面框架中,我们依靠第三方控件库"ModernUI"来实现界面,并对"ModernUI"做深入的定制。在界面框架插件引用该控件时,首先,我们需要将该插件添加到Manifest.xml作为本地程序集,即界面框架插件在运行时需要与该程序集一起才能够正常运行。

接着,可以直接从bin目录来引用该程序集或者添加ModernUI源码项目的程序集引用。

这时候,在界面框架插件中,就可以来直接使用ModernUI程序集的类型了。如下示例。

ModernDialog.ShowMessage("Hello, world!", "Hello", MessageButtongs.Ok);

或者

var dialog = new ModernDialog(){……};

dialog.ShowDialog();

4.5.2 一个程序集如何让所有插件都直接使用

在这个WPF应用程序,每一个插件在开发界面时大部分使用了MVVM架构,它依赖于MVVMLite这个库。为了能够让插件直接使用,并且不需要将其添加到本地程序集的情况下来使用。我们可以在主程序里面直接添加对MVVMLite程序集的依赖,编译后,每一个插件可以直接来引用主程序输出目录下的MVVMLite程序集。

你可以发现,MVVMLite程序集所在的位置。如果是Web应用的话,这些程序集所在目录是bin目录。这样的程序集在OSGi.NET框架中成为全局程序集,默认开启支持该功能。你可以通过设置BundleRuntime.EnableGlobalAssemblyFeature属性开启或者关闭该功能。

全局程序集有以下特点:(1)如果插件包含了另一个程序集,和该程序集名称一样,则会被替换掉;(2)全局程序集不支持多版本。

4.5.3 插件引用了另一个插件的程序集

在该界面框架中,所有UI插件都是基于ModernUI控件库来实现。该控件库在界面框架中包含。因此,我们的功能插件需要引用界面框架插件的ModernUI控件库。

首先,在界面框架,需要将该程序集定义成共享。

接着,在功能插件中,需要添加对界面框架的依赖。

最后,插件就可以直接通过引用,来添加对该程序集的引用,并在代码中来调用了。

4.5.4 插件间的通讯实现

插件间的通讯,有两种方式,第一种是一个插件直接使用另一个插件的程序集的类,如4.5.3的方式;第二种是松耦合的方式,即使用服务。

比如,在演示插件,我们引用了配置服务。配置服务是在配置服务插件来创建的,该服务定义如下所示。

该插件通过Activator来注册服务实例,如下所示。

using System;
using System.Collections.Generic;
using System.Text;
using UIShell.OSGi;

namespace UIShell.ConfigurationService
{
    public class Activator : IBundleActivator
    {
        public void Start(IBundleContext context)
        {
            context.AddService<IConfigurationService>(new ConfigurationService());
        }

        public void Stop(IBundleContext context)
        {

        }
    }
}

演示插件依赖于IConfigurationService接口所在的程序集,通过该接口来获取服务,如下所示。

接着,在演示插件就可以通过以下方式来存储或者获取配置了。

);

或者

);

4.5.5 如何从插件动态的加载类型

从插件加载类型的方式通过插件对象来实现。插件对象由OSGi.NET框架创建,可以通过插件激活器的IBundleContext.Bundle属性获取。

var bundle = Context.Bundle; // 或者var bundle = Context.GetBundleBySymbolicName("DemoPlugin");
var class = bundle.LoadClass("DemoPlugin.CourseManagementUserControl");

5 关于框架的艺术

框架的艺术并不在于技术本身,而是在于能够帮助团队更有效率的进行产品开发。为了提高产品开发效率,框架必须能够提供:

(1)统一的开发模板:通过模板来规范团队成员的编码规则与规范功能模块的架构,减少软件开发的学习成本。比如,我们制作的演示插件模板,在这个模板基础上做功能开发,是不需要你掌握多少关于框架本身的技术,而是专注于业务实现及通用功能的调用;此外,该模板规范了MVVM架构分层,统一了架构思想。

(2)一致的用户体验:通过框架为客户定义了一致的界面风格,这使我们的软件看上起更加的专业。

(3)良好的分工协作:通过框架,团队成员可以专注于不同的功能模块,进行有效率的并行协作。

6 总结

这个教程介绍了漂亮界面框架的架构、实现细节,通过这个教程,你已经能够掌握使用OSGi.NET框架来开发一个漂亮界面框架了。

【源码分享】WPF漂亮界面框架实现原理分析及源码分享的相关教程结束。

《【源码分享】WPF漂亮界面框架实现原理分析及源码分享.doc》

下载本文的Word格式文档,以方便收藏与打印。