MZ-Tools Articles Series: HOWTO: Get a CommandBar by Guid and Id rather than by name from a Visual Studio add-in

Getting a Visual Studio built-in commandbar to add buttons to it is not so easy as it may seem. You have the DTE.CommandBars collection, but:

– To retrieve a commandbar by name, you have to guess its name first. See: HOWTO: Guessing the name of a command bar to add a custom menu entry in Visual Studio .NET add-ins

– If the commandbar is not a “root” commandbar, but a commandbar popup, it may not be indexed in that collection, so it is better to get it from the parent CommandBar.Controls collection, but it is not so easy. See: HOWTO: Locate commandbars in international versions of Visual Studio

– Some command bar names are dupplicated.

There is other way to get a commandbar, using its Guid and Id that are truly unique. But this has also some tricks as explained in my latest article:

HOWTO: Get a CommandBar by Guid and Id rather than by name from a Visual Studio add-in
http://www.mztools.com/articles/2011/MZ2011004.aspx

MZ-Tools Articles Series: PRB: COMException 0x80020003 getting events from commandbar popup in Visual Studio 2010

In the old days of VB5/VB6 extensibility which used the Office commandbars model where there weren’t commands (only CommandBarControls) you had to create buttons (CommandBarButton) directly on commandbars, and you had to use the VBE.Events.CommandBarEvents(CommandBarButton) artifact to get the Click event of a CommandBarButton (you couldn’t even get the Click event using the more natural WithEvents clause).

Somehow this strange artifact got in the extensibility of the new Visual Studio .NET 2002 IDE (likely because it still used the Office commandbar model). However, in Visual Studio you have commands (EnvDTE.Command) and the recommended way is to create CommandBarButtons from commands (Command.AddControl), not directly on commandbars without a command (CommandBar.Controls.Add), and rather than getting a Click event when a CommandBarButton is clicked, you get a method (IDTCommandTarget.Exec) called when the command is executed. So, DTE.Events.CommandBarEvents is no longer necessary. There is still an scenario where you may want to create a CommandBarButton without an underlying command, for example the context menu of a listview of a toolwindow of an add-in, where you want the look and feel of the Visual Studio menus so you use a CommandBarPopup, but you don’t want commands. However, in this scenario you can use the Click event of the CommandBarButton class, without using DTE.Events.CommandBarEvents.

But people were still using DTE.Events.CommandBarEvents for another case: to know when a CommandBarControl which is a CommandBarPopup is clicked just before showing the children CommandBarButtons, presumably to set its state (enabled, disabled, invisible, etc.). Well, it happens that the new WPF-based commandbars of Visual Studio 2010 break that case, and this has caused some inconvenience to some people.This new article documents this problem:

PRB: COMException 0x80020003 getting events from commandbar popup in Visual Studio 2010
http://www.mztools.com/articles/2011/MZ2011003.aspx

Fortunately the remedy is easy: use the IDTCommandTarget.QueryStatus method:

HOWTO: Controlling the state of command in a Visual Studio add-in
http://www.mztools.com/articles/2007/MZ2007025.aspx

The strange case of VisualStudio.DTE.9.0 registry key missing for Visual Studio 2008 Professional

The other day I received a bug report from a customer of my MZ-Tools 6.0 add-in because the setup was not detecting any installation of Visual Studio while he claimed that he had Visual Studio 2008 Professional. The setup of that version of MZ-Tools detects the installation of Visual Studio versions checking the existence of the registry entries HKEY_CLASSES_ROOT\VisualStudio.DTE.<version>, where <version> can be 8.0 for VS 2005, 9.0 for VS 2008, etc. It does so because those entries are not created for Express editions of Visual Studio, which in turn don’t support add-ins.

So, I asked the customer to check the existence of HKEY_CLASSES_ROOT\VisualStudio.DTE.9.0 and he informed me that the registry key didn’t exist, while it should because Visual Studio 2008 Professional creates it. In the next e-mail he mentioned the “educational” version of Visual Studio 2008 Professional. I was not aware of this edition, but I found that there is a Microsoft Education Product Center that offers licenses for “academic” institutions. I don’t know for sure if the “Professional” edition of Visual Studio is different for academic institutions and for companies, but there is another registry key that can be used to detect if Visual Studio is installed:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\<version>

My customer confirmed me that that registry key did exist. My only doubt was if Express editions create those too so I searched the web and I found my own article (I always love when this happen) HOWTO: Detect installed Visual Studio editions, packages or service packs that states that Express editions don’t create:

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\<version>

but:

  • For VB Express: HKEY_LOCAL_MACHINE\Software\Microsoft\VBExpress
  • For VC Express: HKEY_LOCAL_MACHINE\Software\Microsoft\VCExpress
  • For C# Express: HKEY_LOCAL_MACHINE\Software\Microsoft\VCSExpress
  • For J# Express: HKEY_LOCAL_MACHINE\Software\Microsoft\VJSExpress
  • For Web Developer Express: HKEY_LOCAL_MACHINE\Software\Microsoft\VWDExpress

So, future versions of my setup will use HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\<version>.

The strange case of InvalidCastException in Microsoft.VisualStudio.PlatformUI.Automation.CommandBarCustomizer.Remove of VS 2010

At the time of this writing, this is still an unsolved case, but I am posting it anyway with the hope that some day someone can solve it.

Since I included support for Visual Studio 2010 in my add-in MZ-Tools 6.0, I have received bug reports from three customers with this exception:

System.InvalidCastException: Specified cast is not valid.
at System.Runtime.InteropServices.Marshal.ThrowExceptionForHRInternal(Int32 errorCode, IntPtr errorInfo)
at System.Runtime.InteropServices.Marshal.ThrowExceptionForHR(Int32 errorCode)
at Microsoft.VisualStudio.PlatformUI.Automation.CommandBarCustomizer.Remove(ControlCustomizer control)
at Microsoft.VisualStudio.PlatformUI.Automation.CommandBarControl.Delete(Object Temporary)
at Microsoft.VisualStudio.PlatformUI.Automation.CommandBarControl._Marshaler.<>c__DisplayClass10.<Delete>b__f()
at Microsoft.VisualStudio.Shell.ThreadHelper.Invoke(Action action)
at Microsoft.VisualStudio.PlatformUI.Automation.CommandBarControl._Marshaler.Delete(Object Temporary)
at EnvDTE.Command.Delete()

The exception happens when I delete an EnvDTE.Command of the add-in, which in turn deletes the CommandBarButtons created from it, a technique that I described in the article:

HOWTO: Prevent dead CommandBarButtons when Visual Studio or an add-in crashes.
http://www.mztools.com/articles/2009/MZ2009002.aspx

The problem is not reproducible and it has only happened to three customers, always in Visual Studio 2010, not in Visual Studio 2005 or 2008, so it is related to the new WPF-based commandbars of Visual Studio 2010. So, I used Reflector for .NET (freeware, soon to be paid version) to see what could cause the InvalidCastException in the Microsoft.VisualStudio.PlatformUI.Automation.CommandBarCustomizer.Remove(ControlCustomizer control) method. The Microsoft.VisualStudio.PlatformUI.Automation.CommandBarCustomizer class resides in the Microsoft.VisualStudio.Shell.UI.Internal.dll assembly of the folder C:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE and the Remove method is like this:

public void Remove(ControlCustomizer control)
{
   ErrorHandler.ThrowOnFailure(base.Wrapped.Remove(control.Wrapped));
}

The ControlCustomizer class is like this:

public class ControlCustomizer : InterfaceWrapper<IVsControlCustomizerPrivate>, IDisposable

And the InterfaceWrapper<T> interface is like this:

public abstract class InterfaceWrapper<T>
{
   // Fields
   private readonly T _wrapped;

   // Methods
   protected InterfaceWrapper(T wrapped);

   // Properties
   public T Wrapped { get; }
}

So, on the one hand, control.Wrapped returns something of the IVsControlCustomizerPrivate type.

On the other hand, the base of the CommandBarCustomizer is of InterfaceWrapper<IVsCommandBarCustomizerPrivate> type, the Wrapped property returns something of IVsCommandBarCustomizerPrivate type and its Remove method is like this:

IVsCommandBarCustomizerPrivate[PreserveSig]
int Remove([In, MarshalAs(UnmanagedType.Interface)] IVsControlCustomizerPrivate pControl);

which expects a parameter of the IVsControlCustomizerPrivate type, the same type that we are passing!. So, how is that an InvalidCastException can happen? The only scenario where I have seen that two types with the same names (name, full name, assembly names) can’t be cast is because they belong to an assembly that is loaded twice from different locations. I am not sure if somehow this is happening here.

The strange case of VSLangProj80.ProjectProperties3.AbsoluteProjectDirectory

When you have a System.Type that is a component, the best way to get its public properties is to use System.ComponentModel.TypeDescriptor.GetProperties(type), rather than System.Type.GetProperties(). This is so because a component type can have a designer which is able to add new properties to the type, and to remove or change existing properties. For example, controls have a Locked property that is added by the designer of controls (ControlDesigner class), it is not a property that belongs to the System.Windows.Forms.Control type.

If the type is not a component, I guess that System.ComponentModel.TypeDescriptor.GetProperties returns the same public properties than System.Type.GetProperties. System.ComponentModel.TypeDescriptor.GetProperties returns a collection of PropertyDescriptor, which has an IsBrowsable property that tells you if a property is browsable or not.

So, the other day I got an strange issue: I was calling System.ComponentModel.TypeDescriptor.GetProperties on the VSLangProj80.ProjectProperties3 type (which is not a component type but my code was called other times with types that are components), to get the properties that are likely to exist in the EnvDTE.Project.Properties collection. And I got in the results the “AbsoluteProjectDirectory” property, that I noticed that didn’t appear in the Object Browser of Visual Studio, despite its IsBrowsable property returning True. How come?

I was aware that VSLangProj80 stuff is not pure .NET but an imported typelib or something like that, but it was not until I used .NET Reflector that I discovered that the get accessor method of the AbsoluteProjectDirectory property has the System.Runtime.InteropServices.TypeLibFuncFlags attribute with the &H40 value:

<DispId(&H2732)> _
ReadOnly Property AbsoluteProjectDirectory As <MarshalAs(UnmanagedType.BStr)> String
   <MethodImpl(MethodImplOptions.InternalCall, MethodCodeType:=MethodCodeType.Runtime), DispId(&H2732), TypeLibFunc(CShort(&H40))> _
   Get
      ...
   End Get
End Property

which matches the FHidden value:

<Serializable, Flags, ComVisible(True)> _
Public Enum TypeLibFuncFlags
   ' Fields
   FBindable = 4
   FDefaultBind = &H20
   FDefaultCollelem = &H100
   FDisplayBind = &H10
   FHidden = &H40
   FImmediateBind = &H1000
   FNonBrowsable = &H400
   FReplaceable = &H800
   FRequestEdit = 8
   FRestricted = 1
   FSource = 2
   FUiDefault = &H200
   FUsesGetLastError = &H80
End Enum

That explained the issue and I was able to tweak my code to deal with that case. BTW, notice that there is also a TypeLibFuncFlags.FNonBrowsable flag.