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 
OK, so now when we build/run we have a nicely populated list of sites:

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

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! 
Print | posted on Saturday, 26 February 2011 1:00 PM