如何创建一个用户界面的 Add-In

此示例演示如何创建由 WPF 独立应用程序托管的 Windows Presentation Foundation(WPF)加载项。

加载项的用户界面是一个 WPF 的用户控件。 用户控件的内容是单击后显示消息框的单个按钮。 WPF 独立应用程序将插件用户界面作为主应用程序窗口的内容承载。

先决条件

此示例重点介绍 .NET Framework 外接程序模型中的 WPF 扩展如何实现这个场景,并假设以下条件成立:

  • 了解 .NET Framework 加载项模型,包括管道、加载项(外接程序)和主机开发。 如果不熟悉这些概念,请参阅加载项和扩展性。 有关演示管道、加载项和主机应用程序的实现的教程,请参阅 演练:创建可扩展应用程序

  • 了解 .NET Framework 插件模型中的 WPF 扩展功能。 请参阅 WPF Add-Ins 概述

示例:

若要创建作为 WPF UI 的外接程序,需要每个管道段、加载项和主机应用程序的特定代码。

实现协定管道段

当加载项是 UI 时,加载项的协定必须实现 INativeHandleContract。 在此示例中, IWPFAddInContract 实现 INativeHandleContract,如以下代码所示。

using System.AddIn.Contract;
using System.AddIn.Pipeline;

namespace Contracts
{
    /// <summary>
    /// Defines the services that an add-in will provide to a host application.
    /// In this case, the add-in is a UI.
    /// </summary>
    [AddInContract]
    public interface IWPFAddInContract : INativeHandleContract {}
}

Imports System.AddIn.Contract
Imports System.AddIn.Pipeline

Namespace Contracts
    ''' <summary>
    ''' Defines the services that an add-in will provide to a host application.
    ''' In this case, the add-in is a UI.
    ''' </summary>
    <AddInContract>
    Public Interface IWPFAddInContract
        Inherits INativeHandleContract
        Inherits IContract
    End Interface
End Namespace

实现外接程序视图管道段

由于该外接程序是作为FrameworkElement类型的子类实现的,因此外接程序视图也必须是FrameworkElement的子类。 以下代码显示了合同外接程序作为类 WPFAddInView 实现的视图。

using System.AddIn.Pipeline;
using System.Windows.Controls;

namespace AddInViews
{
    /// <summary>
    /// Defines the add-in's view of the contract.
    /// </summary>
    [AddInBase]
    public class WPFAddInView : UserControl { }
}

Imports System.AddIn.Pipeline
Imports System.Windows.Controls

Namespace AddInViews
    ''' <summary>
    ''' Defines the add-in's view of the contract.
    ''' </summary>
    <AddInBase>
    Public Class WPFAddInView
        Inherits UserControl
    End Class
End Namespace

此处,加载项视图派生自 UserControl。 因此,外接程序 UI 还应派生自 UserControl

实现外接程序端适配器管道段

虽然协定是一个 INativeHandleContract,但加载项是一个 FrameworkElement (由外接程序视图管道段指定)。 因此, FrameworkElement 必须在跨越隔离边界之前将转换到 INativeHandleContract 该边界。 这一工作由加载项方适配器通过调用 ViewToContractAdapter 来执行,如以下代码中所示。

using System;
using System.AddIn.Contract;
using System.AddIn.Pipeline;
using System.Security.Permissions;

using AddInViews;
using Contracts;

namespace AddInSideAdapters
{
    /// <summary>
    /// Adapts the add-in's view of the contract to the add-in contract
    /// </summary>
    [AddInAdapter]
    public class WPFAddIn_ViewToContractAddInSideAdapter : ContractBase, IWPFAddInContract
    {
        WPFAddInView wpfAddInView;

        public WPFAddIn_ViewToContractAddInSideAdapter(WPFAddInView wpfAddInView)
        {
            // Adapt the add-in view of the contract (WPFAddInView)
            // to the contract (IWPFAddInContract)
            this.wpfAddInView = wpfAddInView;
        }

        /// <summary>
        /// ContractBase.QueryContract must be overridden to:
        /// * Safely return a window handle for an add-in UI to the host
        ///   application's application.
        /// * Enable tabbing between host application UI and add-in UI, in the
        ///   "add-in is a UI" scenario.
        /// </summary>
        public override IContract QueryContract(string contractIdentifier)
        {
            if (contractIdentifier.Equals(typeof(INativeHandleContract).AssemblyQualifiedName))
            {
                return FrameworkElementAdapters.ViewToContractAdapter(this.wpfAddInView);
            }

            return base.QueryContract(contractIdentifier);
        }

        /// <summary>
        /// GetHandle is called by the WPF add-in model from the host application's
        /// application ___domain to get the window handle for an add-in UI from the
        /// add-in's application ___domain. GetHandle is called if a window handle isn't
        /// returned by other means, that is, overriding ContractBase.QueryContract,
        /// as shown above.
        /// NOTE: This method requires UnmanagedCodePermission to be called
        ///       (full-trust by default), to prevent illegal window handle
        ///       access in partially trusted scenarios. If the add-in could
        ///       run in a partially trusted application ___domain
        ///       (eg AddInSecurityLevel.Internet), you can safely return a window
        ///       handle by overriding ContractBase.QueryContract, as shown above.
        /// </summary>
        public IntPtr GetHandle()
        {
            return FrameworkElementAdapters.ViewToContractAdapter(this.wpfAddInView).GetHandle();
        }
    }
}

Imports System
Imports System.AddIn.Contract
Imports System.AddIn.Pipeline
Imports System.Security.Permissions

Imports AddInViews
Imports Contracts

Namespace AddInSideAdapters
    ''' <summary>
    ''' Adapts the add-in's view of the contract to the add-in contract
    ''' </summary>
    <AddInAdapter>
    Public Class WPFAddIn_ViewToContractAddInSideAdapter
        Inherits ContractBase
        Implements IWPFAddInContract

        Private wpfAddInView As WPFAddInView

        Public Sub New(ByVal wpfAddInView As WPFAddInView)
            ' Adapt the add-in view of the contract (WPFAddInView) 
            ' to the contract (IWPFAddInContract)
            Me.wpfAddInView = wpfAddInView
        End Sub

        ''' <summary>
        ''' ContractBase.QueryContract must be overridden to:
        ''' * Safely return a window handle for an add-in UI to the host 
        '''   application's application.
        ''' * Enable tabbing between host application UI and add-in UI, in the
        '''   "add-in is a UI" scenario.
        ''' </summary>
        Public Overrides Function QueryContract(ByVal contractIdentifier As String) As IContract
            If contractIdentifier.Equals(GetType(INativeHandleContract).AssemblyQualifiedName) Then
                Return FrameworkElementAdapters.ViewToContractAdapter(Me.wpfAddInView)
            End If

            Return MyBase.QueryContract(contractIdentifier)
        End Function

        ''' <summary>
        ''' GetHandle is called by the WPF add-in model from the host application's 
        ''' application ___domain to get the window handle for an add-in UI from the 
        ''' add-in's application ___domain. GetHandle is called if a window handle isn't 
        ''' returned by other means, that is, overriding ContractBase.QueryContract, 
        ''' as shown above.
        ''' </summary>
        Public Function GetHandle() As IntPtr Implements INativeHandleContract.GetHandle
            Return FrameworkElementAdapters.ViewToContractAdapter(Me.wpfAddInView).GetHandle()
        End Function

    End Class
End Namespace

在返回用户界面的加载项模型中(请参阅创建返回用户界面的 Add-In),加载项适配器通过调用ViewToContractAdapterFrameworkElement转换为INativeHandleContractViewToContractAdapter 还必须在此模型中调用,但你需要实现一个方法,从中编写代码来调用它。 通过重写 QueryContract,并实现调用 ViewToContractAdapter 的代码,如果调用 QueryContract 的代码需要一个 INativeHandleContract。 在这种情况下,调用方将是主机端适配器,该适配器在后续子节中介绍。

注释

还需要在此模型中重写 QueryContract 才能在主机应用程序 UI 和外接程序 UI 之间启用制表符。 有关详细信息,请参阅 WPF Add-Ins 概述中的“WPF Add-In 限制”。

由于加载项适配器实现派生自 INativeHandleContract的接口,因此还需要实现 GetHandle,尽管重写时 QueryContract 会忽略此接口。

实现主机视图管道段

在此模型中,主机应用程序通常要求主机视图为 FrameworkElement 子类。 主机端适配器必须在跨越隔离边界后INativeHandleContract将其FrameworkElement转换为INativeHandleContract。 由于主机应用程序未通过方法来获取FrameworkElement,因此主机视图必须通过包含FrameworkElement以进行“返回”。 因此,主机视图必须派生自可以包含其他 UI 的 FrameworkElement 子类,例如 UserControl。 以下代码显示了作为类实现 WPFAddInHostView 的协定的主机视图。

using System.Windows.Controls;

namespace HostViews
{
    /// <summary>
    /// Defines the host's view of the add-in
    /// </summary>
    public class WPFAddInHostView : UserControl { }
}

Imports System.Windows.Controls

Namespace HostViews
    ''' <summary>
    ''' Defines the host's view of the add-in
    ''' </summary>
    Public Class WPFAddInHostView
        Inherits UserControl
    End Class
End Namespace

实现主机端适配器管道段

虽然合同是一个 INativeHandleContract,但主机应用程序需要一个 UserControl(由主机视图指定)。 因此,INativeHandleContract必须在越过隔离边界之后转换为一个FrameworkElement,然后再设置为派生自UserControl的主机视图的内容。

此工作由主机端适配器执行,如以下代码所示。

using System.AddIn.Contract;
using System.AddIn.Pipeline;
using System.Windows;

using Contracts;
using HostViews;

namespace HostSideAdapters
{
    /// <summary>
    /// Adapts the add-in contract to the host's view of the add-in
    /// </summary>
    [HostAdapter]
    public class WPFAddIn_ContractToViewHostSideAdapter : WPFAddInHostView
    {
        IWPFAddInContract wpfAddInContract;
        ContractHandle wpfAddInContractHandle;

        public WPFAddIn_ContractToViewHostSideAdapter(IWPFAddInContract wpfAddInContract)
        {
            // Adapt the contract (IWPFAddInContract) to the host application's
            // view of the contract (WPFAddInHostView)
            this.wpfAddInContract = wpfAddInContract;

            // Prevent the reference to the contract from being released while the
            // host application uses the add-in
            this.wpfAddInContractHandle = new ContractHandle(wpfAddInContract);

            // Convert the INativeHandleContract for the add-in UI that was passed
            // from the add-in side of the isolation boundary to a FrameworkElement
            string aqn = typeof(INativeHandleContract).AssemblyQualifiedName;
            INativeHandleContract inhc = (INativeHandleContract)wpfAddInContract.QueryContract(aqn);
            FrameworkElement fe = (FrameworkElement)FrameworkElementAdapters.ContractToViewAdapter(inhc);

            // Add FrameworkElement (which displays the UI provided by the add-in) as
            // content of the view (a UserControl)
            this.Content = fe;
        }
    }
}

Imports System.AddIn.Contract
Imports System.AddIn.Pipeline
Imports System.Windows

Imports Contracts
Imports HostViews

Namespace HostSideAdapters
    ''' <summary>
    ''' Adapts the add-in contract to the host's view of the add-in
    ''' </summary>
    <HostAdapter>
    Public Class WPFAddIn_ContractToViewHostSideAdapter
        Inherits WPFAddInHostView
        Private wpfAddInContract As IWPFAddInContract
        Private wpfAddInContractHandle As ContractHandle

        Public Sub New(ByVal wpfAddInContract As IWPFAddInContract)
            ' Adapt the contract (IWPFAddInContract) to the host application's
            ' view of the contract (WPFAddInHostView)
            Me.wpfAddInContract = wpfAddInContract

            ' Prevent the reference to the contract from being released while the
            ' host application uses the add-in
            Me.wpfAddInContractHandle = New ContractHandle(wpfAddInContract)

            ' Convert the INativeHandleContract for the add-in UI that was passed 
            ' from the add-in side of the isolation boundary to a FrameworkElement
            Dim aqn As String = GetType(INativeHandleContract).AssemblyQualifiedName
            Dim inhc As INativeHandleContract = CType(wpfAddInContract.QueryContract(aqn), INativeHandleContract)
            Dim fe As FrameworkElement = CType(FrameworkElementAdapters.ContractToViewAdapter(inhc), FrameworkElement)

            ' Add FrameworkElement (which displays the UI provided by the add-in) as
            ' content of the view (a UserControl)
            Me.Content = fe
        End Sub
    End Class
End Namespace

如你所看到的,主机端适配器通过调用加载项适配器QueryContract的方法(这是跨越隔离边界的INativeHandleContract点)来获取INativeHandleContract该适配器。

然后,主机端适配器通过调用ContractToViewAdapterINativeHandleContract转换为FrameworkElement。 最后,将 FrameworkElement 设置为主机视图的内容。

实现外接程序

使用加载项适配器和外接程序视图就位后,可以通过从外接程序视图派生来实现外接程序,如以下代码所示。

using System.AddIn;
using System.Windows;

using AddInViews;

namespace WPFAddIn1
{
    /// <summary>
    /// Implements the add-in by deriving from WPFAddInView
    /// </summary>
    [AddIn("WPF Add-In 1")]
    public partial class AddInUI : WPFAddInView
    {
        public AddInUI()
        {
            InitializeComponent();
        }

        void clickMeButton_Click(object sender, RoutedEventArgs e)
        {
            MessageBox.Show("Hello from WPFAddIn1");
        }
    }
}

Imports System.AddIn
Imports System.Windows

Imports AddInViews

Namespace WPFAddIn1
    ''' <summary>
    ''' Implements the add-in by deriving from WPFAddInView
    ''' </summary>
    <AddIn("WPF Add-In 1")>
    Partial Public Class AddInUI
        Inherits WPFAddInView
        Public Sub New()
            InitializeComponent()
        End Sub

        Private Sub clickMeButton_Click(ByVal sender As Object, ByVal e As RoutedEventArgs)
            MessageBox.Show("Hello from WPFAddIn1")
        End Sub
    End Class
End Namespace

在此示例中,可以看到此模型的一个有趣优势:外接程序开发人员只需实现外接程序(因为它也是 UI),而不是加载项类和外接程序 UI。

实现宿主应用程序

创建主机端适配器和主机视图后,主机应用程序可以使用 .NET Framework 外接程序模型打开管道并获取外接程序的主机视图。 以下步骤显示在以下代码中。

// Get add-in pipeline folder (the folder in which this application was launched from)
string appPath = Environment.CurrentDirectory;

// Rebuild visual add-in pipeline
string[] warnings = AddInStore.Rebuild(appPath);
if (warnings.Length > 0)
{
    string msg = "Could not rebuild pipeline:";
    foreach (string warning in warnings) msg += "\n" + warning;
    MessageBox.Show(msg);
    return;
}

// Activate add-in with Internet zone security isolation
Collection<AddInToken> addInTokens = AddInStore.FindAddIns(typeof(WPFAddInHostView), appPath);
AddInToken wpfAddInToken = addInTokens[0];
this.wpfAddInHostView = wpfAddInToken.Activate<WPFAddInHostView>(AddInSecurityLevel.Internet);

// Display add-in UI
this.addInUIHostGrid.Children.Add(this.wpfAddInHostView);
' Get add-in pipeline folder (the folder in which this application was launched from)
Dim appPath As String = Environment.CurrentDirectory

' Rebuild visual add-in pipeline
Dim warnings() As String = AddInStore.Rebuild(appPath)
If warnings.Length > 0 Then
    Dim msg As String = "Could not rebuild pipeline:"
    For Each warning As String In warnings
        msg &= vbLf & warning
    Next warning
    MessageBox.Show(msg)
    Return
End If

' Activate add-in with Internet zone security isolation
Dim addInTokens As Collection(Of AddInToken) = AddInStore.FindAddIns(GetType(WPFAddInHostView), appPath)
Dim wpfAddInToken As AddInToken = addInTokens(0)
Me.wpfAddInHostView = wpfAddInToken.Activate(Of WPFAddInHostView)(AddInSecurityLevel.Internet)

' Display add-in UI
Me.addInUIHostGrid.Children.Add(Me.wpfAddInHostView)

主机应用程序使用典型的 .NET Framework 外接程序模型代码来激活外接程序,该外接程序隐式将主机视图返回给主机应用程序。 主机应用程序随后从 UserControla Grid.

用于处理与外接程序 UI 交互的代码在外接程序的应用程序域中运行。 这些交互包括:

此活动与主机应用程序完全隔离。

另请参阅