August 27, 2009

Installer CustomAction, Debugging the CustomAction, InstallState

Custom Action

The Custom Action is added to the Setup Project, select the Project node and hit the Custom Action button. This allows you add an Action to a particular phase in the Installation. But first you must create the Custom Action.

To Add a Custom Action you must first have a Custom Action created, this is usually in the form of a Installer Class, this should be created in a seperate project, the Installer Class is actually one of the File Templates in the C# Projects. So it's File->New Project and select Visual C# Projects. Then add a Class Library, this will prompt you for the Class Library Types , select "Installer Class".

Walkthrough - Creating Custom Action (msdn).

Also here's a more comprehensive document on Setup/Installer implementations, it delves into the Registry etc
Getting Started with Setup Projects (SimpleTalk).

Visual Studio Setup Projects and Custom Actions (Simple Talk).

Create your Installer Class and then add it as a Custom Action to the Setup Project.

What happens now is that you can access the Installers data using what are called Installer Properties. How this is done is a bit messy, after you've added your Installer Class as a Custom Action to the Setup Project you must specify arguments to be passed to the Installer Class at runtime, apparantly this is the only way to references installer data after the installation has completed.

Here's an example of how to pass the name of the installation directory to the Custom Action assembly:

This is done in your Setup Project's Custom Action Tab.

Set the property of the Custom Action you have just added to

        /InstallLoc="[TARGETDIR]\"

NOTE: the extra \" at the end is required to return a directory.

This tells the Custom Action dll that it can access the TARGETDIR (which is the installation location) with the InstallLoc arg, here's how it's done in the Custom Action class:

            this.Context.Parameters["InstallLoc"];

see CustomEventInstaller.Installer

private void AfterInstallEventHandler(object sender, InstallEventArgs e)
{
//Copy the assemblies to the location _installationLoc
//From the command line (passed in by the Setup)
//_installationLoc is also used in the generated .targets file
//InstallLoc="[TARGETDIR]""
_installationLoc = this.Context.Parameters["InstallLoc"];


For multiple Custom Action parameters use a single space between each parameter.



/appsetting1=[EDITA1] /appsetting2=[EDITA2] /installLoc="[TARGETDIR]\"

accessible in your Custom Action code with

_installLoc = Context.Parameters["installLoc"];
_appsetting1 = Context.Parameters["appsetting1"];//first edit
_appsetting2 = Context.Parameters["appsetting2"];//second edit


For Common Properties used in installer applications and other like TARGETDIR see
http://msdn.microsoft.com/library/default.asp?url=/library/en-us/msi/setup/property_reference.asp

http://msdn2.microsoft.com/en-us/library/9cdb5eda.aspx



Debugging the CustomAction

Debugging the CustomAction can be tricky.

To attach the Visual Studio Debugger to the installation process, msiexec.exe is nigh on impossible. The best solution is to add a

Debugger.Break();

or a

System.Windows.Forms.MessageBox.Show(............);

The Debugger.Break() will force the runtime to prompt the installer to attach a Debugger this is your opportunity. Attach to a Visual Studio session that has the CustomAction project source code open and debug as normal once attached.

You may however find that the Debugger.Break() or other code you've just added to your CustomAction does not get picked up even though you've rebuilt, this has caught me out a few times, usually when I'm trying to debug the UnInstall. The usual reason is that the code the installer is using as the CustomAction is not the one you've just built. To resolve this drop the newly built assembly into the Target installation location, e.g. if your CustomAction is built to a dll named MyCustomAction.dll and you've just installed to C:\Program Files\MyCompany\MyApp then drop the newly built dll and it's pdb into here and then try to uninstall. The uninstall picks up the CustomAction from the target location (this is also why you see you CustomAction's assembly in the target installation directory after install).


InstallState, stateSaver and savedState IDictionary

In your CustomAction code you'll see an IDictionary being passed around the Install and UnInstall overrides. This IDictionary is a HashTable. Because the Install and UnInstall are 2 completely different instances the UnInstall knows nothing of member variables set in the Install, instead it relies on an IDictionary, this IDictionary uses a text file to store it's data, thsi is how the Install and the UnInstall can comunicate i.e. the actual have no communication but the Install writes to a file and later the Uninstall reads from that same file. The IDictionary contains paths to the components that the CustomAction has installed (these also get written to a file in the Target installation location which has an extension ".InstallState").

At install time the IDictionary, "stateSaver", records the component names and their paths that were installed, this gets serialized in the file ".InstallState". At UnInstall time, this .InstallState file is deserialized into the IDictionary, "stateSaved", which the Uninstall override uses.

public override void Uninstall(IDictionary savedState)

{

base.Uninstall(savedState);

}

With the call to baseUninstall(stateSaved) it can uninstall all that was installed.

In the Install override you're free to add content yourself, the key can be any string really, remember the key must be unique, the value however should be a path to a file or the full path to a registry entry. The base.Uninstall(savedState) goes through each of these entries and attempts to remove them. The IDictionary can also be used to store values you'd like to explicitly uninstall yourself, remember you cannot depend on member variables you must have some persitant storage to store variables and that is this IDictionary.

Here's an example of a registry entry which was created by the CustomAction on Install and so must be deleted by the CustomAction on UnInstall

public override void Install(IDictionary stateSaver)

{

RegistryKey expressionEvaluatorPackageVersionKey = expressionEvaluatorInProcKey.CreateSubKey(packageVersion);

add the entry to the IDictionary with unique name for the Key and full registry path for the Value

stateSaver.Add("expressionEvaluatorPackageVersionKey" + guidString + packageVersion, expressionEvaluatorInProcKey.OpenSubKey(packageVersion).ToString());

}

At UnInstall pull the entry from the IDictionary and Explictly uninstall

It's unusual to have to explicitly do this, this should be done by the call to base.Uninstall(savedState).

public override void Uninstall(IDictionary savedState)

{

RegistryKey hKeyClassesRootCLSID = Registry.ClassesRoot.OpenSubKey("CLSID", true) as RegistryKey;

foreach (object key in savedState.Keys)

{

if (key.ToString().Contains("expressionEvaluatorPackageVersionKey"))

{

if (savedState[key] != null)

{

int index = savedState[key].ToString().IndexOf(@"\");

string keyStringWithoutHKEY_CLASSES_ROOT = savedState[key].ToString().Substring(index + 1);

RegistryKey expressionEvaluatorPackageVersionKey = Registry.ClassesRoot.OpenSubKey(keyStringWithoutHKEY_CLASSES_ROOT, true) as RegistryKey;

if(Registry.ClassesRoot.OpenSubKey(keyStringWithoutHKEY_CLASSES_ROOT, true) != null) Registry.ClassesRoot.DeleteSubKeyTree(keyStringWithoutHKEY_CLASSES_ROOT);

}


3 comments:

Samarjit said...

Was very helpful and solved my problem for Getting Dynamic Path for Setup.exe

Website Designing said...

Really informative solution.

- J.
Web Designing

Web Solutions said...

Hi

These information are helpful for knowledge and a custom action is creating first. This is usually in the form of a Installer Class and the installer file use for file template.

- J.
Web Solutions