65.9K
CodeProject 正在变化。 阅读更多。
Home

启用AddIn的应用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.93/5 (87投票s)

2008年5月7日

CPOL

12分钟阅读

viewsIcon

239381

downloadIcon

3884

使用 AddIn 模型管道。

引言

本文主要介绍如何使用 .NET 3.5 的 System.Addin 命名空间。本文在很大程度上借鉴了 Mathew McDonald(在我看来是一位超级天才)关于 WPF 的精彩著作。

本文试图演示的内容其实相当简单。

有一个 AddIn 合同(INumberProcessorContract,实现了 IContract),它定义了所有 add-in 应该做什么。还有一个合同规定了 add-in 如何向 Host 应用程序通信。这个合同称为 IHostObjectContract,同样实现了 IContract

有一个管道来支持 AddIn 模型,最后,有一个宿主应用程序来宿主 add-in。本质上,本文就是关于这个的。

在开始深入探讨细节之前,值得一提的是演示应用程序提供的 add-in 实际做什么。我已经创建了几个 add-in,它们都提供了符合 AddIn INumberProcessorContract 合同(实现了 IContract)中方法签名的一些功能。基本上,演示应用程序中的所有 add-in 都将提供一个具有以下签名的方法:

List<int> ProcessNumbers(int fromNumber, int toNumber)

我创建了三个独立的 add-in,它们可以完成各种与数字相关的操作。

  • 斐波那契数列 add-in:返回 fromNumbertoNumber 之间的斐波那契数列。
  • 循环数字 add-in:返回 fromNumbertoNumber 之间的数字列表。
  • 素数 add-in:返回 fromNumbertoNumber 之间的素数列表。

在附带的演示应用程序中,我想展示 add-in 如何将进度报告给宿主应用程序,因此这也是附带演示代码的一部分。

现在我们将继续讨论 .NET 3.5 的 System.Addin 命名空间是如何让我们创建 add-in 的(尽管不容易,但它确实允许这样做)。

以下子章节将更详细地讨论这个问题:

什么是 AddIns

(有时也称为 Plugins) 是独立的编译组件,应用程序可以在运行时(动态地)定位、加载和使用它们。一个被设计成使用 add-in 的应用程序,可以通过开发更多的 add-in 来增强,而无需修改、重新编译和测试原始应用程序(尽管 add-in 应该经过测试)。在 .NET 中,开发人员很早就能够创建使用 add-in 的应用程序,但很可能使用了 System.Reflection 命名空间,并且工作量相当大。随着 .NET 3.5 的到来,出现了一个新的命名空间,即 System.Addin 命名空间。 .NET 团队创建了一个非常灵活的框架来构建 add-in,这被称为 AddIn 管道 (System.AddIn.Pipeline)。通过使用 AddIn 管道,已经具备了必要的底层机制来支持 AddIn 模型的工作。这是一件好事。缺点是,为了实现最基本的 AddIn 模型,你必须实现至少 7 个不同的组件。下一节将讨论 AddIn 管道的概念(尽管不讨论 System.AddIn.Pipeline 命名空间),并将讨论为了正确设置 AddIn 模型启用的应用程序所需的 7 个(最小)组件中每个组件发生的情况。

注意:AddIn 模型同样适用于 WinForms 或 WPF(甚至可能适用于 ASP.NET,尽管我没有尝试过)。我将使用 WPF,因为它是我喜欢的,尽管此处讨论的主题同样适用于使用 WinForms。

AddIn 管道

Addin 模型的核心是管道。管道是一系列组件,允许应用程序与 add-in 通信。管道的一端是 Host 应用程序,另一端是 add-in,中间有五个其他组件,用于促进 add-in 的操作和交互。考虑以下图示:

此图的中心是合同(实现 System.AddIn.Contract.IContract),其中包含一个或多个接口,用于定义宿主应用程序和 add-in 如何交互。合同还可以包含计划在宿主应用程序和 add-in 之间使用的可序列化类型。微软在设计 AddIn 模型时考虑了可扩展性,因此,宿主应用程序和实际的 add-in 实际上并不直接使用合同;相反,它们使用各自的合同版本,称为视图。宿主应用程序使用宿主视图,而 add-in 使用 AddIn 视图。视图通常包含一个抽象类,该类反映了合同接口中的接口(尽管视图不继承自合同或实现 System.AddIn.Contract.IContract 接口)。

尽管视图和合同具有相似的方法签名,但它们完全独立;适配器负责将它们连接起来。有两个适配器:一个用于宿主应用程序,它继承自抽象的宿主视图类;而 AddIn 适配器继承自 System.AddIn.Pipeline.ContractBase 以及合同组件中定义的实际应用程序特定接口。需要注意的是,由于 AddIn 适配器继承自 System.AddIn.Pipeline.ContractBase,而后者又继承自 MarshalByRefObjectMarshalByRefObject 允许跨应用程序域边界访问支持 Remoting 的应用程序中的对象),因此 add-in 被允许跨越应用程序域边界。

流程大致如下:

  1. 宿主应用程序调用宿主视图中的某个方法。但宿主视图实际上是一个抽象类。实际上发生的是,宿主应用程序通过宿主视图调用宿主适配器上的方法。这是可能的,因为宿主适配器继承自宿主视图。
  2. 然后,宿主适配器调用合同接口中的正确方法,该方法由 AddIn 适配器实现。
  3. AddIn 适配器调用 AddIn 视图中的方法。此方法由实际执行工作的 add-in 实现。

考虑以下管道中单个 add-in 的图示:

此图显示了单个 add-in 的管道外观。如果我们希望创建更多 add-in(如演示应用程序所示),我们只需创建更多继承自抽象 AddIn 视图类的具体类即可。

AddIn 模型文件夹结构

AddIn 模型依赖于严格的目录结构。然而,所需的目录结构实际上独立于应用程序,因此将项目放在其他地方是可以的,只要它们构建到公共文件夹即可。例如,演示应用程序在解决方案下的一个 Output 文件夹中,该文件夹必须具有以下子文件夹:

如上所示,AddinIns 文件夹必须为每个可用的 add-in 都有一个单独的文件夹,如下所示(使用演示应用程序的三个 add-in):

此文件夹结构并非可选项。为了使 AddIn 模型正常工作,它必须完全按照所示的顺序排列。此示例假定宿主应用程序部署在 Output 文件夹中。

准备一个使用 AddIn 模型的解决方案

由于 AddIn 模型的文件结构是强制性的,如果您遗漏或错命名了某个项目,您会遇到运行时异常。以下是一个逐步的 Visual Studio 指南,说明了我如何让演示应用程序工作的。

  1. 创建一个顶层目录来存放所有单独的组件。我称我的为 AddInTest
  2. 创建一个基于 WinForms 或 WPF 的宿主应用程序。它的名称无关紧要,但它必须放置在步骤 1 的顶层文件夹中。
  3. 为每个管道组件添加一个新的类库项目。您还需要为至少一个 add-in 组件创建一个项目。这在 Visual Studio 中应该会给您类似以下的内容(请注意,此处显示的解决方案中实际上有三个 add-in):
  4. 接下来,您需要在顶层目录(演示应用程序为 AddInTest\)中创建一个构建目录。一旦编译完成,AddIn 管道组件将放置在此处。在演示应用程序中,此文件夹称为 "Output"。
  5. 在构建和编译每个管道组件时,您需要修改构建路径,使其指向顶层构建文件夹下的相应子目录。如下图所示(我们之前已经看到过):
  6. 可以通过右键单击项目并打开项目设置来在 Visual Studio 中更改构建路径。请参见下图:

引用管道组件

在构建启用了 AddIn 的应用程序时,还有一个最后的需要注意的问题。在一个管道组件项目内引用一个或多个 AddIn 管道组件显然是必要的。但是,由于 AddIn 模型依赖于上面提到的严格的强制性文件结构,因此在添加对另一个管道组件的引用时,需要格外小心,不要复制引用的 DLL,以便 AddIn 模型使用位于相关 AddIn 模型文件系统文件夹内的 DLL。为了确保一切按预期工作,我们需要确保任何引用的管道 DLL 的 "复制本地" 属性设置为 "False"。这确保了在宿主应用程序中使用的是位于正确文件系统位置的 DLL。基本上,宿主应用程序在特定的文件系统位置查找各种组件。可以通过单击引用的 DLL 并检查属性网格中的 "复制本地" 属性值来更改 "复制本地" 属性。如果设置为 "True",则需要为任何引用的管道组件将其更改为 "False"。让我们看一个例子。

下图显示了当在 HostSideAdapter 项目中引用 Contract DLL 时的情况:

代码

好的,好的,说够了,您想看代码,对吧?有很多不同的项目,但我认为代码相当简单。我将依次介绍它们。

契约

这仅包含由宿主和 AddIn 端适配器实现的两个接口。

namespace Contract
{
    /// <summary>
    /// The actual AddIn contract that is implemented by the
    /// <see cref="AddInSideAdapter.NumberProcessorViewToContractAdapter">AddIn Adapter</see>
    /// </summary>
    [AddInContract]
    public interface INumberProcessorContract : IContract
    {
        List<int> ProcessNumbers(int fromNumber, int toNumber);
        void Initialize(IHostObjectContract hostObj);
    }

    /// <summary>
    /// The actual Host contract that is implemented by the
    /// <see cref="HostInSideAdapter.HostObjectViewToContractHostAdapter">Host Adapter</see>
    /// </summary>
    public interface IHostObjectContract : IContract
    {
        void ReportProgress(int progressPercent);
    }
}

AddIn 适配器类

这仅包含两个类。NumberProcessorViewToContractAdapter 类允许 AddIn 适配器与视图和合同进行交互。而 HostObjectContractToViewAddInAdapter 类允许 AddIn 适配器与 Host 视图进行交互。在这种情况下,这允许 AddIn 报告进度。

namespace AddInSideAdapter
{

    /// <summary>
    /// Adapter use to talk to AddIn
    /// <see cref="Contract.INumberProcessorContract">AddIn Contract</see>
    /// </summary>
    [AddInAdapter]
    public class NumberProcessorViewToContractAdapter : ContractBase, 
                 Contract.INumberProcessorContract
    {
      .......
      .......
    }

    /// <summary>
    /// Allows AddIn adapter to talk back to HostView
    /// </summary>
    public class HostObjectContractToViewAddInAdapter : AddInView.HostObject
    {
      .......
      .......
    }
}

Host 适配器类

这仅包含两个类。NumberProcessorContractToViewHostAdapter 类允许 Host 端适配器与 Host 视图进行交互。而 HostObjectViewToContractHostAdapter 类允许 Host 适配器与 Host 视图进行交互,在这种情况下,这允许 AddIn 报告进度。

namespace HostSideAdapter
{

    /// <summary>
    /// Adapter use to talk to <see
    /// cref="HostView.NumberProcessorHostView">Host View</see>
    /// </summary>
    [HostAdapter]
    public class NumberProcessorContractToViewHostAdapter : 
                 HostView.NumberProcessorHostView
    {
      .......
      .......
    }


    /// <summary>
    /// Allows Host side adapter to talk back to HostView
    /// </summary>
    public class HostObjectViewToContractHostAdapter : ContractBase, 
                 Contract.IHostObjectContract
    {
      .......
      .......
    }
}

AddIn 视图类

这仅包含两个抽象类。NumberProcessorAddInView 类由任何 AddIn 具体类继承。而 HostObject 由需要与 Host 合同和视图适配器通信的对象继承。

namespace AddInView
{    
    /// <summary>
    /// Abstract base class that should be inherited by all AddIns
    /// </summary>
    [AddInBase]
    public abstract class NumberProcessorAddInView
    {
        public abstract List<int> ProcessNumbers(int fromNumber, int toNumber);

        public abstract void Initialize(HostObject hostObj);
    }

    /// <summary>
    /// Abstract class that should be inherited by an object that needs to communicate
    /// between the host Contract to View adapter
    /// <see cref="AddInSideAdapter.HostObjectContractToViewAddInAdapter">
    /// HostObjectContractToViewAddInAdapter</see>
    /// </summary>
    public abstract class HostObject
    {
        public abstract void ReportProgress(int progressPercent);
    }
}

Host 视图类

这仅包含两个抽象类。NumberProcessorHostView 类由任何 Host 端适配器具体类继承。而 HostObject 由宿主应用程序中能够利用报告进度的一个类继承。

namespace HostView
{
    /// <summary>
    /// Abstract base class that should be inherited by the Host view
    /// </summary>
    public abstract class NumberProcessorHostView
    {
        public abstract List<int> ProcessNumbers(int fromNumber, int toNumber);

        public abstract void Initialize(HostObject host);
    }

    /// <summary>
    /// Abstract base class that should be inherited by a class within the host
    /// application that can make use of the reported progress
    /// </summary>
    public abstract class HostObject
    {
        public abstract void ReportProgress(int progressPercent);
    }
}

Host 应用程序

这是宿主 AddIns 的 WinForms 或 WPF(此处为 WPF)应用程序。它还负责调用所选 add-in 的方法,并通过使用内部类 AutomationHost(继承自 HostView.HostObject 类)允许 add-in 报告进度。

namespace ApplicationHost
{
    /// <summary>
    /// The main host application. Simply shows a list of AddIns and the 
    /// results of running the Addin
    /// </summary>
    public partial class Window1 : Window
    {
        #region Data
        private AutomationHost automationHost;
        private HostView.NumberProcessorHostView addin;
        #endregion

      .......
      .......

        #region Private Methods
        /// <summary>
        /// Loads a list of all available AddIns
        /// </summary>
        private void Window_Loaded(object sender, RoutedEventArgs e)
        {                     
            string path = Environment.CurrentDirectory;            
            AddInStore.Update(path);

            string[] s = AddInStore.RebuildAddIns(path);

            IList<AddInToken> tokens = 
              AddInStore.FindAddIns(typeof(HostView.NumberProcessorHostView), path);
            lstAddIns.ItemsSource = tokens;
            automationHost = new AutomationHost(progressBar);
        }


        /// <summary>
        /// Use the selected AddIn
        /// </summary>
        private void btnUseAddin_Click(object sender, RoutedEventArgs e)
        {
            if (lstAddIns.SelectedIndex != -1)
            {
                // get selected addin
                AddInToken token = (AddInToken)lstAddIns.SelectedItem;
                addin = 
                  token.Activate<HostView.NumberProcessorHostView>(
                  AddInSecurityLevel.Internet);
                addin.Initialize(automationHost);

                // process addin on new thread
                Thread thread = new Thread(RunBackgroundAddIn);
                thread.Start();
            }
            else
                MessageBox.Show("You need to select an addin first");
        }

        /// <summary>
        /// Runs Selected AddIn new thread
        /// </summary>
        private void RunBackgroundAddIn()
        {            
            // Do the work.
            List<int> numbersProcessed = addin.ProcessNumbers(1, 20);
            
            // update UI on UI thread
            this.Dispatcher.BeginInvoke(DispatcherPriority.Normal,
                    (ThreadStart)delegate()
                    {
                        lstNumbers.ItemsSource = numbersProcessed;
                        progressBar.Value = 0;

                        // Release the add-in
                        addin = null;
                    }
                );                            
        }

      .......
      .......
    }

    /// <summary>
    /// A wrapper class that allows the reported progress within the
    /// <see cref="HostView.HostObject">host view </see> to display
    /// progress on a ProgressBar within the host app
    /// </summary>
    internal class AutomationHost : HostView.HostObject
    {
        .......
        .......
    }
}

正如我所说,这是 WPF(但也可以是 WinForms);UI 不重要,而且是临时性的。但是,它看起来是这样的:

这里有一个找到的 add-in 列表,然后用户可以应用这些 add-in。这可能需要一些解释。有一个名为 AddInStore 的类,它允许 .NET 代码重建 add-in 列表。这会在文件系统上创建一个新文件。

AddInStore 还允许应用程序代码查找 add-in。所以这正是我用以下几行所做的,其中 add-in 被刷新然后添加到 ListBox 中:

string path = Environment.CurrentDirectory;            
AddInStore.Update(path);

string[] s = AddInStore.RebuildAddIns(path);

IList<AddInToken> tokens = 
  AddInStore.FindAddIns(typeof(HostView.NumberProcessorHostView), path);

因此,只剩下 add-in 具体类本身了;这些相当简单(您会很高兴听到……因为 add-in 模型相当可怕,不适合胆小的人,而且没有 Mathew McDonald 关于 WPF 的出色书籍,我无法写这篇文章)。所以,让我们看看 add-in 的代码。

实际的 AddIns

回想一下,有三个 add-in,每个都继承自抽象的 AddInView.NumberProcessorAddInView 类。

  • 斐波那契数列 add-in:返回 fromNumbertoNumber 之间的斐波那契数列。
  • 循环数字 add-in:返回 fromNumbertoNumber 之间的数字列表。
  • 素数 add-in:返回 fromNumbertoNumber 之间的素数列表。

好吧,它们都大致工作方式相同。为了完整起见,我将把它们的所有代码都放在这里,尽管这是容易的部分。

斐波那契数列 AddIn

使用递归返回 fromNumbertoNumber 之间的斐波那契数列。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.AddIn;
using System.Threading;

namespace FibonacciAddIn
{
    [AddIn("Fibonacci Number Processor", Version = "1.0.0.0", 
           Publisher = "Sacha Barber", 
           Description = "Returns an List<int> of fiibonacci number integers within the " +
            "to/from range provided to the addin")]
    public class FibonacciNumberProcessor : AddInView.NumberProcessorAddInView
    {
        private AddInView.HostObject host;

        public static int Fibonacci(int n)
        {
            if (n == 0 || n == 1)
                return n;
            else
                return Fibonacci(n - 1) + Fibonacci(n - 2);
        }

        public override List<int> ProcessNumbers(int fromNumber, int toNumber)
        {
            List<int> results = new List<int>();

            double factor = 100 / toNumber - fromNumber;

            for (int i = fromNumber; i < toNumber; i++)
            {
                host.ReportProgress((int)(i * factor));
                results.Add(Fibonacci(i));
            }
            host.ReportProgress(100);
            return results;
        }


        public override void Initialize(AddInView.HostObject hostObj)
        {
            host = hostObj;
        }
    }
}

循环数字 AddIn

返回 fromNumbertoNumber 之间的数字列表。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.AddIn;
using System.Threading;

namespace LoopAddIn
{
    [AddIn("Loop Number Processor", Version = "1.0.0.0", Publisher = "Sacha Barber",
        Description = "Returns an List<int> of looped number integers within the " +
                "to/from range provided to the addin")]
    public class LoopNumberProcessor : AddInView.NumberProcessorAddInView
    {

        private AddInView.HostObject host;

        public override List<int> ProcessNumbers(int fromNumber, int toNumber)
        {
            List<int> results = new List<int>();

            double factor = 100 / toNumber - fromNumber;

            for (int i = fromNumber; i < toNumber; i++)
            {
                host.ReportProgress((int)(i * factor));
                results.Add(i);
            }
            host.ReportProgress(100);
            return results;
        }

        public override void Initialize(AddInView.HostObject hostObj)
        {
            host = hostObj;
        }
    }
}

素数 AddIn

返回 fromNumbertoNumber 之间的素数列表。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.AddIn;
using System.Threading;


namespace PrimeNumberAddIn
{
    [AddIn("Prime Number Processor", Version = "1.0.0.0", Publisher = "Sacha Barber",
        Description = "Returns an List<int> of prime number integers within the" +
            "to/from range provided to the addin")]
    public class PrimeNumberProcessor : AddInView.NumberProcessorAddInView 
    {

        private AddInView.HostObject host;

        public override List<int> ProcessNumbers(int fromNumber, int toNumber)
        {
            List<int> results = new List<int>();
            int[] list = new int[toNumber - fromNumber];
            double factor = 100 / toNumber - fromNumber;

            // Create an array containing all integers between the two specified numbers.
            for (int i = 0; i < list.Length; i++)
            {
                list[i] = fromNumber;
                fromNumber += 1;
            }

            //find out the module for each item in list, divided by each d, where
            //d is < or == to sqrt(to)
            //mark composite with 1, and primes with 0 in mark array
            int maxDiv = (int)Math.Floor(Math.Sqrt(toNumber));

            int[] mark = new int[list.Length];

            for (int i = 0; i < list.Length; i++)
            {
                for (int j = 2; j <= maxDiv; j++)
                    if ((list[i] != j) && (list[i] % j == 0))
                        mark[i] = 1;

                host.ReportProgress((int)(i * factor));
            }

            //get the marked primes from original array
            for (int i = 0; i < mark.Length; i++)
                if (mark[i] == 0)
                    results.Add(list[i]);

            host.ReportProgress(100);
            return results;
        }

        public override void Initialize(AddInView.HostObject hostObj)
        {
            host = hostObj;
        }
    }
}

这是 add-in 在 WPF 应用中运行的截图(我用 WPF DataTemplate 让它看起来不错,但这并不重要)。

AddIn 帮助

正如您所见,这是一个复杂的安排,需要花很多精力去理解,而且工作量很大。CLR AddIn 团队已经听取了关于此模型工作量过大的初步担忧,并专门为 Visual Studio AddIn 创建了一个 CodePlex 页面,以帮助创建启用了 AddIn 的应用程序。他们还提供了一个空白的 AddIn 管道项目供您开始。VS AddIn 有助于创建宿主和 AddIn 端适配器。CLR AddIn 团队的 CodePlex 页面可以在以下链接找到:托管可扩展性和 Add-In 框架

结论

我认为可以公平地说,AddIn 模型并不容易理解,但这个例子向您展示了您需要了解的大部分内容。我没有在本文中包含所有代码,我试图只包含我认为最重要的部分(Nish,如果你在看,看,我听了你的)。因此,您需要参考本文的演示应用程序才能理解 AddIn 模型的全部工作原理。我希望您从本文中学到了东西,如果您喜欢,请为它投票。

历史

  • V1.0:2008/05/07。
© . All rights reserved.