Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

Continuing from Part 4, we look at the creation of a custom action assembly to populate our custom dialog, and apply the selected values.

Step 8: Custom Action Assembly

In Part 3 we made use of a custom action to execute aspnet_regiis.exe.  Aside from executing an external executable, we can also use this technique to run script or use our own custom action assembly.

We need to obtain a list of web sites to provide to our custom dialog.  We also then need to get the user selection, and apply that to our variables/fields and store it for use during uninstall/maintenance, etc.

My first attempt at this was via a script I found here, but this lead to my initial ‘getting bitten’ post.  Also, whilst playing with this approach I read numerous posts that suggested creating a custom action assembly was more ‘desirable’…and given I prefer managed coding, I took a look at the following posts:-

I used these as the basis for the approach I have taken, but given that these approaches require IIS compatibility to be installed, some refinement was required to arrive at a solution that support IIS 6 upwards without needing to install compatibility on IIS7+.

Just prior to publishing this post, I found that John Robbins’ had been following a similar path to myself, and had posted a great example of a custom action that uses IIS7 via the managed API.

However, let’s take a look at a solution that combines these various techniques so we can install our applications on IIS6 OR IIS7+ without need for any legacy support!

Create Custom Action Assembly

The first step is to create a C# Custom Action Project (I called my demo one IISCA), and then add a reference to this project from your actual setup project.  (Given this is pretty generic, you can make repeated use of this assembly with your other web application setup projects.)

Then copy-paste the following:

IISCA - CustomAction.cs
// Copyright 2011 (c) Planet Software Pty Ltd. This work is
// licenced under the Creative Commons Attribution 2.5 Australia License. To view
// a copy of this licence, visit http://creativecommons.org/licences/by/2.5/au
// The following helped form an initial basis for this work:
// http://blog.torresdal.net/2008/10/24/WiXAndDTFUsingACustomActionToListAvailableWebSitesOnIIS.aspx
// http://www.cmcrossroads.com/cm-basics/12907-installing-a-web-application-to-an-existing-iis-website-using-wix3

using System;
using System.DirectoryServices;
using System.Globalization;
using Microsoft.Deployment.WindowsInstaller;
using Microsoft.Web.Administration;
using Microsoft.Win32;

namespace IISCA
{
    public static class CustomActions
    {
        #region Private Constants
        private const string IISEntry = "IIS://localhost/W3SVC";
        private const string SessionEntry = "WEBSITE";
        private const string ServerBindings = "ServerBindings";
        private const string ServerComment = "ServerComment";
        private const string CustomActionException = "CustomActionException: ";
        private const string IISRegKey = @"Software\Microsoft\InetStp";
        private const string MajorVersion = "MajorVersion";
        private const string IISWebServer = "iiswebserver";
        private const string GetComboContent = "select * from ComboBox";
        private const string AvailableSites
            = "select * from AvailableWebSites";
        private const string SpecificSite
            = "Select * from AvailableWebSites where WebSiteID=";
        #endregion

        #region Custom Action Methods
        [CustomAction]
        public static ActionResult GetWebSites(Session session)
        {
            ActionResult result = ActionResult.Failure;

            try
            {
                if (session == null) { throw new ArgumentNullException("session"); }

                View comboBoxView = session.Database.OpenView(GetComboContent);
                View availableWSView = session.Database.OpenView(AvailableSites);

                if (IsIIS7Upwards)
                {
                    GetWebSitesViaWebAdministration(comboBoxView, availableWSView);
                }
                else
                {
                    GetWebSitesViaMetabase(comboBoxView, availableWSView);
                }

                result = ActionResult.Success;
            }
            catch (Exception ex)
            {
                if (session != null)
                {
                    session.Log(CustomActionException + ex.ToString());
                }
            }

            return result;
        }

        [CustomAction]
        public static ActionResult UpdatePropsWithSelectedWebSite(Session session)
        {
            ActionResult result = ActionResult.Failure;

            try
            {
                if (session == null) { throw new ArgumentNullException("session"); }

                string selectedWebSiteId = session[SessionEntry];
                session.Log("CA: Found web site id: " + selectedWebSiteId);

                using (View availableWebSitesView = session.Database.OpenView(
                    SpecificSite + selectedWebSiteId))
                {
                    availableWebSitesView.Execute();

                    using (Record record = availableWebSitesView.Fetch())
                    {
                        if ((record[1].ToString()) == selectedWebSiteId)
                        {
                            session["WEBSITE_ID"] = selectedWebSiteId;
                            session["WEBSITE_DESCRIPTION"] = (string)record[2];
                            session["WEBSITE_PATH"] = (string)record[3];
                            session.DoAction("SetApplicationRootDirectory");
                        }
                    }
                }

                result = ActionResult.Success;
            }
            catch (Exception ex)
            {
                if (session != null)
                {
                    session.Log(CustomActionException + ex.ToString());
                }
            }

            return result;
        }
        #endregion

        #region Private Helper Methods
        private static void GetWebSitesViaWebAdministration(View comboView,
            View availableView)
        {
            using (ServerManager iisManager = new ServerManager())
            {
                int order = 1;

                foreach (Site webSite in iisManager.Sites)
                {
                    string id = webSite.Id.ToString(CultureInfo.InvariantCulture);
                    string name = webSite.Name;
                    string path = webSite.PhysicalPath();

                    StoreSiteDataInComboBoxTable(id, name, path, order++, comboView);
                    StoreSiteDataInAvailableSitesTable(id, name, path, availableView);
                }
            }
        }

        private static void GetWebSitesViaMetabase(View comboView, View availableView)
        {
            using (DirectoryEntry iisRoot = new DirectoryEntry(IISEntry))
            {
                int order = 1;

                foreach (DirectoryEntry webSite in iisRoot.Children)
                {
                    if (webSite.SchemaClassName.ToLower(CultureInfo.InvariantCulture)
                        == IISWebServer)
                    {
                        string id = webSite.Name;
                        string name = webSite.Properties[ServerComment].Value.ToString();
                        string path = webSite.PhysicalPath();

                        StoreSiteDataInComboBoxTable(id, name, path, order++, comboView);
                        StoreSiteDataInAvailableSitesTable(id, name, path, availableView);
                    }
                }
            }
        }

        private static void StoreSiteDataInComboBoxTable(string id, string name,
            string physicalPath, int order, View comboView)
        {
            Record newComboRecord = new Record(5);
            newComboRecord[1] = SessionEntry;
            newComboRecord[2] = order;
            newComboRecord[3] = id;
            newComboRecord[4] = name;
            newComboRecord[5] = physicalPath;
            comboView.Modify(ViewModifyMode.InsertTemporary, newComboRecord);
        }

        private static void StoreSiteDataInAvailableSitesTable(string id, string name,
            string physicalPath, View availableView)
        {
            Record newWebSiteRecord = new Record(3);
            newWebSiteRecord[1] = id;
            newWebSiteRecord[2] = name;
            newWebSiteRecord[3] = physicalPath;
            availableView.Modify(ViewModifyMode.InsertTemporary, newWebSiteRecord);
        }

        // determines if IIS7 upwards is installed so we know whether to use metabase
        private static bool IsIIS7Upwards
        {
            get
            {
                bool isV7Plus = false;

                using (RegistryKey iisKey = Registry.LocalMachine.OpenSubKey(IISRegKey))
                {
                    isV7Plus = (int)iisKey.GetValue(MajorVersion) >= 7;
                }

                return isV7Plus;
            }
        }

        #endregion
    }
}

You will also need to add two references:-

  • System.DirectoryServices
  • Microsoft.Web.Administration (normally: C:\Windows\System32\inetsrv\Microsoft.Web.Administration.dll)

Now don’t panic at this point…you may be concerned that adding the Microsoft.Web.Administration reference now constrains us to only running on an IIS7 box…but it doesn’t if you are careful thanks to JIT!  You will see that the GetWebSites method performs a check to determine which version of IIS we are targeting, and then calls a specific method for IIS6 or IIS7 upwards.

The rest of the code should be fairly obvious, so I won’t delve into a boring explanation of it…but you’ll also need a couple of extension methods to get it all compiling:

IISCA - ExtensionMethods.cs
// Copyright 2011 (c) Planet Software Pty Ltd. This work is
// licenced under the Creative Commons Attribution 2.5 Australia License. To view
// a copy of this licence, visit http://creativecommons.org/licences/by/2.5/au
using System;
using System.DirectoryServices;
using System.Linq;
using Microsoft.Web.Administration;

namespace IISCA
{
    public static class ExtensionMethods
    {
        private const string IISEntry = "IIS://localhost/W3SVC/";
        private const string Root = "/root";
        private const string Path = "Path";

        public static string PhysicalPath(this Site site)
        {
            if (site == null) { throw new ArgumentNullException("site"); }

            var root = site.Applications.Where(a => a.Path == "/").Single();
            var vRoot = root.VirtualDirectories.Where(v => v.Path == "/")
                .Single();

            // Can get environment variables, so need to expand them
            return Environment.ExpandEnvironmentVariables(vRoot.PhysicalPath);
        }

        public static string PhysicalPath(this DirectoryEntry site)
        {
            if (site == null) { throw new ArgumentNullException("site"); }

            string path;

            using (DirectoryEntry de = new DirectoryEntry(IISEntry
                + site.Name + Root))
            {
                path = de.Properties[Path].Value.ToString();
            }

            return path;
        }
    }
}

All we need to do now is make a few adjustments to our main setup project.  Insert the following into WebSites.wxi:

Modify WebSites.wxi
<Property Id="WEBSITE_DESCRIPTION">
    <RegistrySearch Id="WebSiteDescription" Name="WebSiteDescription" Root="HKLM"
                    Key="SOFTWARE\!(loc.CompanyName)\!(loc.ProductName)\Install"
                    Type="raw" />
  </Property>
  <Property Id="WEBSITE_ID">
    <RegistrySearch Id="WebSiteID" Name="WebSiteID" Root="HKLM"
                    Key="SOFTWARE\!(loc.CompanyName)\!(loc.ProductName)\Install"
                    Type="raw" />
  </Property>
  <Property Id="WEBSITE_PATH">
    <RegistrySearch Id="WebSitePath" Name="WebSitePath" Root="HKLM"    
                    Key="SOFTWARE\!(loc.CompanyName)\!(loc.ProductName)\Install"
                    Type="raw" />
  </Property>
  <Property Id="WEBSITE_VD">
    <RegistrySearch Id="WebSiteVD" Name="WebSiteVD" Root="HKLM"    
                    Key="SOFTWARE\!(loc.CompanyName)\!(loc.ProductName)\Install"
                    Type="raw" />
  </Property>

  <CustomTable Id="AvailableWebSites">
    <Column Id="WebSiteID" Category="Identifier" PrimaryKey="yes" Type="int"
            Width="4" />
    <Column Id="WebSiteDescription" Category="Text" Type="string"
            PrimaryKey="no"/>
    <Column Id="WebSitePath" Category="Text" Type="string" PrimaryKey="no"
            Nullable="yes" />

    <Row>
      <Data Column="WebSiteID">0</Data>
      <Data Column="WebSiteDescription">Dummy</Data>
      <Data Column="WebSitePath"></Data>
    </Row>
  </CustomTable>

This reads in any previously stored settings, and ensures we have a custom table defined to put web site data into.

Now open Product.wxs, and insert another component beneath the EnableASPNet4Extension component:

Product.wxs - Add component
<Component Id="PersistWebSiteValues" Guid="C3DAE2E2-FB49-48ba-ACB0-B2B5B726AE65">
  <RegistryKey Action="create" Root="HKLM"
               Key="SOFTWARE\!(loc.CompanyName)\!(loc.ProductName)\Install">
    <RegistryValue Name="WebSiteDescription" Type="string" Value="[WEBSITE_DESCRIPTION]"/>
    <RegistryValue Name="WebSiteID" Type="string" Value="[WEBSITE_ID]"/>
    <RegistryValue Name="WebSitePath" Type="string" Value="[WEBSITE_PATH]"/>
    <RegistryValue Name="WebSiteVD" Type="string" Value="[VD]"/>
  </RegistryKey>
</Component>

This component will ensure that we save our settings…we need to do this so we can uninstall!

Add our new custom actions above the existing one:

New Custom Actions
  <!-- Define our custom actions -->
  <CustomAction Id="GetIISWebSites" BinaryKey="IISCA" DllEntry="GetWebSites"
                  Execute="immediate"Return="check" />
  <Binary Id="IISCA" SourceFile="$(var.IISCA.TargetDir)$(var.IISCA.TargetName).CA.dll" />

  <CustomAction Id="SetApplicationRootDirectory" Directory="INSTALLLOCATION"
                Value="[WEBSITE_PATH]\[VD]" />  

And add the following beneath:

InstallUISequence
<!-- Install UI Sequence - allows us to schedule custom action -->
<InstallUISequence>
  <Custom Action="GetIISWebSites" After="CostFinalize"
          Overridable="yes">NOT Installed</Custom>
</InstallUISequence>

So this basically ensures when we run, we call our custom action method to get the web sites and populate the storage so the UI can display in the ComboBox.

And finally, you just need to add in our persistence component to the Feature section:

Add to Feature
<Feature Id="ProductFeature" Title="!(loc.ProductName)" Level="1">
  <ComponentRef Id='WebVirtualDirComponent' />
  <ComponentRef Id='EnableASPNet4Extension'/>
  <ComponentGroupRef Id="MyWebApp_Project" />
    <ComponentGroupRef Id="Product.Generated" />
  <ComponentRef Id="PersistWebSiteValues" />
</Feature>

Phew…bet you’re glad you took your jacket off now Winking smile

OK, so now when we build/run we have a nicely populated list of sites:

image

And when we step on, we can see the selected/appended path, and change if required:

image

And best of all, this works on IIS6 and IIS7+ so you can install on Server 2003, Windows 7, Server 2008, etc. with no requirement on the IIS compatibility being installed! Smile

Print | posted on Saturday, 26 February 2011 1:00 PM

Feedback

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by Jamie at 26/04/2011 7:03 PM Gravatar
Great blog, am using this now to create some generic installers.

Have you tried making Cassini based versions of any of your installers?

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by Paul at 27/04/2011 9:53 AM Gravatar
Hi Jamie

Thanks on blog comment...and yes, I have considered it, but simply lacked the time to follow up ;-)

Paul.

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by Lance at 21/06/2011 11:21 PM Gravatar
Excellent blog post covering this. I'm experienceing a problem; however, though if I change the ProductId and Version of my installer for an upgrade and run the installer, the Site dropdown is not being populated with any web sites. It's only being populated if the installer is being run for the first time.

Do you have any idea what might be causing this to happen?

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by Paul at 22/06/2011 8:29 AM Gravatar
Hi

I've not tried an upgrade (tend to uninstall and reinstall!).

However, it is most likely due to the following check:

<!-- Install UI Sequence - allows us to schedule custom action -->
<InstallUISequence>
<Custom Action="GetIISWebSites" After="CostFinalize"
Overridable="yes">NOT Installed</Custom>
</InstallUISequence>

So GetIISWebSites only runs if NOT Installed, and this is not the case with an upgrade.

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by Phil at 24/07/2011 1:32 AM Gravatar
Great example!

Would really like to see how you would include the ability to create a new website, especially juggling the different properties.
All the WiX examples I've seen related to IIS cover either [a]new website install or [b]choose existing. None seem to include guidance on how to support both scenarios.

Would you end up putting an additional website element under a permanent component and then conditioning it based on property values? since the website locator won't help you with creating a new one?

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by cristi at 18/08/2011 8:30 PM Gravatar
Hy Paul, indeed you did a great job.
I have an error when I build the appilcation, in the main file:

...\MyWebAppTest\MySetupTest\Product.wxs(83,0): error LGHT0094: Unresolved reference to symbol 'WixComponentGroup:MyWebAppTest' in section 'Product:{D5B59D53-5FBB-4DD4-A2AC-1E97D70710B3}'. It links to the fallowing fragment of code, more exactly :

<ComponentGroupRef Id="MyWebAppTest" />

in this fragment(in Product.wxs) :

<Feature Id="ProductFeature" Title="!(loc.ProductName)" Level="1">
<ComponentRef Id='WebVirtualDirComponent' />
<ComponentRef Id='EnableASPNet4Extension'/>
<ComponentGroupRef Id="MyWebAppTest" />
<ComponentGroupRef Id="Product.Generated" />
<ComponentRef Id="PersistWebSiteValues" />
</Feature>

If you have an ideea, please help. Thanks in advance!

# developpement site internet

left by Matthias Sormer at 9/09/2011 8:19 PM Gravatar
Thank you for posting the whole code, and special thanks to Paul for adding that remark in the comments, I had been stuck with this problem for a long time working on this web development project. :-)

Kind regards,

Matthias Sormer
developer @ creation site, Suisse

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by abbie at 5/11/2011 7:35 AM Gravatar
Hi,

I've followed all the steps mentioned by you from Part 1- Part 5, but everytime I do a MSBuild I keep getting the following issue : Undefined preprocessor variable '$(var.IISCA.TargetDir)'
I am very new to MSBuild and I bet am missing some really simple key information.

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by Brian at 4/04/2012 10:06 PM Gravatar
Hi, excellent set of articles.

I've now gone through part 1 -> 5, after part 2 I was able to compile an msi and it installed in my Program Files (as the article said it would). After going through part 3 -> 5, I now get a compile error for each of the files referenced in the "INSTALLLOCATION" DirectoryRef.

Any ideas on how this could be solved? The auto generated file is still being generated each time I compile, and I have checked my Wix project configuration and it still seems to be as expected, and described in the article you link to.

Any suggestions would be most appreciated, otherwise I'll just plug away at trying to find out what's wrong.

Cheers,
Brian

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by Roman at 10/04/2012 7:42 PM Gravatar
Could you cover also several important things? a) permission on the folders, b) UAC compatability . It would be nice to have all in one place.

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by w1zeman1p at 23/01/2013 9:43 AM Gravatar
hey abbie and all with the

$(var.IISCA.TargetDir) error

change IISCA to the name of the custom action project.

I named mine MyWebApp.CustomAction so I needed to change all of the references to IISCA
to MyWebApp.CustomAction

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by Jim at 8/03/2013 3:45 PM Gravatar
"Now don’t panic at this point…you may be concerned that adding the Microsoft.Web.Administration reference now constrains us to only running on an IIS7 box…but it doesn’t if you are careful thanks to JIT! You will see that the GetWebSites method performs a check to determine which version of IIS we are targeting, and then calls a specific method for IIS6 or IIS7 upwards."

I thought it would work too, but it didn't (on an XP machine), it demanded the Microsoft.Web.Administration DLL. Got around that by lots of awkward reflection calls

Assembly currentExternalAssembly = Assembly.Load("Microsoft.Web.Administration, Version=7.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35");

//get the type
Type t = currentExternalAssembly.GetType("Microsoft.Web.Administration.ServerManager");
Type poolType = currentExternalAssembly.GetType("Microsoft.Web.Administration.ApplicationPool");
object iisManager = Activator.CreateInstance(t);

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by Rahul Neekhra at 3/04/2013 9:27 PM Gravatar
I have created msi installer using the given steps. Once installer is created and when I am trying to install below error is coming and installation failed.

Product: My Web Services -- Error 1723. There is a problem with this Windows Installer package. A DLL required for this install to complete could not be run. Contact your support personnel or package vendor. Action GetIISWebSites, entry: GetWebSites, library: C:\Users\neekhrar\AppData\Local\Temp\MSIDF79.tmp

Please suggest the possible solution.

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by Matan Liberman at 2/05/2013 5:31 PM Gravatar
Hi,

I'm trying to install wcf service application over the IIS 8 on windows server 2012.
Is this the only way or there's a different way of doing it?

thank you
Matan

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by college paper at 12/08/2013 6:04 PM Gravatar
For this Step 8: Custom Action Assembly, you gave your avid readers some really nice tips on how to do it. And they're pretty much easy to understand as well. Thanks for sharing this to us.

# re: Creating a Web Application Installer with WiX 3.5 and Visual Studio 2010–Part 5

left by click into business at 4/10/2013 7:53 PM Gravatar
I think step 8 proves to be the easiest step, which was provided by the author. All the other steps, which were posted by the author earlier, too were helpful. Cheers mate. Keep posting quality works in future also.
Title  
Name
Email (never displayed)
Url
Comments   
Please add 8 and 2 and type the answer here: