span.sup { vertical-align:text-top; }
CLR Inside Out
Extend Windows Forms Apps Using System.AddIn
Mueez Siddiqui
Code download available at:CLRInsideOut2008_07.exe(472 KB)
Contents
Adapting the Host
Exposing the Object Model
Adapters, Contracts, and Add-In Views
Writing the Add-In
The Collaboration Add-In
Wrap-Up
Did you know that you can now make Windows® Forms applications extensible by using the new Microsoft® .NET add-in framework (System.AddIn)? In this column I will modify the ShapeApp drawing application to expose its object model through the add-in framework. This will allow me to create add-ins that automatically perform tasks on the host application (automation add-ins). Also, I'll discuss the kinds of problems commonly encountered in this scenario and present solutions.
If you need background information on the add-in framework before you get started, a good place to begin is the resources page on the CLR Add-In Team Blog at go.microsoft.com/fwlink/?LinkId=117519. You might also consider reading the previous CLR Inside Out columns on the add-in framework.
ShapeApp is a drawing application that lets you make drawings using basic shapes. You can insert new shapes, move them around, change their color and size, and so on. It allows you to save drawings to files and to open multiple drawings in tabs. A simple drawing created in ShapeApp can be seen in Figure 1.
Figure 1 Screenshot of a ShapeApp
ShapeApp was developed by the Visual Studio® Tools for Applications (VSTA) team as a sample, which lets you embed the Visual Studio IDE into your application. The IDE can then be used to write extensions for the application. They have provided versions of the application with and without their extensibility code. In this sample, I start from the base version and adapt it to work with the add-in framework.
Adapting the Host
In this section I'll adapt the application to expose its object model, similar to the Document Object Model (DOM) in HTML. This will allow me to create add-ins that automate tasks in the application.
The first step in adapting the host is to define an object model that the host will expose (note that I don't need to write any code yet). The isolation and versioning capabilities of the add-in framework impose constraints on the object model. In general, the object model should define all the types that the add-in will use that aren't built-in to the .NET Framework; in other words, the view assembly should not reference any assemblies other than the .NET Framework assemblies. MarshalByRefObject types are explicitly disallowed. For a more detailed breakdown of the allowable types, see the blog post on contracts at go.microsoft.com/fwlink/?LinkId=117520.
Figure 2 lists the types in the ShapeApp's object model. All of the classes you see there exist internally in the host application. I will be exposing them through the pipeline later.
Figure 2 ShapeApp Types
Type | Description |
---|---|
ShapeApplication | The main application object. |
Drawing | An object that represents a drawing in the application. |
Shape | An object that represents a shape in a drawing. |
DrawingCollection | A collection of drawing objects. |
ShapeCollection | A collection of shape objects. |
EventArgs-related types | Several types derived from System.EventArgs, used for custom event arguments. |
The next step is to add a way for the host to discover and activate add-ins. For this I use a simple add-in manager on the host. This includes both a static class, AddInManager, that loads and keeps track of add-ins and a form that lets the user manage add-ins. The form is activated through a menu item on the host, requiring minimal changes to the host. The form is shown in Figure 3.
Figure 3 The Add-In Manager Form
The add-in manager code is very simple. Here are the three lines required to find available add-ins:
// update the add-in store to include add-ins located in the
// current program folder
string[] warnings = AddInStore.Update(PipelineStoreLocation.ApplicationBase);
// get the view type defined in the host views assembly
System.Type hostViewOfAddIn = typeof(ShapeAppHostViews.IShapeAddIn);
// find all add-ins that implement the view
ICollection<AddInToken> addIns = AddInStore.FindAddIns(hostViewOfAddIn,
PipelineStoreLocation.ApplicationBase);
I use the first line, AddInStore.Update, to search for available add-ins and pipeline components. PipelineStoreLocation.ApplicationBase tells it to look in the application's folder. For a detailed description of the folder structure, see go.microsoft.com/fwlink/?LinkId=117521. Next, I get the type of the add-ins I want to activate. In this case it's the IShapeAddIn type defined in the host views assembly. Finally, I call AddInStore.FindAddIns, which returns a collection of available add-in tokens that I can inspect and activate. Once I have selected a token from the collection, I can use one line of code to activate it:
// activate the add-in in full trust mode
ShapeAppHostViews.IShapeAddIn addIn =
token.Activate<ShapeAppHostViews.IShapeAddIn>(
AddInSecurityLevel.FullTrust);
I can also choose to activate add-ins out of process or under a different trust level. For a list of isolation levels, you can go to go.microsoft.com/fwlink/?LinkId=117522. For a performance comparison of isolation levels, see the blog post on performance, available at go.microsoft.com/fwlink/?LinkId=117523.
Exposing the Object Model
Once the empty pipeline and add-in manager are in place, I can start exposing functionality from the host to the add-in. To expose the types included in the object model, I need to first create a host view in the HostViews assembly for each type that I want to expose. I can expose properties, methods, and events in the view. The host view can be either an abstract base class or an interface.
In the ShapeApp, I have used interfaces for three reasons. First, they are needed for events to work correctly when used from Visual Basic®. Second, C# does not support multiple inheritance, so a class cannot inherit from two base classes. Therefore, the view should be an interface to allow all host classes to implement it (even if they already have a base). Third, interfaces allow EIMI (explicit interface method implementation). I will explain the significance of this later.
Figure 4 shows what the host view looks like for the ShapeApplication object. The types used in members can either be built-in framework types, such as EventHandler<T>, or other views, such as IDrawing. You can download FxCop rules from our CodePlex site at go.microsoft.com/fwlink/?LinkId=117524 to verify that the views are easily adaptable into a complete pipeline.
Figure 4 ShapeApplication Object Host View
namespace ShapeAppHostViews
{
public interface IShapeApplication
{
// the drawing present in the selected tab
IDrawing ActiveDrawing { get; }
// a collection of available shapes (square, circle, etc)
IShapeCollection AvailableShapes { get; }
// a collection of drawings currently open in the application
IDrawingCollection Drawings { get; }
// main window visibility
bool Visible { get; set; }
// create a new drawing in the application
IDrawing NewDrawing();
// completely exit the application
void Quit();
// event fired when a drawing is created
event EventHandler<CreatedDrawingEventArgs> CreatedDrawing;
}
}
Once I have created the views assembly, I modify the host's classes to inherit from corresponding views. The changes to the ShapeApplication class are shown bolded below:
public class ShapeApplication : System.Windows.Forms.IWin32Window
{
...
}
public class ShapeApplication : ShapeAppHostViews.IShapeApplication,
System.Windows.Forms.IWin32Window
{
...
}
Next, I need to implement each member from the host view. For members that use built-in types as arguments and return values, there is no special implementation required (other than ensuring the member is public in the host's class). For example, the Visible property in the ShapeApplication class has the same signature as the Visible interface member:
public bool Visible
{
get {...}
set {...}
}
This means that the existing implementation of Visible in the host's ShapeApplication class will be used as the view implementation.
However, members that use other types defined in the host views need to be implemented explicitly. For example, the ActiveDrawing property has different signatures in the ShapeApplication class and in the IShapeApplication host view. The original version in the host's class returns a Drawing object:
public Drawing ActiveDrawing
{
get {...}
}
The version in the host view, IShapeApplication, can only return the corresponding view, IDrawing:
IDrawing ActiveDrawing
{
get;
}
To implement the second version in the host's class, I need to use EIMI. I do this by adding a new property in the host's class with the same name, ActiveDrawing. Now the host's ShapeApplication class has two properties called ActiveDrawing (see Figure 5).
Figure 5 ActiveDrawing Property
// original property
public Drawing ActiveDrawing
{
get {...}
}
// implementation of the host view's version of ActiveDrawing
ShapeAppHostViews.IDrawing
ShapeAppHostViews.IShapeApplication.ActiveDrawing
{
get
{
// a call to the original property
return this.ActiveDrawing;
}
}
The new implementation implicitly casts the Drawing object to the IDrawing type. If you are exposing an object model for a Windows Forms application, it is likely that some of the exposed members will access and modify Windows Forms controls. If such members are called from a thread other than the one that created the control, it causes unpredictable behavior in the GUI.
Since members exposed to the add-in can be called from any thread, I must ensure that any exposed members that access or modify GUI controls do so safely by using Control.InvokeRequired and Control.Invoke. For example, the ShapeApplication.NewDrawing function accesses the ApplicationForm object and therefore requires the use of Invoke to be safely implemented.
Here's the old implementation:
public Drawing NewDrawing()
{
Drawing newDrawing = new Drawing(this);
...
this.ApplicationForm.drawingsTabControl.TabPages.Add(
newDrawing.DrawingSurface);
...
return newDrawing;
}
The new implementation is shown in Figure 6; the changes are bolded.
Figure 6 NewDrawing Method with Invoke
// delegate added to allow invoke
private delegate Drawing NewDrawingDelegate();
public Drawing NewDrawing()
{
// check if we need an invoke
if (this.ApplicationForm.InvokeRequired)
{
// invoke this method using the ApplicationForm object
NewDrawingDelegate del = new NewDrawingDelegate(NewDrawing);
return (Drawing) ApplicationForm.Invoke(del);
}
else
{
Drawing newDrawing = new Drawing(this);
...
this.ApplicationForm.drawingsTabControl.TabPages.Add(
newDrawing.DrawingSurface);
...
return newDrawing;
}
}
Adapters, Contracts, and Add-In Views
To allow the add-in to access functionality exposed by the host, I need to create adapters, contracts, and add-in views. The pipeline components can be automatically generated from the host view. The Pipeline Builder does just that, and it can be found at go.microsoft.com/fwlink/?LinkId=117525. This works great for the first version of the application.
Alternately, the pipeline can be coded by hand. This is required when writing cross-version adapters. In Visual Studio, one can set up a pipeline by adding projects for each required assembly (views, adapters, contracts, and so forth) into the ShapeApp solution. A step-by-step guide to creating a pipeline in Visual Studio can be found at go.microsoft.com/fwlink/?LinkId=117526.
In this sample, I coded the pipeline manually. Since this is the first version of the ShapeApp object model, the host and add-in views are identical. Therefore, I can use one assembly for both views. When coding by hand, it is helpful to set up an empty pipeline initially. The view for such a pipeline looks like this:
public interface IShapeAddIn
{
void Initialize(IShapeApplication application);
}
public interface IShapeApplication
{
// TODO: Implement this.
}
The Initialize method is used to pass the main application object to the add-in when it is loaded. This allows the add-in to control the host at any time, without the host explicitly asking for any services (that's why it's called an automation add-in).
After I have created an empty pipeline, I can expose the rest of the functionality from the host one bit at a time, compile it, and test it. This iterative approach helps avoid a seemingly endless string of compile errors. Of course, once the interfaces have been finalized and the application is released, I can't add any more methods to the existing interfaces. What adapters I'll need depends on where an object lives and where it is accessed. Generally, objects can fall into one of the following three categories:
Host-Side Objects These objects live on the host side and are accessed from the add-in (for example, the ShapeApplication class). To expose them to the add-in, I need the pipeline components shown in Figure 7. Since the object exists on the host side, I start from the host view on the top. I use a view-to-contract host adapter to convert it to a contract, and contract-to-view add-in adapter to convert it to the add-in view. These two adapters let the add-in access the host-side object.
Figure 7 Pipeline Components Required
Host-Side Objects | Add-In-Side Objects |
---|---|
Host (object lives here) | Add-in (object lives here) |
Host view | Add-in view |
View-to-contract host adapter | View-to-contract add-in adapter |
Contract | Contract |
Contract-to-view add-in adapter | Contract-to-view host adapter |
Add-in view | Host view |
Add-in (object is accessed here) | Host (object is accessed here) |
Add-In-Side Objects These objects live on the add-in side and are accessed from the host (for example, the ShapeAddIn class). The pipeline components are similar here, but the direction of the adapters is reversed to allow the host access to the add-in-side object. The view-to-contract adapter is now on the add-in side instead of the host side, and the contract-to-view adapter is now on the host side. Note that the rest of the pipeline (the views and the contract) is identical.
Either-Side Objects These are objects that can live on either side and can be accessed from either side. These require four adapters, two for each side, since I need to adapt them either way. Note that if you're using the same assembly for the host and add-in view, you can re-use adapters as well. In that case you would need only two.
Contracts do not support events natively. However, events can be simulated in contracts by using the following pattern (example shown from the IShapeApplication.CreatedDrawing event):
Host views:
public interface IShapeApplication
{
...
// event fired when a drawing is created
event EventHandler<CreatedDrawingEventArgs> CreatedDrawing;
}
Corresponding contracts:
public interface IShapeApplicationContract : IContract
{
...
void CreatedDrawingAdd(ICreatedDrawingEventHandlerContract handler);
void CreatedDrawingRemove(ICreatedDrawingEventHandlerContract
handler);
}
public interface ICreatedDrawingEventHandlerContract : IContract
{
void Handler(ICreatedDrawingEventArgsContract args);
}
You can see that I have created a new contract for the event handler, ICreatedDrawingEventHandlerContract, because delegates like System.EventHandler<CreatedDrawingEventArgs> cannot be used in a contract.
The add-in-side adapter registers a handler on the host side through the Add and Remove methods, and it maintains a local event that the add-in can subscribe to. The host-side adapter calls the Handler function when the event is fired.
This added complexity is mostly transparent to the add-in developer, except for one caveat: the add-in must register and unregister itself on the same adapter instance of an object. Otherwise, unregistering will not work.
Next, I will describe how a host object is able to have two (or more) sets of adapter objects that reference it. When an object on the host side is returned to the add-in (through a property access or function call), two adapter objects (host-side adapter and add-in-side adapter) are created that allow access to the host object. When the same object is returned again to the add-in, two new adapters are created.
For example, accessing ShapeApplication.ActiveDrawing twice will return two different object references to the add-in, and ShapeApplication.ActiveDrawing.ReferenceEquals(ShapeApplication.ActiveDrawing) will return false. The presence of two (or more) adapters for the same host object can be a problem when registering/unregistering events and storing host objects in collections, among other things.
To help solve these problems, the .Equals and .GetHashCode functions can be overridden on the adapters to call the corresponding functions on the real host object. This allows us to put host objects into collections in the add-in, and methods like .Contains work fine. Of course, it's also true that the add-in developer still has to ensure that events are registered and unregistered on the same object. And you should know that the .Equals function can help with this.
As you may have guessed, another option is to cache adapters. However, this is difficult in practice, because there is no easy way to store a weak mapping of objects to adapters (here, weak refers to weak object references, which the garbage collector ignores). Using a regular dictionary will prevent adapters and objects from being garbage collected.
Writing the Add-In
Once the pipeline is in place, I can write add-ins against the add-in view. Writing the add-in is as simple as possible. All an add-in developer needs to do is to create a class that inherits from the add-in view, and mark the add-in implementation with the AddIn attribute. The rest of the code can be written almost as if there were no pipeline between the host and the add-in. There are a couple of caveats, however. One is the object identity issue I discussed earlier, and the second is performance, depending on what isolation boundary is used. For performance benchmarks, see go.microsoft.com/fwlink/?LinkId=117527.
Currently, add-ins cannot directly display Windows Forms controls on the host application's forms. However, they can use any of the following three methods: they can display their own forms (note that if the add-in is running in some partial trust scenarios—like the Internet trust level—the user will see a security warning on the add-in form when it is loaded); they can display Windows Presentation Foundation (WPF) controls directly on the host application's forms (please see go.microsoft.com/fwlink/?LinkId=117528); and they can use Windows Forms controls that are wrapped in WPF containers (see go.microsoft.com/fwlink/?LinkId=117529).
Windows Forms have some threading requirements that must be met. For example, an add-in extending a command-line application must create a new thread to construct a form and process its events. This is because command-line applications use the multithreaded apartment (MTA) model by default, and Windows Forms require the single-threaded apartment (STA) model for UI threads. The solution is simple: add-ins extending a Windows Forms application can just include two lines to display forms (given the add-in is activated and used from the host's UI thread), like so:
AddInForm form = new AddInForm();
form.Show();
The Collaboration Add-In
Using the exposed interface, one can create really powerful add-ins. One example is the collaboration add-in included with this sample. It allows two users of the ShapeApp using two different machines to edit a drawing together in real time. The collaboration add-ins connect to each other through Windows Communication Foundation (WCF), allowing this interaction to work globally over the Internet. A screenshot of the connect screen can be seen in Figure 8.
Figure 8 Collaboration Add-In UI
Once two add-ins are connected to each other, any new documents created or opened on either end are shared; that is, changes on one machine are sent in real time to the other. This is accomplished using events. When the collaboration add-in is loaded, it subscribes to all the application's events. When a new drawing is created, the CreatedDrawing event is fired. The add-in receives this event and subscribes to all events for the new drawing. Similarly, it subscribes to all shape-related events when shapes are created. This allows the add-in to track all user actions and propagate them over to its peer.
Figure 9 shows the path taken by an event through the collaboration add-in. On Machine 1, the user performs an action (such as changing the position of a shape). This makes the host fire an event, and the collaboration add-in receives it. The collaboration add-in creates a message and sends it through WCF to the add-in on Machine 2. This add-in then performs the same action on the host. Note that it temporarily unhooks event handlers when performing actions on the host. This prevents the event from coming back to the add-in, which would result in an infinite cycle.
Figure 9 Path Taken by an Event through the Collaboration Add-In
More interesting scenarios are made possible by using WCF. Since the collaboration add-in hosts a service, any number of clients can connect to it. This allows more than two people to work on a drawing together, seamlessly. Figure 10 shows the connections among three machines. The collaboration add-in on each machine is connected to all the others.
Figure 10 Three Instances of the ShapeApp Connected Together
Now you've seen how the ShapeApp is adapted to host add-ins using the .NET add-in framework. You should have a good idea of the capabilities of the add-in framework and how it can be used to create an add-in that seamlessly turns the ShapeApp into a real-time collaborative editor. Please feel free to leave feedback or ask questions on the Add-In Team Blog at go.microsoft.com/fwlink/?LinkId=117530.
Mueez Siddiqui is a Software Development Engineer in the CLR security and extensibility team at Microsoft.