Welcome to MSDN Blogs Sign in | Join | Help

Happy Holidays!

As a special treat to all of you, we are going to mostly step away from the signing example for a little bit this time around, and do something a little more fun: we will write a post-build step to allow a user to launch an application at the end of install. I first alluded to this process very early on, pointing out the steps on Aaron Stebner's blog.

(Two asides: 1) since I wrote that first entry, Aaron and I have both moved from our old teams (me from VB, Aaron from I don't know where, but I think it has something to do with Windows Media Player) to the Deployment Technology Group. 2) I think this gives me license to tease Aaron: I'm pretty sure he got that jscript from me, but failed to give me my props).

So, here is the converted task. It has 2 required parameters:

  • Database (string): the name of the database that will be modified
  • FileName (string): the name of the file that will be launched when the installation is finished.

Additionally, there are three optional parameters:

  • Arguments (string): command line arguments to pass to the executable when run. This wasn't used in the escript, but I added them in for completeness.
  • CheckboxText (string): the display string shown to the user asking if they want to launch the application
  • CheckoxChecked (boolean): the default value for the checkbox. Checked means to launch the application when finished

The full task is given below:

public class EnableLaunchApplication : Task
{
	private string checkboxText = "Launch [ProductName]";
	private bool checkboxChecked = true;
	private string commandLine = string.Empty;
	private string database = string.Empty;
	private string fileName = string.Empty;

	public string CheckboxText
	{
		get { return checkboxText; }
		set { checkboxText = value; }
	}

	public bool CheckboxChecked
	{
		get { return checkboxChecked; }
		set { checkboxChecked = value; }
	}

	public string CommandLine
	{
		get { return commandLine; }
		set { commandLine = value; }
	}
		
	[Required]
	public string Database
	{
		get { return database; }
		set { database = value; }
	}

	[Required]
	public string FileName
	{
		get { return fileName; }
		set { fileName = value; }
	}

	public override bool Execute()
	{
		try
		{
			Type classType = Type.GetTypeFromProgID("WindowsInstaller.Installer");
			Object installerClassObject = Activator.CreateInstance(classType);
			Installer installer = (Installer)installerClassObject;

			Database msi = installer.OpenDatabase(Database, MsiOpenDatabaseMode.msiOpenDatabaseModeTransact);
			string fileId = FindFileIdentifier(msi);
			if (fileId == null)
			{
				Log.LogError("Unable to find '{0}' in File table.", FileName);
				return false;
			}
			string sql = "SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BannerBmp'";
			View view = msi.OpenView(sql);
			view.Execute(null);
			Record record = view.Fetch();
			record.set_StringData(11, "CheckboxLaunch");
			view.Modify(MsiViewModify.msiViewModifyReplace, record);
			view.Close();

			// Resize the BodyText and BodyTextRemove controls to be reasonable
			sql = "SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BodyTextRemove'";
			view = msi.OpenView(sql);
			view.Execute(null);
			record = view.Fetch();
			record.set_IntegerData(7, 33);
			view.Modify(MsiViewModify.msiViewModifyReplace, record);
			view.Close();

			sql = "SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BodyText'";
			view = msi.OpenView(sql);
			view.Execute(null);
			record = view.Fetch();
			record.set_IntegerData(7, 33);
			view.Modify(MsiViewModify.msiViewModifyReplace, record);
			view.Close();

			// Insert the new CheckBox control
			sql = "INSERT INTO `Control` (`Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help`) VALUES ('FinishedForm', 'CheckboxLaunch', 'CheckBox', '18', '117', '343', '12', '3', 'LAUNCHAPP', '{\\VSI_MS_Sans_Serif13.0_0_0}" + CheckboxText + "', 'CloseButton', '|')";
			view = msi.OpenView(sql);
			view.Execute(null);
			view.Close();

			// Modify the Order of the EndDialog event of the FinishedForm to 1
			sql = "SELECT `Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering` FROM `ControlEvent` WHERE `Dialog_`='FinishedForm' AND `Event`='EndDialog'";
			view = msi.OpenView(sql);
			view.Execute(null);
			record = view.Fetch();
			record.set_IntegerData(6, 1);
			view.Modify(MsiViewModify.msiViewModifyReplace, record);
			view.Close();

			// Insert the Event to launch the application
			sql = "INSERT INTO `ControlEvent` (`Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering`) VALUES ('FinishedForm', 'CloseButton', 'DoAction', 'VSDCA_Launch', 'LAUNCHAPP=1', '0')";
			view = msi.OpenView(sql);
			view.Execute(null);
			view.Close();

			// Insert the custom action to launch the application when finished
			sql = "INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) VALUES ('VSDCA_Launch', '210', '" + fileId + "', '" + CommandLine + "')";
			view = msi.OpenView(sql);
			view.Execute(null);
			view.Close();

			if (CheckboxChecked)
			{
				// Set the default value of the CheckBox
				sql = "INSERT INTO `Property` (`Property`, `Value`) VALUES ('LAUNCHAPP', '1')";
				view = msi.OpenView(sql);
				view.Execute(null);
				view.Close();
			}
			msi.Commit();

			System.Runtime.InteropServices.Marshal.FinalReleaseComObject(record);
			System.Runtime.InteropServices.Marshal.FinalReleaseComObject(view);
			System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
			System.Runtime.InteropServices.Marshal.FinalReleaseComObject(installer);
		}
		catch(Exception ex)
		{
			Log.LogErrorFromException(ex);
			return false;
		}
		return true;
	}

	private string FindFileIdentifier(Database msi)
	{
		// First, try to find the exact file name
		string sql = "SELECT `File` FROM `File` WHERE `FileName`='" + FileName + "'";
		View view = msi.OpenView(sql);
		view.Execute(null);
		Record record = view.Fetch();
		if (record != null)
		{
			string value = record.get_StringData(1);
			view.Close();
			return value;
		}
		view.Close();

		// The file may be in SFN|LFN format.  Look for a filename in this case next
		sql = "SELECT `File`, `FileName` FROM `File`";
		view = msi.OpenView(sql);
		view.Execute(null);
		record = view.Fetch();
		while (record != null)
		{
			if (record.get_StringData(2).EndsWith("|" + FileName, StringComparison.OrdinalIgnoreCase))
			{
				string value = record.get_StringData(1);
				view.Close();
				return value;
			}
				record = view.Fetch();
		}

		view.Close();
		return null;
	}
}

Using the task is pretty easy: follow the same form as the other projects, with the following post build project file:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <UsingTask TaskName="EnableLaunchapplication" AssemblyFile="$(MSBuildExtensionsPath)\SetupProjects.Tasks.dll" />

    <Target Name="EnableLaunchApplication">
        <EnableLaunchApplication 
            Database="$(BuiltOutputPath)"
            FileName="CommandLineArgs.exe"
            CommandLine="Test arguments"
        />
    </Target>
</Project>

Building and running the installer shows the behavior we expect. 

This task mostly has the same form of our other tasks:

  • Create an installer
  • Open the Database
  • Look up or insert stuff

Sounds like we can do some refactoring. Let's create a base class, SetupProjectTask. This base task will have the required parameter Database just like all of other tasks. The Execute method will create an installer and open the database, then hand off to an abstract method to do the work for the actual task. This base task is below:

public abstract class SetupProjectTask : Task
{
	private string database = string.Empty;
	private Installer installer = null;
	private Database msi = null;
		
	[Required]
	public string Database
	{
		get { return database; }
		set { database = value; }
	}

	protected Database Msi
	{
		get { return msi; }
	}

	protected Installer Installer
	{
		get { return installer; }
	}

	protected MsiOpenDatabaseMode OpenMode
	{
		get { return MsiOpenDatabaseMode.msiOpenDatabaseModeTransact; }
	}

	public override bool Execute()
	{
		bool fSucceeded = false;
		Type classType = Type.GetTypeFromProgID("WindowsInstaller.Installer");
		Object installerClassObject = Activator.CreateInstance(classType);
		installer = (Installer)installerClassObject;

		msi = installer.OpenDatabase(Database, OpenMode);
		try
		{
			fSucceeded = ExecuteTask();
		}
		catch (Exception ex)
		{
			Log.LogErrorFromException(ex);
			return false;
		}
		finally
		{
			System.Runtime.InteropServices.Marshal.FinalReleaseComObject(msi);
			System.Runtime.InteropServices.Marshal.FinalReleaseComObject(installer);
		}
			
		return fSucceeded;	}

	protected abstract bool ExecuteTask();
}

Notice that in addition to the MSBuild input parameter (the regrettably named Database), there are 3 protected readonly properties available for derived tasks:

  • Msi: the Windows Installer database opened by the base task
  • Installer: the Windows Installer base object
  • OpenMode: specifies how the Msi should be opened (essentially for reading or writing). For example, this would be msiOpenDatabaseModeTransact for the EnableLaunchApplication task, and msiOpenDatabaseModeReadOnly for the FindExternalCabs task.

Let's see how the EnableLaunchApplication task looks after updated to derive from this base task:

public class EnableLaunchApplication : SetupProjectTask
{
	private string checkboxText = "Launch [ProductName]";
	private bool checkboxChecked = true;
	private string commandLine = string.Empty;
	private string fileName = string.Empty;

	public string CheckboxText
	{
		get { return checkboxText; }
		set { checkboxText = value; }
	}

	public bool CheckboxChecked
	{
		get { return checkboxChecked; }
		set { checkboxChecked = value; }
	}

	public string CommandLine
	{
		get { return commandLine; }
		set { commandLine = value; }
	}
	
	[Required]
	public string FileName
	{
		get { return fileName; }
		set { fileName = value; }
	}

	protected override bool ExecuteTask()
	{
		try
		{
			string fileId = FindFileIdentifier(Msi);
			if (fileId == null)
			{
				Log.LogError("Unable to find '{0}' in File table.", FileName);
				return false;
			}
			string sql = "SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BannerBmp'";
			View view = Msi.OpenView(sql);
			view.Execute(null);
			Record record = view.Fetch();
			record.set_StringData(11, "CheckboxLaunch");
			view.Modify(MsiViewModify.msiViewModifyReplace, record);
			view.Close();

			// Resize the BodyText and BodyTextRemove controls to be reasonable
			sql = "SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BodyTextRemove'";
			view = Msi.OpenView(sql);
			view.Execute(null);
			record = view.Fetch();
			record.set_IntegerData(7, 33);
			view.Modify(MsiViewModify.msiViewModifyReplace, record);
			view.Close();

			sql = "SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BodyText'";
			view = Msi.OpenView(sql);
			view.Execute(null);
			record = view.Fetch();
			record.set_IntegerData(7, 33);
			view.Modify(MsiViewModify.msiViewModifyReplace, record);
			view.Close();

			// Insert the new CheckBox control
			sql = "INSERT INTO `Control` (`Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help`) VALUES ('FinishedForm', 'CheckboxLaunch', 'CheckBox', '18', '117', '343', '12', '3', 'LAUNCHAPP', '{\\VSI_MS_Sans_Serif13.0_0_0}" + CheckboxText + "', 'CloseButton', '|')";
			view = Msi.OpenView(sql);
			view.Execute(null);
			view.Close();

			// Modify the Order of the EndDialog event of the FinishedForm to 1
			sql = "SELECT `Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering` FROM `ControlEvent` WHERE `Dialog_`='FinishedForm' AND `Event`='EndDialog'";
			view = Msi.OpenView(sql);
			view.Execute(null);
			record = view.Fetch();
			record.set_IntegerData(6, 1);
			view.Modify(MsiViewModify.msiViewModifyReplace, record);
			view.Close();

			// Insert the Event to launch the application
			sql = "INSERT INTO `ControlEvent` (`Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering`) VALUES ('FinishedForm', 'CloseButton', 'DoAction', 'VSDCA_Launch', 'LAUNCHAPP=1', '0')";
			view = Msi.OpenView(sql);
			view.Execute(null);
			view.Close();

			// Insert the custom action to launch the application when finished
			sql = "INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) VALUES ('VSDCA_Launch', '210', '" + fileId + "', '" + CommandLine + "')";
			view = Msi.OpenView(sql);
			view.Execute(null);
			view.Close();

			if (CheckboxChecked)
			{
				// Set the default value of the CheckBox
				sql = "INSERT INTO `Property` (`Property`, `Value`) VALUES ('LAUNCHAPP', '1')";
				view = Msi.OpenView(sql);
				view.Execute(null);
				view.Close();
			}
			Msi.Commit();

			System.Runtime.InteropServices.Marshal.FinalReleaseComObject(record);
			System.Runtime.InteropServices.Marshal.FinalReleaseComObject(view);
		}
		catch(Exception ex)
		{
			Log.LogErrorFromException(ex);
			return false;
		}
		return true;
	}


	private string FindFileIdentifier()
	{
		// First, try to find the exact file name
		string sql = "SELECT `File` FROM `File` WHERE `FileName`='" + FileName + "'";
		View view = Msi.OpenView(sql);
		view.Execute(null);
		Record record = view.Fetch();
		if (record != null)
		{
			string value = record.get_StringData(1);
			view.Close();
			return value;
		}
		view.Close();

		// The file may be in SFN|LFN format.  Look for a filename in this case next
		sql = "SELECT `File`, `FileName` FROM `File`";
		view = Msi.OpenView(sql);
		view.Execute(null);
		record = view.Fetch();
		while (record != null)
		{
			if (record.get_StringData(2).EndsWith("|" + FileName, StringComparison.OrdinalIgnoreCase))
			{
				string value = record.get_StringData(1);
				view.Close();
				return value;
			}

			record = view.Fetch();
		}

		view.Close();
		return null;
	}
}

Okay, so its pretty much the same, just a few lines removed. I won't include the changes to the other tasks in this blog entry, but will include them in the zip. Also, I updated the formerly regrettably named "Database" to the better "MsiFileName".

        Published Thursday, December 28, 2006 6:59 PM by mwade

        Attachment(s): MWadeBlog_06_12_28.zip

        Comment Notification

        If you would like to receive an email when updates are made to this post, please register here

        Subscribe to this post's comments using RSS

        Comments

        # Refactoring the EnableLaunchApplication task

        Tuesday, February 27, 2007 3:43 PM by Re-inventing The Wheel

        Now its time to head back to the old post-build tasks example that I had been using before. The last

        # Moving into Del Boca Vista with the Costanza's

        Friday, June 01, 2007 4:37 PM by Re-inventing The Wheel

        In case you haven't noticed, there are a couple of gaps within setup projects regarding how the generated

        Leave a Comment

        (required) 
        required 
        (required) 

          
        Enter Code Here: Required
         
        Page view tracker