I have been
working on a VSTO 2005 project in the past few weeks and although the progress
made since VSTO 2003 is substantial in terms of productivity and freedom, there
are still a few gotchas. In the next few days I will be posting information on
some of the issues I have come across and any helpful tips I have discovered.
The project was a Word Template one, so some of the posts will be specific to
that, but most of the code will apply to VSTO 2005 in general.
Adding a
toolbar is quite a common task and it is not that difficult, but there are a
few little details that you need to be aware of. Let’s have a look at some code
first
private
Office.CommandBar AddToolbar()
{
// Create the
toolbar
Office.CommandBar
toolbar = this.Application.CommandBars.Add(
"Toolbar
name", MsoBarPosition.msoBarTop,
missing, false);
// Add a button
to it
Office.CommandBarButton
button = (Office.CommandBarButton)
toolbar.Controls.Add(MsoControlType.msoControlButton, missing,
missing, missing, false);
// Set the
button's style
button.Style = Office.MsoButtonStyle.msoButtonCaption;
// Set the
button's caption
button.Caption = "Click
me";
// Assign a tag
value and the Click event will be fired once
// for every
document that is open
//button.Tag = “a
unique value”;
// Ensure the
toolbar is visible, by default it won't be
toolbar.Visible = true;
// Return the
newly created toolbar
return
toolbar;
}
Let’s start
with the first line of code that actually creates the CommandBar object. If the
code is placed in the ThisDocument.cs file then you have access to the Word
application through the homonymous property; otherwise you can use Globals.ThisDocument.Application.
First you pass in the name of the new toolbar, then where you want it (top,
bottom, floating, etc.), then whether you would like to replace the currently
active menu bar or not (it is a Boolean parameter, but you can use missing
which defaults to false) and lastly and most interestingly whether you would
like to add the new toolbar as a temporary or a permanent one. The difference
between the two is that a permanent toolbar is saved to the template and will
be available next time a document is created from the template without having
to execute this code. A temporary one is not saved in the template and it will
have to be added every time you create a document from the template. Unless
there is a reason to do otherwise, I prefer to add the toolbar as permanent to
save myself some time during the creation of a new document.
The second
line of code adds a control to the toolbar. You specify what type of control
you would like (drop-down, popup, combo, etc.), in this case a button, then you
have some parameters that apply to built in controls which we are not
interested in here and finally again whether the control should be temporary or
permanent. After that, the style of the button is set (caption, image, etc.)
and since we chose a caption button we give it a caption. The line of code that
is commented out line refers to the Tag property of the button which when given
a value, the Click event of the button will be fired in every open document
that was based on the template; otherwise the event will be fired only in the
currently active one. This is quite handy if you need to perform an action
across multiple open documents. The rest is for making the toolbar visible,
which is the same as right clicking on Word’s toolbar pane and selecting a
toolbar by name.
As you may
have noticed, the code above does not subscribe to the button’s Click event.
This is not an omission; there is a very good reason for it. If you remember, a
toolbar can be added either as permanent or not and here we are adding it as a
permanent one. This means that after the fist time, any document that gets
created from the template, the toolbar will already exist and will not be
necessary to add it again. In that case, the above does not need to and should
not be executed. If the code that listens to the Click event of the button was
added here, the button for any document created from the template after the
first one would not work. Don’t get ahead of yourself and think that if the
toolbar is added as a temporary one, then this is not an issue. The toolbar,
regardless of whether it is a temporary one or not, is added to a Word
application (i.e. process) and therefore if another document is created from
the template whilst the first one is still open, by default, it will be created
within the same Word application and the toolbar will therefore already exist.
So, now that I have convinced you that you should not subscribe to the buttons’
Click event during creation of the toolbar, let’s move on to the code that
takes care of this issue
private
void SubscribeToToolbarEvents(Office.CommandBar toolbar)
{
// Go through
each control in the toolbar
foreach
(Office.CommandBarControl control in toolbar.Controls)
{
// If the
control is a button
if
(control is Office.CommandBarButton)
{
// Cast
the control to a button
Office.CommandBarButton
button = (Office.CommandBarButton)control;
//
Subscribe to the button's Click event
button.Click += new Office._CommandBarButtonEvents_ClickEventHandler(
ThisDocument_Click);
// Add
the button to a class scope collection of buttons
this.toolbarButtons.Add(button);
}
}
}
This piece of
code is pretty straight forward, given a toolbar you go through its buttons and
subscribe to its Click event. There is a small detail, however, which might go
unnoticed but is of great importance; each button is added to a class scope
collection. If this step is not taken, after the first pass of the Garbage
Collector the Click event of the buttons will stop firing. This might appear
very strange at first, but if you consider that what we are dealing with here
is COM Interop, it actually makes sense. The instances of CommandBarButton that we get from accessing a
toolbar’s Controls property, are not the actual
objects stored in the toolbar; they are just wrappers. As a result, unless you
keep a reference to them by adding them to a class scope collection, nothing
else has a reference to them and they get garbage collected. The API needs to
be as lightweight as possible, so the VSTO team has decided to not keep the
wrapper objects around unless they are required; otherwise for every unmanaged
object in Word exposed through VSTO would have a managed counterpart making
Word’s memory consumption almost double as soon as VSTO is used.
Alright, now
that we have gone through each individual issue, let’s put the code together
private
void ThisDocument_Startup(object sender, System.EventArgs
e)
{
// See if the
toolbar already exists
Office.CommandBar
toolbar = this.GetToolbar(TOOLBAR_NAME);
// If it does not
exist
if (toolbar
== null)
{
// Add the
toolbar
toolbar = this.AddToolbar();
}
// Start
listening to the events fired by the toolbar and its buttons
this.SubscribeToToolbarEvents(toolbar);
}
private
Office.CommandBar GetToolbar(string toolbarName)
{
// Go through
each toolbar in the application
foreach
(Office.CommandBar toolbar in this.Application.CommandBars)
{
// Iff the
toolbar has the specified name
if
(toolbar.Name.CompareTo(toolbarName) == 0)
{
// Return
it
return
toolbar;
}
}
// A toolbar with
the specified name was not found, return null
return null;
}
private
Office.CommandBar AddToolbar()
{
// Create the
toolbar
Office.CommandBar
toolbar = this.Application.CommandBars.Add(
"Toolbar
name", MsoBarPosition.msoBarTop, missing, false);
// Add a button
to it
Office.CommandBarButton
button = (Office.CommandBarButton)
toolbar.Controls.Add(MsoControlType.msoControlButton, missing,
missing, missing, false);
// Set the
button's style
button.Style = Office.MsoButtonStyle.msoButtonCaption;
// Set the
button's caption
button.Caption = "Click
me";
// Assign a tag
value and the Click event will be fired once
// for every
document that is open
//button.Tag =
Guid.NewGuid().ToString();
// Ensure the
toolbar is visible, by default it won't be
toolbar.Visible = true;
// Return the
newly created toolbar
return
toolbar;
}
private
void SubscribeToToolbarEvents(Office.CommandBar toolbar)
{
// Go through
each control in the toolbar
foreach
(Office.CommandBarControl control in toolbar.Controls)
{
// If the
control is a button
if
(control is Office.CommandBarButton)
{
// Cast
the control to a button
Office.CommandBarButton
button = (Office.CommandBarButton)control;
//
Subscribe to the button's Click event
button.Click += new Office._CommandBarButtonEvents_ClickEventHandler(
ThisDocument_Click);
// Add
the button to a class scope collection of buttons
this.toolbarButtons.Add(button);
}
}
}
Those who pay
close attention to detail might have noticed that the variable for the toolbar
is not at class scope. The reason is that I have come across
problems doing that It appears that there is some issue with the
wrapper object and the underlying actual toolbar and after a while using the
wrapper to access the toolbar causes issues. The one I have come across is with
toolbar.Delete() during document shutdown, which will throw an exception if
used on a wrapper object held at class scope, but will work correctly if the
toolbar wrapper is created again using the GetToolbar
method. I have made it my standard practice now to always use this method
instead of keeping a variable at class scope for the toolbar.
And there you have
it, how to add a toolbar in VSTO 2005.