Install‎ > ‎

A List of Checkable Items in the MSI GUI

Introduction

The GUI that can be displayed from a Windows Installer package is unfortunately quite limited.  In particular, it has no support for a scrollable list of checkable items.  In Win32 parlance, I'd refer to this as a ListView with checkbox support, and I would simply drop one on a dialog and use it.  The MSI ListView wrapper unfortunately neglects to expose a large amount of functionality.  In particular, it does not support multiselect or checkboxes on items.

On a work project, I needed a panel in my MSI install that would let the user select zero or more of several options.  I wanted the install to be as "pure MSI" as possible, so I wanted to accomplish this with native MSI GUI.  I immediately ran into the obvious problem that the MSI ListView does not support checkboxes or multiselect.  I eventually found a solution to this problem, and solved many other component problems along the way.  These included:
1.  How do I update the GUI when a property value changes?
2.  How do I store and retrieve a list of objects in an MSI property?
3.  How do I store and retrieve a list of strings in an MSI property?
4.  How do I use sprintf in MSI JavaScript?
5.  How do I clone an object of unknown type in JavaScript?
6.  How do I make this solution re-usable so an install could have multiple checkbox list dialogs?

This is the only "pure MSI" solution to the problem, but it is not the only way to solve it.  You can also write a DLL custom action in C++ that presents it's own GUI, but this will not be a seamless part of your install experience.  If you are using InstallShield with an InstallScript project, I have seen references to directly manipulating the ListView control in InstallScript CA's via ListView's Win32 SendMessage API.  However, remember you're hacking a control that MSI owns and it might not always like this.  Also, it may be possible to create controls at install init time or during runtime, but this is not considered "cannon" and is unsupported as far as I can tell.  Here's what it looks like when implemented:


Page 0 of a list of 9 items (unsorted)

Page 1 of a list of 9 items (the overflow)

To learn how I did this AND get a fully-functional solution that you can use and copy for free, read on!  All code is freely available and without warranty, expressed or implied.

Assumptions

1.  The list of items to be displayed does not change once it is set initially.*
2.  A scrollbar is not required.
3.  Full row select is not required; selection on a checkbox only is sufficient.
4.  The list of items to be displayed are an array of text strings.
5.  Each item has a unique (case insensitive) name.
6.  Items will be displayed in pages of 8.**
7.  Page boundaries are fixed at numbers evenly divisible by 8.***
8.  The dialog follows the template provided here, using a fixed name format for checkbox text and selected properties.
9.  The standard Next and Back buttons will be used to advance/retract the currently displayed page of items.****
10. The user can only display valid page numbers.
11. If there are no items, the user will see nothing on the dialog.
*The item list can be changed between displays of the dialog, but not while it is in use.
**The solution can be easilly modified to use a different page size.  Because this solution relies on pre-existing controls, it is not possible to make the page size configureable.
***It would not be difficult to modify this solution to "scroll" a half-page or even single element at a time, but this is an exercise for the user.
****This solution could be modified to use special purpose paging buttons without much difficulty.

Solution Outline

Figuring this out was hard, but replicating the work is not difficult, although it is detail sensitive.  I highly recommend you read the entire article before attempting to integrate this.  After that, I guestimate it would take only a couple hours to implement, depending on your level of expertise.

The basic components of this solution are:
1.  A template dialog
2.  A set of nine properties:
3.  A set of four custom actions

The solution uses an MSI dialog with a list of 8 checkboxes to display the items.  An initialize CA sets up the list of items that can be selected and the list of selected items is stored in another property.  Both lists are stored in a specially formatted property.  The user moves back and forth through the list using the standard Back/Next buttons on the dialog.  After the final page of entries, the list of selected items is available in the selected list property.

Template Dialog

The template dialog contains a list of 8 checkboxes.  The checkbox names are numbered 0 - 7 and are laid out in numerical order from top to bottom.  Each checkbox display a single selectable item.  The checkboxes correspond to the 8 display slots on screen, and all properties referenced by a checkbox's fields, conditions, or CAs use the same display slot number.

Each checkbox is configured as follows:
1.  Name incorporates the checkbox #.  For example, Checkbox0.
2.  Property = <name determined by ItemLabelNameFormat>.  For example, Property=CHECKPAGE_DISPLAYITEM0_CHECKED.
3.  Text = [<name determined by ItemCheckedNameFormat>].  For example, Text=[CHECKPAGE_DISPLAYITEM0_LABEL].
4.  Visible = false.
5.  The control event (InstallShield:  User Interface|Dialogs|CheckDialog|Behavior|Events) for the checkbox calls the _UpdateSelected custom action always (condition = 1).
6.  The conditions for the checkbox are the following:
 Action Condition
 Show CHECKPAGE_DISPLAYITEM0_LABEL
 Hide NOT CHECKPAGE_DISPLAYITEM0_LABEL

These configuration settings make the checkboxes display property text as their labels, store their check state in a property, and use that property specify their checked state.  In MSI, checked-state property is non-blank if checked and blank if not checked.  The Visible default and conditions ensure that if there is less than a full page of items, then the unneeded items will not be shown.  The UpdateSelected CA reads the selection state from the dialog and updates the SelectedList so this list is always accurate.

A significant problem I encountered when implementing this is that the MSI UI is not designed to be programatically interactive.  It is designed with a mindset of a static GUI that the script displays and acquires input from.  What little interactive elements MSI has, such as the TreeView, are special purposed and so are useless to this solution.  For example, a common situation is to have a text box bound to a property that specifies a directory, and a browse button to select that directory.  Clicking the browse button calls a custom action that does something magical to select the directory - displaying a GUI most likely - and then it updates the property the text field is bound to.  This will all work and the MSI logs will reflect the change in property is registered, but the value of the text field will not change!

After significant research, I discovered two ways to resolve this, neither of which are documented or understood well.  Both assume that the property is changed during a control event.
1.  Present a new dialog, or change the displayed dialog.  For example, you could display a modal dialog.  On return from this, MSI will reflect all property changes on the GUI.  The most typical usage of this is the "two dialog" solution.  Two copies of the same dialog are implemented, and whenever a property used on the GUI is changed, a control event CA is called to change to the other dialog.  Because the dialogs are identical, the user sees no flicker and it appears as if the dialog is working normally.
2.  Whenever a property used by the GUI is changed in a control event, use a special form control event set-property CA to set the CA equal to itself.  This is a documented type of standard action.  The action name is the property name in square brackets, and the argument is the property name, also in square brackets.  The MSI logs will not report that a property value was set, but this will cause the GUI to pickup the change to the property.  This must be done for EVERY property used by the GUI that may change.  Credit for this workaround goes to this page.

In addition, any control using a property that may change must be bound to it.  This is inherently done in the checkbox implementation and for edit boxes, but it is not inherent to text boxes.  A text box needs to be bound to the property in addition to using the property in the text box's Text field.  Text boxes may use multiple properties in their Text field, but will only reflect changes when the bound property is updated.  Here's an image of the dialog in a designer, though please note the property names here are not fully qualified - use the fully qualified names, above.


The Properties

A set of MSI properties are needed to store the state of the checkbox dialog in a persistent form whose lifespan exceeds that of the individual CAs used to implement this solution, and permits the results to be used later in the install.  The purpose of these properties is described below.  This solution is implemented so it can be easily reused, so the actual property names used in the example do not equal the names in the Property column here.  Within the custom action code, the names of the actual property for each of these is specified in a CheckPageInfo object.

 Property Description
 SourceList The list of text items, formatted as a single string with a separator between each item.
 SelectedList The list of items the user has selected, formatted as a single string to a separator between each item.
 DisplayRangeStart The element number of the first item from SourceList displayed.  This item will be the topmost item on the display.
 DisplayRangeEnd The element number of the last item from SourceList displayed.  This item will be the bottommost item on the display.
 CurrPageNo The currently displayed page of items, 0-based.
 NoPages The number of pages of items that can be displayed.  The last page may not be fully populated:  a SourceList of 9 items requires 2 pages.
 LastPageNo The number of the last valid page.  Used by the GUI next button to determine if it should scroll to the next checkbox page set or display the next GUI dialog.
 ItemLabelNameFormat A format specifier that describes the name of the properties for the checkbox labels.  This should incorporate a single field, specified by {0}, that is the number of the checkbox.
 ItemCheckedNameFormat A format specifier that describes the name of the properties for the checkbox selected state.  This should incorporate a single field, specified by {0}, that is the number of the checkbox.

The actual property names used in this example are listed below.
 PropertyActual Property Name in Example
 SourceListCHECKPAGE_SOURCELIST
 SelectedListCHECKPAGE_SELECTEDLIST
 DisplayRangeStartCHECKPAGE_DISPLAYRANGESTART
 DisplayRangeEndCHECKPAGE_DISPLAYRANGEEND
 CurrPageNoCHECKPAGE_CURRPAGENO
 NoPagesCHECKPAGE_NOPAGES
 LastPageNoCHECKPAGE_LASTPAGENO
 ItemLabelNameFormatCHECKPAGE_DISPLAYITEM{0}_LABEL
 ItemCheckedNameFormatCHECKPAGE_DISPLAYITEM{0}_CHECKED

The Custom Actions

This solution relies on a set of four custom actions that are called before the checklist dialog is displayed, on it's Back and Next buttons, and when items are checked.  It is structured so scrolling through the list of items uses the same Back/Next buttons used to advance dialog panels for simplicity.  The custom actions used are:
 Custom Action Name Description
 CheckPage_Initialize Loads the sourcelist and initializes properties for use.
 CheckPage_NextPage Advances display to the next page of items.
 CheckPage_PreviousPage Steps back to the previous page of items.
 CheckPage_UpdateSelected Called when a checkbox is clicked to update the list of selected items

The _Initialize custom action should contain the logic that finds or otherwise specifies the items that will be selected, and then stores them in SourceList.  It should also create a CheckPageInfo, specify the properties and property name formats, and pass that to CheckPage_Initialize to initialize the page for use.  This CA should be called before the dialog is presented, conditioned to run only if SourceList is empty.  In implemented, you will probably want a companion locate CA to locate the things that will be displayed on the check page.

The source for the custom actions is available here.

The control events for the Back button of the dialog should be configured to go to the previous dialog if the user is on page 0, and otherwise decrement the current page number and update the display.  In other words:
 Action Argument Condition
 NewDialog LicenseAgreement CHECKPAGE_CURRPAGENO = 0
 DoAction CheckPage_PreviousPage CHECKPAGE_CURRPAGENO <> 0
           
The control events for the Next button of the dialog should be configured to goto the next dialog if the user is on the last page, and otherwise increment the page number and update the display.  In other words:
 Action Argument Condition
 NewDialog CustomerInformationCHECKPAGE_CURRPAGENO = CHECKPAGE_LASTPAGENO
 DoAction CheckPage_NextPageCHECKPAGE_CURRPAGENO <> CHECKPAGE_LASTPAGENO

BOTH the Back and Next buttons need to call the special form set property CA to refresh all properties used by the dialog that are changed by the custom action.  This means BOTH of those CAs need to also have each of the following entries, sequenced AFTER the examples shown above.
 Action Argument Condition
 [CHECKPAGE_DISPLAYITEM0_LABEL] [CHECKPAGE_DISPLAYITEM0_LABEL] 1
 [CHECKPAGE_DISPLAYITEM1_LABEL] [CHECKPAGE_DISPLAYITEM1_LABEL] 1
 [CHECKPAGE_DISPLAYITEM2_LABEL] [CHECKPAGE_DISPLAYITEM2_LABEL] 1
 [CHECKPAGE_DISPLAYITEM3_LABEL] [CHECKPAGE_DISPLAYITEM3_LABEL] 1
 [CHECKPAGE_DISPLAYITEM4_LABEL] [CHECKPAGE_DISPLAYITEM4_LABEL] 1
 [CHECKPAGE_DISPLAYITEM5_LABEL] [CHECKPAGE_DISPLAYITEM5_LABEL] 1
 [CHECKPAGE_DISPLAYITEM6_LABEL] [CHECKPAGE_DISPLAYITEM6_LABEL] 1
 [CHECKPAGE_DISPLAYITEM7_LABEL] [CHECKPAGE_DISPLAYITEM7_LABEL] 1
 [CHECKPAGE_DISPLAYITEM0_CHECKED] [CHECKPAGE_DISPLAYITEM0_CHECKED] 1
 [CHECKPAGE_DISPLAYITEM1_CHECKED] [CHECKPAGE_DISPLAYITEM1_CHECKED] 1
 [CHECKPAGE_DISPLAYITEM2_CHECKED] [CHECKPAGE_DISPLAYITEM2_CHECKED] 1
 [CHECKPAGE_DISPLAYITEM3_CHECKED] [CHECKPAGE_DISPLAYITEM3_CHECKED] 1
 [CHECKPAGE_DISPLAYITEM4_CHECKED] [CHECKPAGE_DISPLAYITEM4_CHECKED] 1
 [CHECKPAGE_DISPLAYITEM5_CHECKED] [CHECKPAGE_DISPLAYITEM5_CHECKED] 1
 [CHECKPAGE_DISPLAYITEM6_CHECKED] [CHECKPAGE_DISPLAYITEM6_CHECKED] 1
 [CHECKPAGE_DISPLAYITEM7_CHECKED] [CHECKPAGE_DISPLAYITEM7_CHECKED] 1
 [CHECKPAGE_DISPLAYRANGESTART] [CHECKPAGE_DISPLAYRANGESTART] 1
 [CHECKPAGE_DISPLAYRANGEEND] [CHECKPAGE_DISPLAYRANGEEND] 1
    
All of the CheckBoxes must call the CheckPage_UpdateSelected when they are checked.  So this means that each of Checkbox0 - Checkbox7 should have a control event like this:
 Action Argument Condition
 DoAction CheckPage_UpdateSelected 1

Finally, each checkbox must have a condition field to make sure they are only shown if they are needed.  The conditions for each checkbox should look similar to this - the exact property names used should equal that of the checkbox.

 Action Condition
 Show NOT CHECKPAGE_DISPLAYITEM0_LABEL
 Hide CHECKPAGE_DISPLAYITEM0_LABEL


Reuse

This CheckList implementation is designed for easy re-use, meaning there can be several of them in an install if needed.  To accomplish this:
1.  Pick a unique prefix to differentiate each instance of the CheckList page.
2.  Clone the dialog and custom actions, renaming them with the prefix selected.
3.  In the cloned dialog, update all references to the custom actions to use the prefixed custom actions.
4.  Edit the cloned custom actions.  Update the field values of the CheckPageInfo object so each property name is prefixed with the selected prefix.
5.  In the cloned dialog, update all property references to use the prefixed properties.  In particular, check:
    a.  The Text and Property fields of all checkboxes.
    b.  The Text field of the single text box.
    c.  The control event and control conditions for all of the checkbox elements.
    d.  The control events for the Next and Back buttons.
6.  Sequence the prefixed Initialize CA into the UI sequence in the appropriate location.

The SubProblems

A recap of the subproblems I encountered and the solution to each is listed below.
1.  How do I update the GUI when a property value changes?
Use a special form set property action, called after you change the value of the property.  This special form CA sets the property equal to itself.
2.  How do I store and retrieve a list of objects in an MSI property?
Serialize them to a string, using a different separator for fields of an object vs. objects.
3.  How do I store and retrieve a list of strings in an MSI property?
See StringList_* functions in the custom action code.
4.  How do I use sprintf in MSI JavaScript?
See the sprintf code towards the top of the custom action code.  Note that unlike function definitions, this must appear before it's used.
5.  How do I clone an object of unknown type in JavaScript?
Create an Object, use the special for x in object syntax to enumerate the object's properties, then set the values in the Object via [] syntax.
6.  How do I make this solution re-usable so an install could have multiple checkbox list dialogs?
By using a CheckPageInfo object to specify the names of all the properties used to manage a check page, and then using page-specific property names.  The easy way to do this is to replace the "CHECKPAGE" prefix in each property name with the name of the actual page used.
Comments