江南白衣

陌上發花,可以緩緩醉矣忍把浮名,換了淺斟低唱 我不是聖賢豪士,我衹有一腔熱血
posts - 107, comments - 431, trackbacks - 14, articles - 0
  博客园 :: 首页 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理

公告

AddIn Enabled Applications

Posted on 2008-07-15 21:07 江南白衣 阅读(...) 评论(...) 编辑 收藏

原文:http://www.codeproject.com/KB/dotnet/AddInModel.aspx

Introduction

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

  • Finobacci numbers AddIn : Returns a list of Fibonacci numbers between the fromNumber and toNumber
  • Loop numbers AddIn : Returns a list of numbers between the fromNumber and toNumber
  • Prime numbers AddIn : Returns a list of Prime numbers between the fromNumber and toNumber



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.

What Are AddIns

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 AddIn Pipeline

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

  1. The host application calls the one of the methods in the host view. But the host view is really an abstract class. What is actually happening is that the host application is really calling a method on the host adapter though the host view. This is possible as the host adapter inhertis from the host view
  2. The host adpater then calls the correct method in the contract interface, which is implemented by the AddIn adapter
  3. The AddIn adapter calls a method in the AddIn view. This method is implemented by the AddIn that actually does the work

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.

AddIn Model Folder Structure

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.

Preparing A Solution To Use The AddIn Model

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

  1. Create a top level directory to hold all the seperate components. I called mine AddInTest
  2. Create either a WinForms or WPF based host application. It doesnt matter what its called but it must be placed in top level folder from Step1
  3. Add a new Class Library project for each pipeline component. You will also need to create a project for at least 1 AddIn component. This should give you something like the following within Visual Studio (note there are actually 3 AddIns in the solution shown here)



  4. Next your need to create a build directory in the top level directory (AddInTest\ for the demo app). This is where the AddIn pipeline components will be placed once they are compiled. This folder is called "Output" in the demo application.
  5. As each of the pipeline components is constructed and built you will need to modify the build path, to point to the relevant sub directory underneath the top level build folder. This is shown in the figure below (which we saw earlier)

    .

    The build path can be chnaged using Visual Studio, by right clicking the Project and getting up the Project settings. See the figure below

    .

Referencing Pipeline Components

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

.

The Code

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

Contract

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);
}
}

AddIn Adapter Class

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
{
.......
.......
}
}

Host Adapter Class

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
{
.......
.......
}
}

AddIn View Class

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);
}
}

Host View Class

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);
}
}

Host Application

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

 

Collapse

 

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

The Actual AddIns

Recall that there were 3 AddIns, each of which inherits from the abstract AddInView.NumberProcessorAddInView class

  • Finobacci numbers AddIn : Returns a list of Fibonacci numbers between the fromNumber and toNumber
  • Loop numbers AddIn : Returns a list of numbers between the fromNumber and toNumber
  • Prime numbers AddIn : Returns a list of Prime numbers between the fromNumber and toNumber

Well they all roughly work the same, ill put the code from them all up for completness, thiough this is the easy part

Finobacci numbers AddIn

Uses recursion to return a list of fibonacci numbers between the fromNumber and toNumber sequence

 

Collapse

 

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;
}
}
}

Loop numbers AddIn

Returns a list of of numbers between the fromNumber and toNumber sequence

 

Collapse

 

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;
}
}
}

Prime numbers AddIn

Returns a list of of prime numbers between the fromNumber and toNumber sequence

 

Collapse

 

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)

AddIn Help

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

Conclusion

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.