WPF 外接程序概述

.NET Framework提供了一个外接程序模型,开发人员可以用它来创建支持外接程序扩展性的应用程序。 借助此外接程序模型,可以创建与应用程序功能集成并进行扩展的外接程序。 在某些情况下,应用程序还需要显示外接程序所提供的用户界面。本主题演示 WPF 如何扩展 .NET Framework 外接程序模型来实现这些方案,并演示它的体系结构、优点及局限性。

先决条件

要求熟悉 .NET Framework 外接程序模型。 有关详细信息,请参阅外接程序和扩展性

外接程序概述

为了需要对应用程序重新编译和重新部署才能引入新功能这一复杂过程,应用程序实现了扩展机制,使开发者(包括第一方和第三方)能够创建与应用程序集成的其他应用程序。 支持此扩展性类型的最常见方式是,使用外接程序(也称为“加载项”和“插件”)。 通过外接程序公开扩展性的实际应用程序示例包括:

  • Internet Explorer 加载项。

  • Windows Media Player 插件。

  • Visual Studio 外接程序。

例如,通过 Windows Media Player 外接程序模型,第三方开发人员可以实现“插件”,从而以各种方式对 Windows Media Player 进行扩展,包括为 Windows Media Player 本身不支持的媒体格式(例如 DVD、MP3)创建解码器和编码器,创建音频效果和外观。 尽管有一些实体和行为是所有外接程序模型共有的,但会生成每个外接程序模型以公开某个应用程序的特有功能。

典型外接程序扩展性解决方案的三大实体是“协定”、“外接程序”和“主机应用程序”。 协定会定义外接程序与主机应用程序之间的两种集成方式:

  • 外接程序集成主机应用程序所实现的功能。

  • 主机应用程序公开供外接程序集成的功能。

为了使外接程序能发挥作用,主机应用程序需要在运行时找到它们并进行加载。 因此,支持外接程序的应用程序需要承担以下附加的职责:

  • 发现:查找遵循主机应用程序所支持的协定的外接程序。

  • 激活:加载和运行外接程序并与它们建立通信。

  • 隔离:使用应用程序域或进程建立隔离边界,以保护应用程序不受外接程序潜在安全问题和执行问题的影响。

  • 通信:通过调用方法和传递数据,允许外接程序和主机应用程序跨过隔离边界相互通信。

  • 生存期管理:通过可预测的干净方式来加载和卸载应用程序域和进程(请参阅应用程序域)。

  • 版本管理:确保在创建主机应用程序或外接程序的新版本后,它们仍可进行通信。

总之,开发一个可靠的外接程序模型不是一项简单的任务。 为此,.NET Framework 提供了一种用于生成外接程序模型的基础结构。

注意

有关外接程序的更多详细信息,请参阅外接程序和扩展性

.NET Framework 外接程序模型概述

.NET Framework 外接程序模型(位于 System.AddIn 命名空间中)包含一组类型,旨在简化外接程序扩展性的开发过程。 .NET Framework 外接程序模型的基本单元是“协定”,用于定义主机应用程序与外接程序之间的通信方式。 会使用特定于主机应用程序的协定视图向主机应用程序公开协定。 同样,向外接程序公开特定于外接程序的协定视图。 使用适配器,主机应用程序和外接程序可以在它们各自的协定视图之间进行通信。 协定、视图和适配器称为管道段,一组相关的管道段组成一条管道。 将管道作为基础,以便 .NET Framework 外接程序模型可以支持发现、激活、安全隔离、执行隔离(同时使用应用程序域和进程)、通信、生存期管理和版本管理。

所有这些支持使得开发人员能够生成与主机应用程序的功能相集成的外接程序。 但在某些情况下,主机应用程序需要显示外接程序所提供的用户界面。由于 .NET Framework 中的每种显示技术都有自己的用户界面实现模型,因此 .NET Framework 外接程序模型不支持任何特定的显示技术。 而由 WPF 对 .NET Framework 外接程序模型进行扩展,来支持外接程序的 UI。

WPF 外接程序

WPF 与 .NET Framework 外接程序模型相结合,能够处理各种要求主机应用程序显示外接程序用户界面的方案。具体而言,这些方案由 WPF 使用以下两种编程模型进行处理:

  1. 外接程序返回 UI。 外接程序按照协定的定义,通过方法调用将 UI 返回给主机应用程序。 此方案可用于以下情况:

    • 外接程序返回的 UI 的外观依赖于仅在运行时存在的数据或条件,例如动态生成的报告。

    • 外接程序提供的服务 UI 不同于可以使用该外接程序的主机应用程序 UI。

    • 外接程序主要执行主机应用程序的某项服务,并通过 UI 向主机应用程序报告状态。

  2. 外接程序为 UI。 正如协定所定义的那样,外接程序为 UI。 此方案可用于以下情况:

    • 外接程序提供的服务都需要显示,例如广告。

    • 外接程序提供的服务 UI 为可以使用该外接程序的所有主机应用程序(如计算器或颜色选取器)所共用。

这些方案要求 UI 对象可以在主机应用程序和外接应用程序域之间传递。 由于 .NET Framework 外接程序模型依赖于远程处理来在不同的应用程序域之间进行通信,因此在它们之间传递的对象必须可远程处理。

可远程处理的对象是某个类的实例,并执行以下一个或多个任务:

注意

有关创建可远程处理的 .NET Framework 对象的详细信息,请参阅使对象可远程处理

WPF UI 类型不可远程处理。 为解决此问题,WPF 扩展了 .NET Framework 外接程序模型,允许从主机应用程序显示外接程序所创建的 WPF UI。 此支持由 WPF 通过两种类型提供:INativeHandleContract 接口和 FrameworkElementAdapters 类实现的两个静态方法:ContractToViewAdapterViewToContractAdapter。 从较高层面来看,这些类型和方法按以下方式使用:

  1. WPF 要求由外接程序提供的用户界面是直接或间接从 FrameworkElement(如形状、控件、用户控件、版式面板和页面)派生的类。

  2. 无论协定在何处声明在外接程序和主机应用程序之间传递 UI,都必须将 UI 声明为 INativeHandleContract(而不是 FrameworkElement);INativeHandleContract 是可跨隔离边界传递的外接程序 UI 的可远程处理表示形式。

  3. 从外接程序的应用程序域传递之前,通过调用 ViewToContractAdapterFrameworkElement 打包为 INativeHandleContract

  4. 在传递到主机应用程序的应用程序域之后,必须通过调用 ContractToViewAdapterINativeHandleContract 重新打包为 FrameworkElement

使用 INativeHandleContractContractToViewAdapterViewToContractAdapter 的方式取决于特定方案。 下面几节介绍每个编程模型的详细信息。

外接程序返回用户界面

若要使外接程序向主机应用程序返回 UI,需要满足以下要求:

  1. 必须按照 .NET Framework 外接程序和扩展性文档的描述,创建主机应用程序、外接程序和管道。

  2. 协定必须实现 IContract,并且为了返回 UI,协定必须使用 INativeHandleContract 类型的返回值声明方法。

  3. 在外接程序和主机应用程序之间传递的 UI 必须直接或间接从 FrameworkElement 派生。

  4. 外接程序返回的 UI 必须先从 FrameworkElement 转换为 INativeHandleContract,然后才能跨越隔离边界。

  5. 返回的 UI 在跨越隔离边界之后必须从 INativeHandleContract 转换为 FrameworkElement

  6. 主机应用程序显示返回的 FrameworkElement

有关演示如何实现返回 UI 的外接程序的示例,请参阅创建返回 UI 的外接程序

外接程序为用户界面

当外接程序为 UI 时,必须满足以下要求:

  1. 必须按照 .NET Framework 外接程序和扩展性文档的描述,创建主机应用程序、外接程序和管道。

  2. 外接程序的协定接口必须实现 INativeHandleContract

  3. 传递到主机应用程序的外接程序必须直接或间接从 FrameworkElement 派生。

  4. 必须先将外接程序从 FrameworkElement 转换为 INativeHandleContract,然后才能跨越隔离边界。

  5. 在跨越隔离边界之后,必须将外接程序从 INativeHandleContract 转换为 FrameworkElement

  6. 主机应用程序显示返回的 FrameworkElement

有关演示如何实现作为 UI 的外接程序的示例,请参阅创建作为 UI 的外接程序

从外接程序返回多个 UI

外接程序通常向主机应用程序提供多个需要其显示的用户界面。 例如,假设一个作为 UI 的外接程序,它同时还向主机应用程序(也是 UI)提供状态信息。 此类外接程序可以通过结合使用外接程序返回用户界面外接程序为用户界面模型中的技术来实现。

外接程序和 XAML 浏览器应用程序

到目前为止,示例中的主机应用程序都安装为独立应用程序。 但是,如果满足以下附加的生成和实现要求,XAML 浏览器应用程序 (XBAP) 也可以承载外接程序:

  • 必须专门配置 XBAP 应用程序清单,以便将管道(文件夹和程序集)和外接程序程序集下载到客户端计算机上的 ClickOnce 应用程序缓存中 XBAP 所在的文件夹中。

  • 用于发现和加载外接程序的 XBAP 代码必须将 XBAP 的 ClickOnce 应用程序缓存用作管道和外接程序位置。

  • 如果外接程序引用位于源站点的松散文件,则 XBAP 必须将外接程序加载到专门的安全上下文中;在由 XBAP 承载时,外接程序只能引用位于主机应用程序源站点的松散文件。

下面几个小节将详细介绍这些任务。

配置用于 ClickOnce 部署的管道和外接程序

XBAP 将下载到 ClickOnce 部署缓存中的安全文件夹并从该文件夹运行。 为了使 XBAP 能够承载外接程序,还必须将管道和外接程序程序集下载到该安全文件夹。 为此,需要将应用程序清单配置为包含要下载的管道和外接程序程序集。 这在 Visual Studio 中最容易实现,但为使 Visual Studio 检测到管道程序集,管道和外接程序程序集需要位于宿主 XBAP 项目的根文件夹中。

因此,第一步是通过设置每个管道程序集和外接程序程序集项目的生成输出,向 XBAP 项目的根文件夹生成管道和外接程序程序集。 下表显示管道程序集项目和外接程序程序集项目的生成输出路径,这些路径位于与宿主 XBAP 项目相同的解决方案和根文件夹中。

表 1:XBAP 承载的管道程序集的生成输出路径

管道程序集项目 生成输出路径
合约 ..\HostXBAP\Contracts\
加载项视图 ..\HostXBAP\AddInViews\
加载项方适配器 ..\HostXBAP\AddInSideAdapters\
宿主端适配器 ..\HostXBAP\HostSideAdapters\
外接程序 ..\HostXBAP\AddIns\WPFAddIn1

下一步是通过在 Visual Studio 中执行以下操作,将管道程序集和外接程序程序集指定为 XBAP 内容文件:

  1. 通过在“解决方案资源管理器”中右键单击每个管道文件夹,然后选择“包括在项目中”,将管道和外接程序程序集包括在项目中。

  2. 在“属性”窗口中,将每个管道程序集和外接程序程序集的“生成操作”都设置为“内容”

最后一步是配置应用程序清单,以包含要下载的管道程序集文件和外接程序程序集文件。 这些文件应位于 XBAP 应用程序所占用的 ClickOnce 缓存的根文件夹下的文件夹中。 通过执行以下操作,可以在 Visual Studio 中实现该配置:

  1. 右键单击 XBAP 项目,依次单击“属性”、“发布”,然后单击“应用程序文件”按钮

  2. 在“应用程序文件”对话框中,将每个管道和外接程序 DLL 的“发布状态”都设置为“包括(自动)”,并将每个管道和外接程序 DLL 的“下载组”都设置为“(必需)”

从应用程序基使用管道和外接程序

为 ClickOnce 部署配置管道和外接程序时,它们将下载到 XBAP 所在的 ClickOnce 缓存文件夹中。 若要从 XBAP 中使用管道和外接程序,XBAP 代码必须从应用程序基获取它们。 用于处理管道和外接程序的 .NET Framework 外接程序模型的各种类型和成员为这种情况提供特殊支持。 首先,路径由 ApplicationBase 枚举值标识。 将此值用于使用管道的相关外接程序成员的重载,这些成员包括:

访问宿主的源站点

为确保外接程序可以引用源站点的文件,必须使用等效于主机应用程序的安全隔离来加载外接程序。 此安全级别由 AddInSecurityLevel.Host 枚举值标识,并在外接程序激活时传递到 Activate 方法。

WPF 外接程序体系结构

在最高级别,正如我们所见,WPF 使 .NET Framework 外接程序可以使用 INativeHandleContractViewToContractAdapterContractToViewAdapter 实现用户界面(直接或间接派生自 FrameworkElement)。 结果是向主机应用程序返回从主机应用程序 UI 显示的 FrameworkElement

对于简单的 UI 外接程序方案,这些内容已足够详细,可以满足开发者的需要。 对于更复杂的方案,特别是那些尝试使用附加 WPF 服务(如布局、资源和数据绑定)的方案,需要更为深入地掌握 WPF 如何扩展支持 UI 的 .NET Framework 外接程序模型的知识,才能了解它的优点和限制。

基本上,WPF 不会将 UI 从外接程序传递到主机应用程序;WPF 通过使用 WPF 互操作性传递 UI 的 Win32 窗口句柄。 因此,当 UI 从外接程序传递到主机应用程序时,会发生以下情况:

存在 HwndHost,用于显示来自 WPF 用户界面的窗口句柄标识的用户界面。 有关详细信息,请参阅 WPF 和 Win32 互操作

总之,INativeHandleContractViewToContractAdapterContractToViewAdapter 的存在,使 WPF UI 的窗口句柄能够从外接程序传递到主机应用程序,并在主机应用程序中由 HwndHost 封装并由主机应用程序的 UI 显示。

注意

由于主机应用程序获取 HwndHost,因此主机应用程序无法将 ContractToViewAdapter 返回的对象转换为由外接程序实现的类型(如 UserControl)。

从本质上来说,HwndHost 具有某些限制,这些限制会影响主机应用程序使用它们的方式。 但 WPF 针对外接程序方案的几项功能,对 HwndHost 进行了扩展。 下面介绍这些优点和限制。

WPF 外接程序的优点

由于使用派生自 HwndHost 的内部类从主机应用程序显示 WPF 外接程序用户界面,因此在 WPF UI 服务(如布局、呈现、数据绑定、样式、模板和资源)方面,这些用户界面受到 HwndHost 功能的限制。 但是,WPF 扩展了其内部 HwndHost 子类的附加功能,包括:

  • 在主机应用程序的 UI 和外接程序的 UI 之间的 Tab 键切换功能。 请注意,无论外接程序是完全信任还是部分信任,“外接程序为 UI”编程模型都要求使用外接程序端适配器重写 QueryContract 以实现 Tab 键切换功能。

  • 满足从主机应用程序用户界面显示的外接程序用户界面的可访问性要求。

  • 使 WPF 应用程序可在多个应用程序域方案中安全运行。

  • 在外接程序使用安全隔离(即部分信任安全沙盒)运行时,防止非法访问外接程序 UI 窗口句柄。 调用 ViewToContractAdapter 可确保此安全性:

    • 对于“外接程序返回 UI”编程模型,跨隔离边界传递外接程序 UI 窗口句柄的唯一方式是调用 ViewToContractAdapter

    • 对于“外接程序为 UI”编程模型,要求对加载项方适配器重写 QueryContract 并调用 ViewToContractAdapter(如前面几个示例所示),就像从宿主端适配器调用加载项方适配器的 QueryContract 实现一样。

  • 提供多个应用程序域执行保护。 由于应用程序域的限制,因此即使存在隔离边界,外接程序应用程序域中引发的未经处理的异常也会导致整个应用程序出现故障。 但是,WPF 和 .NET Framework 外接程序模型提供了一种简单方式来解决此问题并提高应用程序稳定性。 如果主机应用程序为 WPF 应用程序,则显示 UI 的 WPF 外接程序会为应用程序域的运行线程创建 Dispatcher。 通过处理 WPF 外接程序的 DispatcherUnhandledException 事件,可以检测应用程序域中发生的所有未经处理的异常。 可以从 CurrentDispatcher 属性获取 Dispatcher

WPF 外接程序限制

对于从主机应用程序显示的外接程序用户界面,除了 WPF 向 HwndSourceHwndHost 和窗口句柄所提供的默认行为增加的优点之外,也存在一些限制:

  • 从主机应用程序显示的外接程序用户界面不遵从主机应用程序的剪辑行为。

  • 互操作性方案中的“空域”概念也适用于外接程序(请参阅技术区概述)。

  • 主机应用程序的 UI 服务(如资源继承、数据绑定和命令)不会自动提供给外接程序用户界面。 若要向外接程序提供这些服务,需要更新管道。

  • 外接程序 UI 不能旋转、缩放、倾斜,也不受转换的影响(请参阅转换概述)。

  • 通过 System.Drawing 命名空间的绘图操作呈现的外接程序用户界面内的内容可以包含 alpha 值混合处理。 但是,包含它的外接程序 UI 和主机应用程序 UI 都必须是 100% 不透明;换言之,二者的 Opacity 属性必须设置为 1。

  • 如果包含外接程序 UI 的主机应用程序中窗口的 AllowsTransparency 属性设置为 true,则外接程序不可见。 即使外接程序 UI 是 100% 不透明(即 Opacity 属性的值为 1)也是如此。

  • 外接程序 UI 必须出现在同一顶级窗口中其他 WPF 元素之上。

  • 外接程序 UI 的任何部分都不能使用 VisualBrush 呈现。 外接程序可以获取生成的 UI 的快照,从而创建一个位图,通过协定所定义的方法可将该位图传递到主机应用程序。

  • 无法从外接程序 UI 的 MediaElement 中播放媒体文件。

  • 主机应用程序既不会接收,也不会引发为外接程序 UI 生成的鼠标事件,主机应用程序 UI 的 IsMouseOver 属性值为 false

  • 当焦点在外接程序 UI 的控件之间移动时,主机应用程序既不会接收,也不会引发 GotFocusLostFocus 事件。

  • 在打印时,包含外接程序 UI 的主机应用程序部分将显示为空白。

  • 在所有者外接程序卸载之前,如果主机应用程序继续执行,则必须手动关闭由外接程序 UI 创建的所有调度程序(请参阅 Dispatcher)。 协定可以实现一些方法,主机应用程序使用这些方法,可以在外接程序卸载之前通知外接程序,从而允许外接程序 UI 关闭自己的调度程序。

  • 如果外接程序 UI 是 InkCanvas 或包含 InkCanvas,则不能卸载该外接程序。

性能优化

默认情况下,如果使用多个应用程序域,则每个应用程序所需的各种 .NET Framework 程序集都会加载到该应用程序的域中。 因此,创建新应用程序域和在应用程序域中启动应用程序所需的时间可能会影响性能。 但 .NET Framework 提供了一种减少启动时间的方法,即指示应用程序共享各应用程序域的程序集(如果已经加载)。 为此,可以使用 LoaderOptimizationAttribute 属性,该属性必须应用于入口点方法 (Main)。 这种情况下,只能使用代码来实现应用程序定义(请参阅应用程序管理概述)。

另请参阅