原文:http://www.codeproject.com/KB/dotnet/AddInModel.aspx
This article is all about using the .NET 3.5 System.Addin
namespace. This article owes much to the fabulous WPF book by Mathew McDonald (Who is an uber genius in my opinion).
What this article is attempting to demomstrate is fairly simple actually.
There is 1 AddIn contract (INumberProcessorContract
implementing IContract
) that defines how all addins should do. There is also another Contract that specifies how the AddIn should communicate back to the Host application. This Contract is called IHostObjectContract which again implement IContract
.
There is a pipeline to support the AddIn model, and finally there is a host application that will host the AddIns. In essence that is all this article is about.
Before I start diving into the details its probably worth mentioning what the AddIn(s) provided by the demo application actually do. I have created several Addins that all provide some functionality that matches the method signature defined within the AddIn INumberProcessorContract contract (implementing IContract
). Basically all the AddIns in the demo app, will provide a single method that has the following signature
List<int> ProcessNumbers(int fromNumber, int toNumber)
I have created 3 seperate AddIns, which do various things to do with numbers
And within the attached demo application I wanted to show how the AddIn could report progress back to the host application, so this is also part of the attached demo code.
So what we will now go on to discuss is how the .NET 3.5 System.Addin
namespace allows us to create AddIns (not easily, but it does allow it)
The following sub sections will go into this in a little more detail.
AddIns (sometimes called Plugins) are seperately compiled components that an application can locate, load and make use of at runtime (dynamically). An application that has been designed to use AddIns can be enhanced (by developing more AddIns) without the need for the orginal application to be modified or recompiled and tested (though the AddIns should be tested). For some time .NET has allows developers to create applications that use AddIns but this more than likely used the System.Reflection
namespace, and was probably a fair bit of work. With .NET 3.5 comes a new namespace, namely the System.Addin
namespace. The .NET team have created a very flexible framework for building AddIns, this is known as the AddIn Pipeline (System.AddIn.Pipeline
). By using the AddIn Pipeline, the neseccary plumbing is already in place to allow the AddIn model to work. So thats a good thing. The down side to this is that you must implement at least 7 different components to implement the most basic AddIn model. The next section will discuss the AddIn pipeline concept (though it will not discuss the System.AddIn.Pipeline
namespace) and shall discuss what happens in each of the 7 (minimum) components that are required in order to correctly setup a AddIn model enabled application
NOTE : The AddIn model is equally at home in Winforms or WPF (and possibly ASP .NET, though I didnt try that). I shall be using WPF as I like it, though the topics discussed here after, would be equally suited to working with WinForms.
The heart of the Addin model is the pipeline. The pipeline is a chain of components that allows the application to communicate with the AddIn. At one end of the pipeline is the Host Application and at the other end is the AddIn, and in between are 5 other components that facilitate the AddIn operation and interaction. Consider the following Diagram
At the center of this figure is the Contract (implements System.AddIn.Contract.IContract
), which includes one or more interfaces that define how the host application and the AddIns can interact. The Contract can also include serializable types that are planned to between the host application and the AddIn. Microsoft designed the AddIn model with extensibility in mind, as such both the host application and the actual AddIn dont actually use the Contract directly, rather they use their own respective versions of the Contract, called Views. The host application uses the host view, while the AddIn uses the AddIn view. The views normally contain an abstract class that reflect the interfaces in Contract interface (though the view dont inherit from the Contract or implement the System.AddIn.Contract.IContract
interface).
Although the views and the Contract have similiar method signatures they are totally seperate, its up to the Adapters to join them together. There are 2 Adapters one for the host application which inherits from the abstract host view class, whilst the AddIn adapter inherits from System.AddIn.Pipeline.ContractBase
and the actual application specific interface that is defined in the Contract component. One thing to note here is that beacuse the AddIn adapter inherits from System.AddIn.Pipeline.ContractBase
, which in turn inherits from MarshalByRefObject
(MarshalByRefObject, Enables access to objects across application domain boundaries in applications that support remoting), the AddIn is allowed to cross application domain boundaries.
The flow is something like this
Consider the following figure for a single AddIn within the pipeline
This diagram shows what the pipeline would look like for a single AddIn. If we wish to create more AddIns (as the demo application does) we simply need to create more concrete classes that inherit from the abstract AddIn view class.
The AddIn model relies on a strict directory structure. However the required directory structure is actually seperate from the application, so its fine to have the projects elsewhere as long as they build to a common folder. For example the demo application uses a Output folder under the solution, which MUST have the following sub folders
The AddinIns folder above MUST also have a seperate folder for each available AddIn, as shown below (using 3 AddIn for demo application)
This folder structure is not optional at all. It MUST be exactly as shown in order for the AddIn model to work correctly. This example assumes that the host application is deployed in the Output folder.
As the AddIn model file structure is mandatory, if you leave out or misname a particular project you WILL get a runtime Exception. Here is a step by step Visual Studio guide of how I managed to get the demo application to work
There is one last issue that you have to be aware of when building AddIn enabled applications. There will obviously be a need to reference one or more of the AddIn pipeline components within another pipeline component project. However as the AddInModel relies on the strict MANDATORY file structure mentioned above, when adding a reference to another of the pipeline components, care needs to be taken that the referenced Dll is not copied, such that the AddIn model will ONLY ever use the Dll that is found within the related AddIn model fole system folder. To make sure this all works as expected we need to make sue that any referenced pipeline Dll is set with the "Copy Local" property set "False". This ensures that the correct file system placed Dll is used within the host application. Basically the host application looks for the various components at certain file system locations. The "Copy Local" property may be changed by clicking on the referenced Dll, and examining the value of the "Copy Local" property within the property grid. If its set to "True" this needs to be changed to "False" for any referenced pipeline component. Lets see an example of that
The following figure shows this for the Contract Dll when references within the HostSideAdapter project
Ok Ok enough talk, you want to see some code right. There are a lot of different projects, but the code is faily simply I think. So Ill go through each of them in turn
This simply contains 2 interfaces that are implemented by the host and AddIn side adapters
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); } }
This simply contains 2 classes. The NumberProcessorViewToContractAdapter class allows the AddIn adapter to to interact with the View and the Contract. Whilst the HostObjectContractToViewAddInAdapter class allows the AddIn adapter to to interact with the Host view. In this case this allows the AddIn to report progress
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 { ....... ....... } }
This simply contains 2 classes. The NumberProcessorContractToViewHostAdapter class allows the Host side adapter to to interact with the host View. Whilst the HostObjectViewToContractHostAdapter class allows the Host adapter to to interact with the Host view which in this case this allows the AddIn to report progress
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 { ....... ....... } }
This simply contains 2 abstract classes. The NumberProcessorAddInView class is inhertied by any AddIn concreate class. Whilst the HostObject inherited by an object that needs to communicate between the host Contract to View adapter
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); } }
This simply contains 2 abstract classes. The NumberProcessorHostView class is inhertied by any host side adapter concreate class. Whilst the HostObject is inherited by by a class within the host application that can make use of the reported progress
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); } }
This is the Winforms or WPF (WPF in this case) application that is hosting the AddIns found. It is also responsible for calling the selected Addins methods, and allows the AddIn to report progress by using an internal class AutomationHost which inherits from the HostView.HostObject class
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 { ....... ....... } }
As I stated this is WPF (but could be WinForms) the UI is not important and is a throw away. However it looks like this
Where there is a list of AddIns found, and then the user is able to apply the AddIns. This probably needs a little bit of an explanation. There is a class called AddInStore
which allows the .NET code to rebuild the AddIn list. This results in a new file being created on the file system
The AddInStore
also allows the app code to find AddIns. So this is exactly what I do with the following lines, where the AddIns are refreshed and then added to a ListBox
string path = Environment.CurrentDirectory; AddInStore.Update(path); string[] s = AddInStore.RebuildAddIns(path); IList<AddInToken> tokens = AddInStore.FindAddIns(typeof(HostView.NumberProcessorHostView), path);
So that really only leaves the AddIn concreate classes themselves, these are faily trivial (you'll be pleased to hear...As the AddIn model is fairly scary, and not for the faint hearted, and without Mathew McDonalds excellent book on WPF, I could not have written this article)...So lets see the AddIns code
Recall that there were 3 AddIns, each of which inherits from the abstract AddInView.NumberProcessorAddInView class
Well they all roughly work the same, ill put the code from them all up for completness, thiough this is the easy part
Uses recursion to return a list of fibonacci numbers between the fromNumber and toNumber sequence
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; } } }
Returns a list of of numbers between the fromNumber and toNumber sequence
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; } } }
Returns a list of of prime numbers between the fromNumber and toNumber sequence
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; } } }
And here is a screen shot of the AddIns running inside a WPF app (so I have made it look nice with a WPF DataTemplate
but thats not important)
As you can see this is a complicated arrangement, and it is quite a lot to get your head around, and quite a lot of work. The CLR AddIn team have listened to initial concerns that this model is too much work, and have dedicated a codeplex page to a Visual Studio AddIn that aids in the development of creating AddIn enabled apps. They also supply a blank AddIn pipeline project, for you to start with. The VS AddIn aids in the creation of both the host and AddIn side adapters. The CLR AddIn teams codeplex page can be found at the following link Managed Extensibility and Add-In Framework
I think its fair to say that the AddIn model is not that easy to understand, but this example shows you most of what you will need to know. I have not included all the code in this article, I have tried to only include what I consider to be the most important parts (Nish if you are reading, see I've listened to you) of the code. As such you will need to refer to the articles demo application in order to understand the full workings of the AddIn model. I hope you learnt something from this article, and if you liked it please leave a vote for it.