Skip to content

Deployment scenarios

Oleg Shilo edited this page Oct 3, 2023 · 34 revisions

Overview

Deployment scenarios

While most of the deployment scenarios reflected in the product samples are self-explanatory some of them may benefit from some extra explanations:

Identities and Naming

Many of the MSI concepts are not very intuitive and over complicated. One of the the biggest MSI blunder is the deployment identities. For example, one would think that MSI identity ProductId uniquely identifies a product. And when you hear 'product' you may immediately think of Notepad++, Visual Studio 2015 or Windows 10... However in MSI domain ProductId is a unique identifier of a single msi file. Thus two msi for Notepad++ v6.7.4 and v6.7.5 have different ProductIds even if they are deploying the same product Notepad++.

From the other hand, ProductUpgradeCode may seem like something that is associated with a version of a Product so upgrades (versions) can be easily identified. But... no, it is the identifier that is shared by all versions of a product. Thus it is in fact something that is associated with the Product as marketing understands it.

MSI is using ProductUpgradeCode to implement upgrades. It is in fact a vital component, without which it is impossible to define relationship between different versions of the same product. And yet... MSI considers it as optional. Meaning that MSI built with the default settings installs the product that is impossible to upgrade ever. You can remove it and install a new version manually but you cannot upgrade it.

Wix# is trying to bring some order into this. Instead of already taken ProductId and UpgradeCode (which are still available if required) it introduces ProductGUID (Project.GUID). Wix# compiler uses Project.GUID as a seed for deterministic auto-generation of both product Id and UpgradeCode. Thus you can have your deployment for two versions of the product as follows:

//script AB
project.Version = new Version("1.0.0.0");
project.GUID = new Guid("6f330b47-2577-43ad-9095-1861ba25889b");
 
//script B
project.Version = new Version("2.0.0.0");
project.GUID = new Guid("6f330b47-2577-43ad-9095-1861ba25889b");

Both produced msi files will have the same ProductUpgradeCode and unique ProductId exactly as MSI requires. In fact, if you are building a one off msi you can even skip assigning Project.GUID and Wix# will use a random one. Unconditional initialization of UpgradeCode by Wix# compiler also allows every msi produced with it to be 'upgradable'.

The easiest way to illustrate this is to look at the routine that is executed by the WixSharp engine before building the msi file.

public void GenerateProductGuids()
{
    if (!GUID.HasValue)
        GUID = Guid.NewGuid();

    if (!UpgradeCode.HasValue)
        UpgradeCode = GUID;

    if (!ProductId.HasValue)
        ProductId = CalculateProductId(guid.Value, Version);
}

Naming is another form of identity of the MSI/WiX entities. For example MSI/WiX requires ever directory to have both Name and Id. Th id is used as an internal reference and the name is used as the name of the directory created on the target system after deployment. Wix# allows omitting use of Id in C# code:

new Project("MyProduct",
    new Dir(@"%ProgramFiles%\My Company\My Product",
        new File(@"Files\Bin\MyApp.exe"),
        new Dir("Docs",
            new File(@"Files\Docs\Manual.txt"))));

In the sample above the Ids are auto-generated from the entity names. The Id generation algorithm takes care about possible Id duplications by using special suffixes. The auto-Ids are generated on the first access thus various Id evaluations in your code (e.g. if (file.Id.EndsWith("Services.exe"))) may affect on the final Id allocation outcome.

Thus if you are really need to have 100% deterministic id allocation deterministic you may want to use explicit Ids:

new Project("MyProduct",
    new Dir(new Id("PRODUCT_INSTALLDIR"), @"%ProgramFiles%\My Company\My Product",
        new File(new Id("App_File"), @"Files\Bin\MyApp.exe"),
        new Dir("Manual",
            new File(new Id("Manual_File"), @"Files\Docs\Manual.txt"))));

Though you will find that in most cases Wix# Id allocation is just fine. A part from being convenient Id auto-allocation allows avoiding potential id clashes associated with the manual id assignments from initializers. See Custom_IDs sample for details.

Component ID Considerations

Component ID is one of the deployment concepts that have been grossly over-engineered and bring more confusion than benefits (similar to MSM and MinorUpgrade).

Thus a given component id must be globally unique. Otherwise other products can accidentally trigger MSI ref-counting by installing more than one product with the same component id.

The problem is caused by the mind bending MSi concept that the component identity does not logically belong to the product but to the target system. Thus if two different products happens too define a file with the same component id MSI will treat them as the same deployable artifact (component).

Using GUID seems to be a good technique to overcome this limitation. Though it's not very common as the wast majority of the samples (e.g. WiX) use human readable ids, which are of course not unique. The excellent reading on the topic can be found here: http://geekswithblogs.net/akraus1/archive/2011/06/17/145898.aspx

WixSharp tries to hide and automate component id allocation. By default it uses conservative technique that simply takes the id of the component's child entity, like File. And since the File by default has the id determined by the file path on the target system, this mechanism does not warranty global uniqueness of the derived component id.

This can be improved by setting Compiler.AutoGeneration.ForceComponentIdUniqueness = true:

string compId = wFile.GenerateComponentId(wProject);
...
string GenerateComponentId(Project project, string suffix = "")
{
    return this.GetExplicitComponentId() ??
            project.ComponentId($"Component.{this.Id}{suffix}");
}
...
string ComponentId(string seed)
{
    if (Compiler.AutoGeneration.ForceComponentIdUniqueness)
        return $"{seed}.{this.GUID.ToString().Replace("-", "")}";
    else
        return seed;
}

This would allow both readability and uniqueness. Though as per release v1.9.3 Compiler.AutoGeneration.ForceComponentIdUniqueness is disabled by default to allow users to have some time for adopting this new technique.

And you can also define an explicit component id with attributes:

new File(@"Files\Bin\MyApp.exe") 
{ 
    AttributesDefinition = "Component:Id=Component.myapp_exe" 
}                   

This is what WixSharp does for component id with respect of automatic allocation and uniqueness, but the actual runtime behavior based on the component id is entirely controlled by MSI. Thus if you have problems caused by the specific component id values then your bast chances to find the solution are to consult MSI/WiX documentation.

ID allocation customization

The need for user defined ID allocation algorithm was expressed in the multiple reports. The existing ID auto-generation algorithm exhibited certain practical limitations in some scenarios. While manual ID allocation gives the user the complete control over ID allocation, having a hybrid solution when user has ability to customize the auto-allocation was an attractive option.

Starting from this release user as an ability to provide a customid-allocation algorithm, which is invoked by WixSharp if no explicit Id for the WixEntity is specified:

project.CustomIdAlgorithm = 
        entity =>
        {
            if (entity is File file)
            {
                var target_path = this.GetTargetPathOf(file);

                var dir_hash = Math.Abs(target_path.PathGetDirName().GetHashCode32());
                var file_name = target_path.PathGetFileName();

                return $"File.{dir_hash}.{file_name}"; // pass to default ID generator
            }
        };

WixSharp also provides an alternative ready to go id-allocation algorithm, which addresses the reported limitations. Thus instead of emitting indexed IDs it produces ID with the target path hash for all File entities/elements. Note this feature does not affect user ability to set IDs manually.

// globally via configuration
Compiler.AutoGeneration.LegacyDefaultIdAlgorithm = false;

// for project only via delegate
project.CustomIdAlgorithm = project.HashedTargetPathIdAlgorithm;

The feature implementation took into account the community feedback provided via #204.

The new approach is more reliable but it will affect the users who stores the generated WXS files under source control. That's why the new API allows switching back to legacy ID-allocation algorithm if required.

Currently (in this release) the new hash-based algorithm is disabled by default to allow the users some time to get used to the new algorithm. But it will be made enabled by default in the next release.

Installation Directory

The target install directory has special importance. The install directory is a the first directory containing the files to be installed. It is the one that user can change in the Installation Directory dialog when using UI.

In the code sample above it will be %ProgramFiles%/My Company/My Product. And if user doesn't specify the Id for the install directory Wix# always assigned it to the INSTALLDIR. Or if to be more accurate the value of the Compiler.InstallDirDefaultId.

The most obvious way of specifying he installation directory (e.g. '%ProgramFiles%/My Company/My Product') is to define a Dir` tree:

new Dir("%ProgramFiles%",
    new Dir("My Company",
        new Dir("My Product", ...

However you can simplify the definition by using a path as the directory name:

new Dir("%ProgramFiles%\My Company\My Product", ...

In this case the Dir constructor will automatically create (unfold) the required tree structure.

Note, that the constructor is smart enough to tunnel the arguments to the last Dir item as it should:

new Dir(new Id("my_prod_id"), "%ProgramFiles%\My Company\My Product", ...

// is equivalent of 

new Dir("%ProgramFiles%",
    new Dir("My Company",
        new Dir(new Id("my_prod_id"), "My Product", ...

Be careful, the unfolding may lead to some unexpected results when used without proper attention:

var dir = new Dir("%ProgramFiles%\My Company\My Product", ...
string name = dir.Name;

In the code above the value of name is "%ProgramFiles%". if you want to post-access the "My Product" dir you can do it via Project:

var dir = project.FindDir(@"%ProgramFiles%\CustomActionTest");
string name = dir.Name;

In the code above now the value of name is "My Product".

Sometimes it is required to have multiple root directories. In this case WixSharp incorrectly identify the one that has to play the role of the "installation directory". In this case user can explicitly indicate that the directory is the "installation directory" by assigning IsInstallDir property of the Dir object. In the later versions of WixSharp there is a dedicated InstallDir class that is identical to class Dir, except it has IsInstallDir set to true by default:

var project = 
    new ManagedProject("Test",
        new Dir("%ProgramFiles%",
            new InstallDir("MyProduct", 
                new File("setup.cs")),
            new Dir(@"MSBuild\MyProduct", 
                new File("readme.txt"))));

Working with registry

Sample: Registry

When deploying registry entries you have the option to add the registry values one by one or you can import the values from the registry file:

var project =
    new Project("MyProduct",
        ...
        new RegFile("MyProduct.reg"), 
        new RegValue(RegistryHive.LocalMachine, @"Software\.....         
        new RegValue(RegistryHive.LocalMachine @"Software\.....

When importing the registry file it is important to remember that not all the types of the reg file are supported by MSI. Only the following reg file values can be mapped to the MSI registry value types:

DWORD
REG_BINARY
REG_EXPAND_SZ
REG_MULTI_SZ
REG_SZ

By default Wix# compiler will throw an exception if unsupported registry value type is found but this behaviour can be overwritten by setting the RegFileImporter.SkipUnknownTypes flag.

You also have an option to import all RegValues form the regfile manually and then add them to the project either one by one or as the whole collection.

RegValue[] values = Tasks.ImportRegFile("MyProduct.reg");
...
var project = new Project("MyProduct", values.ToWObject(), ...
//or 
project.ImportRegFile("MyProduct.reg");

When writing to the registry on x64 systems by default the values go to the WOW6432Node hive, which is a destination of all registry IO for Win32 applications. If you want to ensure with are writing to the default x64 hive you need to specify it explicitly:

new RegValue(RegistryHive.LocalMachine, @"Software\My Company\My Product", "LICENSE_KEY", "01020304")
{
    Win64 = true
}

Windows Services

Sample: WinService/With_InstrallUtil; WinService/With_WiX

Installing windows services can be done either by using built-in standard .NET installUtil.exe. Wix# implemented dedicated routines for installing/uninstalling and starting/stopping windows services. If you decide to deploy the service this way you need to call the service control routine from the custom action:

var project =
    new Project("My Product",
        new Dir(@"%ProgramFiles%\My Company\My Product",
            new File(@"..\SimpleService\MyApp.exe")),
        new ElevatedManagedAction("InstallService", 
                                  Return.check, 
                                  When.After, 
                                  Step.InstallFiles, 
                                  Condition.NOT_Installed),
        new ElevatedManagedAction("UnInstallService", 
                                  Return.check, 
                                  When.Before, 
                                  Step.RemoveFiles, 
                                  Condition.BeingRemoved));
...
 
public class CustomActions
{
    [CustomAction]
    public static ActionResult InstallService(Session session)
    {
        return session.HandleErrors(() =>
        {
            Tasks.InstallService(session.Property("INSTALLDIR") + "MyApp.exe", true);
            Tasks.StartService("WixSharp.SimpleService", false);
        });
    }
    ...

Alternatively you can deploy windows service using native WiX functionality. In many cases you would prefer this service deployment model as it is fully consistent with the WiX recommended practices.

File service;
var project =
    new Project("My Product",
        new Dir(@"%ProgramFiles%\My Company\My Product",
            service = new File(@"..\SimpleService\MyApp.exe")));
 
service.ServiceInstaller = new ServiceInstaller
                            {
                                Name = "WixSharp.TestSvc",
                                StartOn = SvcEvent.Install,
                                StopOn = SvcEvent.InstallUninstall_Wait,
                                RemoveOn = SvcEvent.Uninstall_Wait,
                            };

.NET prerequisite

Sample: LaunchConditions

There are various ways of validating that a specific version of .NET is present on the target system. The recommended one is by calling SetNetFxPrerequisite of the project. When calling this method you should pass the string expression representing a presence of the desired version of .NET. This in turn sets the corresponding LaunchConditions based on the corresponding property of the WiX NetFxExtension extension:

var project = new Project("Setup",
    new Dir(@"%ProgramFiles%\My Company\My Product",
        new File(@"Files\MyApp.exe")));
 
project.SetNetFxPrerequisite("NETFRAMEWORK20='#1'");
 
Compiler.BuildMsi(project);

Though in some cases the conditions can be more complicated:

project.SetNetFxPrerequisite("NETFRAMEWORK45 >= '#378389'", ...);
project.SetNetFxPrerequisite("NETFRAMEWORK30_SP_LEVEL and NOT NETFRAMEWORK30_SP_LEVEL='#0'", ...);

The full list of properties and values for can be found here: http://wixtoolset.org/documentation/manual/v3/customactions/wixnetfxextension.html

.NET compatibility

WixSharp.dll, which implements built-in Custom Actions and Standard UI Dialogs, is compiled against .NET v3.5. There is a good reason for this. This version represents a good compromise between functionality and availability on the potential target systems. Of course it doesn't mean that you can only build MSI that targets systems hosting .NET v3.5. No. The by default Wix# will pack any embedded assembly with the app.config file that controls how the assembly will be hosted on the target system during the installation. The same config file also controls CLR compatibility. The below is the default config file content:

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <startup useLegacyV2RuntimeActivationPolicy="true">
        <supportedRuntime version="v[NoRevisionVersionOfBuildSystem]"/>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/>
        <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/>
        <supportedRuntime version="v2.0.50727"/>
        <supportedRuntime version="v2.0.50215"/>
        <supportedRuntime version="v1.1.4322"/>
    </startup>
</configuration>

However if for whatever reason the default config file is not suitable for your case you can specify any config file of your choice:

project.CAConfigFile = "app.config";

Note there was at least one compatibility issue report regarding external UI setup being run on .NET 4.6 target system: http://wixsharp.codeplex.com/discussions/645142

Including files dynamically (working with wild card source location)

Sample: LaunchConditions, Release Folder, WildCard Files

Dealing with the static file structure (to be deployed) is straight forward. However in many cases you may want include in your MSI different sets of files depending on various conditions. For example you create the Wix# project for deploying all files from the Release folder of the Visual Studio project. Once you defined Wix# build script you don't want to update it every time your VS project is changed (e.g. )

Sometimes the set of files to be installed is a subject to the application (to be installed) configuration. For example every new release can contain different set of the dlls or the documentation files. It would very impractical to redefine the Wix# Project every time the new release setup file is to be built. Wix# allows dynamic inclusion of the files in the project by using Files class with the wildcard path definition:

var project =
    new Project("MyProduct",
        new Dir(@"%ProgramFiles%\My Company\My Product",
            new Files(@"Release\*.*"),
            new ExeFileShortcut("Uninstall My Product", 
                                "[System64Folder]msiexec.exe", 
                                "/x [ProductCode]")));

The code sample above shows how to include into project all files from the 'Release' folder and all it's subfolders. The Files constructor accepts exclusion attributes making it a convenient option for processing the Visual Studio build directories:

new Files(@"Release\*.*", "*.pdb", "*.obj"),...

NOTE: It is important to remember that the wild cards are resolved into a collection of files during the Compiler.Build call. Thus if you want to aqccess any of these files before calling Compiler.Build you need to to trigger resolving the wildcard definitions explicitly. The following code snippet shows how to ensure the executable form the release folder will have the corresponding shortcut file created during the installation:

project.ResolveWildCards()
       .FindFile((f) => f.Name.EndsWith("MyApp.exe"))
       .First()
       .Shortcuts = new[] {
                               new FileShortcut("MyApp.exe", "INSTALLDIR"),
                               new FileShortcut("MyApp.exe", "%Desktop%")
                           };

When you need to have more control over the project subdirectories generation during resolving the wild cards you may want to use DirFiles class. It is a logical equivalent of Files class except it only collects the files from the top directory:

new Project("MyProduct",
    new Dir(@"%ProgramFiles%\MyCompany\MyProduct",
        new DirFiles(@"\\BuildServer\MyProduct\LatestRelease\*.*"),
        new Dir("Docs", 
            new DirFiles(@"\\DocumentationServer\MyProduct\*.chm"),

Product upgrades

Sample: MajorUpgrade1,MajorUpgrade2

Upgrading is a relatively simple deployment concept. However its MSI implementation is a bit convoluted and not very intuitive. Wix# allows handling the upgrade scenario in a relatively simple way. Working with the product identities is described in the 'Identities and Naming' section and this section covers the actual upgrade implementation.

MSI recognizes Major and Minor Upgrade scenarios. However the practical application of Minor Upgrades is almost non existent and the latest MS deployment technology ClickOnce does not even allow any equivalent of Minor Upgrade.

Wix# API provides a special class MajorUpgradeStrategy that defines the upgrade model desired for your product. It includes (as per MSI specification):

  • Range of the versions allowed to be upgraded by your msi (UpgradeVersions)
  • Range of the versions forbidden to be upgraded by your msi (PreventDowngradingVersions)
  • Error message to be displayed on the restricted downgrade attempt (NewerProductInstalledErrorMessage)

In the vast majority of case upgrades are not concerned about the specific versions and can be expressed by a single statement: "Upgrade if the installed version is older".

This can be coded as follows:

project.MajorUpgradeStrategy = new MajorUpgradeStrategy
{
    UpgradeVersions = VersionRange.OlderThanThis,
    PreventDowngradingVersions = VersionRange.NewerThanThis,
    NewerProductInstalledErrorMessage = "Newer version already installed"
};

In fact MajorUpgradeStrategy already has the code predefined as Default so the code above can be further simplified:

project.MajorUpgradeStrategy = MajorUpgradeStrategy.Default

Note: MajorUpgradeStrategy yields WiX UpgradeVersion element, which is arguably the most comprehensive upgrade definition. However in the later versions of WiX a simplified upgrade definition has been introduced. It relies on MajorUpgrade WiX element. For most of the upgrade scenarios you will find MajorUpgrade allows achieve the same result with much less effort. Wix# supports MajorUpgrade element via MajorUpgrade member:

project.MajorUpgrade = new MajorUpgrade
{    
    Schedule = UpgradeSchedule.afterInstallInitialize,
    DowngradeErrorMessage = "A later version of [ProductName] is already installed. Setup will now exit."
};

Sometimes it is required to implement conditional installation based on detecting upgrade (as opposite to install) session. It's not as straightforward as one may expect. Read this article for details: https://github.com/oleg-shilo/wixsharp/wiki/Detecting-upgrade-at-runtime.

Working with environment variables

Sample: EnvVariables

Wix# support for environment variables includes both handling the build environment variables during the MSI authoring and deployment of the environment variables on the target system.

Compile Time
You can embed environment variables in any path value of the project or Dir/File property:

new Dir(@"%ProgramFiles%\My Company\My Product",
    new File(@"%bin%\MyApp.exe"),
...
project.OutDir = @"%LATEST_RELEASE%\MSI";
project.SourceBaseDir = "%LATEST_RELEASE%";

Deploy Time
The support for deploying environment variables is fairly consistent with the WiX model for environment variables:

var project =
    new Project("MyProduct",
        ...
        new EnvironmentVariable("MYPRODUCT_DIR", "[INSTALLDIR]"),
        new EnvironmentVariable("PATH", "[INSTALLDIR]") 
        { Part = EnvVarPart.last });
 

Properties

Sample: Properties, PropertyRef, SetPropertiesWith

Wix# implementing properties is straightforward:

var project =
    new Project("MyProduct",
        ...
        new Property("Gritting", "Hello World!"),
        new Property("Title", "Properties Test Setup"),

However the code above is only appropriate for setting the properties to the hardcoded values but not to the values that include references to other properties. MSI/WiX prohibits this and requires a custom action instead. With Wix# this problem is solved with the syntax very similar to the code above:

var project =
    new Project("MyProduct",
        ...
        new Property("Gritting", "Hello World!"),
        new SetPropertyAction("Title", "[PRODUCT_NAME] Setup"),

And of course you can always set the property to any value if you do this from the ManagedCustomAction:

[CustomAction]
public static ActionResult OnInstallStart(Session session)
{
    session["Title"] = session["PRODUCT_NAME"] + " Setup";

    return ActionResult.Success;
}

Referencing any external property can be achieved with PropertyRef class:

var project = new Project("Setup",
                  new PropertyRef("NETFRAMEWORK20"),

Property NETFRAMEWORK20 is defined and initialized by the WiX framework outside of the user code.

Deferred actions

Sample: DeferredActions

Deferred Actions, without too much details, are custom actions that are executed in a different runtime context comparing to the regular custom actions and standard actions. Deferred Actions play important role in addressing the MSI restrictions associated with the user security context. For example Deferred Actions are the only way to perform during setup any activity that require user elevation. Though Deferred Actions themselves associated with some serious limitations.

Thus Deferred Actions cannot access any session property as the session is terminated at the time of the action execution. MSI's standard way of overcoming this limitation is to:

  • create a new custom action for setting the property
  • set the property name to the name of the deferred action
  • set the property value to the specially formatted map
  • schedule the execution of the custom action
  • access the mapped properties only via Microsoft.Deployment.WindowsInstaller.Session.CustomActionData.

With Wix# all this can be done in a single hit. Wix# fully automates creation of the all mapping infrastructure. But before you start using mapped properties you need to understand the MSI property mapping paradigm.

With MSI mapping you are required to declare upfront what standard properties (or property containing values) you are going to use in the runtime context of the Deferred action. Declaring is achieved via specially formatted string:
<deferred_prop1>=<standard_prop1>[;<deferred_propN>=<standard_propN>]

The following is the example of how ElevateManagedAction (it is automatically initialized as differed one) maps standard properties:

new ElevatedManagedAction("OnInstall")
{
    UsesProperties = "D_INSTALLDIR=[INSTALLDIR];[D_PRODUCT]=[PRODUCT]"
}

Though Wix# syntax is more forgiving and you can use coma as a delimiter and you can also skip the assignment if you want to use the same names for the both properties. Thus the following two statements are semantically identical.

UsesProperties = "INSTALLDIR=[INSTALLDIR];[PRODUCT]=[PRODUCT]"
UsesProperties = "INSTALLDIR, PRODUCT"   

In fact all Managed actions have always UILevel and INSTALLDIR mapped by default:

class ManagedAction : Action
{
   public string DefaultUsesProperties = "INSTALLDIR,UILevel";

Consuming the mapped properties isn't straightforward. You cannot use session["INSTALLDIR"] as the session object is already cleared at the time of deferred execution. Thus you have to use an alternative set of properties session.CustomActionData["INSTALLDIR"]. Though Wix# simplifies this by providing an extension method that return the property value transparently regardless of which property collection contains it:

[CustomAction]
public static ActionResult OnInstall(Session session)
{
    session.Log(session.Property("INSTALLDIR"));

x64 components

Sample: Install on x64

MSI does not provide any solution for CPU neutral setup. Thus individual setups need to be built for every CPU architecture to be deployed on. Unfortunately the WiX follows the case and the WiX code, which targets x86 has to be heavily modified before it can be used to build x64 setup. This includes a different name for the 'Program Files' directory, special attributes for the Package and all Component elements.

Wix# allows all this to be done with a single Platform assignment statement:

var project =
    new Project("MyProduct",
        new Dir(@"%ProgramFiles%\My Company\My Product",
            new File(@"Files\Bin\MyApp.exe"),
            ...
 
project.Platform = Platform.x64;

Note: while Wix# compiler auto-maps %ProgramFiles% into platform specific path you can still use %ProgramFiles64Folder% (or semantically identical %ProgramFiles64%) if required.

Managed Custom Actions

Sample: DTF (Managed CA), Different Scenarios\Debugging, Different Scenarios\Embedded, Different Scenarios\EmbeddedMultipleActions, Different Scenarios\External C# file, Different Scenarios\ExternalAssembly, Different Scenarios\STAThread

As opposite to MSI/WiX, which requires external modules for implementing Custom Actions, Wix# allows them to be implemented directly in the build script. And more importantly using the same language, which is used for the setup definition - C#.

The simplest custom action (ManagedAction) can be defined as follows:

var project = 
    new Project("CustomActionTest",
        new ManagedAction("MyAction", Return.check, 
                                      When.After, 
                                      Step.InstallInitialize, 
                                      Condition.NOT_Installed));
 
...

public class CustomActions
{
    [CustomAction]
    public static ActionResult MyAction(Session session)
    {
        MessageBox.Show("Hello World!", "Embedded Managed CA");
        session.Log("Begin MyAction Hello World");
 
        return ActionResult.Success;
    }
}

The name of the class implementing custom action is irrelevant as long as it is public. Similarly, the actual custom action is any public static method with the ActionResult Method(Session) signature. And the last touch, the method has to be marked with the CustomAction attribute.

What can be done from ManagedAction? Practically anything. The possibilities are limitless. You can completely take advantage the full scale rich runtime, which CLR is. You can interact with the file system, check the registry, connect to the online resource, interact with the user. You have the full power of .NET at your disposal. And of course you can interact with the MSI runtime via the session object passed into the action method as a parameter. The most common use of the session object is to read and write MSI properties values:

[CustomAction]
public static ActionResult OnInstallStart(Session session)
{
    session["Title"] = session["PRODUCT_NAME"] + " Setup";
 
    return ActionResult.Success;
}

See Properties section for details.

If you need elevated privileges then you need to use ElevatedManagedAction class, which schedules the custom action as deferred one. See Deferred actionssection for details.

Sometimes instead of the build script you may want to implement ManagedAction as a separate assembly. In such case you need to specify the location of this assembly as a second string parameter in the ManagedAction constructor:

new ManagedAction("MyAction", @"\bin\myCustomActionAssembly.dll")
new ManagedAction("MyAction", "%this%")

Note: a special value %this% is reserved for the build script assembly. If no assembly path is specified then %this% is assumed.

As any C# routine ManagedAction may depend on some external assemblies. Thus for the ManagedAction to run correctly on the target system the dependency assemblies need to be packaged into MSI setup.

new ManagedAction("CustomAction")
{
    RefAssemblies = new []{"DispalyMessage.dll"}
}

If you need to define multiple custom actions then you may want to define a common set of assemblies to be packaged with all ManagedActions:

project.DefaultRefAssemblies.Add("CommonAsm.dll");   

Thanks to the fact that ManagedAction is implemented in a plain .NET assembly it can be debugged as easily as any other managed code. The most practical way of debugging is to put an assert statement and attach the debugger when prompted:

[CustomAction]
public static ActionResult MyAction(Session session)
{
    Debug.Assert(false);
    ...

Note, when attaching the debugger you need to ensure that you have your Visual Studio is running elevated.

image

image

image

When attaching, you will need to attach to the msiexec.exe process. Normally you will have two

Skipping UI Dialogs

Sample: Skip_UIDialog

Modifying one of the predefined the UI dialogs sequences is relatively simple with WiX. For example skipping the Licence Agreement dialog can be achieved with the following WiX statement:

<UI>
    <Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" 
             Value="InstallDirDlg"   Order="5">1</Publish>
    <Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" 
             Value="WelcomeDlg"   Order="5">1</Publish>
</UI>

The equivalent Wix# statement semantically is very similar. The DialogSequence is a dedicated class for re-wiring UI dialogs sequence. You can use it to connect ("short circuit") the dialogs by associating OnClick actions of Next and Back buttons of the dialogs to be connected:

project.CustomUI = 
        new DialogSequence()
           .On(Dialogs.WelcomeDlg, Buttons.Next, 
               new ShowDialog(Dialogs.InstallDirDlg))
           .On(Dialogs.InstallDirDlg, Buttons.Back, 
               new ShowDialog(Dialogs.WelcomeDlg));

Injecting Custom WinForm dialog

Sample: Custom_UI/CustomCLRDialog

It is also possible to inject a dialog in the predefined UI dialogs sequences. There are two possibilities for that:

  • Native MSI UI Dialog
  • Managed WinForm UI Dialog

Support for building native MSI UI is was available on in the earlier releases of Wix# as an experimental feature and no longer supported. It has been superseded by the "Embedded UI" feature available in v1.0.22.0 and higher. Though Wix# suite contains CustomMSIDialog sample that demonstrates the technique.

Injecting a managed WinForm dialog from other hand represents more practical value as functionality wise WinForms are far superior to native MSI UI and much easier to work with. The image below is the custom dialog from the CustomCLRDialog sample:

image

Strictly speaking user defined managed dialog is not injected in the UI sequence as MSI cannot understand managed UI. Instead a special managed custom action is scheduled as a first "start up action" of a native dialog. This action hides the native dialog and instead displays the managed dialog. When user presses "Next" button the managed dialog becomes invisible and the native one displayed instead.

image

This technique is relatively simple. But it does require a special care when implementing the "injection":

project.InjectClrDialog("ShowCustomDialog", 
                        NativeDialogs.InstallDirDlg, 
                        NativeDialogs.VerifyReadyDlg);

Arguable the "Embedded UI" (particularly Managed Standard UI (Managed Standard UI) offers a much simpler development model with even greater functionality.

Complete Custom External UI

Sample: External_UI/ConsoleSetup, External_UI/WinFormsSetup, External_UI/WpfSetup

However the ultimate UI power comes with the External UI MSI architecture. MSI allows delegating interaction to the external process while maintaining the runtime communication between the external UI and MSI runtime via dedicated MSI Interop interface.

This is the most flexible UI technique available as there is no restriction at all on how your UI implemented and how it is hosted at runtime. It can be anything. It can be a WPF, WinForm or even a console application for that matter. The topic is covered in details in this {CodeProject article}(http://www.codeproject.com/Articles/804584/Wixsharp-WixSharp-UI-Extensions). The following is a custom WPF UI from the WpfSetup sample:

image

WixSharp provides a lean interface for integrating the UI application. This is the fragment of the WinForm based setup sample:

public partial class MsiSetupForm : Form
{
    MyProductSetup session;

    public MsiSetupForm(string msiFile)
    {
        InitializeComponent();
 
        session = new MyProductSetup(msiFile);
        ...
        session.ProgressChanged += session_ProgressChanged;
        session.ActionStarted += session_ActionStarted;
        session.SetupComplete += session_SetupComplete;
    }
    
    void installBtn_Click(object sender, EventArgs e)
    {
        DisableButtons();
        session.StartInstall();
    }

It is important to note that the MSI External UI model separates completely UI, msi file and MSI runtime. That is why a typical distribution model for an external UI setup is an msi file accompanied with the UI executable (often setup.exe).

Note that Wix# provide an extremely thin abstraction layer between the user routine and MSI runtime API thus in many cases you may have to deal with the MSI Interop directly.

Burn bootstrapper

Sample: Bootstrapper/WixBootstrapper*

Wix# allows defining the setup bundle for building a singe file bootstrapper for installing multiple products:

var bootstrapper =
        new Bundle("My Product",
            new PackageGroupRef("NetFx40Web"),
            new MsiPackage(crtMsi),
            new MsiPackage(productMsi) { DisplayInternalUI = true } );
 
bootstrapper.AboutUrl = "https://wixsharp.codeplex.com/";
bootstrapper.IconFile = "app_icon.ico";
bootstrapper.Version = new Version("1.0.0.0");
bootstrapper.UpgradeCode = new Guid("6f330b47-2577-43ad-9095-1861bb25889b");
bootstrapper.Application.LogoFile = "logo.png";
bootstrapper.Application.LicensePath = "licence.html";   

bootstrapper.Build();

With Wix# you can also easily specify what type of the bootatrapper UI you want to use:

//Standard WiX LicenseBootstrapperApplication
bootstrapper.Application = new LicenseBootstrapperApplication();
 
//Wix# CustomBA with no UI
bootstrapper.Application = new();
 
//User defined CustomBA implemented in the same Wix# project 
bootstrapper.Application = new ManagedBootstrapperApplication("%this%");

It is important to note that when you use custom (non WiX) like SilentBootstrapperApplication or even your own. WiX is no longer responsible for detecting (at startup) is the bootstrapper needs to be initialized in the Install or Uninstall mode. Any BA implements a routine responsible for this. The example of such a routine can be found in the WixBootstrapper_NoUI sample.

However if you are using Wix# pre-built SilentBootstrapperApplication then you need to indicate what package in the bundle is the primary one. The presence of this package on the target system will trigger initialization of the bootstrapper in Install or Uninstall mode:

var bootstrapper =
        new Bundle("My Product",
            new PackageGroupRef("NetFx40Web"),
            new MsiPackage(crtMsi),
            new MsiPackage(productMsi) 
            {
                Id = "MyProductPackageId",
                DisplayInternalUI = true 
            }); 
bootstrapper.Application = new SilentBootstrapperApplicationrsion("MyProductPackageId"); 

If primary package Id is not defined then the last package will be treated as the primary one.

Some user feedback on Burn integration has been captured and reflected in this discussion. It is highly recommended that you read this discussion particularly if you are planning to build a custom msi and/or bootstrapper UI. However if you are using SilentBootstraper then it is a must-read.

Embedded UI

Sample: Custom_UI/EmbeddedUI, Custom_UI/EmbeddedUI_WPF

Embedded UI is nothing else but an external UI embedded into MSI so there is no need to distribute separately. A single msi file is fully sufficient for installing the product. Thanks WiX team the integration is simple and straight forward. You just need to build the assembly implements Microsoft.Deployment.WindowsInstaller.IEmbeddedUI interface and then assign Wix# EmbeddedUI member to that assembly:

project.EmbeddedUI = new EmbeddedAssembly(sys.Assembly.GetExecutingAssembly().Location);

Wix# suite comes with the EmbeddedUI and EmbeddedUI_WPF samples , which are based on practically unmodified sample from the WiX toolset samples:

image

Embedded UI limitations

Note that Embedded UI runs instead of the InstallUISequence, so results of standard actions like AppSearch and costing cannot be used in your embedded UI assembly. This is the nature of the MSI Embedded UI architecture. The following blog contains a good description of this constraint: Fun with MSIEmbeddedChainer.

Note that MSI support for Embedded UI is available beginning with Windows Installer 4.5. Thus any OS with earlier version of Windows Installer services (e.g. WinXP) will ignore any EmbeddedUI elements (see this MSDN article). Though you can still do Injecting Custom WinForm dialog.

Thus all InstallUISequence standard and custom actions (if any) will need to be implemented in the user assembly of the class implementing IEmbeddedUI interface. The good candidate for the placement is the IEmbeddedUI.Initialize(...) method.

Managed UI allows simplified mechanism for implementing InstallUISequence actions. You can implement the required actions in the UIInitialized event handler:

project.UIInitialized += UIInitialized;
...
static void UIInitialized(SetupEventArgs e)
{
    if (e.IsInstalling)
    {
        var conflictingProductCode = "{1D6432B4-E24D-405E-A4AB-D7E6D088C111}";
 
        if (AppSearch.IsProductInstalled(conflictingProductCode))
        {
            var msg = string.Format(
                        "Installed '{0}' is incompatible with this product.\n" +
                        "Setup will be aborted.",
                        AppSearch.GetProductName(conflictingProductCode));
 
            MessageBox.Show(msg, "Setup");
 
            e.Result = ActionResult.UserExit;
        }
    }
}

Wix# also implements C# 'assistance' routines that can leverage the well known MSI AppSearch functionality:

bool keyExist = AppSearch.RegKeyExists(Registry.LocalMachine,
                                      @"System\CurrentControlSet\services");
 
object regValue = AppSearch.GetRegValue(Registry.ClassesRoot, ".txt", null);
 
string code = AppSearch.GetProductCode("Windows Live Photo Common")
                       .FirstOrDefault();
 
string name = AppSearch.GetProductName("<GUID>");
 
bool installed = AppSearch.IsProductInstalled("<GUID>");
 
string[] products = AppSearch.GetProducts();

Managed (Standard) UI

Sample: Managed Setup/CustomUIDialog, Managed Setup/CustomUISequence

Managed UI is a recommended Wix# UI customization approach. It is a part of the Managed Setup API model. It is highly recommended that you read the Managed Setup documentation before choosing your MSI authoring development direction.

Managed UI is a Wix# abstraction API layer combined with the set of predefined standard MSI dialogs implemented as WinForms classes. It is the most powerful and straightforward Custom UI model available with Wix#. The combines the strengths of other UI models ad yet eliminates their shortcomings. The conceptual differences between available UI models are described in the Managed UI section of Managed Setup.

A typical Managed UI project (e.g. created form Wix# VS project templates) defines the deployment logic in the ManagedProject instance:

var project = new ManagedProject("ManagedSetup",
                    new Dir(@"%ProgramFiles%\My Company\My Product",
                        new File(@"..\Files\bin\MyApp.exe"),
                        new Dir("Docs",
                            new File("readme.txt"))));
 
project.ManagedUI = new ManagedUI();
 
project.ManagedUI.InstallDialogs.Add(Dialogs.Welcome)
                                .Add(Dialogs.Licence)
                                .Add(Dialogs.SetupType)
                                .Add(Dialogs.Features)
                                .Add(Dialogs.InstallDir)
                                .Add(Dialogs.Progress)
                                .Add(Dialogs.Exit);
 
project.ManagedUI.ModifyDialogs.Add(Dialogs.MaintenanceType)
                               .Add(Dialogs.Features)
                               .Add(Dialogs.Progress)
                               .Add(Dialogs.Exit);

The project has a special field ManagedUI, which defines two collections of UI dialogs. One is for displaying UI during the product installation (e.g. double-clicking msi file) and another one for the product modification (e.g. clicking 'modify' in the control panel 'Programs and Features' view). Note that 'Restore' and 'Uninstall' scenarios are not associated with any custom UI and the MSI runtime is using its native UI instead.

The code defines a typical/standard UI thus instead of re-defining it again and again you can just use set ManagedUI to the predefined 'Default' instance:

project.ManagedUI = ManagedUI.Default;

If you don't want to have any UI at all then you can use another predefined ManagedUI value:

project.ManagedUI = ManagedUI.Empty;

You can freely change the sequence of dialogs (e.g. remove some) or you can insert your own custom dialog. A custom dialog is jut a ordinary WinForm class that implements IManagedDialog interface. You can derive your form from the WixSharp.ManagedForm class, which provides the default interface implementation:

public partial class UserNameDialog : ManagedForm, IManagedDialog
{
    public UserNameDialog()
    {
        InitializeComponent();
    }
 
    void dialog_Load(object sender, EventArgs e)
    {
        banner.Image = MsiRuntime.Session.GetEmbeddedBitmap("WixUI_Bmp_Banner");
 
        name.Text = Defaults.UserName;
        password.Text = MsiRuntime.Session["PASSWORD"];
    }
 
    void back_Click(object sender, EventArgs e)
    {
        Shell.GoPrev();
    }
 
    void next_Click(object sender, EventArgs e)
    {
        MsiRuntime.Session["PASSWORD"] = password.Text;
        MsiRuntime.Session["DOMAIN"] = domain.Text;
 
        Shell.GoNext();
    }
 
    void cancel_Click(object sender, EventArgs e)
    {
        Shell.Cancel();
    }
    ...

Thanks to the MsiRuntime base class member you have the full access to the underlying MSI session. Thus you can extract the embedded MSI resources or access the session properties. MsiRuntime is also responsible for the localization.

image

Thus if you embed any text surrounded by square brackets in any Control.Text of your UI element MsiRuntile will try to substitute it with the value from the localization file or the runtime MSI property. The substitution will happen when dialog is initialized. Though you can trigger the dialog localization at any time by calling base class Localize() method. Alternatively you can localize an individual string values with MsiRuntime.Localize():

nextButton.Text = "[WixUINext]";
Localize();
//or
nextButton.Text = MsiRuntime.Localize("WixUINext");

And scheduling the new custom dialog is straight forward:

project.ManagedUI = new ManagedUI();
project.ManagedUI.InstallDialogs.Add<WelcomeDialog>()
                                .Add<MyProduct.UserNameDialog>()
                                .Add<ProgressDialog>()
                                .Add<ExitDialog>();

It is also possible to change the look and behavior of any standard dialog. This can be achieved by creating the VS project from the "Custom UI" template. The freshly created project will contain a source code for all standard dialogs and the customization will be no different to the customization of any WinForm application:

image

Note that Wix# default implementation of Features Dialog offers an alternative simplified user experience when interacting with features tree view. Thus the all features in the tree have checkboxes instead of the usual clickable icons with context menus.

image

The user experience is very simple and intuitive. If the feature's checkbox is checked the feature will be installed, otherwise, it will not. If the feature is defined as read-only (user cannot change its "to be installed" state) the check box will be disabled and the user will not be able to change its checked state.

Managed WPF UI - Complete UI

The simplest way to enable WPF is to use stock WPF dialogs:

project.ManagedUI = ManagedUI.DefaultWpf; // all stock UI dialogs

If you need complete control over WPF dialogs it is recommended to use Visual Studio templates:

image

Managed WPF UI - Custom Dialog

Sample: Managed Setup/CustomUI.WPF

Starting from v1.17.0 you can define custom dialogs of Manage UI with WPF (XAML) user controls. Interestingly enough even before this feature was rolled out it was possible to host WPF content in the WinFirm custom dialogs of the Managed UI. It was always possible simply because any WinForm container allows hosting WPF content via System.Windows.Forms.Integration.ElementHost. However v1.17.0 brings allows hosting WPF content in a straight forward way without too much effort.

Thus it is enough to define your user control as an instance of wixsharp:WpfDialog base class:

<wixsharp:WpfDialog x:Class="MyProduct.CustomDialogRawView" . . .
public partial class CustomDialogRawView : WpfDialog, IWpfDialog . . .

The base class allows you to access the msi runtime UI infrastructure (e.g. resources, session object).

And you can add the dialog to the UI sequence the same way as WinForm based dialogs:

project.ManagedUI.InstallDialogs.Add<WelcomeDialog>()       // stock WinForm dialog
                                .Add<CustomDialogRawView>() // custom WPF dialog
      

You can use any WPF binding technique as you want (e.g. MVVM frameworks). The same sample contains another dialog CustomDialogView, which is using Caliburn.Micro as a MVVM framework.

With both approaches, you are still responsible for defining the overall layout and handling user interactions in the way that is consistent with the rest of the UI sequence.

However, there is another even more intriguing technique for adding WPF content. WixSharp allows you to define only the client area content that is specific for the custom dialog while letting the rest of the dialog layout and user interactions to be handled by the WixSharp runtime. Basically you need to define a custom panel with the business logic of the custom dialog but the banner and navigation buttons will be automatically generated by WixSharp runtime:

image

And you can add the dialog panel with a very simple syntax: Complete sample

<UserControl x:Class="ConsoleApplication1.CustomDialogPanel".../>
project.ManagedUI.InstallDialogs.Add<WelcomeDialog>()                        // stock WinForm dialog
                                .Add<CustomDialogWith<CustomDialogPanel>>()  // custom WPF dialog (minimalistic);

The VS WixSharp VS project templates for WPF dialogs are yet to be developed. So use this sample console app project as an example of a x`minimalistic WixSharp setup with WPF support: https://github.com/oleg-shilo/wixsharp/blob/master/Documentation/SimpleWPFDialog.7z

Limitations:

  • Windows WinForms TreeView control doesn't natively indicate read-only mode for nodes. Thus Wix# visually indicates read-only mode with greyed out checkbox and text. However, some target systems may not fully support advanced custom rendering (owner draw) for TreeView. Thus Wix# may disable greying out of read-only checkboxes if it detects that the system is rendering incompatible. Alternatively, you can disable advanced rendering unconditionally with project.MinimalCustomDrawing = true. This limitation does not affect feature node text, which will be greyed out always for all read-only nodes.

    Read more in this discussion: https://wixsharp.codeplex.com/discussions/656266

  • ManagedUI, as well as all other implementations of EmbeddedUI, cannot possibly control UAC prompt focus management. It is done by MSI runtime, which has a flaw that leads (at least on some OSs) to the situation when MSI triggers UAC prompt being minimized and flushing on the taskbar.

    While simple clicking the UAC item on the taskbar lets the setup proceed as usual it definitely affects the overall user experience.

    If it is something you want to address more proactively you can use WixSharp UACRevealer. It is a simple class that implements workaround by shifting the focus to the taskbar just before UAC prompt is triggered. This helps to address the problem but you should test it first to ensure it is exactly what you need. You can enable the UACRevealer by placing this statement in your code:

    UACRevealer.Enabled = true; 

    A detailed discussion of the problem can be found here: #151 and #301.