HowTo: Register your MS Office add-in with WiX

If you are using Microsoft's WiX toolset to build a setup for your Add-In, then there are several ways to register the Add-In. I've seen examples that use a custom action to call RegSvr32.exe to do the registration. But since RegSvr32.exe does nothing more than calling the DllRegisterServer function of our Add-In, why not calling that directly from a custom action?

The proper way to register your Add-In however, is to not do this yourself, but let the MSI engine take care of it. All it takes to register the Add-In is adding some keys and values to the registry. The Heat tool of the WiX toolset can build a script fragment for you that contains all the necessary information to register the Add-In.

For example, if we have an Add-In called MyPowerPointAddIn.dll, just enter the following command:

heat file MyPowerPointAddIn.dll -out fragment.wxs

This will create a fragment file containing a TypeLib element and some RegistryValue elements. If you copy that part to your WiX script within the File element that installs your Add-In dll, you are ready to install and register your Add-In. The final script will look something like this:

<File Id="MainFile" KeyPath="yes" Source="MyPowerPointAddIn.dll">
        <TypeLib Id="{YOUR-GUID-HERE}" Description="MyPowerPointAddIn Library"
              HelpDirectory="DirMainFile" Language="0" MajorVersion="1" MinorVersion="0">
        <Class Id="{YOUR-GUID-HERE}" Context="InprocServer32"
              Description="MyPowerPoint Object" ThreadingModel="apartment" Version="1.0">
            <ProgId Id="MyPowerPointAddIn.MyPowerPoint" Description="MyPowerPoint Object" />
        </Class>
        <Interface Id="{YOUR-GUID-HERE}" Name="IMyPowerPoint"
              ProxyStubClassId="{YOUR-GUID-HERE}" ProxyStubClassId32="{YOUR-GUID-HERE}" />
    </TypeLib>
</File>
<RegistryValue Root="HKLM"
        Key="Software\Microsoft\Office\PowerPoint\AddIns\MyPowerPointAddIn.MyPowerPoint"
        Name="FriendlyName" Value="My PowerPoint Add-In 1.0" Type="string" Action="write" />
<RegistryValue Root="HKLM"
        Key="Software\Microsoft\Office\PowerPoint\AddIns\MyPowerPointAddIn.MyPowerPoint"
        Name="Description" Value="My PowerPoint COM Addin" Type="string" Action="write" />
<RegistryValue Root="HKLM"
        Key="Software\Microsoft\Office\PowerPoint\AddIns\MyPowerPointAddIn.MyPowerPoint"
        Name="LoadBehavior" Value="3" Type="integer" Action="write" />
<RegistryValue Root="HKLM"
        Key="Software\Microsoft\Office\PowerPoint\AddIns\MyPowerPointAddIn.MyPowerPoint"
        Name="CommandLineSafe" Value="0" Type="integer" Action="write" />

Although I changed the GUIDs in this example to YOUR-GUID-HERE, the fragment created by the Heat tool contains the correct GUIDs, so no need to change them. What you do need to change is the Id attribute of the File element, and the HelpDirectory attribute of the TypeLib element. This one should contain the Id of the directory where you install your Add-In.

If your Add-In is targeting Office 2007 and above, then this is all  you need to register it. However, if your Add-In is targeting Office 2003 and below, you might find that after uninstalling your Add-In, any CommandBar you added to the host application will still be there. You can partly solve this by setting the Temporary property to True. But if you cannot do that, or if this turns out to not solve your problem, you have to revert to manual registration of your Add-In. We can do this by writing a custom action that simply calls the DllRegisterServer function from our Add-In to do the registration. Likewise, we create another custom action to call the DllUnregisterServer function when our Add-In is uninstalled.

Note. When you follow this approach, you no longer need the code produced by the Heat tool, so if you already added that to your script, you can remove it again.

First, we are going to create a custom action dll that we will simply call MyCustomActions.dll and that exports two functions called RegisterAddIn and UnregisterAddIn. The RegisterAddIn function could look like this:

function RegisterAddIn(hInstall: MSIHANDLE): DWORD; stdcall;
var
  LibHandle: HMODULE;
  DllRegisterServer: function: HResult; stdcall;
  FileName: string;
begin
  LogMSIMessage(hInstall, 'Begin RegisterAddIn()');
  FileName := GetMSIProperty(hInstall, 'CustomActionData');
  LogMSIMessage(hInstall, FileName);
  LibHandle := LoadLibrary(PChar(FileName));
  if LibHandle > 0 then
  begin
    @DllRegisterServer := GetProcAddress(LibHandle, 'DllRegisterServer');
    if Assigned(DllRegisterServer) then
    begin
      Result := DllRegisterServer;
      LogMSIMessage(hInstall, 'DllRegisterServer returned: 0x' + IntToHex(Result, 8));
    end
    else
      Result := HResultFromWin32(ERROR_FUNCTION_NOT_CALLED);
    FreeLibrary(LibHandle);
  end
  else
  begin
    LogMSIMessage(hInstall, 'Could not load: ' + FileName);
    Result := HResultFromWin32(ERROR_FILE_NOT_FOUND);
  end;
  LogMSIMessage(hInstall, 'End RegisterAddIn()');
end;

The UnregisterAddIn function looks exactly the same, but it calls the DllUnregisterServer function instead.

The function first retrieves information from an MSI property called CustomActionData. Because our custom action is going to be executed outside of the normal installation session, we cannot use properties set during the normal installation session. We therefore use a special property called CustomActionData that we will fill with the information we need just before calling our custom action. It will contain the file name of our Add-In dll, so after retrieving that property, we load the dll by calling LoadLibrary, find the address of the DllRegisterServer function and then call that function, returning whatever value this function returns.

This example uses two helper functions: one to retrieve an MSI property (just a wrapper function around the MSIGetProperty function) and another to log messages for debugging purposes. They are implemented as follows:

{ Procedure to write messages to the MSI log file }
procedure LogMSIMessage(hInstall: MSIHANDLE; const Msg: string);
var
  RecordHandle: MSIHANDLE;
begin
  RecordHandle := MsiCreateRecord(2);
  MsiRecordSetString(RecordHandle, 0, 'MyCustomAction [1]: [2]');
  MsiRecordSetString(RecordHandle, 1, PChar(FormatDateTime('h:nn:ss', Now)));
  MsiRecordSetString(RecordHandle, 2, PChar(Msg));
  MsiProcessMessage(hInstall, INSTALLMESSAGE_INFO, RecordHandle);
  MsiCloseHandle(RecordHandle);
end;
 
{ Function to retrieve an MSI property }
function GetMSIProperty(hInstall: MSIHANDLE; const Name: string): string;
var
  Buffer: PChar;
  CharacterCount: DWord;
begin
  CharacterCount := 2048;
  GetMem(Buffer, CharacterCount * SizeOf(Char));
  MsiGetProperty(hInstall, PChar(Name), Buffer, CharacterCount);
  Result := Buffer;
  FreeMem(Buffer);
end;

After building our custom action dll, we have to add it to our WiX script. Under the Product element, add the following:

<Binary Id="MyCustomActions" SourceFile="MyCustomActions.dll" />
 
<CustomAction Id="RegisterAddIn" BinaryKey="MyCustomActions" DllEntry="RegisterAddIn"
     Execute="deferred" Impersonate="no" />
<CustomAction Id="UnregisterAddIn" BinaryKey="MyCustomActions" DllEntry="UnregisterAddIn"
     Execute="deferred" Impersonate="no" />
<CustomAction Id="RegisterAddIn.SetProperty" Property="RegisterAddIn" Value="[#MainFile]" />
<CustomAction Id="UnregisterAddIn.SetProperty" Property="UnregisterAddIn"
     Value="[#MainFile]" />

We declare two custom actions to call the RegisterAddIn and UnregisterAddIn function of our custom action dll. We set the Execute property to deferred so that our custom action is called when the installation script is executed. If we don't do this, we either don't have access to the Add-In dll, because it hasn't been installed yet, or we cannot register our Add-In because we don't have administrator privileges anymore. And since we need administrator privileges to register our Add-In, we also have to set the Impersonate property to no. If we don't set this property, it defaults to yes, which will cause our custom action to be executed as a normal user.

We also declare two custom actions to set a property to the value of the installation path of our main Add-In dll. Make sure you change the Value attribute to the Id of your File element that installs the Add-In dll. These two custom actions will set the CustomActionData property, that we will retrieve in our custom action dll. Make sure that the Property attribute is set to the exact same name as the Id of the custom action where we will be retrieving that property.

The Impersonate property is only necessary for Windows Vista and above. On Windows XP and below we can only run the installation as an administrator and therefore no impersonation can take place. If we do add the Impersonate attribute, installing our Add-In will be successful, but uninstalling will again fail to remove the toolbar from the host application. So, if we are running on Windows XP and below, we will not use the Impersonate property, and if we are running Windows Vista and above, we will use it. To differentiate between the two situations, we have to create two separate sets of custom actions. The following custom actions replace the four we added earlier.

<CustomAction Id="RegisterAddInXP" BinaryKey="MyCustomActions" DllEntry="RegisterAddIn"
     Execute="deferred" />
<CustomAction Id="UnregisterAddInXP" BinaryKey="MyCustomActions" DllEntry="UnregisterAddIn"
     Execute="deferred" />
<CustomAction Id="RegisterAddIn.SetProperty.XP" Property="RegisterAddInXP"
     Value="[#MainFile]" />
<CustomAction Id="UnregisterAddIn.SetProperty.XP" Property="UnregisterAddInXP"
     Value="[#MainFile]" />
<CustomAction Id="RegisterAddInVista" BinaryKey="MyCustomActions" DllEntry="RegisterAddIn"
     Execute="deferred" Impersonate="no" />
<CustomAction Id="UnregisterAddInVista" BinaryKey="MyCustomActions" DllEntry="UnregisterAddIn"
     Execute="deferred" Impersonate="no" />
<CustomAction Id="RegisterAddIn.SetProperty.Vista" Property="RegisterAddInVista"
     Value="[#MainFile]" />
<CustomAction Id="UnregisterAddIn.SetProperty.Vista" Property="UnregisterAddInVista"
     Value="[#MainFile]" />

The only thing left to do is make sure that our custom actions are called during the installation. To do this, add the following to the InstallExecuteSequence element:

<Custom Action="RegisterAddIn.SetProperty.XP" 
     After="RegisterComPlus">Not Installed And (VersionNT &amp;lt; 600)</Custom>
<Custom Action="RegisterAddInXP" 
     After="RegisterAddIn.SetProperty.XP">Not Installed And (VersionNT &amp;lt; 600)</Custom>
<Custom Action="RegisterAddIn.SetProperty.Vista" 
     After="RegisterAddInXP">Not Installed And (VersionNT &amp;gt;= 600)</Custom>
<Custom Action="RegisterAddInVista" 
     After="RegisterAddIn.SetProperty.Vista">Not Installed And (VersionNT &amp;gt;= 600)</Custom>

The first action we call is the one that sets the CustomActionData property for us. We call this right after the RegisterComPlus action. Next, we call our own custom action. Both actions are only executing when we are installing our product, hence the Not Installed condition. Likewise, we add the following two, that are executed when uninstalling our product.

<Custom Action="UnregisterAddIn.SetProperty.XP" 
     After="UnregisterComPlus"> Installed And (VersionNT &amp;lt; 600)</Custom>
<Custom Action="UnregisterAddInXP" 
     After="UnregisterAddIn.SetProperty.XP"> Installed And (VersionNT &amp;lt; 600)</Custom>
<Custom Action="UnregisterAddIn.SetProperty.Vista" 
     After="UnregisterAddInXP"> Installed And (VersionNT &amp;gt;= 600)</Custom>
<Custom Action="UnregisterAddInVista" 
     After="UnregisterAddIn.SetProperty.Vista"> Installed And (VersionNT &amp;gt;= 600)</Custom>

And that's all it takes to install and register your Add-In using the WiX toolset.

2 Comments

  • Lev Waisberg - Offisync says:

    Hello,

    I’m using wix and what i did is to add the adxloader.dll and manifest as part of the files in the installation-directory and add the following custom actions:

    And the following action runs (under ):
    (NOT UPGRADINGPRODUCTCODE) AND (REMOVE=”ALL”)

    NOT Installed

    It seems to work fine in all the test we ran so far (the wix-installer version still isn’t in production).
    Can you please elaborate how the method above is better than what i’m using?

    Thanks,
    Lev Waisberg
    Senior Developer – Offisync

  • Fokke Post says:

    Hi Lev,

    I’m not sure whether I fully understand your question. It seems to me that parts are missing, as you talk about custom actions, which I cannot see.

    The method I describe – at least the first part – leaves the registration of the add-in up to the installer logic. There’s no need for any custom actions. My problem was that uninstalling my add-in left the office toolbar it created – now completely useless – in PowerPoint. I solved that by making the toolbar temporary by setting the Temporary property to True. (This only applies to Office 2003 and less. If you’re only targeting Office 2007 and above, then you don’t have to worry about temporary toolbars and you can leave the installation and uninstallation up to the installer logic.)

    The second part describes how you can use a custom action to manually call the DllRegisterServer function to install, and the DllUnregisterServer to uninstall your add-in. You would only need this if you are targeting Office 2003 and less, and cannot make your toolbar temporary.

    You mention the adxloader.dll and a manifest, so I assume you have a .NET add-in. I must admit that I’m not too familiar with .NET. The method I described (second part) works for VCL add-ins.

    Best regards,
    Fokke Post

Post a comment

Have any questions? Ask us right now!