This blog is used as a memory dump of random thoughts and interesting facts about different things in the world of IT. If anyone finds it useful, the author will be just happy! :-)

Thursday, September 17, 2009

Going to Agileee 2009

Today I’m heading to the Agileee 2009 conference being held in Kiev on September, 18 – 19th. This is rather new field to me – I’ve never been practicing Agile or Scrum before. We’ll see how it goes. At least, I’m expecting to learn many new and interesting things and see how to apply this in the Modules team.

Tuesday, August 18, 2009

WebDirProperties: AnonymousUser attribute is not obligatory

When you specify a WebDirProperties element to be used by the sites you install (configure) with WiX, you might also want to allow anonymous access to this site. Fortunately, there’s an attribute AnonymousAccess, which being set to ‘yes’ allows anonymous access to IIS web site.

NOTE: If you don’t address any property of “authorization” group (AnonymousAccess, BasicAuthentication, DigestAuthentication, PassportAuthentication or WindowsAuthentication) in your WebDirProperties, the site inherits those from w3svc root. If you set at least one explicitly, you need to set others the way you wish, because WiX defaults might not work for you.

The wix.chm states that “When setting this (AnonymousAccess) to 'yes' you should also provide the user account using the AnonymousUser attribute, and determine what setting to use for the IIsControlledPassword attribute.” But it turns out you are not forced to provide the AnonymousUser attribute and I suppose you never wanted to – you should provide a password as well, but who knows the password of IUSR on a target machine?

Instead, just omit the AnonymousUser attribute and this part of IIS metabase will stay untouched. The username/password will inherit from higher node (again, w3svc). And yes, don’t forget IIsControlledPassword=”yes”.

Hope this helps you tuning the website during the installation.

Sunday, August 16, 2009

A warning to VB.NET developers

Avoid defining methods with default arguments!

Today I have been exploring the “Member design” chapter of a great book of Cwalina/AbramsFramework Design Guidelines”, and found a guideline which shocked me a bit. No, the guideline itself is correct and valuable. I just was never thinking it works like this.

VB.NET has a language feature called default arguments. When you define a method in your class, you can specify default values to the optional parameters to be taken when this parameter is omitted. As far as I understand, this is a kind of alternative to the method overloading.

Consider the following code:

Public Class DefaultValues
    Public Function Sum(ByVal a As Integer, Optional ByVal b As Integer =
        Sum = a + b
    End Function
  End Class

(I speak C# myself, so excuse me my poor VB ;-))

Let’s say we compile this code into a DLL and we have a client console application to utilize that library:

Module TestDefaultValues
    Sub Main()
        Dim df As DefaultValues.DefaultValues = New DefaultValues.DefaultValues()
    End Sub
  End Module

Compile everything and run TestDefaultValues.exe. The result is predictable: 155.

Now change the default value from 100 to 200 and compile only the library. DO NOT recompile the client application. Run it again, and it is still 155!

This is why it is strongly not recommended to use default arguments instead of normal method overloading. And this issue is why C# doesn’t expose this technique.

Be careful, VB.NET developers! And long live C#! :-)

Sunday, July 19, 2009

XSLT: inline blocks of managed code

It’s not a secret that XSLT supports blocks of code, written in another language, to be used inside the stylesheet. It seems to have been there from the very beginning – at least, XSLT 1.1 understands it.

However, Microsoft enriched this option with their own element, msxsl:script. It offers pretty much the same functionality, but you can also write the code in C# or any other language of .NET platform. XSLT gurus might argue that it is superfluous stuff and it is unnecessary in 99% of cases. Well, as for me, XSLT lacks a number of useful functions in the standard library, such as ToLower/ToUpper, EndWith, etc. You never think about such low level things when programming C#, but you often have to invent a wheel trying to do the same with XSLT.

More details can be found in the official documentation, but here is a brief extract:

  • guess an extra prefix and let XSLT processor know about it:

    <xsl:stylesheet version="1.0"

    Also, pay attention how msxsl prefix is defined – it is required to use msxsl:script syntax.
  • code your extension function:

    <msxsl:script language="C#" implements-prefix="ext">
       public string ToUpper(string inString)
          return inString.ToUpper();
  • and finally use it:

    <xsl:value-of select="ext:ToUpper(@Name))"/>

Obviously, it is not a good idea to write lots of code this way. It makes the XSLT stylesheet larger and a bit harder to maintain. And, according to Microsoft, you should “avoid script blocks from XSLT files, because they require loading the script engine multiple times”. Actually, if you created an XSLT stylesheet to fill it with tones of .NET code, you’re definitely doing something wrong. But it seems to be good addition to small, but useful “one-line” operations.

Sitecore and msxsl:script

If you plan to take advantage of inline blocks of C# code in Sitecore XSL rendering, you’ll have to do one more step. By default, .NET API to handle the XSL transforms disables the possibility to use msxsl:script. It is probably done for security reason. But the web.config of your Sitecore solution contains the setting EnableXslScripts, which you can easily set to true and be happy:

      Determine whether XSLT script support should be enabled.
      If script support is not enabled, it will be an error if the XSLT file contains script blocks.
      Default value: false.
<setting name="EnableXslScripts" value="true" />

The performance seems to be the same for this simple code either written in msxsl:script block, or wrapped into XSL extension. So, the choice is yours.

WiX and msxsl:script

The heat.exe utility of the WiX toolset has an option to run the harvested authoring against XSLT transform. This is a checkpoint when you can mutate the output before it is done. INHO, it is the most powerful extension option of Heat, because you can do anything with the XML fragment in XSLT.

However, it was a bit disappointing to find out the scripts are disabled by default, and it is not customizable, and the easiest way to fix this is to patch Heat itself and prepare custom WiX build. It would be great if this option is available one day in the base, either as a command line argument, or a configuration setting.

That’s it. If you have some experience with this trick, knowing its pros and cons deeper, share it here. And as usual, any comments are welcome.

P.S. this post was written with the help of Windows Live writer :-)

Sunday, March 22, 2009

Validating the source of TreeList

Sitecore 6 validation was designed to validate the field values. Recently, I also found it useful to control the source of the complex field types, like TreeList. In this post, I'll explain this option taking the TreeList field type as an example.

I'm skipping the validation basics here, since this topic is covered by Alexey Rusakov in his validation series.

You can define a number of parameters in the source of TreeList field type. The complete list is described in the paragraph 2.4.2 "How to Control the List of Items in a Selection Field" of the Data Definition cookbook. These parameters can filter the available and visible items in the content tree (IncludeTemplatesForSelection, ExcludeItemsForDisplay, etc.), define the tree root (DataSource), control multiple selection (AllowMultipleSelection), etc.

But modifying this long list of parameters in a one-line edit field can lead to a simple typos, both in the parameters' names and values. Let's examine how this can be "solved" by introducing a source validator.

The BaseValidator class, the very root of the validator hierarchy in Sitecore API, has a protected method GetField(), which returns an instance of a Field - the one we validate. Hence, the Source property is also available. We want to validate only complex source here, thus skipping if it is an ID or an item path:

        protected override ValidatorResult Evaluate()
            ValidatorResult result = ValidatorResult.Valid;

            Field field = GetField();
            if (field != null)
                string fieldSource = field.Source;
                if (!string.IsNullOrEmpty(fieldSource) && !ID.IsID(fieldSource) 
                    && !fieldSource.StartsWith("/", StringComparison.InvariantCulture))
                    result = EvaluateSourceParameters(fieldSource);

            return result;

Ok, let's start the validation from just the verification if the source is "well-formed". It might happen that a certain parameter was left without a value, or a typo was introduced to the well-known name. Sitecore will never throw an error in such a case, but instead you may receive an orphaned field with nothing to choose from. Thus, the simplest validation includes these two checks, otherwise it keeps the name/value pairs for further analysis:

        ValidatorResult EvaluateSourceParameters(string fieldSource)
            SafeDictionary parameters = new SafeDictionary();
            string[] sourceParts = fieldSource.Split('&');
            foreach (string part in sourceParts)
                if (string.IsNullOrEmpty(part))
                if (!part.Contains("=") || part.EndsWith("="))
                    Text = string.Format("The value is not set for source parameter '{0}'", part.TrimEnd('='));
                    return GetFailedResult(ValidatorResult.Error);
                    string parameterName = part.Substring(0, part.IndexOf('=')).ToLower();
                    if (!sourceParameters.Contains(parameterName))
                        Text = string.Format("Unknown source parameter '{0}'", parameterName);
                        return GetFailedResult(ValidatorResult.Error);
                        string parameterValue = part.Substring(part.IndexOf('=') + 1);
                        parameters.Add(parameterName, parameterValue);
            return EvaluateWellFormedParameters(parameters);

The further validation can go deeper and verify the presence of the specified template or item. The method EvaluateWellFormedParameters in this example just iterates the name/value pairs of parameters and applies a certain validation strategy, for instance:

        ValidatorResult EvaluateTemplates(string value, Database database)
            string[] templates = value.Split(new char[] { ',' });
            foreach (string template in templates)
                if (!string.IsNullOrEmpty(template) && Query.SelectSingleItem(string.Format("/sitecore/templates//*[@@key='{0}']", template.ToLower()), database) == null)
                    Text = string.Format("The template '{0}' doesn't exist in the '{1}' database", template, database.Name);
                    return ValidatorResult.Warning;
            return ValidatorResult.Valid;

I'm attaching the full code of this example

There are several notes to consider:
  • The DatabaseName parameter is not validated, because Sitecore takes over this. Try specifying DatabaseName=nosuchdb, and press Save
  • The parameter names are case-insensitive. This is because the parameters are extracted with the StringUtil.ExtractParameter() method, which ignores the case
  • The TreeList field type doesn't "tolower" the values of IncludeItemsForDisplay and ExcludeItemsForDisplay parameters. Hence, be sure to specify an item key instead of an item name here
  • The content tree filter is built out of the "ForDisplay" parameters using 'and' operation. Thus, if IncludeItemsForDisplay contain items of other templates than those specified in IncludeTemplatesForDisplay, this results in an empty tree. This can also be a point of extension of this validator's functionality
Hope anyone finds this article useful. As usual, I would appreciate any comments.

Monday, February 2, 2009

Extended logging options in WiX custom actions

The best and maybe the only way to find out what's going wrong with the installation is investigating the MSI log file. Fortunately, the Windows Installer respects log writing very much. You can find the ways to enable logging and different logging options here.

The verbose log contains all the information MSI can generate. Though it is really useful when it comes to troubleshooting the failed installation, it is quite hard to read, especially for newbies. Again, fortunately, there's a super chapter "Using the Windows Installer log" in a super book called "The Definitive Guide to Windows Installer" by Phil Wilson, which guides you through the basics of log file reading and understanding.

I used to generate the log file with /L*v options, which means verbose. But, if you use WiX custom actions, it turns out that you can make them logging even more verbose.

If you browse the WiX source code, you can find the lines like this in its custom actions:

WcaLog(LOGMSG_STANDARD, "Error: Cannot locate User.User='%S'", wzUser);

The first argument is a logging level. It can be 
  • LOGMSG_STANDARD, which is "write to log whenever informational logging is enabled", which in most cases means "always"
  • LOGMSG_TRACEONLY, which is "write to log if this WiX build is a DEBUG build" (is often used internally to dump CustomActionData contents)
  • LOGMSG_VERBOSE, which is "write to log when LOGVERBOSE"
Wait a minute, what does the last option means? I've already set the verbose logging by /L*v, but I don't see more entries there. Here is the algorithm WiX CA use to know whether to write a log entry marked as LOGMSG_VERBOSE level:
  • Check if the LOGVERBOSE property is set (can be set in the command-line since it is public)
  • Otherwise, check if the MsiLogging property is set (MSI 4.0+)
  • Otherwise, check the logging policy in the registry

So, the following is the easiest way in my opinion to make your MSI (WiX-based) log file even more verbose:

   msiexec /i package.msi ... LOGVERBOSE=1 /L*v install.log

Hope this helps.

P.S. This is just a brief extract of what's there in the source code. As usual, code is the best documentation ;-)

Tuesday, January 20, 2009

IIS extension: WebAppPool

Another challenge - another piece of fun with WiX. Imagine the following requirement: the installation program must install an application pool on IIS6+ environments; the multiple installed instances should use the same application pool. In other words, the application pool must be created with the first instance installation, and must be removed with the last instance uninstallation. 

A special element for maintaining IIS AppPools in IIS extension is called WebAppPool. As usual, we'll wrap it into a separate component, so that it is created on install. Later, we'll create a special custom action to deceive the standard removing mechanism on uninstall:

      <Component DiskId="1" Id="CreateIISAppPool" Guid="{YOURGUID-6C5B-4980-AD0B-E32FA2DBC1F4}" Directory="WebsiteFolder">
         <Condition>IISMAJORVERSION <> "#5"</Condition>
         <iis:WebAppPool Id="IISSiteAppPool6" Name="[IISAPPPOOL_NAME]" MaxWorkerProcesses="1" Identity="networkService" />
         <RegistryKey Root="HKLM" Key="$(var.ParentKey)">
            <RegistryValue Name="IISAppPoolName" Type="string" Value="[IISAPPPOOL_NAME]"/>

As you can see, the component is installed once the target system has IIS 6+. It creates a WebAppPool with the name provided in IISAPPPOOL_NAME public property. It also writes this name into a registry value, which resides under the instance-specific registry key. 
With this component included into the MSI package, the app pool is created when the first instance is installed, and nothing happens for second and subsequent instances. 

Let's examine the uninstall behavior. The MSI behaves natural - when it meets the component to uninstall, it removes the WebAppPool specified in it. But the IIS extension which performs the actual deletion of app pool, needs the name to be passed in it. So, the only thing we should do is to supply this action with a fake app pool name each time, except for the last instance uninstall.

Here is the algorithm:
  1. search the registry for the app pool name as usual
  2. schedule a special action on unistall after AppSearch, which detects if this is the last instance being uninstalled, and if not, "breaks" the app pool name into something non-existent
The first point is quite straight-forward:

      <Property Id="IISAPPPOOL_NAME">
         <RegistrySearch Id="IISAppPoolName" Root="HKLM" Key="$(var.ParentKey)" Name="IISAppPoolName" Type="raw" />

The second one is not natural, like any hack:

      public static ActionResult ChangeWebAppPoolNameToDeceiveUninstall(Session session)
         int numberOfInstalled = 1;
         foreach (ProductInstallation product in ProductInstallation.GetRelatedProducts(session["UpgradeCode"]))
            if ((session["ProductCode"] != product.ProductCode) && product.IsInstalled)

         if (numberOfInstalled > 1)
            session["IISAPPPOOL_NAME"] += string.Format("|{0}", DateTime.Now.ToLongTimeString());

         return ActionResult.Success;

It iterates the related products (those sharing the UpgradeCode), and if it finds others installed, except for itself, it changes the app pool name we retrieved from registry into something unique, for instance, appends a unique string.

Thus, the IIS custom action which is going to delete the app pool fails to find the one with the provided name, and does nothing. When, otherwise, it is the last instance being uninstalled, the retrieved app pool name remains unchanged, and the app pool is successfully removed.

Note that the mentioned action should be immediate, should occur after AppSearch on uninstall.

That's it! I would appreciate any comments as usual.

Monday, January 19, 2009

IIS extension: WebSite

Ok, it's time for another portion of the installation fun, now about the IIS web sites.

The IIS extension in WiX is probably the most tricky and unobvious. That's my personal impression, of course. But, anyway, it gives you an option to tweak any property of a website, virtual directory or web directory. 

When installing a web application on Windows XP and thus IIS 5.1, it is natural to create an "ad hoc" virtual directory during install and remove it on uninstall. That's basically quite common case, but what if the application requires to reside under the site root directly, not virtual directory? 

In this case the root of the Default Web Site should just be switched to the installation directory - nothing is created on install and nothing is removed on uninstall. Let's see how this can be done with WiX IIS extension.

The iis:WebSite element has two "modes": if it resides under Component element, it is created during install, otherwise it is there just for reference from other elements. Fortunately, it has a special attribute ConfigureIfExists. Setting it to 'yes' avoids an attempt to create a new site, configuring the existent one instead:

      <Component DiskId="1" Id="ModifyIISSite5" Guid="{YOURGUID-2023-4D19-90D2-EE9101C71E44}" Directory="WebsiteFolder" Permanent="yes">
         <Condition>IISMAJORVERSION = "#5"</Condition>
         <iis:WebSite Id="IISSite5" Description="[IISSITE_NAME]" Directory="WebsiteFolder" ConfigureIfExists="yes">
            <iis:WebAddress Id="IISSiteAddress5" Port="[IISSITE_PORT]"/>

Note, that in this case you should make sure you specified the existent website data. The website is uniquely identified by the description, port and header. The first is an attribute of a WebSite element itself, others belong to the child mandatory element WebAddress. 

The previous snippet highlights another attribute as bold - Permanent="yes". It makes the hosting component permanent, thus preventing it from being deleted on uninstall. Internally, the Windows Installer engine just keeps an extra reference to this component forever, thus it reference count is never equal to 0.

One last thing I'd like to point your attention to is a component condition. It uses the property called IISMAJORVERSION. This property, as well as another one called IISMINORVERSION, is brought by the IIS extension. They are populated from the target system registry during the AppSearch action. Before using them in your authoring make sure you add a couple of references:

    <PropertyRef Id="IISMAJORVERSION"/>
    <PropertyRef Id="IISMINORVERSION"/>

That's it! As usual, any comments are highly appreciated.

Saturday, January 10, 2009

Attach / Detach database during installation

It seems I have finally managed to implement full database support in my installation program. And it also seems that I stepped on every rake one could imagine in this area. But, the harder the battle, the sweeter the victory.

I had the following requirements: the application is distributed with the MDF/LDF files, which must be attached during installation and detached during uninstallation. Both Windows and SQL authentication must be supported.

Fortunately, the kind WiX developers implemented a wonderful SQL extension. So, let's take advantage of the sql:SqlDatabase element. The documentation says, it can be placed either under Component, or under Fragment/Module/Product. In the first case the database will always be created when the component is being installed. This doesn't suite our needs with attach, so let's stick with another option:

<sql:sqldatabase id="SqlMasterDBWinAuth" server="[SQL_SERVER]" database="master"/>

As you can see, we specify the standard Master database in this element. That's because the database must exist on the target computer by the moment Windows Installer tries to connect. This syntax will instruct the custom action to open the connection using currently logged-on Windows account.

The next step is to provide the appropriate sql:String elements for attach/detach. It is better to put these elements inside the component which installs MDF/LDF files, but this is not the rule. And if you have different conditions for installing the files and running attach, you'll have to move the scripts into a separate component.

<Component DiskId="1" Id="MSSQLCore" Guid="YOURGUID-4E94-4B28-B995-DCBFD50B9F07">
<Condition>YOUR CONDITION GOES HERE</Condition>
<File Id="MSSQLCoreFile" Name="$(var.CoreFileName)" KeyPath="yes" />
<File Id="MSSQLCoreLogFile" Name="$(var.CoreFileLogName)" />

<sql:SqlString Id="DetachCore" Sequence="1" ContinueOnError="yes" ExecuteOnUninstall="yes" SqlDb="SqlMasterDBWinAuth" SQL="EXEC master.dbo.sp_detach_db @dbname = N'[INSTANCENAME]Core', @skipchecks=N'true'"/>

<sql:SqlString Id="AttachCore" Sequence="2" ContinueOnError="no" ExecuteOnInstall="yes" SqlDb="SqlMasterDBWinAuth" SQL="CREATE DATABASE [\[][INSTANCENAME]Core[\]] ON ( FILENAME = N'[DB_FOLDER]$(var.CoreFileName)' ), ( FILENAME = N'[DB_FOLDER]$(var.CoreFileLogName)' ) FOR ATTACH"/>


At this point I should mention one reef. An SqlString string element also has an attribute SQLUser. If you provide both SqlDb attribute, pointing to the "WinAuth" definition of the database, and SqlUser attribute, pointing to the "sa user", it will lead to unpredictable and very strange behavior. I would avoid this.

Ok, now we should take care about the rollback actions: during install and uninstall correspondently. It is obvious that RollbackOnInstall should detach databases, if they got installed before failure, and RollbackOnUnistall should attach the databases back, if the failure occurred during uninstall.

Thanks to the hint of Rob Mensching in one of his replies to the WiX mailinglist, I managed to overcome another trick. Right after the database is attached, there is sometimes a connection left to this database. I can see this by opening the SQL Management studio and looking at the database status (Normal). If you detach the database in this moment, it flushes the permissions on a physical file to a logon account only. I didn't dig very deep into this, it probably corresponds to the rules of permissions change during attach/detach. As a result, the windows installer can't access the file afterwards, and the uninstallation is rolled back.

To fix this, perform "SET OFFLINE" query before detaching the database and you'll never face with this behavior again.

Thus, the final version will look similar to this:

<Component DiskId="1" Id="MSSQLCore" Guid="YOURGUID-4E94-4B28-B995-DCBFD50B9F07">
<Condition>YOUR CONDITION GOES HERE</Condition>
<File Id="MSSQLCoreFile" Name="$(var.CoreFileName)" KeyPath="yes" />
<File Id="MSSQLCoreLogFile" Name="$(var.CoreFileLogName)" />

<sql:SqlString Id="RollbackDetachCore" Sequence="1" ContinueOnError="yes" RollbackOnUninstall="yes" SqlDb="SqlMasterDBWinAuth" SQL="CREATE DATABASE [\[][INSTANCENAME]Core[\]] ON ( FILENAME = N'[DB_FOLDER]$(var.CoreFileName)' ), ( FILENAME = N'[DB_FOLDER]$(var.CoreFileLogName)' ) FOR ATTACH"/>
<sql:SqlString Id="OfflineCoreDatabase" Sequence="2" ContinueOnError="yes" ExecuteOnUninstall="yes" SqlDb="SqlMasterDBWinAuth" SQL="ALTER DATABASE [\[][INSTANCENAME]Core[\]] SET OFFLINE WITH ROLLBACK IMMEDIATE" />
<sql:SqlString Id="DetachCore" Sequence="3" ContinueOnError="yes" ExecuteOnUninstall="yes" SqlDb="SqlMasterDBWinAuth" SQL="EXEC master.dbo.sp_detach_db @dbname = N'[INSTANCENAME]Core', @skipchecks=N'true'"/>
<sql:SqlString Id="RollbackAttachCore" Sequence="4" ContinueOnError="yes" RollbackOnInstall="yes" SqlDb="SqlMasterDBWinAuth" SQL="EXEC master.dbo.sp_detach_db @dbname = N'[INSTANCENAME]Core', @skipchecks=N'true'"/>
<sql:SqlString Id="AttachCore" Sequence="5" ContinueOnError="no" ExecuteOnInstall="yes" SqlDb="SqlMasterDBWinAuth" SQL="CREATE DATABASE [\[][INSTANCENAME]Core[\]] ON ( FILENAME = N'[DB_FOLDER]$(var.CoreFileName)' ), ( FILENAME = N'[DB_FOLDER]$(var.CoreFileLogName)' ) FOR ATTACH"/>


Ok, but what about Sql Authentication? Well, this requires some kind of duplicating the code. The SqlDb attribute of the SqlString element can't accept MSI properties, thus can't be dinamically changed during runtime. We must author another element SqlDatabase for referencing it from another set of scripts.

<util:User Id="SQLUser" Name="[SC_SQL_SERVER_USER]" Password="[SC_SQL_SERVER_PASSWORD]"/>
<sql:SqlDatabase Id="SqlMasterDBWinAuth" Server="[SC_SQL_SERVER]" Database="master" />
<sql:SqlDatabase Id="SqlMasterDBSqlAuth" Server="[SC_SQL_SERVER]" Database="master" User="SQLUser" />

The first element defines a user to connect to the database. In this example, the username and password are read from the public properties. The user is not created, it is just referenced. The second element should be familiar - it was described above. And the last one differs only in one attribute - SQLUser.
This does the trick: if you want Windows Authentication way to use, reference SqlMasterDBWinAuth in your scripts, otherwise - use SqlMasterDBWinAuth. Obviously, you need another set of the similar SqlString elements in a different component.

Tired? The last thing.

If you implemented something similar to what I've described, you should have mentioned that in case of Sql Auth the database is attached as read-only. This happens because the SQL service account (NETWORK SERVICE in my case) doesn't have enough permissions to the [DB_FOLDER] and files by the moment attach starts.
No problem, let's assign the necessary rights. Put the following snippet into your component which contains the SqlAuth scripts:

<util:PermissionEx GenericAll="yes" User="NetworkService" />

Note: Don't forget to reference WIX_ACCOUNT_NETWORKSERVICE property.

But, wait, the ShedSecureObjects is scheduled after the InstallSqlData, this doesn't help!
Right, the sequence should also be changed like this:

<Custom Action="InstallSqlData" After="SchedSecureObjects">NOT SKIPINSTALLSQLDATA AND VersionNT > 400</Custom>

That's it! I know, this can't seem easy at first glance, but, as for me, it is much more controlled and customizable, than with InstallShield. I might be wrong, though.

Good luck! I would appreciate any comments and notes to this.