Chris Surfleet's MentalReboot

Code insights tinged with a spot of insanity

After reading this post from Sayed (a great blogger by the way!) I downloaded and installed the VS2010 Web Publish Updates. All was well until I realised that all my WiX projects were now showing as unavailable in solution explorer. The system is failing looking for a .wpp.targets file which does not exist. I've tried all sorts of things including:

  • creating the file it wants (it still thinks it doesn't exist)
  • overriding the publish profile properties in the wixproj file (no change)
  • using alternative targets (no change)

After 4hrs of digging around in the 10.5 targets files I'm drawing a blank on all fronts. I'll keep working on it but in the meantime I'd recommend staying off the upgrade.

If you're a brave soul and want to help me fix this, I'm keeping a question on StackOverflow updated with my attempts, get involved!

Did you find this post useful or interesting? I'd be really grateful if you could return the favour by clicking this google link:



Hey, it’s been a while! The usual #lifegetsintheway excuses all apply, but you knew that right? Fear not though, treasured reader, for today we are going to delve into the adrenaline-filled world of MsBuild/TFS interaction!

But Why?

There are plenty of reasons for wanting to interact with your TFS server from MsBuild. Maybe you want to ensure that you always get the latest version of a referenced DLL on each build? You could need to update a source-controlled log file when a certain configuration is built on your continuous integration machine? All of these sorts of things are possible and I’ll be going over some of the more complex scenarios in future articles. For now though I want to fix a bit of an annoyance with the web or service installers I went through in previous posts. If you haven’t worked through these don’t worry, you can get the finished source for the service installer here: WixServiceInstallerExample.zip (184.90kb). (You will need WiX installed, but that is a good thing)

Lets Go

Lets start by recreating the problem; step one is to add the service installer solution to source control and check everything in. Once done, try to build the installer project. Oh no!

Unable to execute transformation. Access to the path ‘……..’ is denied.

What is happening here? The installer build process needs write access to the file WixServiceInstallerExample.wxs, which is denied because it is currently checked in. You can verify this by checking the file out and building again, which will work. So all we need to do is make this automatic.

Open up the project file for the installer (right click > Unload project, right click > Edit WixServiceInstaller.wixproj). Add the following three properties to the top PropertyGroup element:

<TfsExeLocation>$(Vs110ComnTools)..\IDE\tf.exe</TfsExeLocation>
<CheckinComment>Checkin for installer build.</CheckinComment>
<CheckinOverrideReason>Checkin for new installer build.</CheckinOverrideReason>

The first item is the path to the ft.exe file on your computer. Unfortunately we have to do all of this stuff using the command line! The nice thing about defining it here though is that if you have a wacky install on your build box like I had to deal with for one client, causing the TFS build to break, you can override this property in your TFS build definition. If you are using VS2010 then use Vs100ComnTools instead.

The other two items are there for when we check the file back in again, ensuring we don’t get any policy blocks on checkins.

Now, right at the beginning of the BeforeBuild target at the bottom of the file, add this line:

<Exec Command=""$(TfsExeLocation)" checkout %(ProjectReference.Filename).wxs"
               Condition="'%(ProjectReference.ContentProject)'=='True'" />

The Exec command is deliciously powerful; suddenly the power of any command-line tool you can find or build is at your beck and call. There really isn’t a huge amount to this; we pass a ‘checkout’ command and the path to the wxs file to the ef.exe command. Next add the following right at the end of the BeforeBuild target:

<Exec Command=""$(TfsExeLocation)" checkin %(ProjectReference.Filename).wxs
                 /comment:"$(CheckinComment)" /noprompt
                 /override:"$(CheckinOverrideReason)""
        Condition="'%(ProjectReference.ContentProject)'=='True'" />

This time we’re checking the file in. It’s a pretty similar command to the last one but we are just passing a few more arguments specifying comments and policy override reasons to be stored next to the checkin. The noprompt option tells tf.exe not to pop up any dialogs etc.

Check out the show!

That was easy huh? You can go ahead and reload the project now. Make sure all files are checked in and run the build. Keep watching the solution explorer window and you should see the wxs file’s icon changing to checked out, and back to checked in as the build progresses. Happy days.

Limitations

Just bear in mind that this code is always run when you build. That means that even if the wxs file is checked out before you build, it will end up checked in afterwards.

Hopefully this post will have been of use to someone, but I’m off to get started on the Christmas celebrations. Merry Christmas to those of you who celebrate it!

Did you find this post useful or interesting? I'd be really grateful if you could return the favour by clicking this google link:



A quick one today guys, it’s Friday and I’m sure you have beer to drink, so we’ll keep this short!

In my last post on creating WiX windows service installers you might have noticed a glaring omission. If you try to install the service on a box where you don’t have enough permissions to install or start a service then the install will bomb out near the end. It doesn’t cause any real problems, but it would be nicer if it warned you right at the start instead of letting you through all the screens first.

Fear not though, for it is a really easy change. There is a WiX element called Condition, which has its condition statement as its text, and a Message attribute for the message to display on failure. Here are a few examples of things you can do, although there are a loads of other possibilities! These can be placed right after the Package node in your Product.wxs file.

Require Admin Rights

<Property Id="MSIUSEREALADMINDETECTION" Value="1" />
<Condition
  Message="This installer can only be used by admin users."
  >Installed OR (Privileged AND AdminUser)</Condition>

Require .Net 4.0

<PropertyRef Id="NETFRAMEWORK40FULL"/>
<Condition
  Message="This application requires .net version 4.0 or higher."
  >Installed OR NETFRAMEWORK40FULL</Condition>

Require IIS 7

<PropertyRef Id="IISMAJORVERSION"/>
<Condition
  Message="This application requires IIS 7 or higher."
  >IISMAJORVERSION >= "#7"</Condition>

 

And that's it! Try it out.

Did you find this post useful or interesting? I'd be really grateful if you could return the favour by clicking this google link:



Those of you who’ve been following my blog lately will know that I’ve been writing a lot on the subject of WiX installers. Basically I just think they’re awesome! The previous set of posts focused on the basics and then some of the specific stuff you can do with website installers. This time I’m going to walk through the creation of a service installer, and introduce you to a simple technique that I’ve found loads of uses for.

I’m assuming you already know the basics, if you’ve never touched WiX and don’t know about running the Heat command from your project file take a look at the first two posts on the subject.

What Do We Want From This Installer?

A good installer will take all of the complications out of installing a product, and this one should be no different. Our installer should:

  • Install to any given directory;
  • Start automatically;
  • Pull the correct configuration information in with it.

Not a long list, so this shouldn't take long!

Creating The Service

Let’s start by creating a new windows service, mine’s called WixServiceInstallerExample but you could call yours something more boring if you like. In the Service1 code-behind remove the two overridden methods; we don’t actually need our service to do anything for this example!

Creating The Installer

OK, now that the service is created, let’s get on with the installer. Add a new WiX Setup project to the solution. You need to add a reference to your service project first, then references to the WixNetFxExtension and WixUIExtension dll files from the WiX install folder.

OK, now we’ll make a couple of changes to the Product.wxs file. For now we’re just ripping out a bit of the stuff we don’t need and telling WiX to use the WixUI_InstallDir standard user interface. Replace the Directory and Feature nodes with the following:

<Directory Id="TARGETDIR" Name="SourceDir">
  <Directory Id="INSTALLLOCATION" Name="WixServiceInstaller">
  </Directory>
</Directory>

<Feature Id="ProductFeature" Title="WixServiceInstaller" Level="1">
  <ComponentGroupRef Id="WixServiceInstallerExample_Project" />
</Feature>

<Property Id="WIXUI_INSTALLDIR">INSTALLLOCATION</Property>
<UIRef Id="WixUI_InstallDir" />

We’ll be back in here later to do some cool service-oriented stuff, but for now you can close the file.

Next we’ll do something fairly similar to what we did with heat in a previous post, with a couple of funky changes. Unload the installer project and edit the project file.  Locate the ProjectReference node which points to your referenced project and add the following as its last child:

<ContentProject>True</ContentProject>

We’ll use this to ensure we don’t end up running heat on every reference. Now add a new BeforeBuild target right before your closing </Project> tag:

<Target Name="BeforeBuild">
  <MSBuild Projects="%(ProjectReference.FullPath)" Targets="Build"
    Properties="Configuration=$(Configuration);Platform=AnyCPU"
    Condition="'%(ProjectReference.ContentProject)'=='True'" />
  <PropertyGroup>
    <DefineConstants>BasePath=%(ProjectReference.RootDir)%(ProjectReference.Directory)bin\$(Configuration);</DefineConstants>
    <LinkerBaseInputPaths>%(ProjectReference.RootDir)%(ProjectReference.Directory)bin\$(Configuration)\</LinkerBaseInputPaths>
  </PropertyGroup>
  <HeatDirectory OutputFile="%(ProjectReference.Filename)-temp.xml"
    Directory="%(ProjectReference.RootDir)%(ProjectReference.Directory)bin\$(Configuration)\"
    DirectoryRefId="INSTALLLOCATION"
    ComponentGroupName="%(ProjectReference.Filename)_Project"
    SuppressCom="true"
    SuppressFragments="true"
    SuppressRegistry="true"
    SuppressRootDirectory="true"
    AutoGenerateGuids="false"
    GenerateGuidsNow="true"
    ToolPath="$(WixToolPath)"
    PreprocessorVariable="var.BasePath"
    Condition="'%(ProjectReference.ContentProject)'=='True'" />
  <XslTransformation
    XmlInputPaths="%(ProjectReference.Filename)-temp.xml"
    XslInputPath="XslTransform.xslt"
    OutputPaths="%(ProjectReference.Filename).wxs"
    Condition="'%(ProjectReference.ContentProject)'=='True'" />
  </Target>

So what’s going on here? Firstly we build the required project to make sure it’s available for heat. Then we run heat on the built content just like we would with a published web project. Notice though that we’re outputting to a temp.xml file. This is because the xml file definitions created by heat aren’t quite what we need in this case. Instead we output to the temporary file and then in the final step we perform an xsl transformation, saving the final file as WixServiceInstallerExample.wxs to be used by the installer.

You’re probably wondering what we need to do this little transformation for. The problem we have is that we need to do some specific service oriented stuff with the actual WixServiceInstallerExample.exe file in our Product file. If we define one file in two places WiX will have a fit, so to keep the peace we’ll remove it from the auto generated file. We’re done with the project file so close it and reload the project in visual studio. Add a new XslTransform.xslt file to your project (You can do this by adding a text file but just giving it an xslt extension).

 

Add the following to the file:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  version="1.0"
  xmlns:w="http://schemas.microsoft.com/wix/2006/wi">

  <xsl:template match="@*|node()">
    <xsl:copy>
      <xsl:apply-templates select="@*|node()" />
    </xsl:copy>
  </xsl:template>

  <xsl:template match="w:Component[w:File/@Source='$(var.BasePath)\WixServiceInstallerExample.exe']">
    <w:Component>
      <xsl:attribute name="Id">
        <xsl:value-of select="@Id"/>
      </xsl:attribute>
      <xsl:attribute name="Guid">
        <xsl:value-of select="@Guid"/>
      </xsl:attribute>
      <xsl:attribute name="KeyPath">yes</xsl:attribute>
    </w:Component>
  </xsl:template>

</xsl:stylesheet>

It’s not really too complicated. We duplicate everything in the source file except for the exe file, which is removed. Awesome, we’re nearly there now! Build your project and you should get an error about an unresolved reference to WixServiceInstallerExample_Project. This is because it has been created but not yet added to the project. Click to view hidden files and include the file WixServiceInstallerExample.wxs in your project.

If you take a look in this file you’ll notice that the first Component doesn’t have a File in it – this is where we stripped out the main exe.

You should be able to build your project now, but it’s still not installing any service specific stuff. Open up your Product.wxs file and add the following inside the INSTALLLOCATION Directory node:

<Component Id="ServiceComponent" Guid="YOUR_GUID_HERE">
  <CreateFolder />
  <File Id="MainExecutable" KeyPath="yes"
    Source="$(var.BasePath)\WixServiceInstallerExample.exe" />
  <ServiceInstall Id="ServiceInstaller"
    Type="ownProcess"
    Vital="yes"
    Name="WixServiceInstallerExample"
    DisplayName="Wix Service Installer Example"
    Description="An example service."
    Start="auto"
    Account="LocalSystem"
    ErrorControl="ignore"
    Interactive="no" />
  <ServiceControl Id="StartService"
    Start="install"
    Stop="both"
    Remove="uninstall"
    Name="WixServiceInstallerExample"
    Wait="yes" />
</Component>

It’s that easy. We specify the file we stripped out earlier and then first install and then start the service. All that’s left is to add a reference to the component into your Feature section:

<ComponentRef Id="ServiceComponent" />

Testing

We’re done! Go ahead and build your project and then run the resulting msi file. If you watch the statuses as they flash past you’ll see that the files are copied and then the service is installed and started. Take a look at your services list and it should be visible and started!

Why This Way?

The technique I’ve shown you here of using xslt files for modifying heat output has many more uses than the one outlined above, such as applying permissions on certain files. I’ll do a quick post on how to do that sometime soon.

Adding App.config Transformations

One final little thing; if you have read my post on App.config transformations you’ll know that my favoured way of applying them is during an WiX installer build. This is actually really simple:

  1. Firstly set up the transformations in your service project;
  2. Next unload your installer project and edit the project file;
  3. In the BeforeBuild section, change the MsBuild node so that its Targets attribute is set to TransformAppConfig (or whatever you called your target in the service project);
  4. There is no step 4!

Finished

I hope this post has been useful to somebody. I’ll be following up soon with a couple of posts on using xslt to add file permissions, and how to put pre-requisites on an installer (try running your installer somewhere you don’t have local admin permissions, or somewhere with an old version of the .net framework!) so you should subscribe to my rss feed to be notified when I post them.

Source Code

Full source code is available here: WixServiceInstallerExample.zip (184.90 kb)

Did you find this post useful or interesting? I'd be really grateful if you could return the favour by clicking this google link:



Hello, and welcome back to my series on deployment with WiX. Its been a couple of weeks since the last instalment which has been for a couple of reasons: I took my lovely girlfriend on a trip to Amsterdam (I fully recommend the zoo), and we've been getting ready for the version one release of the application we've been developing since September. This is the largest development we've done with our current team and everyone is understandably on edge - we have to coordinate the installation of 10 web services, 2 web applications, a windows service and 5 SQL databases across a total of 6 servers. The WiX installers have played a large part in keeping me calm though - we've reached the point where they aren't just a nicety but a major time and complication saver - hopefully the live install will take around an hour in total (I'll keep you posted).

Anyway, enough rambling, this post will be dealing with one of the cooler features of WiX; the ability to create your own custom dialog screens to perform any functionality you like. It leans heavily on the work we did in the previous post on IIS Integration so you should really go work through that one first, alternatively you could just start at the beginning.

At the end of the last post we had a working installer which set up all our IIS settings for us, what we'd like to do now though is allow the user to define couple of those settings; namely the virtual directory name, website and application pool. To do this we'll create a new dialog called IisSettingsDlg.

In your installer project, create a new file called IisSettingsDlg.wxs and put the following inside the Fragment node:

<EnsureTable Id="ComboBox"/>
<Property Id="VIRTUALDIRECTORYVALUE" Value="WixInstallerExampleWeb"/>
<UI>
  <Dialog Id="IisSettings" Title="WiX Installer Example" Width="370" Height="270">
    <Control Id="Next" Type="PushButton" Default="yes" Text="Next" X="236" Y="243" Width="56" Height="17">
      <Condition Action="disable">WEBSITEVALUE = "" OR APPPOOLVALUE = "" OR VIRTUALDIRECTORYVALUE = ""</Condition>
      <Condition Action="enable"><![CDATA[WEBSITEVALUE <> "" AND APPPOOLVALUE <> "" AND VIRTUALDIRECTORYVALUE <> ""]]></Condition>
    </Control>
        
    <Control Id="Back" Type="PushButton" Text="Back" X="180" Y="243" Width="56" Height="17" />
    <Control Id="Cancel" Type="PushButton" Cancel="yes" Text="Cancel" X="304" Y="243" Width="56" Height="17">
      <Publish Event="SpawnDialog" Value="CancelDlg">1</Publish>
    </Control>

    <Control Id="Title" Type="Text" Transparent="yes" NoPrefix="yes" Text="IIS Settings" X="15" Y="6" Width="219" Height="28" />

    <Control Id="BannerLine" Type="Line" X="0" Y="44" Width="370" Height="0" />
    <Control Id="BottomLine" Type="Line" X="0" Y="234" Width="370" Height="0" />

    <Control Type="Text" Id="WebSiteLabel" Text="Web Site:" Width="290" Height="10" X="20" Y="105" />
    <Control Id="WebSiteCombo" Type="ComboBox" Property="WEBSITEVALUE" Sorted="yes" ComboList="yes" X="20" Y="117" Width="250" Height="16" />

    <Control Type="Text" Id="AppPoolLabel" Text="Application Pool:" Width="290" Height="10" X="20" Y="140" />
    <Control Id="AppPoolCombo" Type="ComboBox" Property="APPPOOLVALUE" Sorted="yes" ComboList="yes" X="20" Y="152" Width="250" Height="16" />

    <Control Type="Text" Id="VirtualDirectoryLabel" Text="Virtual Directory:" Width="290" Height="10" X="20" Y="175" />
    <Control Type="Edit" Id="VirtualDirectoryTextbox" Property="VIRTUALDIRECTORYVALUE" Width="250" Height="15" X="20" Y="187" />
  </Dialog>
</UI>

There is your custom dialog! So we'll work our way through this. Firstly we have an EnsureTable node, which is making sure we have data for our combo boxes, more on this later. Next up is a property called VIRTUALDIRECTORYVALUE; this will hold the user's choice of virtual directory name and by default is set to WixInstallerExampleWeb.

Gotcha: When using properties they must be defined in all UPPER case. Otherwise you won't get errors, just empty values which can be a pain to debug.

While we're on the subject of properties, we'll be dealing with 3 properties here: VIRTUALDIRECTORYVALUE, WEBSITEVALUE and APPPOOLVALUE. I'll let you take a wild stab of what is held in each!

Up next is the UI itself. We've called our dialog IisSetting to match the filename and inside we have a series of Control nodes which will be pretty easy to figure out if you've ever used html (you have used html right?). The only thing to watch out for is that all these controls are absolutely positioned; their order in the code is not important at all. So what have we got?

  • A next button. This is either enabled or disabled depending on the values of our 3 properties;
  • A back button;
  • A cancel button which will spawn a cancellation dialog when pushed;
  • A title and a couple of banner lines;
  • A web site combo box which stores its selected value in WEBSITEVALUE;
  • An app pool combo box which stores its value in APPPOOLVALUE;
  • A text box to modify the default VIRTUALDIRECTORYVALUE if required.

Not exactly the most taxing exercise ever. The nice thing is that this dialog can now be used for any application installed on IIS, be it a web app or web service.

Next let's include this dialog in the install sequence, if you've been following the sequence this should be easy. In your UserInterface file between the LicenceAgreementDlg and InstallDirDlg sections add this:

<Publish Dialog="IisSettings" Control="Back" Event="NewDialog" Value="LicenseAgreementDlg" Order="1">1</Publish>
<Publish Dialog="IisSettings" Control="Next" Event="NewDialog" Value="InstallDirDlg" Order="1">1</Publish>

This wires up the back and next buttons on the IIS dialog to navigate to the correct screens. You will also need to change the Value attribute of the Next/Back buttons on the LicenceAgreementDlg and InstallDirDlg to point to the new IisSettings dialog. Following OK?

Alrighty, so we have a new dialog and have included it in the dialog flow, but how do we populate the combo boxes with the available sites and app pools? Welcome back screen left, your Custom Action assembly! This already has code for querying sites and app pools so we just need to add a little code to let it communicate with the installer. Open the CustomAction.cs file and add this custom action method:

[CustomAction]
public static ActionResult GetWebSites(Session session)
{
  try
  {
    if (session == null) { throw new ArgumentNullException("session"); }

    View comboBoxView = session.Database.OpenView("select * from ComboBox");

    int order = 1;

    foreach (IisWebSite site in IisManager.GetIisWebSites())
    {
      Record newComboRecord = new Record("WEBSITEVALUE", order++, site.ID, site.Name);
      comboBoxView.Modify(ViewModifyMode.InsertTemporary, newComboRecord);
    }

    return ActionResult.Success;
  }
  catch (Exception ex)
  {
    if (session != null)
      session.Log("Custom Action Exception: " + ex);

    return ActionResult.Failure;
  }
}

What we're doing here is plugging into our GetIisWebSites method and using this to push web site data to the combo box associated with the WEBSITEVALUE property. Easy! We do almost exactly the same with the app pools:

[CustomAction]
public static ActionResult GetAppPools(Session session)
{
  try
  {
    if (session == null) { throw new ArgumentNullException("session"); }

    View comboBoxView = session.Database.OpenView("select * from ComboBox");

    int order = 1;

    foreach (string appPool in IisManager.GetIisAppPools())
    {
      Record newComboRecord = new Record("APPPOOLVALUE", order++, appPool);
      comboBoxView.Modify(ViewModifyMode.InsertTemporary, newComboRecord);
    }

    return ActionResult.Success;
  }
  catch (Exception e)
  {
    if (session != null)
      session.Log("Custom Action Exception " + e);
  }

  return ActionResult.Failure;
}

Again, notice this time we're targeting the APPPOOLVALUE property.

Now to wire the two together. Firstly you'll need the custom action in your installer so go ahead and build it. If you look in the bin folder you should see a file called IisManager.CA.dll; this needs to be added into the root of your installer project (as a file NOT a reference!).

Finally, in your Product.wxs file you need a couple of new sections. After the Media node add:

<Binary Id="IisManager" SourceFile="IisManager.CA.dll" />

This lets us reference the actions in the DLL. Finally, put this after the MapVirtualDirectory CustomAction node:

<CustomAction Id="GetIISWebSites" BinaryKey="IisManager" DllEntry="GetWebSites" Execute="immediate" Return="check" />
<CustomAction Id="GetIISAppPools" BinaryKey="IisManager" DllEntry="GetAppPools" Execute="immediate" Return="check" />

<InstallUISequence>
  <Custom Action="GetIISWebSites" After="CostFinalize" Overridable="yes">NOT Installed</Custom>
  <Custom Action="GetIISAppPools" After="CostFinalize" Overridable="yes">NOT Installed</Custom>
</InstallUISequence>

The first two lines get a reference to the custom action methods, and the InstallUISequence section causes them to be run when the installer is launched.

OK, you can build and run your installer now. You should see your new custom dialog and the combos should be filled with your sites and app pools. Awesome! Only one problem. If you finish the installation you'll notice that your choices have been completely ignored! We've got to map the values back to the entries in the Product.wxs file.

If you remember from earlier, the way to slot a property value in somewhere is using square brackets. Here is a list of the things in Product.wxs you need to change (working from the top):

  • iis:WebSite node: change the SideId attribute to "[WEBSITEVALUE]";
  • iis:WebAppPool node: change the Name attribute to "[APPPOOLVALUE]";
  • iis:WebVirtualDir node: change the Alias attribute to "[VIRTUALDIRECTORYVALUE]";
  • iis:WebApplication node: change the Name attribute to "[VIRTUALDIRECTORYVALUE]";
  • The MapVirtualDirectory CustomAction node: change the ExeCommand to '[ASPNETREGIIS] -norestart -s "W3SVC/[WEBSITEVALUE]/ROOT/[VIRTUALDIRECTORYVALUE]"'.

Phew! This drops the values we've set on the custom dialog into our install process. You're all done now so go have a play! This is the end of the current sequence of posts, but I'll be writing soon about some of the topics mentioned earlier such as file permissions and service installation, so you should subscribe to my feed to be notified when they are posted.

Source Code

The complete source code for this example is available here: WixInstallerExample-05.zip (2.16 mb)

Did you find this post useful or interesting? I'd be really grateful if you could return the favour by clicking this google link:



Welcome back to my series on the Wix installer technologies. Previously we've looked at:

Today we're going to start dealing with a much more complex topic - adding IIS integration. To do this we'll be using the built in WiX IIS extensions, creating and calling some custom C# code and adding a new dialog screen for the options. This is where our installer starts to become really useful, and hopefully it will open your eyes to some of the possibilities of what you can automate. This article will show you the first part of this function - accessing IIS from C# code and connecting WiX to IIS. Next time we'll build on that to add a custom dialog allowing the user to select their choice of websites and app pools.

If you haven't been following the series so far, you can find the code so far here: WixInstallerExample-03.zip (731.69 kb)

We're going to start this from the wrong end logically, by writing some C# code to access IIS and read out Website and App pool information.

WiX Custom Actions

When we want to communicate with .net code from WiX we use a Custom Action assembly. This is just a normal .net assembly with all its dependencies packaged up within it and given a .CA.DLL extension. We can then access this from the WiX declarations. So now go ahead and add a new Custom Action Project called IisManager to your solution. This will open up a single file called CustomAction.cs, which will be the class WiX communicates with later. For now though you can close it down and create a new class called IisWebSite:

public class IisWebSite
{
  public IisWebSite(string id, string name)
  {
    ID = id;
    Name = name;
  }

  public string ID { get; set; }
  public string Name { get; set; }
}

This will hold the details of any web sites we find.

Next we need the logic for actually communicating with IIS. As we'll be supporting both IIS 6 and 7 we need 2 assemblies here, so import System.DirectoryServices and Microsoft.Web.Administration. Next create a new static class called IisManager and create a property called IisVersion:

public static int IisVersion
{
  get
  {
    int iisVersion;

    using (RegistryKey iisKey = Registry.LocalMachine.OpenSubKey(@"Software\Microsoft\InetStp"))
    {
      if (iisKey == null)
        throw new Exception("IIS is not installed.");

      iisVersion = (int)iisKey.GetValue("MajorVersion");
    }

    return iisVersion;
  }
}

This simply reads the version number out of the registry or warns us IIS is not installed. Next up is reading websites from IIS 6:

public static IList<IisWebSite> GetIis6Sites()
{
  List<IisWebSite> sites = new List<IisWebSite>();
  using (DirectoryEntry iisRoot = new DirectoryEntry("IIS://localhost/W3SVC"))
  {
    iisRoot.RefreshCache();

    sites.AddRange(iisRoot.Children.Cast<DirectoryEntry>().
      Where(w => w.SchemaClassName.ToLower(CultureInfo.InvariantCulture) == "iiswebserver").
      Select(w => new IisWebSite(w.Name, w.Properties["ServerComment"].Value.ToString())));
  }

  return sites;
}

This uses DirectoryEntry (the same class you'd use for accessing network user information) to identify IIS sites by the SchemaClassName and map the info to an IisWebSite object.

The IIS7 version is very similar but uses an IIS-specific set of classes:

public static IList<IisWebSite> GetIis7UpwardsSites()
{
  List<IisWebSite> sites = new List<IisWebSite>();

  using (ServerManager iisManager = new ServerManager())
  {
    sites.AddRange(iisManager.Sites.Select
      (s => new IisWebSite(s.Id.ToString(CultureInfo.InvariantCulture), s.Name)));
  }

  return sites;
}

Great, let's just add one last little method to bring it all together for ease of use later:

public static IList<IisWebSite> GetIisWebSites()
{
  return (IisVersion < 7) ? GetIis6Sites() : GetIis7UpwardsSites();
}

The app pools are similar but slightly simpler as we're just dealing with names so we can use strings instead of a custom object:

public static IList<string> GetIis6AppPools()
{
  List<string> pools = new List<string>();
  using (DirectoryEntry poolRoot = new DirectoryEntry("IIS://localhost/W3SVC/AppPools"))
  {
    poolRoot.RefreshCache();

    pools.AddRange(poolRoot.Children.Cast<DirectoryEntry>().Select(p => p.Name));
  }

  return pools;
}

public static IList<string> GetIis7UpwardsAppPools()
{
  List<string> pools = new List<string>();

  using (ServerManager iisManager = new ServerManager())
  {
    pools.AddRange(iisManager.ApplicationPools.Select(p => p.Name));
  }

  return pools;
}

public static IList<string> GetIisAppPools()
{
  return (IisVersion < 7) ? GetIis6AppPools() : GetIis7UpwardsAppPools();
}

Now is a good time to note down the details of the IIS site and app pool you'll be using. You could create a little unit test to load each and step through if you like, I created a really simple windows app which can be reused later (it's included in the code download at the end of the article):

We'll leave the custom action alone for a moment now and get back to our WiX installer.

WiX Native IIS Integrations

Here we're going to connect our installer to a specific site and app pool, so you'll need the details you noted down above. Firstly, let's get a reference to the IIS functionality. Add a reference into your installer project to WixIIsExtension.dll and WixNetFxExtension.dll from the WiX install's bin folder. This will allow us to play with websites, app pools and virtual directories. Now, in your Product.wxs add a new iis namespace to the Wix node:

<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
  xmlns:iis="http://schemas.microsoft.com/wix/IIsExtension">

What we're going to do now is add nodes representing a web site, app pool and virtual directory in IIS. After the Media node add the following:

<iis:WebSite Id="DefaultWebSite" Description="WebRoot" SiteId="1724599388">
  <iis:WebAddress Id="AllUnassigned" Port="80" />
</iis:WebSite>

<iis:WebAppPool Id="AppPool" Name="AppPool-DotNet4" />

You'll have to replace the Description, SiteId and Name attributes with your own values. This gives us a reference to the right website and app pool. It would be totally possible for you to create your own instead as part of the install process, but to do that they would have to be included inside a Component element inside the Directory node structure, which is exactly what we're about to do with the virtual directory. Add the following inside your INSTALLOCATION Directory node (remember to replace the guids with your own):

<Component Id="VirtualDirectoryComponent" Guid="GUID-HERE">
  <CreateFolder />

  <iis:WebVirtualDir Id="VirtualDirectory" Alias="WixInstallerExampleWeb"
       Directory="INSTALLLOCATION" WebSite="DefaultWebSite">
    <iis:WebApplication Id="WebSiteApp" Name="WixInstallerExampleWeb"
         WebAppPool="AppPool" />
    <iis:WebDirProperties Id="WebSite_Properties" AnonymousAccess="yes"
         WindowsAuthentication="no" DefaultDocuments="Default.aspx"
         Script="yes" Read="yes" />
  </iis:WebVirtualDir>
</Component>

<Component Id="ASPNet4Extension" Permanent="yes" Guid="GUID-HERE">
  <CreateFolder />
  <iis:WebServiceExtension Id="ASPNet4Extension" Group="ASP.NET v4.0.30319"
       Allow="yes" File="[ASPNETISAPIDLL]"
       Description="ASP.NET v4.0.30319" UIDeletable="no" />
</Component>

Pretty cool right? We've got easy access to all the settings you'd usually be able to set through the property pages on a virtual directory in IIS. Visual Studio will have full code-completion running for this so have a play and check out the settings you can use.

You'll probably have noticed the ASPNet4Extension component too - this will ensure that .net 4 is registered with IIS. It also has a property called Permanent - this ensures that when we uninstall the app that we don't remove .net4!

If you've really been following closely you'll also notice we're using a variable here - ASPNETISAPIDLL. This is the path to the .net4 isapi dll which potentially needs registering with IIS. We set this and one more variable just after the Package node at the top of the file:

<PropertyRef Id="NETFRAMEWORK40FULLINSTALLROOTDIR"/>
<SetProperty Id="ASPNETISAPIDLL" Sequence="execute" Before="ConfigureIIs"
  Value="[NETFRAMEWORK40FULLINSTALLROOTDIR]aspnet_isapi.dll" />
<SetProperty Id="ASPNETREGIIS" Sequence="execute" Before="ConfigureIIs"
  Value="[NETFRAMEWORK40FULLINSTALLROOTDIR]aspnet_regiis.exe" />

Brill, we've got a reference to not only the isapi dll, but the regiis exe. Why? Well while we've created a virtual directory and ensured we have .net4 available, we haven't actually told the virtual directory to use that version of the framework! Unfortunately this isn't just a property on the WebDirProperties node but it's still not too tricky. Add in the following right before the Feature node:

<CustomAction Id="MapVirtualDirectory"
  Directory="INSTALLLOCATION"
  Return="asyncNoWait"
  ExeCommand='[ASPNETREGIIS] -norestart -s "W3SVC/1724599388/ROOT/WixInstallerExampleWeb"' />

<InstallExecuteSequence>
  <Custom Action="MapVirtualDirectory" After="InstallFinalize"
    >ASPNETREGIIS AND NOT Installed</Custom>
</InstallExecuteSequence>

The CustomAction node makes a call to the regiis exe and passes the path to register - this includes the ID of the website and the name of the virtual directory so you'll need to change these values. The InstallExecuteSequence then tells WiX when we want the CustomAction run, and also provides a condition to make sure we have access to the regiis exe in the first place.

We're almost there now; all that remains is to add references to our new components into the Feature node to ensure they get installed:

<Feature Id="ProductFeature" Title="Wix Installer Example Web" Level="1">
  <ComponentGroupRef Id="WixInstallerExampleWeb_Project" />
  <ComponentRef Id="VirtualDirectoryComponent" />
  <ComponentRef Id="ASPNet4Extension" />
</Feature>

Checking It Works

Great, we've got a working installer! Build your app and run the installer. This time you should see "Configuring IIS" listed as one of the messages while the install progress bar builds up and right at the end a command window will appear for a second - this is the regiis exe doing it's stuff. Take a look in IIS and you should see your new site all set up and working as you would expect.

Uninstalling

Now, go to add/remove programs and remove the app you just installed. Not only will it remove the files, but it removes all references to it from IIS. Awesome huh?

I'm going to end it here even though really we haven't finished. The C# code we wrote isn't being used anywhere, and we don't want to be hard coding the website and app pool information into the installer. In the next article I'll move onto creating a custom WiX dialog for the IIS settings and connect it to the C# code we wrote earlier. If you'd like to be notified when I post this next instalment you can subscribe to my RSS feed.

See you soon!

Source Code

Here is the full source code for this example: WixInstallerExample-04.zip (1.82 mb)

Did you find this post useful or interesting? I'd be really grateful if you could return the favour by clicking this google link:



Welcome back to my series of posts on the WiX installer technologies. Previously we've looked at the basics of using WiX and Visual Studio, and learnt how to use packaged project output in your installers. This time we'll move onto the user interface side of the equation and discover how to allow your end users to begin customizing their installs. We'll do all this with Wix's custom UI functionality which will better prepare you for the more complex implementations we'll be using in future articles.

We'll be using the same application as in the last 2 posts. You can find the code to start from here: WixInstallerExample-02.zip (617.77 kb).

Preparing For A User Interface

The first step is to import the correct WiX extensions to start playing with the UI. Add a reference to WixUIExtension.dll from the bin folder inside your WiX install; this will pull in all the required UI templates. Next create a new wxs file in your installer project called UserInterface.wxs - this will contain all our custom UI coding. Inside the Fragment tag place the following:

<UIRef Id="WixUI_Common" />

This will get us a reference to all the common dialogs we need. Next add in the following:

<UI Id="MyCustomUI">
  <TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
  <TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
  <TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />

  <Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />

  <DialogRef Id="ErrorDlg" />
  <DialogRef Id="FatalError" />
  <DialogRef Id="FilesInUse" />
  <DialogRef Id="UserExit" />
</UI>

The main UI tag here will contain all our custom UI stuff. The first section with the TextStyle and Property nodes is just setting up some font settings and the DialogRef tags give us some references to standard dialogs for handling errors and early exits from the dialog sequence.

That's the basic setup done - now we need to think about the dialogs we want and how control will flow between them.

Dialogs and Flow

There are two main entry points to any installer - a welcome for new installs, or a welcome for changing a current install. We also want a licensing dialog and a way for the user to specify the install location. Our flow will then look something like this:

Luckily, WiX has dialogs included which can perform all these actions; all we have to do is specify how the control flows between them. To do this add the following right after your DialogRef tags:

<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog"
  Value="LicenseAgreementDlg">NOT Installed</Publish>

<Publish Dialog="LicenseAgreementDlg" Control="Back" Event="NewDialog"
  Value="WelcomeDlg">1</Publish>
<Publish Dialog="LicenseAgreementDlg" Control="Next" Event="NewDialog"
  Value="InstallDirDlg">LicenseAccepted = "1"</Publish>

<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog"
  Value="LicenseAgreementDlg">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath"
  Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction"
  Value="WixUIValidatePath" Order="2">1</Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog"
  Value="InvalidDirDlg" Order="3"
  ><![CDATA[NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog"
  Value="VerifyReadyDlg" Order="4">WIXUI_INSTALLDIR_VALID="1"</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty"
  Value="[WIXUI_INSTALLDIR]" Order="1">1</Publish>
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog"
  Value="BrowseDlg" Order="2">1</Publish>

<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction"
  Value="WixUIValidatePath" Order="3">1</Publish>
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog"
  Value="InvalidDirDlg" Order="4"><![CDATA[WIXUI_INSTALLDIR_VALID<>"1"]]></Publish>

To reiterate: this code is simply the rules for how we move from one dialog to the other. All of these Publish nodes have common attributes:

  • Dialog - the dialog screen we are adding a rule to;
  • Control - the control on the dialog we are adding a rule to;
  • Event - the event to handle;
  • Value - the action to take; depending on the event this could be the name of a dialog to move to, a reference to a variable to be updated or a new event to call.

In addition, the content of the Publish node is a condition for it to be executed. A condition of 1 will always run. Let's run through this flow:

  1. We start with the WelcomeDlg and, as long as the application is not installed, when the Next button is pushed we open the LicenseAgreementDlg;
  2. When Back is pushed on LicenceAgreementDlg we always go back to WelcomeDlg, or when Next is pushed we go to the InstallDirDlg as long as the agreement has been accepted;
  3. InstallDirDlg is the complicated one, simply it:
    1. Always lets you go back;
    2. When you push browse (ChangeFolder) it sets the start location for the browse to the value of the WIXUI_INSTALLDIR variable and spawns the BrowseDlg;
    3. When you push next it sets the target path to WIXUI_INSTALLDIR then validates it and either spawns an invalid path message or lets you continue to the VerifyReadyDlg.
  4. BrowseDlg validates the selected path when you click OK.

 

Following me so far?

We've defined the left hand column of dialogs from the diagram above, and done all the hard stuff. We'll add the maintenance dialogs next - put this code right after the previous Publish nodes:

<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog"
  Value="MaintenanceTypeDlg">1</Publish>

<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog"
  Value="VerifyReadyDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog"
  Value="VerifyReadyDlg">1</Publish>
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog"
  Value="MaintenanceWelcomeDlg">1</Publish>

<!-- Greys out the 'Change' button on MaintenanceTypeDlg -->
<Property Id="ARPNOMODIFY" Value="1" />

This is much simpler than the new install sequence - the MaintenanceWelcomeDlg will always move to MaintenanceTypeDlg which will always go straight to the VerifyReadyDlg. The APPNOMODIFY property will grey out the modify button on the MaintenanceTypeDlg as we don't have any optional components which could be modified. Notice that nowhere in all of this have we specified a start screen - WiX is smart enough that it will load either WelcomeDlg or MaintenanceWelcomeDlg depending on weather the application is already installed.

The only thing missing from the UI now is handling the back button from VerifyReadyDlg and early exits from the installer:

<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog"
  Value="InstallDirDlg" Order="1">NOT Installed</Publish>
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog"
  Value="MaintenanceTypeDlg" Order="2">Installed</Publish>

<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog"
  Value="Return" Order="999">1</Publish>

You should be able to figure this out with little trouble at this point.

That's it for the UI setup, all that remains is to tell WiX to use our interface.

Bringing It All Together

Specifying the UI to use is a simple task - back in the Product.wxs file add the following after the Feature tag:

<Property Id="WIXUI_INSTALLDIR">INSTALLLOCATION</Property>
<UIRef Id="MyCustomUI" />

This maps the WIXUI_INSTALLDIR variable used in the UI to the INSTALLLOCATION we use in the Directory setup and ensures that WebRoot\WixInstallerExampleWeb is the default. The UIRef node simply tells WiX to use the custom UI we defined.

There is still one final task - if you try to build now you'll get an error because WiX can't find EULA.rtf. Create one and add it to the root of the installer project.

The project should now build fine and you'll be able to see the various dialogs:

 

If you run the installer again you'll be able to repair or remove the install.

Why?

Now you've done all that hard work, a confession. WiX supplies a UI which does almost exactly this and which could have been added with 2 lines of code, so why have we gone to the trouble of creating it ourselves? Well for almost any scenario more complex than choosing a location on disk to install files to, you'll find yourself needing to use a custom UI, when doing so it will pay to already be familiar with how this stuff works.

That's it once again, but in part 4 we'll take a look at adding a custom dialog page to the installer which will add IIS integration, and in part 5 we'll go back and look again at Heat.exe to see how we can specify permissions on a directory. If you'd like to be notified when the next article is published you can subscribe to my RSS feed.

Source Code

Here is the full source code for this example: WixInstallerExample-03.zip (731.69 kb)

Did you find this post useful or interesting? I'd be really grateful if you could return the favour by clicking this google link:



Last time, I showed you how to create a basic installer based on an even more basic web application. If you didn't read that one go take a look now; it's right here. This time we're going to dig a little deeper and show you how to install the actual project output, rather than the full source code. In reality this post doesn't contain much WiX functionality; it's mostly MsBuild code. Strap yourself in - we're diving into the build system!

I should state right now that the method of grabbing project output described here I first learnt from the excellent Paraesthesia blog. This guy writes some cool posts and you should check it out. I've made a few changes to his code to fix some issues I had, and to make things easier when we get onto the later articles.

MsBuild - The 30 Second Overview

MsBuild is one of those topics that people seem to find quite scary, but in reality it's fairly easy to do pretty cool stuff with. There are 2 concepts to get your head round.

The Project File

All your project (.csproj) file is, is an MsBuild script, which gets maintained for you by Visual Studio in the background. Anything that the compiler does to your project - building, packaging, different configurations, whatever - goes through that script.

Targets

MsBuild scripts are run via targets. This is simply an entry point - think of it as a static class - each target is one of its methods. In exactly the same way that these methods can call each other, targets can (and will!) call each other. Targets are things like 'Build' or 'Package'. You can define your own too, although we won't be covering that here.

Now, on with the fun!

The Problem

At the end of the last post, we had an installer which copied all the code for our application to a folder, including all the cs files and our untransformed web configs:

Ideally what we'd prefer is to only install the files required to run the application. We could do this manually, but a much cleaner solution is to get the build process to package the application and then use that as the location to run Heat on. Next stop - using MsBuild to package the app.

Packaging applications via MsBuild

To do this you'll need to give your installer application a reference to your web application. When this is done unload the installer application and edit the wixproj file. Toward the bottom you should see a ProjectReference node which points to your web's csproj file. You're going to add a new node to this called PackageThisProject with a value of true:

<ProjectReference Include="..\WixInstallerExampleWeb\WixInstallerExampleWeb.csproj">
  <Name>WixInstallerExampleWeb</Name>
  <Project>{d23a374d-764c-40ba-b566-4d7c55319236}</Project>
  <Private>True</Private>
  <DoNotHarvest>True</DoNotHarvest>
  <RefProjectOutputGroups>Binaries;Content;Satellites</RefProjectOutputGroups>
  <RefTargetDir>INSTALLLOCATION</RefTargetDir>
  <PackageThisProject>True</PackageThisProject>
</ProjectReference>

Next, in the BeforeBuild target, replace the PropertyGroup and HeatDirectory nodes you had before with the following:

<MSBuild Projects="%(ProjectReference.FullPath)" Targets="Package"
  Properties="Configuration=$(Configuration);Platform=AnyCPU"
  Condition="'%(ProjectReference.PackageThisProject)'=='True'" />
    
<Copy SourceFiles="%(ProjectReference.RootDir)%(ProjectReference.Directory)obj\$(Configuration)\TransformWebConfig\transformed\web.config"
  OverwriteReadOnlyFiles="true"
  DestinationFolder="%(ProjectReference.RootDir)%(ProjectReference.Directory)obj\$(Configuration)\Package\PackageTmp\"
  Condition="'%(ProjectReference.PackageThisProject)'=='True'" />

<PropertyGroup>
  <LinkerBaseInputPaths>%(ProjectReference.RootDir)%(ProjectReference.Directory)obj\$(Configuration)\Package\PackageTmp\</LinkerBaseInputPaths>
</PropertyGroup>

<HeatDirectory OutputFile="%(ProjectReference.Filename).wxs"
  Directory="%(ProjectReference.RootDir)%(ProjectReference.Directory)obj\$(Configuration)\Package\PackageTmp\"
  DirectoryRefId="INSTALLLOCATION"
  ComponentGroupName="%(ProjectReference.Filename)_Project"
  SuppressCom="true" SuppressFragments="true" SuppressRegistry="true" SuppressRootDirectory="true"
  AutoGenerateGuids="false" GenerateGuidsNow="true" ToolPath="$(WixToolPath)"
  Condition="'%(ProjectReference.PackageThisProject)'=='True'" />

This isn't really as complicated as it looks. Firstly let's look at the various variables being used here:

  • %(ProjectReference.RootDir)%(ProjectReference.Directory) will be substituted for the path to your web project;
  • $(Configuration) will be substituted for the current build configuration (debug or release);
  • %(ProjectReference.FileName) will be substituted for the name of your web project;
  • %(ProjectReference.PackageThisProject) is the new node we added to the project reference.

Now onto the logic: for each project reference (in this example we are just using the one but it will happily scale to as many as you like) we:

  • Make a call to MsBuild specifying the Package target. This will cause a properly packaged output be placed in obj\Debug\Package\PackageTmp under the web project;
  • Copy a transformed web.config file from obj\Debug\TransformWebConfig\transformed\web.config into the packaged web project. I found this to be required in certain cases to stop template values being installed;
  • Set LinkerBaseInputPaths and run HeatDirectory exactly as before except now we target the packaged output instead of the project source.

Finally, each of the nodes also has a Condition attribute. This checks the PackageThisProject value we set earlier, and ensures that this logic is not applied to any other references we add to the project.

Finishing Up

We're all done, so reload the installer project and build. If you then look at your web project's folder in windows explorer you should see an obj\Debug folder, containing a tree of files mapping to what we've done above:

 

If you now run your newly built installer it should be this content which gets installed - easy huh? That's it for now, but in part 3 we'll start looking at a user interface for customising the install location and displaying a licence agreement, which should lead us on nicely to IIS integration and custom WiX UI. If you want to be notified when the next instalment is available you can find my RSS feed here.

Source Code

Here is the full source code for this example: WixInstallerExample-02.zip (617.77 kb)

Did you find this post useful or interesting? I'd be really grateful if you could return the favour by clicking this google link:



Deployment. I bet it’s your favourite part of development right? It’s not? Well it should be, it’s the moment that wonderfully crafted piece of genius you spent the last 6 months slaving over finally gets released into the wild. Cue rapturous applause from the crowd of adoring fans clustered around your desk and a fat bonus from the boss. But unfortunately it never seems to go that way; instead you pass the built code to the server guys along with a list of instructions which spell out step by step exactly what needs to be done. And they don’t follow it. You hit the homepage only to be greeted by a red screen of death. The crowd wanders off looking for a new hero and the boss gets himself a nice new solid gold mug.

It doesn’t have to be like that though; with a little bit of work you can take complete control of your deployment process and make the server boys happy at the same time. The first port of call for most people is the MsDeploy stuff, but there are a few reasons against:

  • It won’t work with IIS 6 (yes, I know there are workarounds, but they require local admin permissions on the entire box – not feasible for production);
  • Its only for web applications – so if you want a service installing you’re going to have to mess about with batch files or learn about installers anyway;
  • It’s not extensible to any great degree.

Enter WiX – the windows installer XML tool. This is the tool that the guys at Microsoft used to create the office installer, as well as loads of other stuff. It also has a reputation for a stiff learning curve. The bad news is that, yes, the learning curve is a bit steep, but the good news is that it’s also not too high. A couple of hours of head scratching and you’ll save yourself countless hours of fretting later. In this series of posts I’ll take you from the most basic installer possible to a full featured product allowing for automatic IIS registration, file permission settings and installation and startup of your services.

Stop! Hopefully by now I’ve convinced you that WiX will up your pay, make you famous, and get rid of that unfortunate rash. If so, you need to go install the WiX toolset from codeplex. It’s OK, we’ll wait.

The web application

All done? Good, but if we’re going to create an installer, we better have something to install. For this I’ve created the imaginatively titled WixInstallerExampleWeb in Visual Studio, which is a really simple web app containing a single aspx page, a web.config (plus transformations) and a txt file. (You can get the code for all the examples in this post from here if you don’t want to do it yourself.) Our aspx page displays a title and message:

<h1 id="MainHeader" runat="server">WiX installer example app</h1>
    
Welcome to the site! We've logged your visit, thanks for stopping by.

While it's code behind adds a custom bit of configuration text to the title, and logs the visit to the filesystem:

protected void Page_Load(object sender, EventArgs e)
{
MainHeader.InnerText += " - " + ConfigurationManager.AppSettings["InstallationType"];

TextWriter tWriter = new StreamWriter(Server.MapPath("~/DataFiles/UserLogs.txt"));
tWriter.Write("Accessed at " + DateTime.Now.ToString("dd MMM yyyy - hh:mm:ss"));
tWriter.Close();
}

Not exactly rocket science, but enough for our needs so far, and it poses a few deployment challenges:

  • it will need registering with IIS;
  • we will need some permissions on the file system;
  • different build types may need corresponding values in the InstallationType application setting.

We’ll get to that later though. For now let’s just get the installer copying the right files for us.

The installer

First up you need to add a new installation project to your solution. When you add a new Visual Studio project you should now see a new category called Windows Installer XML. You want the standard Setup Project and could call it something flashy like WixInstallerExampleWebInstaller:

 

A Note On GUIDs

In the following examples there will be lots of GUIDs used. It is essential that you replace these with GUIDs you've generated yourself. If you don't things will go seriously wrong, up to and including a dragon crashing through the office window, eating your sandwich and setting fire to the carpet. You have been warned. A nice macro for generating GUIDs right inside visual studio can be found here. Anyway...

Product.wxs

In the new project you’ll see a single file named Product.wxs. This is the main part of your installer; all the other files are just helping out. The main Product node is the installer; there is always exactly one of these: fill in the name, manufacturer and version to match your project. Ignore the Package and Media nodes, they will always stay like this until you start doing really complex stuff. This leaves us with 2 groups of nodes: Directory and Feature.

The Directory Section

The directory section defines the structure of what we'll be installing and where it goes. So we build up our default location using the Directory nodes, and in it place Components (i.e. the application) to install. Edit your Directory section so that it looks like this:

<Directory Id="TARGETDIR" Name="SourceDir">
  <Directory Id="WEBROOT" Name="WebRoot">
    <Directory Id="INSTALLLOCATION" Name="WixInstallerExampleWeb">
       <Component Id="ProductComponent" Guid="63c72a76-d3a8-4da2-849f-c83527b40690">
         <File Id="Default" Source="../WixInstallerExampleWeb/Default.aspx" />
       </Component>
    </Directory>
  </Directory>
</Directory>

This will cause the Default.aspx page to be installed to C:\WebRoot\WixInstallerExampleWeb\ (don't worry, we'll see how to make this configurable later).

You can have as many components here as you want here and they don't just define files, they can also define registry settings, IIS settings, service startup instructions - they are the distinct chunks of your application. For now though we're happy with just the one for our site content.

The Feature Section

While the Directory nodes define where things will go, the Feature section defines which components we actually want to install. You can add 100 components into INSTALLDIRECTORY and nothing will be installed until you reference them in the feature section. That's easily done though - change the Feature node to look like this:

<Feature Id="ProductFeature" Title="Wix Installer Example Web" Level="1">
   <ComponentRef Id="ProductComponent" />
</Feature>

It's not really too complex - you reference a component to install and specify the ID of the component you created in the Directory section.

So that's it, you've created an installer. Learning curve, what learning curve? Give it a build and an MSI file should pop out into the bin/Debug folder. You can go ahead and run it now if you like, and you should see something like this flash up then disappear:

 

Go take a look at C:\WebRoot\WixInstallerExampleWeb. You should see your Default.aspx page sat there. Score 1! Next, go look in your add/remove programs list. Right down at the bottom you should see this:

 

Cool huh?

What we've created here is nice, but it's not exactly functional. For a start we're only installing the Default.aspx page, without the config file or the required DLL, we also haven't set up any IIS settings. We can't even specify an install directory! We'll deal with them one at a time, starting with getting the right files installed. First though, click on the entry in add/remove programs and push the Remove button, which will wipe the app off your computer for you. Next up - Heat.

Heat.exe

The smart people who designed all this had a bit of a quirky moment when they named the exe files which do all the work when you build your installer, everything has a fire reference. For instance, the compiler itself is called candle.exe, there are plenty of others though, the most commonly used of which is heat.exe. What heat will do is inspect a location on disk and output a file containing Directory and Component nodes, which saves you from having to keep your Product.wxs file in line with your application manually. What's even better is that they also included a nifty MsBuild wrapper for it, so we can easily include it right in our build sequence!

If you've never used MsBuild before then don't worry - it's not that hard!

In solution explorer, right click on your installer project and select 'Unload Project'. This will grey out its entry and you can now right click again and select 'Edit WixInstallerExampleWebInstaller.wixproj' which will pop up an XML file representing your project. For those of you familiar with MsBuild, notice that a WixTargetsPath has been pulled in for you in the top PropertyGroup, and Imported at the bottom. Everyone else, all you need to know is that the WiX functionality has been imported for you automatically.

Near the bottom of the file there is a commented out section containing a Target called 'BeforeBuild'. This section funnily enough defines a list of things to do before the project is built. What we want to do is generate a Directory section using heat.exe before the build happens so that it is available to the compiler during the actual build. Uncomment the section and modify it to look like this:

<Target Name="BeforeBuild">
  <PropertyGroup>
    <LinkerBaseInputPaths>..\WixInstallerExampleWeb\</LinkerBaseInputPaths>
  </PropertyGroup>
  <HeatDirectory OutputFile="WixInstallerExampleWeb.wxs"
    Directory="..\WixInstallerExampleWeb\"
    DirectoryRefId="INSTALLLOCATION"
    ComponentGroupName="WixInstallerExampleWeb_Project"
    SuppressCom="true" SuppressFragments="true" SuppressRegistry="true"
    SuppressRootDirectory="true" AutoGenerateGuids="false" GenerateGuidsNow="true"
    ToolPath="$(WixToolPath)" />
</Target>

So, we're running heat on the directory "..\WixInstallerExampleWeb\" and outputting the details to WixInstallerExampleWeb.wxs. There are a few Suppress attributes which can safely be ignored for now - I'm sure you can figure them out fairly easily. The important bits are the DirectoryRefId and ComponentGroupName attributes. DirectoryRefId provides a link to the INSTALLLOCATION you put in the Product.wxs file earlier, while the ComponentGroupName is a new ID which you'll use later.

LinkerBaseInputPaths

This awkwardly named little node does a bit of magic, it brings in the files from the referenced folder so that they can be seen by the compiler - notice that so far we haven't even put a reference in to the web project, it's all done via directory references.

We're done with the pre-build for now, so close the file and reload the project. Go on and build it again now and select the 'Show all files' option at the top of the solution explorer. You should see a WixInstallerExampleWeb.wxs file - include it in the project.

Take a look at the file and you'll see it's a collection of Directory, Component and File nodes just as if you'd created the structure yourself in Product.wxs. There are a couple of differences though:

DirectoryRef

This node references the INSTALLLOCATION folder you specified in Product.wxi and the HeatDirectory command. It allows all these nodes to be specified in this external wxs file.

ComponentGroup

This is simply a collection of component references - it means that in the next step we don't need to reference all the generated components separately. Notice that it's ID is the value you specified in the HeatDirectory command.

Bringing It All Together

Right, let's finish this off so we can all get home. In your Product.wxs file, remove the Component and File nodes from the INSTALLLOCATION Directory, and remove the ComponentRef from Feature. Then add a new ComponentGroupRef into Feature with an Id matching the one generated for the ComponentGroup in the generated wxs file. You should end up with something like this:

<Directory Id="TARGETDIR" Name="SourceDir">
  <Directory Id="WEBROOT" Name="WebRoot">
    <Directory Id="INSTALLLOCATION" Name="WixInstallerExampleWeb" />
  </Directory>
</Directory>

<Feature Id="ProductFeature" Title="Wix Installer Example Web" Level="1">
  <ComponentGroupRef Id="WixInstallerExampleWeb_Project" />
</Feature>

Build your project and run the resulting installer.

It's Good, But...

So, now our installer is installing all the files from your project to the output folder. This is good, and it will run if you set it up as a virtual directory, but... we still have issues - we're copying all the non-needed files, such as the csproj file and cs files. We've also got Web.Debug.config and Web.Release.config in there - we really want to have the transformations run for us as part of the compile.

That's it for now, in part 2 I'll show you how to install the properly packed project output instead of the entire project source, and after that we'll look at IIS, a user interface and some of the advanced things you can do with MsBuild and Heat.exe. If you want to be notified when the next instalment is available you can subscribe to my RSS feed here.

Source Code

Here is the full source for this example: WixInstallerExample-01.zip (164.17 kb)

Did you find this post useful or interesting? I'd be really grateful if you could return the favour by clicking this google link:



Month List

Sign in

Top Programming Sites Vote for me at Fuelmyblog