-
Notifications
You must be signed in to change notification settings - Fork 175
Deployment scenarios
- Part 1 - Authoring MSI
- Part 2 - Deployment scenarios (this document)
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
- Component ID Considerations
- Installation Directory
- Working with registry
- Windows Services
- .NET prerequisite
- .NET compatibility
- Including files dynamically (working with wild card source location)
- Product upgrades
- Working with environment variables
- Properties
- Deferred actions
- x64 components
- Managed Custom Actions
- Skipping UI Dialogs
- Injecting Custom WinForm dialog
- Complete Custom External UI
- Burn bootstrapper
- Embedded UI
- Embedded UI limitations
- Managed (Standard) UI
- Managed WPF UI
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 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.
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"))));
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
}
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,
};
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
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
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"),
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.
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 });
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.
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"));
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.
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.
When attaching, you will need to attach to the msiexec.exe process. Normally you will have two
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));
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:
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.
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.
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:
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.
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.
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:
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();
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.
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:
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.
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.
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:
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:
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 theUACRevealer
by placing this statement in your code:UACRevealer.Enabled = true;
A detailed discussion of the problem can be found here: #151 and #301.
- Home - Overview
- Architecture
- Documentation
- Samples Library
- Product Roadmap
- Tips'n'Tricks