Registering a Visual Studio .Net Add-In without an Installer
I’ve recently created an Add-In for Help Builder for Visual Studio .Net. The Add-In is a connector between the VS.Net environment and my un-managed Help Builder application that is basically standalone Windows application written in a non-CLR language. The Add-In interfaces with Help Builder through a simple but effective COM interface that drives both the Help Builder engine and the user interface.
The Visual Studio .Net integration is only a small part of Help Builder, although I’ve found myself using it extensively since I added it myself with the ability to round trip documentation between source code and Help Builder and to easily stuff Help Context Ids (HelpString Topic values really) into controls at design time in the form editor. It all works great.
But when it comes to installation, as always Microsoft provides you a pretty canned solution using the ever so nasty Microsoft Installer technology. Besides the fact that installing Add-Ins this way has been prone with problems (paths and versions being hardcoded) and having little control over the process, in this case the issue is simply that the main application doesn’t use the Microsoft Installer. There are lots of reason I went this route not the least of which was an installation that ends up about half the size of MS Installer installs.
The bottom line is this: I can’t use an MSI package to install the Add-In. So, in order to install the Add-In I decided it should be an option that is enablable (<g>) from within the Help Builder IDE via the Configuration options available. Click a checkbox and Help Builder installs itself as an Add-In in VS.Net 2003. Sounds easy, but what’s really involved in this? It turns out there are three main things that are required:
- Registering the COM component as COM Interop Assembly (RegAsm)
- Creating the Add-In registry keys
- Creating a locale specific resource directory and copying any satellite assemblies there
Registering the Add-In for COM Interop
Add-Ins in VS.Net are primarily COM components, so the Add-In I created in C# is compiled into .Net Assembly DLL which then must be registered for COM interop. To do this one has to run RegAsm.exe from the Framework directory with the /CodeBase extension to register the Assembly as an Interop Assembly with a fixed Codebase location on the disk (meaning the path is written into the registry – the other alternative would be GAC installation).
Help Builder is written in Visual FoxPro, so the code below is the high level registration routine that shows how to do this:
************************************************************************
* RegisterHelpBuilderAddin
****************************************
FUNCTION RegisterHelpBuilderAddin(lcError,llUnregister) as Boolean
LOCAL lcFrameworkPath, lcVersion
IF !llUnregister AND ISCOMOBJECT("HelpBuilder.vsAddin")
lcError = ""
RETURN .T.
ENDIF
*** Try to register
lcFrameworkPath = ""
lcVersion = ""
IF !IsDotNet(@lcFrameworkPath)
lcError = "DotNet Framework not installed or path not found."
RETURN .F.
ENDIF
lcRun = ShortPath(ADDBS(lcFrameworkPath) + "regasm.exe")
IF EMPTY(lcRun) && File doesn't exist
lcError = "Couldn't find RegAsm.exe at:" + CHR(13) +;
lcFrameworkPath + "regasm.exe"
RETURN .F.
ENDIF
lcRun = lcRun +;
[ "] + FULLPATH("vsAddin\HelpBuilderVsAddin.dll") + ;
IIF(llUnregister,[" -unregister],[" /codebase])
_cliptext = lcRun
WAIT WINDOW "Hang on. Trying to register HelpBuilderVsAddin.dll..." + CHR(13) +;
"This may take a few seconds..." NOWAIT
TRY
RUN &lcRun
CATCH
ENDTRY
WAIT CLEAR
IF llUnRegister
RETURN .T.
ENDIF
llResult = IsComObject("HelpBuilder.vsAddin")
IF !llResult
lcError = "Registration of the Addin failed." + CHR(13) + CHR(13)+ ;
"Command Line:" + CHR(13) + ;
"RUN " + lcRun + CHR(13) + CHR(13) +;
"Full deduced RegAsm Path:" + CHR(13) + ;
lcFrameworkPath + "regasm.exe" + CHR(13) + CHR(13) +;
"You can manually register HelpBuilderVsAddIn.dll by running REGASM.EXE" + CHR(13) + ;
"from the framework BIN directory with the following command line: " + CHR(13)+;
"<.Net framework bin path>\RegAsm /codebase HelpBuilderVsAddin.dll" + CHR(13) + CHR(13) + ;
"The command line to register the component has been pasted into your ClipBoard"
ENDIF
RETURN llResult
There are a couple of helper routines – IsComObject and IsDotNet – that check to see if the COM object is already registered (in which case we don’t have to re-register) and if not if the .Net Framework is actually installed:
************************************************************************
* wwUtils :: IsDotNet
****************************************
*** Function: Returns whether .Net is installed
*** Optionally returns the framework path and version
*** of the highest installed version.
************************************************************************
FUNCTION IsDotNet(lcFrameworkPath,lcVersion)
LOCAL loAPI as wwAPI
lcVersion = ""
lcFrameworkPath = ""
loAPI = CREATEOBJECT("wwAPI")
lcWinDir = loAPI.getSystemdir(.t.)
lcVersion = loAPI.Readregistrystring(HKEY_LOCAL_MACHINE,"Software\Microsoft\ASP.Net","RootVer")
IF ISNULL(lcFrameworkpath)
RETURN .F.
ENDIF
lnAt = AT(".",lcVersion,3)
IF lnAt > 0
lcVersion = SUBSTR(lcVersion,1,lnAt) + "0"
ENDIF
lcFrameworkPath = loAPI.Readregistrystring(HKEY_LOCAL_MACHINE,"Software\Microsoft\ASP.Net\"+lcVersion,"PATH")
IF ISNULL(lcFrameworkPath)
lcFrameworkPath = ""
ELSE
lcFrameworkPath = ADDBS(lcFrameworkPath)
ENDIF
RETURN .T.
ENDFUNC
* wwUtils :: IsDotNet
Note that this routing also returns the path to the Framework directory which is required in order to be able to call RegAsm.exe directly from the application. The check for the framework is pretty important – we don’t want to allow registration of the component if .Net is not installed. In fact, the startup code of the Config form checks for this first and disables the checkbox if .Net is not installed in the first place.
Registering the Add-In in the Registry
VS.Net Add-Ins are registered in VS through the registry. Specifically through the following key:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\AddIns
As you can see keys are version specific – Version 7.1 is Visual Studio 2003 in particular. Underneath this key sit all registered Add-Ins with their ProgId (HelpBuilder.vsAddIn for example) as the key name. Underneath this key are a few key settings that determine how the Add-In is displayed in VS.Net (Name, Description, Icon) how it loads (LoadBehavior) and so on.
Registering this information is pretty straight forward using standard registry tools, but because the information might change in the future in this case I decided to use a .Reg file with a template to make it easy to modify the content external to the application in the future. I exported the Reg file after I had initially installed the Add-In in VS.Net and modified it slightly. The problem is that VS.Net creates a Reg file for you when you originally create your Add-In project, but it doesn’t update it after you change settings or worse change the name of your add-in. As most .Net Wizards it’s a one way tool (as is the installer script that’s generated BTW which is yet one more reason I didn’t want to use the built in installer project).
The Reg file I used looks like this:
Windows Registry Editor Version 5.00
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.1\AddIns\HelpBuilder.vsAddin]
"FriendlyName"="Help Builder Addin"
"SatelliteDllPath"="<% lcSatelliteDllPath %>"
"Description"="Help Builder Addin"
"AboutBoxIcon"=hex:00,00,01,00,02,00,20,20,10,00,00,00,00,00,e8,02,00,00,26,00,\
…
00,00,03,00,00,10,03,00,00,f0,03,00,00,f0,03,00,00,ff,7f,00,00,fe,3f,00,00,\
fe,3f,00,00,fe,3f,00,00
"SatelliteDllName"="hbres.dll"
"CommandLineSafe"=dword:00000001
"AboutBoxDetails"="For more information about West Wind Technologies, see the West Wind Technologies web site at http://www.west-wind.com/wwhelp/."
"LoadBehavior"=dword:00000007
"CommandPreload"=dword:00000001
Notice that I needed to customize the SatelliteDllPath. This path is used for any external resources that the Add-In needs – in my case some custom icons for the Command Bars. This folder must point at the Add-In directory under which then there must be a locale specific folder with the actual DLL. More on this in the next section.
The following code demonstrates reading the Reg script and merging it into the registry, along with the high level code that performs the COM/COM Interop registration:
************************************************************************
* wwHelp_Routines :: InstallVsAddin
****************************************
FUNCTION InstallVsAddin(llUninstall)
*** Read in the .Reg File and expand the Satellite DLL path into it
lcAddInPath = SYS(5) + CURDIR() + "vsAddin\"
lcRegFile = lcAddInPath + [vsaddin.reg]
lcSatelliteDllPath = LOWER(STRTRAN( lcAddinPath,"\","\\" ))
lcContent = FILETOSTR(lcRegFile)
lcContent = TEXTMERGE(lcContent,.f.,"<%","%>")
IF llUninstall
*** Prefix reg key with a minus sign
lcContent = STRTRAN(lcContent,"HKEY","-HKEY")
lcError = ""
RegisterHelpBuilderAddin(@lcError,.T.)
ELSE
DECLARE INTEGER GetLocaleInfo IN WIN32API ;
INTEGER, INTEGER,STRING@,INTEGER @
lcLocaleId = SPACE(4)
GetLocaleInfo(0,1,@lcLocaleid,4)
lnLocale = EVAL("0x" + lcLocaleId)
*** Must copy the Satellite Resource DLL into locale specific dir
IF lnLocale != 1033 && 1033 is default and pre-installed
IF !ISDIR(JUSTPATH(lcRegFile) + "\" + TRANSFORM(lnLocale) )
MD (JUSTPATH(lcRegFile) + "\" + TRANSFORM(lnLocale) )
ENDIF
try
COPY FILE (lcAddinPath + "1033\hbres.dll") TO ;
(lcAddinPath + TRANSFORM(lnLocale) + "\hbres.dll")
CATCH
*** Ignore error
MESSAGEBOX("Couldn't copy the icon resources." + CHR(13) + CHR(13) +;
"The add-in will work without them, but it's recommended" + CHR(13) +;
"that you shut down VS.Net and retry setting this option.",;
0 + 48,WWHELP_APPNAME)
ENDTRY
ENDIF
*** Register the DLL for COM interop
lcError = ""
IF !RegisterHelpbuilderAddin(@lcError)
MESSAGEBOX("Error registering the Add-in" + CHR(13) + CHR(13) +;
lcError,48,WWHELP_APPNAME)
ENDIF
ENDIF
STRTOFILE(lcContent,lcRegFile+".install")
lcRun = [regedit /s "] + lcRegFile + [.install"]
RUN /N2 &lcRun
ENDFUNC
* wwHelp :: InstallVsAddin
This code acts as the high level Add-In installer routine. It deals with opening the .Reg file, updating the script path, copying the SatelliteDll into the appropriate locale directory and then updating a temporary RegFile with the new DLL path. It then runs RegEdit against this Reg file to merge it into the registry (there were problems using just the .Reg extendended file with ShellExecute).
This code also allows uninstallation. It does this by using the same .Reg file and changing the installation option of the key to install to an uninstall by using the – prefix. The following line accomplishes this:
lcContent = STRTRAN(lcContent,"HKEY","-HKEY")
which effectively uninstalls the key in question.
Installing theSatellite Dlls
Satellite DLLs are used to hold external resources – in my case I needed a couple of icons for the CommandBar menus. These resources must be contained in an external DLL. There’s no easy way to create these resources in a .Net based project, but it’s fairly easy to do using a C++ project and by adding a Resource to it, then adding the images or other resources. You simply add the resources to the Resource View and compile into a plain jane Windows DLL. Your Resource IDs will be the ids you use inside of the Add-In (for example for a CommandBar ID).
Installing these resource DLLs is also tricky because according to the docs at least you need to install them into a locale specific directory. This means for the
The install code needs to therefore be aware which locale it’s installing itself for and create and copy the DLL into the appropriate directory. By default I install a 1033 directory for US, and during installation check the Locale. If the Locale is other than 1033 I create a new directory and copy the resource from the 1033 directory into it.
This is messy and seems like a real hack – especially for a DLL that has nothing that really is locale specific – but it works effectively.
All of this ended up being a lot more work than I anticipated, but I’m happy with the way this has turned out. In addition to providing a clean interface to my application, this interface is also nice to quickly enable and disable the plug in for debug sessions. I can disable, start VS.Net with my Add-In project, then re-enable once the project is start to start debugging my test project using the add-in…