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.
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"); }
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.
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:
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:
I modified my code to delete the control. Here is my code.
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 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); } }
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.
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
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.
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.
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 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); }
To provide context, here is the complete sample:
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
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) { } }