CustomTaskPanes in a .NET Shared COM Add-in


I was working with a customer that had a requirement to create a dynamic control for a CustomTaskPane in Excel and Word. In VSTO, this is very easy as the reference object found in ThisAddIn has a CustomTaskPanes collection. From there you can .Add() a CustomTaskPane and pass it a UserForm Control you build at runtime. This is very powerful in that you can create very dynamic TaskPanes. Smile

With a COM Shared Add-in, however, this is not as easy. The main reason is that you have to directly implement the TaskPane interface Office.ICustomTaskPaneConsumer an it does not give you the nifty CustomTaskPanes collection. What it does give you is an ICTPFactory object which exposes a method called CreateCTP(). However, this method requires a registered ActiveX control CLSID name for the TaskPane control you want to appear in the pane. It will not accept a dynamically created User control. How this works is detailed here:

ICustomTaskPaneConsumer.CTPFactoryAvailable Method (Office)
http://msdn.microsoft.com/en-us/library/office/ff863874.aspx

And this article details the full methodology:

Creating Custom Task Panes in the 2007 Office System
http://msdn.microsoft.com/en-us/library/office/aa338197%28v=office.12%29.aspx

A fellow employee (http://blogs.msdn.com/b/cristib/) that also does a lot of blogging on VSTO, pointed me to a possible solution:

  • Use the method detailed in the second article
  • But create a VSTO UserForm Control and expose it as a COM Control
  • However, he suggested adding all the controls I might possibly need to the control form and show/hide them as needed. E.g. making it pseudo dynamic. But my customer needed a fully dynamic pane…

So, I took it two steps further: Hot smile

  • First, I actually simplified the control idea greatly. My exposed COM control was a basic, simple, empty control. My design was to add the control to the TaskPane and then get a reference to the base UserForm control. From there I can call an exposed property (ChildControls) and then add anything I want to it. I can attach a button and then hook to it’s click event, etc. I can build a control dynamically at runtime, or build it as a separate project at design time.
  • Next, I recreated the CustomTaskPanes collection and CustomTaskPane objects in mirror classes so working with CustomTaskPanes in a Shared Add-in was as simple as in a VSTO add-in.

First, lets look at the base control I created. You will see that I exposed it as COM Visible. After I built the project, I used REGASM to register it:

C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe /codebase <path>\CustomTaskPaneControl.BaseControl.dll

Here is the code:

[ComVisible(true)]
[ProgId("CustomTaskPaneControl.BaseControl")]
[Guid("DD38ADAB-F63A-4F4A-AC1A-343B385DA2AF")]
public partial class BaseControl : UserControl
{
    public BaseControl()
    {
        InitializeComponent();
    }

    [ComVisible(true)]
    public ControlCollection ChildControls
    {
        get
        {
            return this.Controls;
        }
    }
}

That is it – that is all there is. It is a simple placeholder, empty shell, ready to be filled with anything you add the the ChildControls property.

Next, I created two new classes zCustomTaskPanesCollection and zCustomTaskPanes. I placed these in their own source code file and added it to the project Namespace. I also added the project reference to the base control above so I could directly cast it.

Here is the code:

/// <summary>
/// This class mirrors the Office.CustomTaskPaneCollection
/// </summary>
public class zCustomTaskPaneCollection
{
    // Public list of TaskPane items
    public List<zCustomTaskPane> Items = new List<zCustomTaskPane>();
    private Office.ICTPFactory _paneFactory;

    /// <summary>
    /// CTOR - takes the factor from the interface method
    /// </summary>
    /// <param name="CTPFactoryInst"></param>
    public zCustomTaskPaneCollection(Office.ICTPFactory CTPFactoryInst)
    {
        _paneFactory = CTPFactoryInst;
    }

    /// <summary>
    /// Adds a new TaskPane to the collection and takes a 
    /// User Form Control reference for the contents of the
    /// Control Pane
    /// </summary>
    /// <param name="control"></param>
    /// <param name="Title"></param>
    /// <returns></returns>
    public zCustomTaskPane Add(Control control, string Title)
    {
        // create a new Pane object
        zCustomTaskPane newPane = new zCustomTaskPane(control, Title, _paneFactory);
        Items.Add(newPane); // add it to the collection
        return newPane; // return a reference
    }

    /// <summary>
    /// Remove the specific pane from the list
    /// </summary>
    /// <param name="pane"></param>
    public void Remove(zCustomTaskPane pane)
    {
        Items.Remove(pane);
        pane.Dispose(); // dispose the pane
    }

    /// <summary>
    /// Get a list
    /// </summary>
    public int Count
    {
        get
        {
            return Items.Count;
        }
    }
}

/// <summary>
/// This class mirrors the Office.CustomTaskPane class 
/// </summary>
public class zCustomTaskPane
{
    private string _title = string.Empty;
    private Control _control = null;
    private Office.CustomTaskPane _taskPane;
    private BaseControl _base;
    public string Title { get { return _title; } }
    public event EventHandler VisibleChanged;

    /// <summary>
    /// Get or set the dock position of the TaskPane
    /// </summary>
    public Office.MsoCTPDockPosition DockPosition
    {
        get
        {
            return _taskPane.DockPosition;
        }
        set
        {
            _taskPane.DockPosition = value;
        }
    }

    /// <summary>
    /// Show or hide the CustomTaskPane
    /// </summary>
    public bool Visible
    {
        get
        {
            return _taskPane.Visible;
        }
        set
        {
            _taskPane.Visible = value;
        }
    }

    /// <summary>
    /// Reference to the control
    /// </summary>
    public Control Control { get { return _control; } }

    /// <summary>
    /// CTOR
    /// </summary>
    /// <param name="control"></param>
    /// <param name="Title"></param>
    /// <param name="CTPFactoryInst"></param>
    public zCustomTaskPane(Control control, string Title, Office.ICTPFactory CTPFactoryInst)
    {
        // create the taskpane control and use the factory to create a new instance of
        // our base control "CustomTaskPaneControl.BaseControl"
        _control = control;
        _title = Title;
        _taskPane = CTPFactoryInst.CreateCTP("CustomTaskPaneControl.BaseControl", Title);
        _taskPane.Width = control.Width + 2;
        _base = (BaseControl)_taskPane.ContentControl;
        _base.ChildControls.Add(control);
        _base.Height = control.Height + 2;
        // when the visibility changes fire an event
        _taskPane.VisibleStateChange += (Office.CustomTaskPane CustomTaskPaneInst) =>
            {
                VisibleChanged(this, new EventArgs());
            };
    }

    /// <summary>
    /// Dispose of the control and collect
    /// </summary>
    public void Dispose()
    {
        try
        {
            _taskPane.Visible = false;
        }
        catch { }
        try
        {
            _control.Dispose();
            _control = null;
            _base.Dispose();
            _base = null;
        }
        catch { }
        GC.Collect();
        GC.Collect();
        GC.WaitForPendingFinalizers();
    }
}

Finally, I added hooked up my Shared Add-in to use the ICustomTaskPaneConsumer interface. At that point, everything was hooked up ready to go:

public class Connect : Object, Extensibility.IDTExtensibility2, Office.ICustomTaskPaneConsumer, Office.IRibbonExtensibility
{
    public zCustomTaskPaneCollection CustomTaskPaneCollection;
    void Office.ICustomTaskPaneConsumer.CTPFactoryAvailable(Office.ICTPFactory CTPFactoryInst)
    {
        CustomTaskPaneCollection = new zCustomTaskPaneCollection(CTPFactoryInst);
    }

Now, I was able to build a fully dynamic control on a Ribbon Button click, like this:

// create a dynamic control on the fly
UserControl dynamicControl = new UserControl();
// add a button to it
Button btnTest = new Button();
btnTest.Name = "btnTest";
btnTest.Text = "Hello";
// when the user clicks the button on the TaskPane,
// say hello...
btnTest.Click +=(object sender, EventArgs e) =>
    {
        MessageBox.Show("Hello World!");
    };
// add the button to the control 
dynamicControl.Controls.Add(btnTest);
// now create the taskPane - looks exactly the same
// as how you would create one in VSTO
zCustomTaskPane myPane = CustomTaskPaneCollection.Add(dynamicControl, "My Pane");
myPane.VisibleChanged += (object sender, EventArgs e) =>
    {
        // when the taskpane is hidden, invalidate the ribbon
        // so my toggle button can toggle off
        ribbon.Invalidate();
    };
// dock to the right
myPane.DockPosition = Office.MsoCTPDockPosition.msoCTPDockPositionRight;
// show it
myPane.Visible = true;

Once fully implemented this looks and acts just like the CustomTaskPaneCollection from VSTO and is just as easy to use and allows you to create dynamic, on the fly controls. Winking smile

2 thoughts on “CustomTaskPanes in a .NET Shared COM Add-in

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s