WiX installer for VSTO projects
A few people have been asking me for something akin to an MSI-based installer for my NoReplyAll tool but I've not been able to treat this as particularly high priority. Those of you with Office 2013 might have noticed Outlook pointing the finger of shame at add-ins which take what it deems to be too long to start up and, alas, NoReplyAll is one of those. I knew the add-in was doing almost nothing at startup, and a brief performance analysis showed up nothing. What was worse, when I ran the project with a temporary signing key, it started promptly, but the deployed version signed with Microsoft's key was slow. I stumbled across this VSOD blog post which had a few suggestions; to cut a long story short, the change that had most effect was to use VSTO fast path... And that boils down to having the add-in installed under %ProgramFiles%, which means I've now got a much stronger need for an MSI installer. As it happens, it's also the case that Visual Studio 2012 removed the simple (and limited) installer builder, so I thought I'd treat this as an exercise in learning WiX.
When you install WiX, you get a bunch of new project types: the first of interest here is the "Setup Project" - this groups together output of other projects into a MSI file (there are other options, but this is what I was interested in). The first step is to add references into the WiX project for the projects to incorporate, but there's a small snag. For other project types that I've tried, the WiX UI in Visual Studio does this just fine, but the UI for adding references doesn't seem to recognise VSTO projects at all. However, WiX does understand the dependency, it's just the UI that doesn't, so you can add the project reference by hand. If you open the .wixproj file in a text editor, references to other projects (and WiX libraries) appear directly after this block (as you'll see if you happen to have any other references):
<ItemGroup>
<Compile Include="Product.wxs" />
</ItemGroup>
If my NoReplyAll project happened to live in a sibling directory to the WiX project, I could add an ItemGroup like the following:
<ItemGroup>
<ProjectReference Include="..\NoReplyAll\NoReplyAll.csproj">
<Name>NoReplyAll</Name>
<Project>{04646315-0BAE-450D-A972-C2C095E040F6}</Project>
<Private>True</Private>
<DoNotHarvest>True</DoNotHarvest>
<RefProjectOutputGroups>Binaries;Content;Satellites</RefProjectOutputGroups>
<RefTargetDir>INSTALLFOLDER</RefTargetDir>
</ProjectReference>
</ItemGroup>
The <Project> GUID can be found in the NoReplyAll.csproj file or in the solution file referencing both projects.
You can, of course, use WiX without a project reference, but the reference lets you use some convenient WiX variables and, more importantly, build dependencies will be handled automatically.
The core of a WiX project is a bunch of components (defining things - files, registry settings, etc. - to be installed) and features (to determine which components to install). There are many ways to organise a WiX project into separate sections or files but I lobbed everything in one bucket for this small project. My Product.wxs components and single feature look like:
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFilesFolder">
<Directory Id="INSTALLFOLDER" Name="NoReplyAll">
<Component Id="CMP_AddIn">
<File Id="FILE_AddIn" Source="$(var.NoReplyAll.TargetPath)" KeyPath="yes" />
</Component>
<Component Id="CMP_DllManifest">
<File Id="FILE_DllManifest" Source="$(var.NoReplyAll.TargetPath).manifest" KeyPath="yes" />
</Component>
<Component Id="CMP_VstoManifest">
<File Id="FILE_VstoManifest" Source="$(var.NoReplyAll.TargetDir)$(var.NoReplyAll.TargetName).vsto" KeyPath="yes" />
<RegistryKey Root="HKLM" Key="Software\Microsoft\Office\Outlook\Addins\NoReply">
<RegistryValue Name="Description" Value="NoReplyAll Add-In" Type="string" Action="write" />
<RegistryValue Name="FriendlyName" Value="NoReplyAll" Type="string" Action="write" />
<RegistryValue Name="LoadBehavior" Value="3" Type="integer" Action="write" />
<RegistryValue Name="Manifest" Value="[#FILE_VstoManifest]|vstolocal" Type="string" Action="write" />
</RegistryKey>
</Component>
<Component Id="CMP_ToolsCommon">
<File Id="FILE_ToolsCommon" Source="$(var.NoReplyAll.TargetDir)Microsoft.Office.Tools.Common.v4.0.Utilities.dll" KeyPath="yes" />
</Component>
<Component Id="CMP_ToolsOutlook">
<File Id="FILE_ToolsOutlook" Source="$(var.NoReplyAll.TargetDir)Microsoft.Office.Tools.Outlook.v4.0.Utilities.dll" KeyPath="yes" />
</Component>
</Directory>
</Directory>
</Directory>
<Feature Id="ProductFeature" Title="NoReplyAll" Level="1">
<ComponentRef Id="CMP_AddIn" />
<ComponentRef Id="CMP_DllManifest" />
<ComponentRef Id="CMP_VstoManifest" />
<ComponentRef Id="CMP_ToolsCommon" />
<ComponentRef Id="CMP_ToolsOutlook" />
</Feature>
The files are those that appear in the bin directory when you build the VSTO add-in, and the registry settings are those defined in this MSDN page. Note that I've placed the registry items under HKLM (rather than the HKCU that ClickOnce favours) - that seemed sensible since the files are going into a machine-wide location rather than user space, but could cause a problem with people who have Office 2007 since that doesn't look for add-ins under HKLM by default, but can be persuaded to via another registry setting.
Rather than require users to set this value, we can add it to the installer: another component (not forgetting the ComponentRef in the feature element).
<Component Id="CMP_Office2007Hklm" Permanent="yes">
<RegistryKey Root="HKLM" Key="Software\Microsoft\Office\12.0\Common\General">
<RegistryValue Name="EnableLocalMachineVSTO" Value="1" Type="integer" KeyPath="yes" />
</RegistryKey>
</Component>
We could make this conditional on detection of Outlook 2007, since there's no need for it otherwise. (There's also no harm - other than annoying untidiness - in adding this registry key unconditionally.) To make it conditional, we first need a flag to test, defined as the following:
<Property Id="OFFICE2007">
<RegistrySearch Id="Office2007_Installed"
Root="HKLM" Key="Software\Microsoft\Office\12.0\Outlook\InstallRoot" Name="Path" Type="raw" />
</Property>
There may be other ways to detect the presence of Outlook 2007, but this does appear to work - it might falsely trigger on machines which have had Office 2007 and then were upgraded to a newer Office, but I've not been able to examine any such machines to check. Once that variable's defined, the component above can be extended as shown in bold below:
<Component Id="CMP_Office2007Hklm" Permanent="yes">
<Condition><![CDATA[OFFICE2007]]></Condition>
<RegistryKey Root="HKLM" Key="Software\Microsoft\Office\12.0\Common\General">
<RegistryValue Name="EnableLocalMachineVSTO" Value="1" Type="integer" KeyPath="yes" />
</RegistryKey>
</Component>
If you compile this, you'll end up with a MSI file - and a few others. You need to tell WiX to combine everything with an EmbedCab attribute to tell WiX to put everything in one file:
<MediaTemplate EmbedCab="yes" />
Now you'll end up with a single MSI file, and double clicking it will install the add-in. No UI though, and no checking that you have the necessary prerequisites (viz, .NET and the VSTO runtime). Adding a UI is easy: add a reference to WiXUIExtension, and then the following line to the end of the product element in Product.wxs:
<UIRef Id="WixUI_Minimal"/>
That was easy! You can set variables to tweak the UI or add a license, but that's the core.
Prerequisite checking is similar to the condition checking above. Some commonly used items have been prepared by the WiX authors already, such as .NET. My prerequisite checking section looks like:
<PropertyRef Id="NETFRAMEWORK40CLIENT" />
<Condition Message="This tool requires .NET Framework 4.0. Please install the .NET Framework then run this installer again.">
<![CDATA[Installed OR NETFRAMEWORK40CLIENT]]>
</Condition>
<Property Id="VSTOR40">
<RegistrySearch Id="VSTOR_Installed"
Root="HKLM" Key="SOFTWARE\Microsoft\VSTO Runtime Setup\v4R" Name="VSTORFeature_CLR40" Type="raw" />
</Property>
<Condition Message="This tool requires the VSTO 4.0 Runtime. Please install the VSTO runtime then run this installer again.">
<![CDATA[Installed OR (VSTOR40 OR NOT OFFICE2007)]]>
</Condition>
NETFRAMEWORK40CLIENT is defined in WixNetFxExtension, which needs to be added to project references, and notice that the VSTO runtime check includes a reference to Office 2007. The add-in works with Outlook 2007, 2010 and 2013: however, the runtime only needs to be installed for Office 2007, so there's no point in checking for it for the other versions of Office. Of course, I ought to include a check that the machine does in fact have one of those versions of Office installed but, hey, if you want to use an Outlook add-in on a machine without Outlook, you don't need me to tell you it's not going to work.
It's all fine checking for these prerequisites, but it would be nicer still to go out and grab them, and that's where another WiX project template comes into play, the Bootstrapper Project. As before the first thing to do is add project references - to the previously created setup proiect and, while you're there, to WiX packages WixBalExtension, WixNetFxExtension and WixUtilExtension. The bootstrapper Bundle.wxs file can be viewed as three separate chunks for this project. First comes the UI definition:
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
<bal:WixStandardBootstrapperApplication LicenseFile="mylicense.rtf"
SuppressOptionsUI="yes" LogoFile="logo.png" />
</BootstrapperApplicationRef>
I want a click through license to be shown so specify it here (it is possible to have no license, or to link to it instead of display on the install wizard), and the logo bitmap (64x64 pixels) appears on the top left of the window. As shown earlier, I used a registry search to look for Office 2007 and the VSTO runtime - I need to do the same here, but have to use a different mechanism:
<util:RegistrySearch Id="Office2007_Installed" Variable="OFFICE2007"
Root="HKLM" Key="Software\Microsoft\Office\12.0\Outlook\InstallRoot" Result="exists" />
<util:RegistrySearch Id="VSTOR_Installed" Variable="VSTOR40"
Root="HKLM" Key="SOFTWARE\Microsoft\VSTO Runtime Setup\v4R" Result="exists" />
The final section is the "chain" of the two prerequisites and the MSI file:
<Chain>
<PackageGroupRef Id="NetFx40Web" />
<ExePackage SourceFile="vstor_redist.exe" Permanent="yes" Vital="yes" Cache="no" Compressed="no"
DownloadUrl="https://go.microsoft.com/fwlink/?LinkId=158917"
InstallCommand="/q /norestart"
DetectCondition="VSTOR40"
InstallCondition="OFFICE2007 AND NOT VSTOR40" />
<MsiPackage SourceFile="$(var.NoReplyAllAddInSetup.TargetPath)" Vital="yes" />
</Chain>
The first child element is the very convenient .NET framework check, defined in WiXNetFxExtension; the second is for the VSTO runtime and, again, I've made it conditional on having Office 2007. Note that you do need to have a copy of vstor_redist in the project directory to be able to build, though there's no need to distribute it since the bootstrapper will use the supplied URL if it can't find the file locally. And the third element is the MSI previously built.
Build that, and you end up with a single executable which takes care of everything for you - very nice.