My Word Add-In Creates Duplicate Menu Items. Make it Stop! (Norm Estabrook)

So I want my add-in to place a custom command into the shortcut menu. The shortcut menu is that cool menu that appears when you right-click a document. Great, so I read some articles in MSDN, write some code, run the add-in and voila there it is! I give it to my buddy, he is proud of my accomplishment and and installs my add-in.  Now he hates me because every time he opens up Word, a duplicate menu appears.  Where did I go wrong?

Well actually, I didn’t do anything wrong.  It’s just that Word requires a little more attention when it comes to handling menus. I guess you can say that Word is a bit more “needy” than other Office applications. But being “higher maintenance” does not have to mean “higher maintenance costs”.  Hopefully this post will get your friend talking to you again.

My code

Here is the code that did not work for me.  BTW – I will paste in both Visual Basic and C# examples for this post.

[Visual Basic]

Private MyApplication As Word.Application
Private WithEvents myControl As Office.CommandBarButton

Private Sub ThisAddIn_Startup _
(ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Startup

    MyApplication = Me.Application
    AddMenuItem()

End Sub

Private Sub AddMenuItem()

    Dim menuItem As Office.MsoControlType = _
        Office.MsoControlType.msoControlButton

    myControl = CType(MyApplication.CommandBars("Text").Controls.Add _
       (menuItem, 1, True), Office.CommandBarButton)

    myControl.Style = Office.MsoButtonStyle.msoButtonCaption
    myControl.Caption = "My Menu Item"
    myControl.Tag = "MyMenuItem"

End Sub


Sub myControl_Click(ByVal Ctrl As Microsoft.Office.Core.CommandBarButton, _
                    ByRef CancelDefault As Boolean) Handles myControl.Click

    System.Windows.Forms.MessageBox.Show("My Menu Item clicked")

End Sub

[C#]

private Word.Application myApplication;
private Office.CommandBarButton myControl;

private void ThisAddIn_Startup(object sender, System.EventArgs e)
{
    myApplication = this.Application;
    AddMenuItem(); 
}
 
private void AddMenuItem()
{
   Office.MsoControlType menuItem = 
        Office.MsoControlType.msoControlButton;

    myControl = 
        (Office.CommandBarButton)myApplication.CommandBars["Text"].Controls.Add
        (menuItem,missing, missing, 1, true);

    myControl.Style = Office.MsoButtonStyle.msoButtonCaption;
    myControl.Caption = "My Menu Item";
    myControl.Tag = "MyMenuItem";

    myControl.Click += 
        new Microsoft.Office.Core._CommandBarButtonEvents_ClickEventHandler
            (myControl_Click);

}

void myControl_Click(Microsoft.Office.Core.CommandBarButton Ctrl, 
    ref bool CancelDefault)
{
    System.Windows.Forms.MessageBox.Show("My Menu Item clicked");
}

Why my code does not work as expected

Here is one issue I can see right off the bat. Note this line of code for adding a control:

myControl = CType(MyApplication.CommandBars("Text").Controls.Add _ (menuItem, 1, True), Office.CommandBarButton)

I set the last parameter of the Add method to True.  This value specifies that I want my control to be temporary. I am trying to tell Word not to save the control so that duplicate menu items won’t be added every time Word opens.  Only there is a problem here.  Word ignores this parameter (at least for controls in a CommandBar collection anyway). So I can keep it set to true, but it really means false. Lovely.

So what is happening? Well, Word is actually saving your new menu command to the Normal.dot template every time a new instance of Word opens – hence the duplicates.

What can I do about this?

There are probably a billion creative ways to stop the duplicate menus from appearing, but here are three tips that work really well. Here they are:

  • Check for duplicates before adding an item (control) to a menu.
  • Always set the customization context of the application to the same document or template before adding or deleting a control.
  • Because there are no temporary commands in Word, use a custom template to save the commands.

Tip # 1: Check for Duplicates:

This one is pretty easy.  Just add code to your add-in that looks for a control that has the same tag as the control you are about to add.  If one exists, perform one of the following actions:

  • Don’t add the control (It is already there).
  • Delete the control. Then you can add it.

I modified my code to delete the control. Here is my code.

[Visual Basic]

Private Sub RemoveExistingMenuItem()

    Dim contextMenu As Office.CommandBar = _
    MyApplication.CommandBars("Text")

    MyApplication.CustomizationContext = customTemplate

    Dim control As Office.CommandBarButton = contextMenu.FindControl _
        (Office.MsoControlType.msoControlButton, System.Type.Missing, _
         "MyMenuItem", True, True)

    If Not (control Is Nothing) Then
        control.Delete(True)
    End If

End Sub

[C#]

private void RemoveExistingMenuItem()
{
    Office.CommandBar contextMenu = myApplication.CommandBars["Text"];
    myApplication.CustomizationContext = customTemplate;

    Office.CommandBarButton control = 
        (Office.CommandBarButton)contextMenu.FindControl
        (Office.MsoControlType.msoControlButton, missing,
        "MyMenuItem", true, true);

    if ((control != null))
    {
        control.Delete(true);
    }

}

Tip #2: Set the Customization Context

The customization context of the application tells Word where to save your customizations. To specify the customization context, set the CustomizationContext property of the Application object.

By default, Word uses the Normal.dot template as it’s customization context.  This is not reliable and can change. If you do not explicitly set the context, you might search for controls saved to one context such as a document, delete controls from another context such as a custom template and then add the control to another context such as Normal.dot.

To avoid these issues, always set the customization context of the application to the same document or template every time you search for, delete, or add controls to a menu.

Note that in a Word document-level customization, it is probably best to set the customization context to the active document.  That way when the user uninstalls the customization, the document and the menu commands that pertain to that document disappear as expected.

In Word application-level add-in, the best practice is to use a custom template for reasons mentioned later on in this post.

In the following example, I highlighted in bold the line that sets the customization context.  Further along in this post, I will show you where I got customTemplate.

[Visual Basic]

Private Sub AddMenuItem()

    MyApplication.CustomizationContext = customTemplate

    Dim menuItem As Office.MsoControlType = _
        Office.MsoControlType.msoControlButton

    myControl = CType(MyApplication.CommandBars("Text").Controls.Add _
       (menuItem, 1, True), Office.CommandBarButton)

    myControl.Style = Office.MsoButtonStyle.msoButtonCaption
    myControl.Caption = "My Menu Item"
    myControl.Tag = "MyMenuItem"
    customTemplate.Saved = True
    GC.Collect()

End Sub

[C#]

private void AddMenuItem()
{
    myApplication.CustomizationContext = customTemplate;
    Office.MsoControlType menuItem = 
        Office.MsoControlType.msoControlButton;

    myControl = 
        (Office.CommandBarButton)myApplication.CommandBars["Text"].Controls.Add
        (menuItem,missing, missing, 1, true);

    myControl.Style = Office.MsoButtonStyle.msoButtonCaption;
    myControl.Caption = "My Menu Item";
    myControl.Tag = "MyMenuItem";

    myControl.Click += 
        new Microsoft.Office.Core._CommandBarButtonEvents_ClickEventHandler
            (myControl_Click);

    customTemplate.Saved = true;
    GC.Collect();

}

Also – a quick tip – set the Saved property of the template to true after you add the control.  This stops that annoying prompt from appearing that asks if you would like to save your customizations to the template.

Tip #3: Use a Custom Template

It is very difficult to delete a control before the add-in shuts down.  For example, if you attempt to delete a control in the ThisAddIn_Shutdown event handler, you will receive a not so helpful COM exception.  You will get similar results in the Quit event of Word. 

That is because the template that you are using as your application’s customization context is not writable in either of those event handlers.  So if you cannot easily delete the control when Word closes, that means that the control will always live inside of the template.

This is a problem if you are using Normal.dot to persist the controls.  Here is why.  Let’s say the user decides that he does not want to see your command in a menu anymore.  With a caption such as “My Menu Item”, can you really blame him? So the user uninstalls your add-in. However, the menu command still lives in Normal.dot! When that user opens his document in Word, "My Menu Item” still appears. Doooh! Here comes the support calls!

The way around this is to provide your own custom template to store customizations such as custom menus and menu items.  Your setup application can remove the template along with add-in. That way when the user uninstalls the add-in, they also remove the template that contains the menu items.

In the following example, I retrieve a custom template from the users documents folder. Yes, your setup application will probably use a different location to place the custom template, but this is just for an example.

[Visual Basic]

Private Sub GetCustomTemplate()
    Dim TemplatePath As String = Environment.GetFolderPath _
        (Environment.SpecialFolder.MyDocuments) + "\MyCustomTemplate.dotx"
    Dim install As Boolean = True

    For Each installedTemplate As Word.Template In MyApplication.Templates
        If installedTemplate.FullName = DirectCast(TemplatePath, String) Then
            install = False
        End If
    Next

    If install = True Then
        MyApplication.AddIns.Add(TemplatePath.ToString(), True)
    End If

    customTemplate = MyApplication.Templates(TemplatePath)

End Sub

[C#]

private void GetCustomTemplate()
{
    object TemplatePath = Environment.GetFolderPath
        (Environment.SpecialFolder.MyDocuments) +
        "\\MyCustomTemplate.dotx";
    object install = true;
    
    foreach (Word.Template installedTemplate in myApplication.Templates)
    {
        if (installedTemplate.FullName == (string)TemplatePath)
        {
            install = false;
        }
    }
    if ((bool)install)
    {
        myApplication.AddIns.Add(TemplatePath.ToString(), ref install);
    }
    customTemplate = myApplication.Templates.get_Item(ref TemplatePath);

}

Drum roll please .. I present the complete example

To provide context, here is the complete sample:

[Visual Basic]

Public Class ThisAddIn

    Private MyApplication As Word.Application
    Private WithEvents myControl As Office.CommandBarButton
    Private customTemplate As Word.Template

    Private Sub ThisAddIn_Startup _
    (ByVal sender As Object, ByVal e As System.EventArgs) Handles Me.Startup

        MyApplication = Me.Application

        GetCustomTemplate()
        RemoveExistingMenuItem()
        AddMenuItem()

    End Sub

    Private Sub GetCustomTemplate()
        Dim TemplatePath As String = Environment.GetFolderPath _
            (Environment.SpecialFolder.MyDocuments) + "\MyCustomTemplate.dotx"
        Dim install As Boolean = True

        For Each installedTemplate As Word.Template In MyApplication.Templates
            If installedTemplate.FullName = DirectCast(TemplatePath, String) Then
                install = False
            End If
        Next

        If install = True Then
            MyApplication.AddIns.Add(TemplatePath.ToString(), True)
        End If

        customTemplate = MyApplication.Templates(TemplatePath)

    End Sub

    Private Sub RemoveExistingMenuItem()

        Dim contextMenu As Office.CommandBar = _
        MyApplication.CommandBars("Text")

        MyApplication.CustomizationContext = customTemplate

        Dim control As Office.CommandBarButton = contextMenu.FindControl _
            (Office.MsoControlType.msoControlButton, System.Type.Missing, _
             "MyMenuItem", True, True)

        If Not (control Is Nothing) Then
            control.Delete(True)
        End If

    End Sub


    Private Sub AddMenuItem()

        MyApplication.CustomizationContext = customTemplate

        Dim menuItem As Office.MsoControlType = _
            Office.MsoControlType.msoControlButton

        myControl = CType(MyApplication.CommandBars("Text").Controls.Add _
           (menuItem, 1, True), Office.CommandBarButton)

        myControl.Style = Office.MsoButtonStyle.msoButtonCaption
        myControl.Caption = "My Menu Item"
        myControl.Tag = "MyMenuItem"
        customTemplate.Saved = True

        GC.Collect()

    End Sub

    Sub myControl_Click(ByVal Ctrl As Microsoft.Office.Core.CommandBarButton, _
                        ByRef CancelDefault As Boolean) Handles myControl.Click

        System.Windows.Forms.MessageBox.Show("My Menu Item clicked")

    End Sub

    Private Sub ThisAddIn_Shutdown() Handles Me.Shutdown

    End Sub

End Class

[C#]

public partial class ThisAddIn
{
    private Word.Application myApplication;
    private Office.CommandBarButton myControl;
    private Word.Template customTemplate;

    private void ThisAddIn_Startup(object sender, System.EventArgs e)
    {
        myApplication = this.Application;

        GetCustomTemplate();
        RemoveExistingMenuItem();
        AddMenuItem();
    }

    private void GetCustomTemplate()
    {
        object TemplatePath = Environment.GetFolderPath
            (Environment.SpecialFolder.MyDocuments) +
            "\\MyCustomTemplate.dotx";
        object install = true;
        foreach (Word.Template installedTemplate in myApplication.Templates)
        {
            if (installedTemplate.FullName == (string)TemplatePath)
            {
                install = false;
            }
        }
        if ((bool)install)
        {
            myApplication.AddIns.Add(TemplatePath.ToString(), ref install);
        }
        customTemplate = myApplication.Templates.get_Item(ref TemplatePath);

    }

    private void RemoveExistingMenuItem()
    {
        Office.CommandBar contextMenu = myApplication.CommandBars["Text"];
        myApplication.CustomizationContext = customTemplate;

        Office.CommandBarButton control =
            (Office.CommandBarButton)contextMenu.FindControl
            (Office.MsoControlType.msoControlButton, missing,
            "MyMenuItem", true, true);

        if ((control != null))
        {
            control.Delete(true);
        }

    }

    private void AddMenuItem()
    {
        myApplication.CustomizationContext = customTemplate;
        Office.MsoControlType menuItem =
            Office.MsoControlType.msoControlButton;

        myControl =
            (Office.CommandBarButton)myApplication.CommandBars["Text"].Controls.Add
            (menuItem, missing, missing, 1, true);

        myControl.Style = Office.MsoButtonStyle.msoButtonCaption;
        myControl.Caption = "My Menu Item";
        myControl.Tag = "MyMenuItem";

        myControl.Click +=
            new Microsoft.Office.Core._CommandBarButtonEvents_ClickEventHandler
                (myControl_Click);

        customTemplate.Saved = true;

        GC.Collect();

    }

    void myControl_Click(Microsoft.Office.Core.CommandBarButton Ctrl,
        ref bool CancelDefault)
    {
        System.Windows.Forms.MessageBox.Show("My Menu Item clicked");
    }
    private void ThisAddIn_Shutdown(object sender, System.EventArgs e)
    {
    }
}

Now you can issue a patch to your buddy so that he has only one “My Menu Item” appearing in his shortcut menu.  Although .. I am not sure what “My Menu Item” is really suppose to do …