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

Rate This
  • Comments 14

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 …
Leave a Comment
  • Please add 6 and 6 and type the answer here:
  • Post
  • PingBack from http://www.anith.com/?p=16238

  • Thanks for your article.

    This might be a stupid question, but how do I create a blank word template without the addin on it?

    I assume I have to

    1) Create a blank word template without the addin on it, save it as part of the addin project

    2) in ThisAddIn_Startup, copy the blank template created in step 1 to a roaming user's documents (so each user has a different template?)

    3) The addin will use the template exist in user's documents

    4) on ThisAddIn_ShutDown, delete the user template - so each time the user starting word will be using a new template?

    Is this sound about right?

    thanks

  • I tried this example as is and it works. But I am running into some problems and I do not know how to resolve. I need a drop down menu in Word with several items in it. So I changed

    From

    Office.MsoControlType menuItem =

                   Office.MsoControlType.msoControlButton;

    To

    Office.MsoControlType menuItem =

                   Office.MsoControlType.msoControlPopup;

    Now when I exit Word, it always prompts if I want to save the custom template. I tried explicitly calling the Save method and or setting the Saved property to false. None of these resolves the issue! How do I stop this?

  • Last month I posted this article that described how to prevent your add-in from creating duplicate menu

  • Dear Poster,

    maybe it is something very simple problem, but I cannot solve it. I created a template file to the given path.

    On word 2003 startup this addin crashes with the following error message:

    ************** Exception Text **************

    System.Runtime.InteropServices.COMException (0x800A1735): Given Items does not exists

      at Microsoft.Office.Interop.Word.AddIns.get_Item(Object& Index)

      at WordAddIn_sample.ThisAddIn.GetCustomTemplate() in C:\Users\Tom\Documents\Visual Studio 2008\Projects\WordAddIn_sample\WordAddIn_sample\ThisAddIn.cs:line 32

      at WordAddIn_sample.ThisAddIn.ThisAddIn_Startup(Object sender, EventArgs e) in C:\Users\Tom\Documents\Visual Studio 2008\Projects\WordAddIn_sample\WordAddIn_sample\ThisAddIn.cs:line 21

      at Microsoft.Office.Tools.AddIn.OnStartup()

      at WordAddIn_sample.ThisAddIn.FinishInitialization() in C:\Users\Tom\Documents\Visual Studio 2008\Projects\WordAddIn_sample\WordAddIn_sample\ThisAddIn.Designer.cs:line 65

      at Microsoft.VisualStudio.Tools.Applications.Runtime.AppDomainManagerInternal.ExecutePhase(String methodName)

      at Microsoft.VisualStudio.Tools.Applications.Runtime.AppDomainManagerInternal.ExecuteCustomizationStartupCode()

      at Microsoft.VisualStudio.Tools.Applications.Runtime.AppDomainManagerInternal.ExecuteEntryPointsHelper()

      at Microsoft.VisualStudio.Tools.Applications.Runtime.AppDomainManagerInternal.Microsoft.VisualStudio.Tools.Applications.Runtime.IExecuteCustomization2.ExecuteEntryPoints()

    ************** Loaded Assemblies **************

    Could You help me with this?

    Thanks,

    Tom

  • Hi Tom,

    Did you install the template.  The sample in those post checks to see that a template is installed at the path before retreiving it.  Here is the snippet:

       object TemplatePath = Environment.GetFolderPath

           (Environment.SpecialFolder.MyDocuments) +

           "\\MyCustomTemplate.dot";

       object install = true;

       if (myApplication.AddIns.get_Item(ref TemplatePath).Installed != true)

       {

           myApplication.AddIns.Add(TemplatePath.ToString(), ref install);

       }

       customTemplate = myApplication.Templates.get_Item(ref TemplatePath);

    }

  • Yes, I copied the .dot file to the given path. It seems the method get_Item returns null value. I'm using VS2008 and a Word 2003 addin project type. The problem is in the following line:

    if (myApplication.AddIns.get_Item(ref TemplatePath).Installed != true)

    The TemplatePath variable contains this value: "C:\\Users\\Tom\\Documents\\bio.dot"

  • On tip #2, I believe it would be a good practice to check for user's pending changes before setting Saved = True.

    In my code, right before creating the toolbar and the buttons, I check the Saved state:

    'Check for user's pending changes

    Dim blnPendingChanges As Boolean = Not WDoc.Saved

    'WDoc is the active document and the CustomizationContext

    'Here goes the code to create the toolbar and buttons

    'And in the end...

    If blnPendingChanges = False Then

         WDoc.Saved = True

    End If

  • Tom, did you get an answer?  I'm getting the same problem with the line

    if (!this.App.AddIns.get_Item(ref templatePath).Installed)

    can anyone help.

  • Utilisez seulement cette ligne dans la méthode du shut down de l'AddIn Word et éliminez tout le code qui concerne le Template

    ((Microsoft.Office.Interop.Word.Template)word.CustomizationContext).Saved = true;

  • la variable "word" c'est l'application Word.Application

  • I ran into the exception thrown by get_Item.  I had to use a foreach to iterate through the list of addins checking the path against the path of my custom template.  Once I found the one I wanted (or I didn't) I can then call install correctly.

  • Updated the post to address the the exception caused by the following line of code: "(!this.App.AddIns.get_Item(ref templatePath).Installed)". I also added an explicit call to the garbage collector to avoid duplicate calls to a controls event handler. I could not repro the issue relating to the save prompts.

  • Thanks for post the article. Its very useful for every one.

Page 1 of 1 (14 items)

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