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

简介:

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,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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。

1
2
3
4
5
6
7
8
9
10
11
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节点可以嵌套包含子节点。

1
2
3
4
5
6
7
8
9
10
11
12
< 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接口所在的程序集,通过该接口来获取服务,如下所示。

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

BundleActivator.ConfigrationService.Set(BundleActivator.Bundle, "TreeViewColumnWidth", 80);

或者

var width = BundleActivator.ConfigurationService.Get(BundleActivator.Bundle, "TreeViewColumnWidth", 80);

 

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框架来开发一个漂亮界面框架了。


本文转自道法自然博客园博客,原文链接:http://www.cnblogs.com/baihmpgy/p/osgi_muinavtree_fx.html,如需转载请自行联系原作者

目录
相关文章
|
22天前
|
设计模式 前端开发 C#
使用 Prism 框架实现导航.NET 6.0 + WPF
使用 Prism 框架实现导航.NET 6.0 + WPF
63 10
|
2月前
|
编解码 C# 数据库
C# + WPF 音频播放器 界面优雅,体验良好
【9月更文挑战第18天】这是一个用 C# 和 WPF 实现的音频播放器示例,界面简洁美观,功能丰富。设计包括播放/暂停按钮、进度条、音量控制滑块、歌曲列表和专辑封面显示。功能实现涵盖音频播放、进度条控制、音量调节及歌曲列表管理。通过响应式设计、动画效果、快捷键支持和错误处理,提升用户体验。可根据需求扩展更多功能。
|
3月前
|
开发框架 前端开发 JavaScript
循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(12) -- 使用代码生成工具Database2Sharp生成WPF界面代码
循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(12) -- 使用代码生成工具Database2Sharp生成WPF界面代码
循序渐进介绍基于CommunityToolkit.Mvvm 和HandyControl的WPF应用端开发(12) -- 使用代码生成工具Database2Sharp生成WPF界面代码
|
3月前
|
测试技术 C# 开发者
“代码守护者:详解WPF开发中的单元测试策略与实践——从选择测试框架到编写模拟对象,全方位保障你的应用程序质量”
【8月更文挑战第31天】单元测试是确保软件质量的关键实践,尤其在复杂的WPF应用中更为重要。通过为每个小模块编写独立测试用例,可以验证代码的功能正确性并在早期发现错误。本文将介绍如何在WPF项目中引入单元测试,并通过具体示例演示其实施过程。首先选择合适的测试框架如NUnit或xUnit.net,并利用Moq模拟框架隔离外部依赖。接着,通过一个简单的WPF应用程序示例,展示如何模拟`IUserRepository`接口并验证`MainViewModel`加载用户数据的正确性。这有助于确保代码质量和未来的重构与扩展。
63 0
|
3月前
|
C# UED 开发者
WPF动画大揭秘:掌握动画技巧,让你的界面动起来,告别枯燥与乏味!
【8月更文挑战第31天】在WPF应用开发中,动画能显著提升用户体验,使其更加生动有趣。本文将介绍WPF动画的基础知识和实现方法,包括平移、缩放、旋转等常见类型,并通过示例代码展示如何使用`DoubleAnimation`创建平移动画。此外,还将介绍动画触发器的使用,帮助开发者更好地控制动画效果,提升应用的吸引力。
127 0
|
3月前
|
容器 C# 开发者
XAML语言大揭秘:WPF标记的魅力所在,让你轻松实现界面与逻辑分离,告别复杂代码!
【8月更文挑战第31天】XAML提供了一种直观且易于维护的界面设计方式,使得开发者可以专注于逻辑和业务代码的编写,而无需关心界面细节。通过数据绑定、布局管理和动画效果等特性,XAML可以实现丰富的界面交互和视觉效果。在实际开发过程中,开发者应根据具体需求选择合适的技术方案,以确保应用程序能够满足用户的需求。希望本文的内容能够帮助您在WPF应用程序开发中更好地利用XAML语言。
40 0
|
3月前
|
数据处理 开发者 C#
WPF数据绑定实战:从零开始,带你玩转数据与界面同步,让你的应用程序更上一层楼!
【8月更文挑战第31天】在WPF应用开发中,数据绑定是核心技能之一,它能实现界面元素与数据源的同步更新。本文详细介绍了WPF数据绑定的概念与实现方法,包括属性绑定、元素绑定及路径绑定等技术,并通过示例代码展示了如何创建数据绑定。通过数据绑定,开发者不仅能简化代码、提高可维护性,还能提升用户体验。无论初学者还是有经验的开发者,都能从中受益,更好地掌握WPF数据绑定技巧。
62 0
|
3月前
|
C# Windows 开发者
超越选择焦虑:深入解析WinForms、WPF与UWP——谁才是打造顶级.NET桌面应用的终极利器?从开发效率到视觉享受,全面解读三大框架优劣,助你精准匹配项目需求,构建完美桌面应用生态系统
【8月更文挑战第31天】.NET框架为开发者提供了多种桌面应用开发选项,包括WinForms、WPF和UWP。WinForms简单易用,适合快速开发基本应用;WPF提供强大的UI设计工具和丰富的视觉体验,支持XAML,易于实现复杂布局;UWP专为Windows 10设计,支持多设备,充分利用现代硬件特性。本文通过示例代码详细介绍这三种框架的特点,帮助读者根据项目需求做出明智选择。以下是各框架的简单示例代码,便于理解其基本用法。
109 0
|
3月前
|
存储 前端开发 C#
WPF/C#:更改界面的样式
WPF/C#:更改界面的样式
40 0
|
3月前
|
开发框架 JSON 前端开发
WPF应用框架中工作流模块的介绍
WPF应用框架中工作流模块的介绍