启用AddIn的应用程序






4.93/5 (87投票s)
使用 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:返回
fromNumber
和toNumber
之间的斐波那契数列。 - 循环数字 add-in:返回
fromNumber
和toNumber
之间的数字列表。 - 素数 add-in:返回
fromNumber
和toNumber
之间的素数列表。
在附带的演示应用程序中,我想展示 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
,而后者又继承自 MarshalByRefObject
(MarshalByRefObject
允许跨应用程序域边界访问支持 Remoting 的应用程序中的对象),因此 add-in 被允许跨越应用程序域边界。
流程大致如下:
- 宿主应用程序调用宿主视图中的某个方法。但宿主视图实际上是一个抽象类。实际上发生的是,宿主应用程序通过宿主视图调用宿主适配器上的方法。这是可能的,因为宿主适配器继承自宿主视图。
- 然后,宿主适配器调用合同接口中的正确方法,该方法由 AddIn 适配器实现。
- 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 指南,说明了我如何让演示应用程序工作的。
- 创建一个顶层目录来存放所有单独的组件。我称我的为 AddInTest。
- 创建一个基于 WinForms 或 WPF 的宿主应用程序。它的名称无关紧要,但它必须放置在步骤 1 的顶层文件夹中。
- 为每个管道组件添加一个新的类库项目。您还需要为至少一个 add-in 组件创建一个项目。这在 Visual Studio 中应该会给您类似以下的内容(请注意,此处显示的解决方案中实际上有三个 add-in):
- 接下来,您需要在顶层目录(演示应用程序为 AddInTest\)中创建一个构建目录。一旦编译完成,AddIn 管道组件将放置在此处。在演示应用程序中,此文件夹称为 "Output"。
- 在构建和编译每个管道组件时,您需要修改构建路径,使其指向顶层构建文件夹下的相应子目录。如下图所示(我们之前已经看到过):
可以通过右键单击项目并打开项目设置来在 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:返回
fromNumber
和toNumber
之间的斐波那契数列。 - 循环数字 add-in:返回
fromNumber
和toNumber
之间的数字列表。 - 素数 add-in:返回
fromNumber
和toNumber
之间的素数列表。
好吧,它们都大致工作方式相同。为了完整起见,我将把它们的所有代码都放在这里,尽管这是容易的部分。
斐波那契数列 AddIn
使用递归返回 fromNumber
和 toNumber
之间的斐波那契数列。
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
返回 fromNumber
和 toNumber
之间的数字列表。
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
返回 fromNumber
和 toNumber
之间的素数列表。
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。