WPF Add-Ins 概述

.NET Framework 包括一个加载项模型,开发人员可以使用该模型创建支持外接程序扩展性的应用程序。 借助此外接程序模型,可以创建与应用程序功能集成并进行扩展的外接程序。 在某些情况下,应用程序还需要显示加载项提供的用户界面。本主题说明 WPF 如何增强 .NET Framework 加载项模型,以启用这些方案、其背后的体系结构、其优点及其限制。

先决条件

需要熟悉 .NET Framework 加载项模型。 有关详细信息,请参阅 插件和扩展性

Add-Ins 概述

为了避免应用程序重新编译和重新部署的复杂性,以合并新功能,应用程序实现扩展性机制,使开发人员(第一方和第三方)能够创建与它们集成的其他应用程序。 支持这种类型的扩展性的最常见方法是使用加载项(也称为“加载项”和“插件”)。 使用外接程序公开扩展性的实际应用程序的示例包括:

  • Internet Explorer 加载项。

  • Windows Media Player 插件。

  • Visual Studio 插件。

例如,Windows Media Player 外接程序模型允许第三方开发人员实现以多种方式扩展 Windows Media Player 的“插件”,包括为 Windows Media Player(例如 DVD、MP3)、音频效果和外观本机不支持的媒体格式创建解码器和编码器。 每个加载项模型都是为了公开应用程序特有的功能而构建的,尽管所有外接程序模型都有一些实体和行为。

典型的外接程序扩展性解决方案的三个主要实体是 协定加载项,以及 主机应用程序。 协定定义外接程序如何通过两种方式与主机应用程序集成:

  • 加载项与由宿主应用程序实现的功能进行集成。

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

若要使用外接程序,主机应用程序需要查找它们并在运行时加载它们。 因此,支持外接程序的应用程序具有以下附加责任:

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

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

  • 隔离:使用应用程序域或进程建立隔离边界,以保护应用程序免受加载项的潜在安全性和执行问题。

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

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

  • 版本控制:确保在创建任一版本的新版本时,主机应用程序和外接程序仍能进行通信。

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

说明

有关加载项的更多详细信息,请参阅 加载项和扩展性

.NET Framework Add-In 模型概述

System.AddIn 命名空间中找到的 .NET Framework 外接程序模型包含一组类型,旨在简化加载项扩展性的开发。 .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. 传递到主机应用程序的应用程序域后,INativeHandleContract 必须通过调用 ContractToViewAdapter重新打包为 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 的外接程序

Add-In 是用户界面

当加载项具有用户界面时,需要满足以下条件:

  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 中最为容易完成,尽管管道和外接程序程序集需要位于主机 XBAP 项目的根文件夹中,以便 Visual Studio 检测管道程序集。

因此,第一步是通过设置每个管道程序集和外接程序程序集项目的生成输出,向 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 Add-In 体系结构

在最高级别,如我们所看到的,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 外接程序的优点

由于 WPF 外接程序用户界面由派生自 HwndHost的内部类在主机应用程序中显示,因此这些用户界面受到与 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)。 在这种情况下,必须仅使用代码来实现应用程序定义(请参阅 应用程序管理概述)。

另请参阅