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="https://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".

MWadeBlog_06_12_28.zip