Custom Task Panes: Doc-Level and App-Level

With VSTO 2005, you can build doc-level custom task panes (although we called them ActionsPanes). Our doc-level task panes were built on the old ISmartDocument technology – effectively, VSTO offered a streamlined RAD mechanism for building smart doc solutions.

 

With the upcoming “Cypress” release of VSTO 2005, you can also build app-level custom task panes for Office 2007. App-level custom task panes are built on an entirely different mechanism, using the new ICustomTaskPaneConsumer interface. Working with customers over the last year or so, building doc-level solutions that used task panes, it has become apparent that sometimes people build doc-level solutions in order to get the custom task pane, when what they really want is app-level solutions. So, with Office 2007 and VSTO 2005 Cypress or VSTO v3 (Orcas), you can build truly app-level task panes.

 

But now, some people want to have their cake and eat it, too. They want an app-level custom task pane (that’s always there, at app-level), but that also has enough intelligence to be context-aware – specifically, to have knowledge of the currently active document. Some people are just never satisfied. Here’s the scenario, you have a set of Word or Excel templates (or Outlook items, or PowerPoint templates, or InfoPath forms) that you use in your organization. You want to hook these up with the task pane so that different controls become active when different documents based on these templates are created or opened. Can this be done?

 

The answer, of course, is “Yes”. Your templates already have a lot of custom features (special data, headers/footers, cell formula, formatting, and so forth). You probably also have some custom document properties. One way to establish context-awareness in an app-level task pane is to put some custom doc property in your template which the task pane can search for. If it finds this doc property, it can then conditionally render controls that apply to documents with that property value.

 

Here’s a trivial example. Say you have 2 Excel templates, Contoso_Red.xltx and Contoso_Blue.xltx. When the user creates a new workbook (or opens an existing workbook) based on the Contoso_Red template (or opens the Contoso_Red template itself), you want to render the corresponding “red” controls in the task pane:

 

 

Similarly, when the create or open a workbook based on the Contoso_Blue template, you want to render the “blue” controls in the task pane:

 

 

 

Bear in mind that the new custom task panes do not stack; they tile. And you don’t want more than one task pane showing at any one time, because it takes up too much screen real estate. So, how can you achieve this?

 

Here’s how. First, ensure you have some appropriate custom doc property in your templates or documents. In Office 2007, you get to these from the Developer tab | Document Panel | Advanced Properties. In my example, I have a custom doc property called Contoso_TemplateID, set to the value “Blue” in my blue template, and “Red” in my red template:

 

 

 

In my VSTO Excel add-in, I have two UserControls: BlueControl and RedControl. In my main add-in class (called ThisApplication in the VSTO v3 June CTP), I have a method that searches a given Workbook for this doc property. Doc properties are one of a very small number of features exposed from Office apps that are exposed in a late-bindable manner only. For this reason, I have to use reflection to get at them:

 

private string GetTemplateID(Excel.Workbook Wb)

{

   string templateID = String.Empty;

   string customPropString = "Contoso_TemplateID";

   object customProps = Wb.CustomDocumentProperties;

   bool isContosoWorkbook = false;

   Office.DocumentProperties properties =

        (Office.DocumentProperties)

  this.ActiveWorkbook.CustomDocumentProperties;

   for (int i = 1; i <= properties.Count; i++)

   {

       if (properties[i].Name == customPropString)

       {

           isContosoWorkbook = true;

           break;

       }

   }

   if (isContosoWorkbook)

   {

       Type typeCustomProps = customProps.GetType();

       object customProp = typeCustomProps.InvokeMember("Item",

           BindingFlags.Default | BindingFlags.GetProperty,

           null, customProps, new object[] { customPropString });

       if (customProp != null)

       {

           Type typeCustomProp = customProp.GetType();

           templateID = typeCustomProp.InvokeMember("Value",

               BindingFlags.Default | BindingFlags.GetProperty,

               null, customProp, new object[] { }).ToString();

       }

   }

   return templateID;

}

I also declare a dictionary field – this is where I’ll keep the mapping between workbooks and task panes – and an object for Excel Application events:

 

internal Dictionary<Excel.Workbook, CustomTaskPane> workbookPanes;

private Excel.AppEvents_Event appEvents;

 

In ThisApplication_Startup, I intialize this dictionary and hook up the NewWorkbook and WorkbookOpen events:

 

workbookPanes = new Dictionary<Excel.Workbook, CustomTaskPane>();

appEvents = (Excel.AppEvents_Event)this.InnerObject;

appEvents.NewWorkbook += new

      Excel.AppEvents_NewWorkbookEventHandler(appEvents_NewWorkbook);

appEvents.WorkbookOpen += new

      Excel.AppEvents_WorkbookOpenEventHandler(appEvents_WorkbookOpen);

if (this.ActiveWorkbook != null)

   appEvents_WorkbookOpen(this.ActiveWorkbook);

 

In my WorkbookOpen event handler, I find the template ID doc property value, and depending on whether it is one of my Contoso Red or Blue workbooks, I create a custom task pane with either a RedControl or a BlueControl. I make sure to add this task pane to the dictionary, mapped to this specific workbook. If I don’t find either of my Contoso doc properties, the user must have opened some other workbook that I’m not interested in. Either way, I also sink the WindowActivate and WindowDeactivate events.

 

void appEvents_WorkbookOpen(Excel.Workbook Wb)

{

    try

    {

        CustomTaskPane ctp = null;

        switch (GetTemplateID(Wb))

        {

            case "Red":

                ctp = CustomTaskPanes.Add(new RedControl(), "Red");

                break;

            case "Blue" :

                ctp = CustomTaskPanes.Add(new BlueControl(), "Blue");

                break;

        }

        if (ctp != null)

        {

            workbookPanes.Add(Wb, ctp);

            ctp.Visible = true;

        }

      Wb.WindowActivate += new

Excel.WorkbookEvents_WindowActivateEventHandler(Wb_WindowActivate);

        Wb.WindowDeactivate += new

Excel.WorkbookEvents_WindowDeactivateEventHandler(Wb_WindowDeactivate);

    }

    catch (Exception ex)

    {

        MessageBox.Show(ex.ToString());

    }

}

 

Similarly, my NewWorkbook event handler hooks up the same WindowActivate and WindowDeactivate events.

 

void appEvents_NewWorkbook(Excel.Workbook Wb)

{

    try

    {

        Wb.WindowActivate += new

Excel.WorkbookEvents_WindowActivateEventHandler(Wb_WindowActivate);

        Wb.WindowDeactivate += new

Excel.WorkbookEvents_WindowDeactivateEventHandler(Wb_WindowDeactivate);

    }

    catch (Exception ex)

    {

        MessageBox.Show(ex.ToString());

    }

}

 

When I get a WindowDeactivate event, I simply hide all the task panes. Admittedly, I could do this in some more sophisticated manner, but I’m deliberately keeping things simple for this example.

 

void Wb_WindowDeactivate(Microsoft.Office.Interop.Excel.Window Wn)

{

    try

    {

        foreach (KeyValuePair<Excel.Workbook, CustomTaskPane>

keypair in workbookPanes)

        {

            keypair.Value.Visible = false;

        }

    }

    catch (Exception ex)

    {

        MessageBox.Show(ex.ToString());

    }

}

When I get a WindowActivate event, I walk the collection of task pane/workbook mappings to see if I have a task pane for the workbook that has just been activated. If so, I make that task pane visible.

 

void Wb_WindowActivate(Excel.Window Wn)

{

    try

    {

        foreach (KeyValuePair<Excel.Workbook, CustomTaskPane>

keypair in workbookPanes)

        {

            if (keypair.Key == Wn.Application.ActiveWorkbook)

            {

                keypair.Value.Visible = true;

                break;

            }

       }

    }

    catch (Exception ex)

    {

        MessageBox.Show(ex.ToString());

    }

}

 

That’s it. To summarize:

- create a task pane for any workbook the user opens/creates for which I want to show task pane controls

- whenever a window is deactivated, simply hide all the taskpanes.

- whenever a window is activated, find out if that window has an associated task pane, and if so make it visible.

 

This is pretty simple stuff, but it does indicate how you could build a system using app-level task panes (and any other app-level functionality) that is context-sensitive to specific documents. There are other wrinkles to the app-level task pane that I want to explore, but that’s enough for now.