John Robbins' Blog

Web Application Installer in WiX

If all you need to do is install your web application into Default Web Site, life is easy. Especially since Windows Installer XML (WiX) has all that support right in the box. Where things get nasty is if you need an installer that lets the user choose the web site, set the web application name, and the application pool. If you’re newish to WiX, that can be a daunting task. You might be tempted to skip WiX and use a Web Setup Project built into Visual Studio because those do offer a solution to the requirements. Sadly, Web Setup Projects are a giant world of hurt because of their other limitations. Add in the fun fact that Microsoft is dropping support for them in future releases of Visual Studio and it’s pretty obvious you need to avoid them.

Since this seems like a very common request, I thought I’d create an example WiX project that does what the now dying Web Setup Projects so there’s a path to a supported installation technology. While pieces of this type of installer have been covered before, especially the excellent article by Jon Torresdal, but there wasn’t a complete example. As always, if you’ve got any questions, feel free to email me or ask them in the comments. Grab the code for this installer here.

To mimic the Web Setup Project, I needed to do the following tasks:

  • Enumerate the web sites on the server and put them into a combo box.
  • Have an edit control that lets the user set the name of the application and does not allow a blank entry. This name is also used for the virtual directory name as well.
  • Installs the web application under the default directory for the web site.
  • Optionally let the user decide if they want to choose a different application pool for the web application. If they chose the default, the application pool for the web application will be set to the one in use by the web site.
  • Enumerate the application pools on the server and put them into a combo box.
  • Full support for IIS7 and higher. Note that the example does not work with IIS6, but it wouldn’t be hard to add that support.
  • Properly uninstalls the web application, virtual directory, and all files. As anyone who’s used the Web Setup Project knows, it does not correctly uninstall the web application from IIS (at least on IIS7).

As they say, a picture is worth a thousand words, so here’s a dialog that meets the requirements.

image

As the user interface in any MSI-based installer is the hard part, I wanted to get the UI out of the way first. Since my installer is very much like a standard WiXUI_InstallDir, which allows the user to choose the installation path, I downloaded the WiX source code and pulled that file as the basis of my UI. As the file for my UI is a decent size XML file, I didn’t want to just drop it into this article, so you should download the sample project and open.\Installer\HelloWorldInstaller.SLN and look at WixUI_SimpleWebAppInstall.wxs. I’ll discus the highlights here.

If you’ve never done a custom WiX UI project before it can appear pretty daunting. The basic idea is that you reference existing dialogs, DialogRef elements, and add your own custom ones with the Dialog elements. The key to creating dialogs is to use the dialog editor in the great WiXEdit project. Being able to add controls and see their layouts visually drastically speeds up your development. Towards the bottom of WixUI_SimpleWebAppInstall.wxs, you’ll see the XML for two dialogs, the WebAppInstallDlg, which is the dialog shown above, and an error dialog, InvalidWebAliasAliasDlg, that is shown at the bottom of the file.

Defining dialogs isn’t too hard, but the fun work is getting them properly hooked into your installers UI flow. That’s done with the Publish elements in the middle of my file. Once I realized that Publish elements are essentially like your event handlers in normal .NET development, the Windows Installer approach started making a lot more sense to me. Let’s take a look at the button processing for the WebAppInstallDlg.

<!-- Handle the back button-->
<
Publish Dialog="WebAppInstallDlg"

         Control="Back"

         Event="NewDialog"

         Value="LicenseAgreementDlg">1</Publish>
      
<!--
Check to see the web app name has something in it.
-->
<
Publish Dialog="WebAppInstallDlg"

         Control="Next"

         Event="SpawnDialog"

         Value="InvalidWebAliasAliasDlg"

         Order="1">WEB_APP_NAME=""</Publish>
<!--
Set the INSTALLDIR property based on the selected web site's physical path using

     my custom action. -->
<
Publish Dialog="WebAppInstallDlg"

         Control="Next"

         Event="DoAction"

         Value="SetInstallDirBasedOnSelectedWebSite"

         Order="2">1</Publish
<!--
Set the APP_POOL_NAME to the web site's default if that's what the

     user wants.-->
<
Publish Dialog="WebAppInstallDlg"

         Control="Next"

         Event="DoAction"

         Value="SetAppPoolNameToWebSiteDefault"

         Order="3"><![CDATA[USE_CUSTOM_APP_POOL <> 1]]></Publish>
<!--
Finally move to the VerifyReadyDlg if all values are looking good.
-->
<
Publish Dialog="WebAppInstallDlg"

         Control="Next"

         Event="NewDialog"

         Value="VerifyReadyDlg"

         Order="4"><![CDATA[(WEB_APP_NAME<>"")]]></Publish>

At a glance you figured out the Back button processing but it’s the Next button that’s a little more interesting. The first act is to check that the WEB_APP_NAME property is empty. If it’s a null value, I want the InvalidWebAliasDlg to be shown. The second action is to call a custom action I wrote to set the INSTALLDIR property based on the chosen web site. As that element value is 1, that action is always executed. The third action is if the user did not check the use custom app pool checkbox, I want to run another custom action to set the app pool name for the web application to the same that the web site is using. Lastly, if we get to the last Publish element, I’ll move to the VerifyReadyDlg if the WEB_APP_NAME is not null.

One trick I found helpful when developing the UI was to initially insert my dialog and have the next button just go to the VerifyReadDlg. That way I could ensure the dialog was properly being shown and could build up the custom actions and get them hooked up to the real UI.

The final part of WixUI_SimpleWebAppInstall.wxs I want to mention is near the top of the XML and shown below.

<!-- This is very important. As I am filling in the web site and app

     pool combo boxes dynamically, I need to force create the ComboBox

     table.

     This nice little fellow gets it into the output .MSI. However,

     as there's nothing in the table, you're going to get an ICE17

     warning that the combo box associated with WEBSITE_NAME does not

     exist. It's safe to turn off ICE17 as the enumerate custom action

     will take care of doing the filling so it exists before being

     needed. -->

<EnsureTable Id='ComboBox'/>

 

<!-- The custom action DLL itself.-->

<Binary Id="WebAppCA"

    SourceFile="$(var.CAFileLocation)" />

 

<!-- The custom action to enumerate the web sites and app pools into

     the appropriate combo boxes.-->

<CustomAction Id="EnumerateIISWebSitesAndAppPools"

              BinaryKey="WebAppCA"

              DllEntry="EnumerateIISWebSitesAndAppPools"

              Execute="immediate"

              Return="check" />

 

<!-- Make sure the enumerate web sites and app pools custom action

     gets called, but only called if we are doing and install. -->

<InstallUISequence>

  <Custom Action="EnumerateIISWebSitesAndAppPools"

          After="CostFinalize"

          Overridable="yes">NOT Installed</Custom>

</InstallUISequence>

As I have the requirement to let the user chose the web site and optionally the application pool, I need to get those items filled in. As there’s no built in support for enumerating those items in WiX or Windows Installer, I needed to write a custom action, EnumerateIISWebSitesAndAppPools, to do the work for me. The above section of code shows incorporating my custom action and getting it scheduled. What EnumerateIISWebSitesAndAppPools does is add the items to the ComboBox table in the in-memory MSI file that is where all combo boxes go for their data. Once gotcha I ran into is that just declaring a ComboBox in your UI does not create the table so your custom action will fail. The WiX trick to work around that is to use the EnsureTable element to have WiX put the table in the output MSI.

While I had bumped into discussions of custom actions through my reading on WiX and Windows Installer, I’d never had to write one before. Conventional wisdom dictates that you should use native C++ to write your custom actions. As I’m not afraid of C++, I took a look at the IIS 7 documentation for their C++ interfaces for administration. Just as I was afraid of, it’s all PCOM. You know, Painful COM, whose motto is “all the pain of COM and none of the benefits.” Because I didn’t want to spend two weeks in COM hell, I thought it time to take a good look at the Deployment Tools Framework (DTF) which we can use to write out custom actions in any managed language you so desire.

In all, using DTF made writing my custom actions nearly painless especially since the IIS7 managed administration API is very clean. As an example, here’s the code to my custom action that sets the installation directory based on the selected web site.

[CustomAction]
public static ActionResult SetInstallDirBasedOnSelectedWebSite(

                                                     Session session)

{

    if (null == session)

    {

        throw new ArgumentNullException("session");

    }

    try

    {

        // Debugger.Break();

        session.Log("SetInstallDir: Begin");

 

        // Let's get the selected website.

        String webSite = session["WEBSITE_NAME"];

        session.Log("SetInstallDir: Working with the web site: {0}",

                    webSite);

        // Grab that web sites based physical directory and get it's

        // "/"

        // (base path physical directory).

        String basePath;

        using (ServerManager iisManager = new ServerManager())

        {

            Site site = iisManager.Sites[webSite];

            basePath = site.Applications["/"].

                                VirtualDirectories["/"].PhysicalPath;

        }

 

        session.Log("SetInstallDir: Physical path : {0}", basePath);

 

         // Environment variables are used in IIS7 so expand them.

        basePath = Environment.ExpandEnvironmentVariables(basePath);

 

        // Get the web application name and poke that onto the end.

        String webAppName = session["WEB_APP_NAME"];

        String finalPath = Path.Combine(basePath, webAppName);

 

        // Set INSTALLDIR to the calculate path.

        session.Log("SetInstallDir: Setting INSTALLDIR to {0}",

                    finalPath);

        session["INSTALLDIR"] = finalPath;

    }

    catch (Exception ex)

    {

        session.Log("SetInstallDir: exception: {0}", ex.Message);

        throw;

    }

    return ActionResult.Success;
}

Over half of the code there is logging but you can get the idea pretty quickly. There were a few things that I stumbled with developing my custom actions. The first was since I was installing on Server 2008 R2, which is a 64-bit operating system, I was concerned that my custom actions had to be 64-bit as well. It made sense to me, but after experimenting, that’s not the case at all. When the 64-bit MSIEXEC.EXE is executing your custom actions, it’s doing so out of process and takes into account the “bitness” of the DLL you’re executing. That was a pleasant surprise so if you are looking to use custom actions written by others, you don’t have to worry if they are 32-bit only. Obviously, the Windows Installer team hasn’t figured out how to run 64-bit custom actions on 32-bit windows.

Debugging managed custom actions is a bit interesting. There’s the MMSIBREAK environment variable, which breaks into the managed debugger when your custom action is called. What I found even easier was to just put in a call to Debugger.Break at the top of my method. Since your custom actions can be run in various processes, getting the environment variable set system wide can be a pain. You can find a nice overview of debugging managed custom actions at Jon Torresdal’s web site.

The last thing that threw me for a loop debugging custom actions was that I couldn’t seem to get the diagnostic logging for those custom actions I invoked as part of a button click DoAction control event. It was so weird that the custom action worked, but I never saw the logging. After much hair pulling, I realized that it’s a limitation of Windows Installer. It’s sad that Windows Installer is nasty enough without surprises like this floating around. What I ended up doing was adding a bunch of property sets, which do get logged instead of calling Session.Log.

As prompting the user for web sites, app pools, and virtual directories seems to be a very common question on the WiX-Users mailing list, I hope others can use my code to get past that hurdle. Once last thing I need to mention is that accessing the IIS 7 APIs requires full administrator rights. As my code needs to enumerate the web sites and web pool as soon as the MSI loads, if you double click on it from Explorer and are not running elevated, the install will immediately fail. To be a full installer you need to wrap the MSI in a boot strapper EXE that requests elevation before executing the MSI. As WiX 3.6 is all about the Burn, you may want to start there, but it’s still early in the development cycle.

On Feb 22 2011 8:01 PMBy jrobbins Windows InstallerWith 18 Comments

Comments (18)

  1. Hi John

    Good post...seems we've been both going through the same process without realising it :-(

    I've started a series of very similar blog posts:

    http://blogs.planetsoftware.com.au/paul/archive/2011/02/05/using-wix-3.5-with-visual-studio-2010.aspx

    Hopefully we can add some clarity to this aspect of WiX usage!
    Paul.

  2. Nice article, John! It is really requested quite often, and your descriptive article will definitely help!

    One minor note to mention: as you know, of course, the Microsoft.Web.Administration.dll is installed with IIS 7. As a result, you can't rely it is in GAC on a target machine. However, this can be "worked around" by conditioning the CA to run on IIS 7 only.

    Another similar challenge is a build server - it doesn't have to have IIS 7 at all (well, any IIS, to be strict). As a result, in order to just build the CA project we'll have to keep that DLL together with our sources. Theoretically, this might be even dangerous if we keep IIS 7.0 version of that DLL, but install on IIS 7.5, where there's a certain fix, and our DLL doesn't work with that version of IIS...

    Do you think it makes sense?

  3. Excellent, the most comprehensive summary I've seen. It's now one of my permanent bookmarks.

  4. Yan,

    Excellent points! Thanks for the comments.

    Yes, in the example, I do check for IIS 7 or higher in a startup conditional.

    As for the build, what I do in real projects is have a folder called References for all items like Microsoft.Web.Administration.DLL that I build against, but make sure to not copy to the output directories. That way I have the build be completely self contained but load the latest version when run. Make sense?

    Hope it helps!
    - John Robbins

  5. Hi John

    You mention it wouldn't be too hard to add IIS6 support? How would one go about doing that? So far I've tried and tried to come up with a nice way of doing it, but I'm falling short. Have you got an example of how you'd integrate it with the IIS7 CA?

    Regards,
    J

  6. Does it work for you guys if "Default Web Site" has a binding to e.g. port 82?

    In that scenario, my installer will always pick the other site I have set up. (I have IIS 6 and thus changed the enumeration code - looking at the log file I can see that WEBAPP_NAME is set to "Default Web Site" early on)

  7. Hi All,

    Could you please, upload an example for IIS6 ? I really need it for both IIS6 & IIS7.

    Thanks

  8. Hi All,
    I'm experiencing some difficulty.
    I've created a bootstrapper using setupbld.exe and elevated to Admin using manifest and mt.exe.
    So here is what happens.
    If I start a command prompt using run as administrator and use msiexec all works fine.
    ComboBoxes are populated wonderfull.
    If I use the bootstrapper I get no exceptions at all but the ComboBoxes are not populated.
    I debuged the Enumeration Custom Action and it executes without exceptions and the User is Admin.
    Any Suggestions

  9. Great example John.
    Any hints about how adding an item to the website list for "Create New WebSite" would look in the custom action, what additional properties might be needed and conditions, since now we'd need both a website locator record in the wix, and a website creating record.

  10. Jakob Jensen Ladingkaer

    Your article helped me a lot, but the ICE17-warnings annoyed me, and just following your advice on turning off warnings are against my belief.
    "This nice little fellow gets it into the output .MSI. However,
    as there's nothing in the table, you're going to get an ICE17
    warning that the combo box associated with WEBSITE_NAME does not
    exist. It's safe to turn off ICE17 as the enumerate custom action
    will take care of doing the filling so it exists before being
    needed."
    I found that setting the Indirect="yes" attribute in the control will do the trick.
    Fx.
    &amp;lt;Control Type="ComboBox" Property="WEBSITE_NAME" Id="WebSiteCombo" Width="320" Height="16" X="20" Y="80" ComboList="yes" Sorted="yes" Indirect="yes" /&amp;gt;

  11. Rajen Shrestha

    Hi,
    Can somebody tell me how the WebAppInstallCustomActions.CA.dll has created during build of the Custom Action project?
    Thanks in Advance

  12. Rajen Shrestha

    Hi,
    Answer to my question that I have asked previously is located in following link:
    http://www.codeproject.com/Articles/132918/Creating-Custom-Action-for-WIX-Written-in-Managed

  13. Trying to follow your instructions. But I don't understand where all the !(loc.whatever) variables are coming from. None of the files in your solution contain anything that seems to define those values. Am I missing something here?

  14. How to turn off the ice17 errors ? Am I missing out something here? The msi is getting created for me but while installation it shows installation ends prematurely. How can I get it work? Thanks for any help.

Leave a Comment

Archives

Tags