Refactoring the EnableLaunchApplication task

Now its time to head back to the old post-build tasks example that I had been using before. The last blog entry from that time showed how we could modify an MSI to launch the application when it was finished installing. Let's suspend the TDD work we had been doing to clean up that task a little bit.

The task itself consisted of a bunch of inserts and modifications of an MSI. And I mean a bunch: there was quite a bit of duplicated code. Let's begin a refactor by pulling out the integer modifications.

The form of the insertions follows a distinctive pattern:

  • Create a sql statement
  • Open a view
  • Execute the statement
  • Close the view

Sounds like an easy enough function:

 private void InsertIntoMsi(string sql)
{
    View view = Msi.OpenView(sql);
    view.Execute(null);
    view.Close();

    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(view);
}

Note that I've still got the FinalReleaseComObject. I still want to do something about that; perhaps someday down the line. This shrinks our ExecuteTask function to the following:

 protected override bool ExecuteTask()
{
    try
    {
        string fileId = FindFileIdentifier();
        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
        InsertIntoMsi("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', '|')");

        // 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
        InsertIntoMsi("INSERT INTO `ControlEvent` (`Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering`) VALUES ('FinishedForm', 'CloseButton', 'DoAction', 'VSDCA_Launch', 'LAUNCHAPP=1', '0')");

        // Insert the custom action to launch the application when finished
        InsertIntoMsi("INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) VALUES ('VSDCA_Launch', '210', '" + fileId + "', '" + CommandLine + "')");

        if (CheckboxChecked)
        {
            // Set the default value of the CheckBox
            InsertIntoMsi("INSERT INTO `Property` (`Property`, `Value`) VALUES ('LAUNCHAPP', '1')");
        }
        Msi.Commit();

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

Still looks like a lot of excessive code. Lets try to get rid of all those similarities between set_IntegerData.

The form of these modifications is always the same:

  • create a SQL statement to pull a particular line out of a particular table
  • create a view from that statement
  • fetch a record
  • update a particular column to a particular value
  • modify and close the view.

Let's pull this out into a function:

 void ModifyIntegerData(string sql, int column, int value)
{
    View view = Msi.OpenView(sql);
    view.Execute(null);

    Record record = view.Fetch();
    record.set_IntegerData(column, value);

    view.Modify(MsiViewModify.msiViewModifyReplace, record);
    view.Close();

    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(record);
    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(view);
}

This will slim down our function even further:

 protected override bool ExecuteTask()
{
    try
    {
        string fileId = FindFileIdentifier();
        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
        ModifyIntegerData("SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BodyTextRemove'", 7, 33);
        ModifyIntegerData("SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BodyText'", 7, 33);

        // Insert the new CheckBox control
        InsertIntoMsi("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', '|')");

        // Modify the Order of the EndDialog event of the FinishedForm to 1
        ModifyIntegerData("SELECT `Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering` FROM `ControlEvent` WHERE `Dialog_`='FinishedForm' AND `Event`='EndDialog'", 6, 1);

        // Insert the Event to launch the application
        InsertIntoMsi("INSERT INTO `ControlEvent` (`Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering`) VALUES ('FinishedForm', 'CloseButton', 'DoAction', 'VSDCA_Launch', 'LAUNCHAPP=1', '0')");

        // Insert the custom action to launch the application when finished
        InsertIntoMsi("INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) VALUES ('VSDCA_Launch', '210', '" + fileId + "', '" + CommandLine + "')");

        if (CheckboxChecked)
        {
            // Set the default value of the CheckBox
            InsertIntoMsi("INSERT INTO `Property` (`Property`, `Value`) VALUES ('LAUNCHAPP', '1')");
        }
        Msi.Commit();

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

There is one last major chunk of code left: modifying the string data. This is very similar to the integer data modification. Basically, the only difference is the call to "set_StringData". How to rectify this? There are several ways to do this. For now, let's go with the simplest: modify our signature to take an object, and call the appropriate method based on the type of object being passed in. This would look something like this:

 private void ModifyTableData(string sql, int column, object value)
{
    View view = Msi.OpenView(sql);
    view.Execute(null);

    Record record = view.Fetch();
    if (value is int)
    {
        record.set_IntegerData(column, (int) value);
    }
    else
    {
        record.set_StringData(column, value.ToString());
    }

    view.Modify(MsiViewModify.msiViewModifyReplace, record);
    view.Close();

    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(record);
    System.Runtime.InteropServices.Marshal.FinalReleaseComObject(view);
}

We now update our old calls to ModifyIntegerData as well as our string modification to use the new function, allowing us to get rid of the final releases as well:

 protected override bool ExecuteTask()
{
    try
    {
        string fileId = FindFileIdentifier();
        if (fileId == null)
        {
            Log.LogError("Unable to find '{0}' in File table.", FileName);
            return false;
        }

        ModifyTableData("SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BannerBmp'", 11, "CheckboxLaunch");

        // Resize the BodyText and BodyTextRemove controls to be reasonable
        ModifyTableData("SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BodyTextRemove'", 7, 33);
        ModifyTableData("SELECT `Dialog_`, `Control`, `Type`, `X`, `Y`, `Width`, `Height`, `Attributes`, `Property`, `Text`, `Control_Next`, `Help` FROM `Control` WHERE `Dialog_`='FinishedForm' AND `Control`='BodyText'", 7, 33);

        // Insert the new CheckBox control
        InsertIntoMsi("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', '|')");

        // Modify the Order of the EndDialog event of the FinishedForm to 1
        ModifyTableData("SELECT `Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering` FROM `ControlEvent` WHERE `Dialog_`='FinishedForm' AND `Event`='EndDialog'", 6, 1);

        // Insert the Event to launch the application
        InsertIntoMsi("INSERT INTO `ControlEvent` (`Dialog_`, `Control_`, `Event`, `Argument`, `Condition`, `Ordering`) VALUES ('FinishedForm', 'CloseButton', 'DoAction', 'VSDCA_Launch', 'LAUNCHAPP=1', '0')");

        // Insert the custom action to launch the application when finished
        InsertIntoMsi("INSERT INTO `CustomAction` (`Action`, `Type`, `Source`, `Target`) VALUES ('VSDCA_Launch', '210', '" + fileId + "', '" + CommandLine + "')");

        if (CheckboxChecked)
        {
            // Set the default value of the CheckBox
            InsertIntoMsi("INSERT INTO `Property` (`Property`, `Value`) VALUES ('LAUNCHAPP', '1')");
        }
        Msi.Commit();
    }
    catch(Exception ex)
    {
        Log.LogErrorFromException(ex);
        return false;
    }
    return true;
}

The desire of mine to be a good software engineer is currently screaming at me that maybe I should take a closer look at this. And I will. But not this blog post.

MWadeBlog.zip