Windows PowerShell scripts to register a .NET-based add-in for a COM-based host application

Before Visual Studio 2005 introduced XML-based registration for add-ins with an .AddIn file (which enabled X-Copy deployment), add-ins for Microsoft applications required two steps to be registered:

  • To register the add-in dll as ActiveX (COM) component
  • To register the add-in dll as add-in for the host application through some registry entries

This is still true for COM-based add-ins for Visual Studio (any version) and for other hosts such as Microsoft Office or its VBA editor which only support COM-based add-ins.

Some months ago I wrote how to create a COM add-in for the VBA editor of Office using .NET, which is almost the only way to create an add-in for the VBA editor of Office 64-bit, since it doesn’t support 32-bit COM add-ins.

I am working since some months ago on a .NET-based version of my MZ-Tools add-in for the VBA editor of Office 32/64-bit and I always wanted a single script to perform the two steps above. This was a nice excuse to learn Windows PowerShell, so I bought a book and after reading some chapters to get the concepts today I decided to create the scripts that call regasm.exe to register the .Net assembly for COM-Interop and create the registry entries for the add-in to be recognized by the VBA editor:

1) This is the content of a file named Functions.ps1 which contains reusable functions:

# To run .ps1 scripts you need to execute first: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
[System.Reflection.Assembly]::LoadWithPartialName("System.Windows.Forms")
$Regasm32 = 'C:\Windows\Microsoft.NET\Framework\v2.0.50727\regasm.exe'
$Regasm64 = 'C:\Windows\Microsoft.NET\Framework64\v2.0.50727\regasm.exe'

function Register-Assembly32([string]$Assembly, [string]$RegistryKey, [string]$FriendlyName)
{
   Execute-Command -RegAsm $Regasm32  -Arguments '/codebase' -Assembly $Assembly
   Register-AddIn -RegistryKey $RegistryKey -FriendlyName $FriendlyName
}

function Register-Assembly64([string]$Assembly, [string]$RegistryKey, [string]$FriendlyName)
{
   Execute-Command -RegAsm $Regasm64 -Arguments '/codebase' -Assembly $Assembly
   Register-AddIn -RegistryKey $RegistryKey -FriendlyName $FriendlyName
}

function Unregister-Assembly32([string]$Assembly, [string]$RegistryKey)
{
   Execute-Command -RegAsm $Regasm32 -Arguments '/unregister' -Assembly $Assembly
   Unregister-AddIn -RegistryKey $RegistryKey
}

function Unregister-Assembly64([string]$Assembly, [string]$RegistryKey)
{
   Execute-Command -RegAsm $RegAsm64 -Arguments '/unregister' -Assembly $Assembly
   Unregister-AddIn -RegistryKey $RegistryKey
}

function Register-AddIn([string]$RegistryKey, [string]$FriendlyName)
{
   New-Item         -Path $RegistryKey -Force
   New-ItemProperty -Path $RegistryKey -Name Description  -PropertyType String -Value $FriendlyName
   New-ItemProperty -Path $RegistryKey -Name FriendlyName -PropertyType String -Value $FriendlyName
   New-ItemProperty -Path $RegistryKey -Name LoadBehavior -PropertyType DWord  -Value 3
}

function Unregister-AddIn([string]$RegistryKey)
{
   if (Test-Path -Path $RegistryKey)
   {
      Remove-Item -Path $RegistryKey
   }
}

function Execute-Command([string]$RegAsm, [string]$Arguments, [string]$Assembly)
{
   $psi = New-Object System.Diagnostics.ProcessStartInfo
   $psi.CreateNoWindow = $true
   $psi.UseShellExecute = $false
   $psi.RedirectStandardOutput = $true
   $psi.RedirectStandardError = $true
   $psi.FileName = $RegAsm
   $psi.Arguments = $Arguments + ' ' + $Assembly
   $process = New-Object System.Diagnostics.Process
   $process.StartInfo = $psi
   [void]$process.Start()
   $StandardOutput = $process.StandardOutput.ReadToEnd()
   $StandardError = $process.StandardError.ReadToEnd()
   $process.WaitForExit()
   [system.windows.forms.messagebox]::show($StandardOutput + $StandardError)
}

2) Then I have other scripts that include that script:

MyAddInVBA32Registration.ps1:

$ScriptDirectory = Split-Path $MyInvocation.MyCommand.Path
. (Join-Path $ScriptDirectory Functions.ps1)

$Assembly = (get-item Env:USERPROFILE).Value + 'Documents\MyAddIn\Exe\Debug\MyAddIn.dll'

Register-Assembly32 -Assembly $Assembly -RegistryKey 'HKCU:Software\Microsoft\VBA\VBE\6.0\AddIns\MyAddIn.Connect' -FriendlyName 'My Add-In'

MyAddInVBA64Registration.ps1:

$ScriptDirectory = Split-Path $MyInvocation.MyCommand.Path
. (Join-Path $ScriptDirectory Functions.ps1)

$Assembly = (get-item Env:USERP2ROFILE).Value + 'Documents\MyAddIn\Exe\Debug\MyAddIn.dll'

Register-Assembly64 -Assembly $Assembly -RegistryKey 'HKCU:Software\Microsoft\VBA\VBE\6.0\AddIns64\MyAddIn.Connect' -FriendlyName 'My Add-In'

MyAddInVBA32Unregistration.ps1:

$ScriptDirectory = Split-Path $MyInvocation.MyCommand.Path
. (Join-Path $ScriptDirectory Functions.ps1)

$Assembly = (get-item Env:USERPROFILE).Value + 'Documents\MyAddIn\Exe\Debug\MyAddIn.dll'

Unregister-Assembly32 -Assembly $Assembly -RegistryKey 'HKCU:Software\Microsoft\VBA\VBE\6.0\AddIns\MyAddIn.Connect'

MyAddInVBA64Unregistration.ps1:

$ScriptDirectory = Split-Path $MyInvocation.MyCommand.Path
. (Join-Path $ScriptDirectory Functions.ps1)

$Assembly = (get-item Env:USERPROFILE).Value + 'Documents\MyAddIn\Exe\Debug\MyAddIn.dll'

Unregister-Assembly64 -Assembly $Assembly -RegistryKey 'HKCU:Software\Microsoft\VBA\VBE\6.0\AddIns64\MyAddIn.Connect'

To run the scripts you need to enable PowerShell execution first and they need to be run with admin rights.

I am finding PowerShell with a learning curve harder than expected and with some “by-design” issues that makes it “tricky” in my opinion, but I hope to learn it in depth.

MZ-Tools Articles Series updated

Many of the MZ-Tools Articles that I have written in the last years seemed a bit outdated because each one has a header like this:

Author: Carlos J. Quintero (Microsoft MVP) Applies to: Microsoft Visual Studio .NET 2002
Date: March 2010   Microsoft Visual Studio .NET 2003

  Microsoft Visual Studio 2005

Where the date and the list of Visual Studio versions would reflect the moment in time when I wrote them.

I have reviewed all them (200+) and many are now updated to reflect that they apply until the last Visual Studio 2012 version when that is the case:

Author: Carlos J. Quintero (Microsoft MVP) Applies to: Microsoft Visual Studio .NET 2002
Date: March 2010   Microsoft Visual Studio .NET 2003
Updated: March 2013   Microsoft Visual Studio 2005
      Microsoft Visual Studio 2008
      Microsoft Visual Studio 2010
      Microsoft Visual Studio 2012

So, most HOWTO articles are now current. The exceptions are those articles which are BUG or PRB (issue), that I haven’t bothered to check if they are still present.

The strange case of “LoaderLock was detected” with a COM add-in written in .NET

Since some days ago, I was getting the following error when closing Visual Basic 6.0 from the Visual Studio debugger (I am developing a .NET-based version of MZ-Tools for the 64-bit VBA editor of Office, and VB6 will get it too):

LoaderLock was detected
Message: Attempting managed execution inside OS Loader lock. Do not attempt to run managed code inside a DllMain or image initialization function since doing so can cause the application to hang.

This is a warning of the Managed Debugging Assistants (MDA) of Visual Studio.

Today I decided to investigate. Soon it was clear that it was caused by the test-runner add-in that I created to run integration tests within VB 6.0. Since the error was caused during shutdown, I removed initializations (and the corresponding cleanups) to isolate the problem and I discovered that the problem was in this method:

internal List<string> GetAddinProgIds()
{
   List<string> colAddinProgIds;

   colAddinProgIds = new List<string>();

   foreach (AddIn objAddIn in m_objVBE.Addins)
   {
      colAddinProgIds.Add(objAddIn.ProgId);
   }
   return colAddinProgIds;
}

That method gets the registered add-ins of the VBE object (to load them in a combobox and select an add-in to run its test suites).

I soon realized that maybe I should release properly some COM object and certainly the problem was fixed:

internal List<string> GetAddinProgIds()
{
   List<string> colAddinProgIds;
   Addins colAddins;

   colAddinProgIds = new List<string>();

   colAddins = m_objVBE.Addins;
   foreach (AddIn objAddIn in colAddins)
   {
      colAddinProgIds.Add(objAddIn.ProgId);
   }

   // The following statement is to prevent the following error:
   // LoaderLock was detected
   // Message: Attempting managed execution inside OS Loader lock. Do not attempt to run managed code inside a DllMain
   // or image initialization function since doing so can cause the application to hang.
   System.Runtime.InteropServices.Marshal.ReleaseComObject(colAddins);

   colAddins = null;

   return colAddinProgIds;
}

MZ-Tools Articles Series: BUG: DTE.ActiveDocument.ActiveWindow does not return the active document window if toolwindow active

My first small article this year is to document this bug of Visual Studio 2005, 2008, 2010 and 2012:

BUG: DTE.ActiveDocument.ActiveWindow does not return the active document window if toolwindow active
http://www.mztools.com/articles/2013/MZ2013001.aspx

Which I have just reported to Microsoft Connect:

Document.ActiveWindow does not return always the active window
https://connect.microsoft.com/VisualStudio/feedback/details/781522/document-activewindow-does-not-return-always-the-active-window

and that is a variation of a bug that I reported for VS.NET 2002/2003 and that I thought it was definitely fixed in VS 2005 (the relevant factor is that it still fails if a toolwindow is active):

BUG: Document.ActiveWindow does not return always the active window in Visual Studio .NET 2002/2003
http://www.mztools.com/articles/2004/MZ2004013.aspx

The strange case of “Set property ‘System.Windows.ResourceDictionary.DeferrableContent’ threw an exception.”

In the recent days, each time that I clicked the New Project button of the Visual Studio 2012 IDE, I got this exception:

“Set property ‘System.Windows.ResourceDictionary.DeferrableContent’ threw an exception.”

I have been clueless about this problem until today. When a problem happens in Visual Studio, the recommended approach is to launch it in “Safe mode”, because maybe an extension (add-in, package, etc.) is causing it. As a previous step, what I did today is to take a look at the Add-In Manager and I noticed that I had an add-in (a test runner that I created to perform integration tests of my MZ-Tools add-in) marked to load on startup. I unmarked it and then the problem disappeared. Why was this add-in causing this problem?

After some isolation, it happened that this add-in was setting an event handler for the AppDomain.AssemblyResolve event (to locate required assemblies) and a silenced NullReferenceException was happening in the event handler. The following minimal add-in reproduces the issue:

public class Connect : IDTExtensibility2
{
   private DTE2 _applicationObject;
   private AddIn _addInInstance;
   private AppDomain _appDomain;

   public void OnConnection(object application, ext_ConnectMode connectMode, object addInInst, ref Array custom)
   {
      _applicationObject = (DTE2)application;
      _addInInstance = (AddIn)addInInst;

      switch (connectMode)
      {
         case ext_ConnectMode.ext_cm_Startup:
            // OnStartupComplete will be called
            break;

         case ext_ConnectMode.ext_cm_AfterStartup:
            InitializeAddIn();
            break;
       }
   }

   public void OnStartupComplete(ref Array custom)
   {
      InitializeAddIn();
   }

   private void InitializeAddIn()
   {
      _appDomain = AppDomain.CurrentDomain;
      _appDomain.AssemblyResolve += AppDomain_AssemblyResolve;
   }

   public void OnDisconnection(ext_DisconnectMode disconnectMode, ref Array custom)
   {
      _appDomain.AssemblyResolve -= AppDomain_AssemblyResolve;
   }

   public void OnAddInsUpdate(ref Array custom)
   {
   }

   public void OnBeginShutdown(ref Array custom)
   {
   }

   private Assembly AppDomain_AssemblyResolve(object sender, ResolveEventArgs args)
   {
      Assembly objAssembly = null;
      AssemblyName objAssemblyName = null;

      // Force a NullReferenceException
      if (objAssemblyName.Name == "")
      {
      }

      return objAssembly;
   }
}