Logo
HOWTO: Create a command with a custom picture using a managed satellite DLL for a XML-based Visual Studio add-in.

Author: Carlos J. Quintero (Microsoft MVP) Applies to: Microsoft Visual Studio 2005
Date: March 2012   Microsoft Visual Studio 2008
Updated: September 2012   Microsoft Visual Studio 2010
      Microsoft Visual Studio 2012
Introduction

This article explains how to create a managed (VB.NET/C#) satellite dll to provide custom pictures for commands created by XML-based add-ins, which were introduced by Visual Studio 2005 and use an .AddIn file to be recognized in the Add-In Manager of Visual Studio. In previous Visual Studio versions (2002/2003) add-ins were COM-based and the satellite dll had to be native (C++, not .NET).

More information

The following sample shows the code of an add-in that creates a command with a custom picture from a satellite managed dll and adds a button to the "Standard" commandbar of Visual Studio. The steps are the following:

  • Create an add-in using VB.NET/C# with the name "MyAddin".
  • In the AssemblyInfo file:
    • Set or add the AssemblyVersion attribute value to a fixed value (for example "1.0.0.0") and don't change this value on each build.
    • Set or add the AssemblyFileVersion attribute value to any value (for example "1.0.0.100"). You can increment this value on each build to distinguish versions.

      Notice that assembly versions (which is a .NET concept) should not be changed on each build since they are part of the identity of the assembly (along with the name, culture, public key token and platform); only the AssemblyFileVersion attribute (which is a Windows concept) should change for the filesystem, setups, etc. to distinguish versions of files with the same name.
  • Use the following code in the Connect file:
Language: VB.NET   Copy Code Copy Code (IE only)
Imports System
Imports Microsoft.VisualStudio.CommandBars
Imports Extensibility
Imports EnvDTE
Imports EnvDTE80

Public Class Connect
   Implements Extensibility.IDTExtensibility2
   Implements IDTCommandTarget

   Private Const MY_COMMAND_NAME As String = "MyCommand"
   Private Const MY_COMMAND_CAPTION As String = "My command"
   Private Const MY_COMMAND_TOOLTIP As String = "My command tooltip"

   Private applicationObject As EnvDTE.DTE
   Private addInInstance As EnvDTE.AddIn
   Private WithEvents appDomain As AppDomain

   Private myStandardCommandBarButton As CommandBarButton

   Public Sub OnConnection(ByVal application As Object, ByVal connectMode As Extensibility.ext_ConnectMode, _
                           ByVal addInInst As Object, ByRef custom As System.Array) _
      Implements Extensibility.IDTExtensibility2.OnConnection

      Try

         applicationObject = CType(application, EnvDTE.DTE)
         addInInstance = CType(addInInst, EnvDTE.AddIn)

         Select Case connectMode

            Case ext_ConnectMode.ext_cm_UISetup

               ' Do nothing for this add-in with temporary user interface

            Case ext_ConnectMode.ext_cm_Startup

               ' The add-in was marked to load on startup
               ' Do nothing at this point because the IDE may not be fully initialized
               ' Visual Studio will call OnStartupComplete when fully initialized

            Case ext_ConnectMode.ext_cm_AfterStartup

               ' The add-in was loaded by hand after startup using the Add-In Manager
               ' Initialize it in the same way that when is loaded on startup
               AddTemporaryUI()

         End Select

      Catch ex As System.Exception
         System.Windows.Forms.MessageBox.Show(ex.ToString)
      End Try

   End Sub

   Public Sub OnStartupComplete(ByRef custom As System.Array) Implements Extensibility.IDTExtensibility2.OnStartupComplete

      AddTemporaryUI()

   End Sub

   Public Sub AddTemporaryUI()

      Const VS_STANDARD_COMMANDBAR_NAME As String = "Standard"
      Const MY_IMAGE_ID As Integer = 101

      Dim myCommand As Command = Nothing
      Dim standardCommandBar As CommandBar
      Dim commandBars As CommandBars

      Try

         ' Set the AppDomain to receive events when Visual Studio needs to resolve the satellite dll
         ' for cultures other than "en-US"
         appDomain = System.AppDomain.CurrentDomain

         ' Try to get the command if already exists
         Try
            myCommand = applicationObject.Commands.Item(addInInstance.ProgID & "." & MY_COMMAND_NAME)
         Catch
         End Try

         ' Add the command if it does not exist
         If myCommand Is Nothing Then

            myCommand = applicationObject.Commands.AddNamedCommand(addInInstance, MY_COMMAND_NAME, _
               MY_COMMAND_CAPTION, MY_COMMAND_TOOLTIP, False, MY_IMAGE_ID, Nothing, _
               vsCommandStatus.vsCommandStatusSupported Or vsCommandStatus.vsCommandStatusEnabled)

         End If

         ' Get the "Standard" commmandbar
         commandBars = DirectCast(applicationObject.CommandBars, CommandBars)
         standardCommandBar = commandBars.Item(VS_STANDARD_COMMANDBAR_NAME)

         ' Add a button to the commandbar
         myStandardCommandBarButton = DirectCast(myCommand.AddControl(standardCommandBar, _
            standardCommandBar.Controls.Count + 1), CommandBarButton)

         ' Set the properties of the button
         myStandardCommandBarButton.Caption = MY_COMMAND_CAPTION
         myStandardCommandBarButton.Style = MsoButtonStyle.msoButtonIcon
         myStandardCommandBarButton.BeginGroup = True

      Catch ex As System.Exception
         System.Windows.Forms.MessageBox.Show(ex.ToString)
      End Try

   End Sub

   Public Sub OnDisconnection(ByVal RemoveMode As Extensibility.ext_DisconnectMode, ByRef custom As System.Array) _
      Implements Extensibility.IDTExtensibility2.OnDisconnection

      Try

         Select Case RemoveMode

            Case ext_DisconnectMode.ext_dm_HostShutdown, ext_DisconnectMode.ext_dm_UserClosed

               If Not (myStandardCommandBarButton Is Nothing) Then
                  myStandardCommandBarButton.Delete()
               End If

               appDomain = Nothing

         End Select

      Catch ex As System.Exception
         System.Windows.Forms.MessageBox.Show(ex.ToString)
      End Try

   End Sub

   Public Sub OnBeginShutdown(ByRef custom As System.Array) _
      Implements Extensibility.IDTExtensibility2.OnBeginShutdown
   End Sub

   Public Sub OnAddInsUpdate(ByRef custom As System.Array) _
       Implements Extensibility.IDTExtensibility2.OnAddInsUpdate
   End Sub

   Public Sub Exec(ByVal cmdName As String, ByVal executeOption As vsCommandExecOption, _
      ByRef varIn As Object, ByRef varOut As Object, ByRef handled As Boolean) _
      Implements IDTCommandTarget.Exec

      handled = False

      If (executeOption = vsCommandExecOption.vsCommandExecOptionDoDefault) Then

         If cmdName = addInInstance.ProgID & "." & MY_COMMAND_NAME Then
            handled = True
            System.Windows.Forms.MessageBox.Show("Command executed.")
         End If

      End If

   End Sub

   Public Sub QueryStatus(ByVal cmdName As String, ByVal neededText As vsCommandStatusTextWanted, _
      ByRef statusOption As vsCommandStatus, ByRef commandText As Object) _
      Implements IDTCommandTarget.QueryStatus

      If neededText = vsCommandStatusTextWanted.vsCommandStatusTextWantedNone Then

         If cmdName = addInInstance.ProgID & "." & MY_COMMAND_NAME Then
            statusOption = CType(vsCommandStatus.vsCommandStatusEnabled + _
               vsCommandStatus.vsCommandStatusSupported, vsCommandStatus)
         Else
            statusOption = vsCommandStatus.vsCommandStatusUnsupported
         End If

      End If

   End Sub

   Private Function appDomain_AssemblyResolve(ByVal sender As Object, ByVal args As System.ResolveEventArgs) As System.Reflection.Assembly _
      Handles AppDomain.AssemblyResolve

      Dim addinAssembly As System.Reflection.Assembly
      Dim satelliteAssemblyName As String
      Dim resolvedAssembly As System.Reflection.Assembly = Nothing
      Dim satelliteAssemblyCulture As System.Globalization.CultureInfo

      Try

         ' Note: if Visual Studio is localized (for example, in Spanish), it will try to locate the resources dll with the culture "es-ES".
         ' To avoid shipping a resource dll for each culture, here we return the resource dll for the "en-US" culture

         ' Get the assembly of the add-in
         addinAssembly = System.Reflection.Assembly.GetExecutingAssembly()

         ' Compose the name of the resource dll
         satelliteAssemblyName = addinAssembly.GetName().Name & ".resources"

         ' If the searched assembly has the pattern "MyAddin.resources, ..." then return the resource dll for the "en-US" culture
         If args.Name.StartsWith(satelliteAssemblyName & ", ", StringComparison.OrdinalIgnoreCase) Then

            satelliteAssemblyCulture = System.Globalization.CultureInfo.GetCultureInfo("en-US")
            resolvedAssembly = addinAssembly.GetSatelliteAssembly(satelliteAssemblyCulture)

         End If

      Catch ex As System.Exception
         System.Windows.Forms.MessageBox.Show(ex.ToString)
      End Try

      Return resolvedAssembly

   End Function

End Class
Language: C#   Copy Code Copy Code (IE only)
using System;
using Extensibility;
using EnvDTE;
using EnvDTE80;
using Microsoft.VisualStudio.CommandBars;
using System.Resources;
using System.Reflection;
using System.Globalization;
using System.Windows.Forms;

namespace MyAddin
{
   public class Connect : Extensibility.IDTExtensibility2, IDTCommandTarget
   {
      private const string MY_COMMAND_NAME = "MyCommand";
      private const string MY_COMMAND_CAPTION = "My command";
      private const string MY_COMMAND_TOOLTIP = "My command tooltip";

      private EnvDTE.DTE applicationObject;
      private EnvDTE.AddIn addInInstance;
      private System.AppDomain appDomain;

      private CommandBarButton myStandardCommandBarButton;

      public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode,
         object addInInst, ref System.Array custom)
      {
         try
         {
            applicationObject = (EnvDTE.DTE)application;
            addInInstance = (EnvDTE.AddIn)addInInst;

            switch (connectMode)
            {
               case ext_ConnectMode.ext_cm_UISetup:

                  // Do nothing for this add-in with temporary user interface
                  break;

               case ext_ConnectMode.ext_cm_Startup:

                  // The add-in was marked to load on startup
                  // Do nothing at this point because the IDE may not be fully initialized
                  // Visual Studio will call OnStartupComplete when fully initialized
                  break;

               case ext_ConnectMode.ext_cm_AfterStartup:

                  // The add-in was loaded by hand after startup using the Add-In Manager
                  // Initialize it in the same way that when is loaded on startup
                  AddTemporaryUI();
                  break;
            }
         }
         catch (System.Exception ex)
         {
            System.Windows.Forms.MessageBox.Show(ex.ToString());
         }
      }

      public void OnStartupComplete(ref System.Array custom)
      {
         AddTemporaryUI();
      }

      public void AddTemporaryUI()
      {
         const string VS_STANDARD_COMMANDBAR_NAME = "Standard";
         const int MY_IMAGE_ID = 101;

         Command myCommand = null;
         CommandBar standardCommandBar = null;
         CommandBars commandBars = null;
         object[] contextUIGuids = new object[] { };
        
         try
         {
        
            // Set the AppDomain to receive events when Visual Studio needs to resolve the satellite dll
            // for cultures other than "en-US"
            appDomain = System.AppDomain.CurrentDomain;
            appDomain.AssemblyResolve+=new ResolveEventHandler(appDomain_AssemblyResolve);
            
            // Try to get the command if already exists 
            try
            {
               myCommand = applicationObject.Commands.Item(addInInstance.ProgID + "." + MY_COMMAND_NAME, -1);
            }
            catch
            {
            }

            // Add the command if it does not exist
            if (myCommand == null)
            {
               myCommand = applicationObject.Commands.AddNamedCommand(addInInstance,
                  MY_COMMAND_NAME, MY_COMMAND_CAPTION, MY_COMMAND_TOOLTIP, false, MY_IMAGE_ID, ref contextUIGuids,
                  (int)(vsCommandStatus.vsCommandStatusSupported | vsCommandStatus.vsCommandStatusEnabled));
            }

            // Get the "Standard" commandbar
            commandBars = (CommandBars)applicationObject.CommandBars;
            standardCommandBar = commandBars[VS_STANDARD_COMMANDBAR_NAME];

            // Add a button to the commandbar
            myStandardCommandBarButton = (CommandBarButton)myCommand.AddControl(standardCommandBar,
               standardCommandBar.Controls.Count + 1);

            // Set the properties of the button
            myStandardCommandBarButton.Caption = MY_COMMAND_CAPTION;
            myStandardCommandBarButton.Style = MsoButtonStyle.msoButtonIcon;
            myStandardCommandBarButton.BeginGroup = true;
         }
         catch (System.Exception ex)
         {
            System.Windows.Forms.MessageBox.Show(ex.ToString());
         }
      }

      public void OnDisconnection(Extensibility.ext_DisconnectMode RemoveMode, ref System.Array custom)
      {
         try
         {
            switch (RemoveMode)
            {
               case ext_DisconnectMode.ext_dm_HostShutdown:
               case ext_DisconnectMode.ext_dm_UserClosed:

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

                  appDomain.AssemblyResolve -= new ResolveEventHandler(appDomain_AssemblyResolve);
           
                  break;
            }
         }
         catch (System.Exception ex)
         {
            System.Windows.Forms.MessageBox.Show(ex.ToString());
         }
      }

      public void OnBeginShutdown(ref System.Array custom)
      {
      }

      public void OnAddInsUpdate(ref System.Array custom)
      {
      }

      public void Exec(string cmdName, vsCommandExecOption executeOption, ref object varIn,
         ref object varOut, ref bool handled)
      {

         handled = false;

         if ((executeOption == vsCommandExecOption.vsCommandExecOptionDoDefault))
         {
            if (cmdName == addInInstance.ProgID + "." + MY_COMMAND_NAME)
            {
               handled = true;
               System.Windows.Forms.MessageBox.Show("Command executed.");
            }
         }
      }

      public void QueryStatus(string cmdName, vsCommandStatusTextWanted neededText,
         ref vsCommandStatus statusOption, ref object commandText)
      {
         if (neededText == vsCommandStatusTextWanted.vsCommandStatusTextWantedNone)
         {
            if (cmdName == addInInstance.ProgID + "." + MY_COMMAND_NAME)
            {
               statusOption = (vsCommandStatus)(vsCommandStatus.vsCommandStatusEnabled |
                  vsCommandStatus.vsCommandStatusSupported);
            }
            else
            {
               statusOption = vsCommandStatus.vsCommandStatusUnsupported;
            }
         }
      }

      private System.Reflection.Assembly appDomain_AssemblyResolve(Object sender, System.ResolveEventArgs args)
      {
         System.Reflection.Assembly addinAssembly;
         String satelliteAssemblyName;
         System.Reflection.Assembly resolvedAssembly = null;
         System.Globalization.CultureInfo satelliteAssemblyCulture;

         try
         {
            // Note: if Visual Studio is localized (for example, in Spanish), it will try to locate the resources dll with the culture "es-ES".
            // To avoid shipping a resource dll for each culture, here we return the resource dll for the "en-US" culture

            // Get the assembly of the add-in
            addinAssembly = System.Reflection.Assembly.GetExecutingAssembly();

            // Compose the name of the resource dll
            satelliteAssemblyName = addinAssembly.GetName().Name + ".resources";

            // If the searched assembly has the pattern "MyAddin.resources, ..." then return the resource dll for the "en-US" culture
            if (args.Name.StartsWith(satelliteAssemblyName + ", ", StringComparison.OrdinalIgnoreCase))
            {
               satelliteAssemblyCulture = System.Globalization.CultureInfo.GetCultureInfo("en-US");
               resolvedAssembly = addinAssembly.GetSatelliteAssembly(satelliteAssemblyCulture);
            }
         }
         catch (Exception ex)
         {
            System.Windows.Forms.MessageBox.Show(ex.ToString());
         }
         return resolvedAssembly;
      }

   }
}
  • Add a new VB.NET/C# Class Library project named "MyAddinResources" to the solution.
  • Remove the Class1 file.
  • In the AssemblyInfo file:
    • Set or add the AssemblyVersion attribute value to the same fixed value used in the AssemblyInfo file of the add-in project ("1.0.0.0" in our example). Do not change this value on each build.
    • Set or add the AssemblyFileVersion attribute value to any value (for example "1.0.0.200"). You can increment this value on each build to distinguish versions.
  • In the AssemblyInfo file, set or add the AssemblyCulture attribute with the value "en-US".
  • If the add-in assembly is signed with a strong name, sign also the resource dll using the same .snk file.
  • In the properties window of the project:
    • Go to the Application tab and change the Assembly Name to <MyAddinName>.resources, that is, the name of the add-in assembly with the .resources suffix so that the assembly file will be named <MyAddinName>.resources.dll when built. In our example, change the value ""MyAddinResources" to "MyAddin.resources".
    • Go to the Build tab (C# projects) or "Compile" tab (VB.NET projects) and change the output path to a subfolder named "en-US" in the output folder of the add-in assembly. You can use a relative path such as "..\MyAddin\bin\en-US". Build the project and using Windows Explorer check that the managed satellite dll is created in the correct "en-US" subfolder of the add-in output folder.
  • Open the Resources.resx file:
    • For a VB.NET Class Library project, click the "Show All Files" button on the Solution Explorer toolbar, open the "My Project" folder and open the Resources.resx file.
    • For a C# Class Library project, double click the "Properties" folder, go to the "Resources" tab, and click the link "The project does not contain a default resources file. Click here to create one".
  • Click the "Add Resource", "New Image", "BMP Image..." button on the toolbar of the Resources.resx window.
  • Enter a name for the image, for example "MyImage".
  • Make active the image and in the Properties window:
    • Change the dimensions of the bitmap setting the Height and Width properties to 16.
    • Change the Colors property to 24 bit.
  • Draw or paste an image for your bitmap.
  • To use transparency you need to use a special color:
    • Select a color that you don't use in the Colors window.
    • Click the "Image", "Adjust Colors…" menu.
    • Enter the values Red=0, Green=254 (not 255!) and Blue=0 (almost pure green). You can use now this color to fill the transparent areas.
  • In Visual Studio 2012, if you want to exclude the picture from being color-inverted when using the Dark theme, set the pixel of the top-right corner to the Cyan color (Red=0, Green=255, Blue=255).
  • Select the Resources.resx file in the Solution Explorer and open it.
  • Select the image and in the Properties window change the "(Name)" property from "MyImage" to 101, which is the value of the MY_IMAGE_ID constant used when calling the AddNamedCommand method in the code of the add-in.
  • To remove the "The resource name 'n' is not a valid identifier" warning that appears in the Error List window when using a number as name for the "(Name)" property of the previous step, select the Resources.resx file in the Solution Explorer window, and in the Properties window:
    • Clear the "Custom Tool " property.
    • Clear the "Custom Tool Namespace" property.
  • Save all changes.
  • Close the windows.
  • Rebuild the solution.
  • Run the solution, which will open a second Visual Studio instance.
  • Load the add-in.
  • If the custom bitmap doesn't appear in the button of the add-in, it means that the resource dll is not being located in the correct "en-US" folder or that its assembly name, version or public key token values don't match the ones of the add-in assembly. You can put a breakpoint in the event handler of the AssemblyResolve event of the AppDomain or use the Assembly Binding Log Viewer tool of the .NET Framework SDK to diagnose such problems.

Notes

  • Visual Studio will search the satellite dll for the add-in with the culture of the language selected in the "Tools", "Options" window, "Environment", "International Settings" section. For the English language the culture is "en-US" and Visual Studio would find the satellite dll just fine, but for Spanish it would be "es-ES", for French it would be "fr-FR", etc. To avoid the need to create folders with those culture names and one different satellite dll for each culture, the add-in uses the AppDomain.AssemblyResolve event to return the satellite dll with the "en-US" culture in all cases.
  • If you need to reset your commands so they are created again, see HOWTO: Reset a Visual Studio add-in.

Related articles


Go back to the 'Resources for Visual Studio .NET extensibility' section for more articles like this


Top