Registering a Visual Studio .Net Add-In without an Installer

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 US the resource DLL must go into the HelpBuilder/vsAddIn/1033 directory with StatelliteDllPath pointing at the vsAddIn directory. If I install with a German Locale I need to install to a 1031 directory and so on. Note that SatelliteDllPath points at the base directory, but the DLL actually doesn’t live in that path, but instead in one of the locale specific paths.

 

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…

posted on 2005-08-04 23:02  Ооo酷鱼  阅读(1493)  评论(0编辑  收藏  举报

导航