How do I populate a ComboBox at install time in WiX?
Asked Answered
G

2

5

Edit: I've updated the code below so that it now works, thanks to Rob's answer.

I've found a couple of pages that show how to do this (http://www.cmcrossroads.com/content/view/13160/120/, http://www.mail-archive.com/[email protected]/msg05103.html) and looked through the source code for WAI (http://wai.codeplex.com/), but I can't seem to get it to work in my installer no matter what I try. If anyone can spot what I'm doing wrong I'd be very grateful. My WiX fragment for the dialogue looks like this:

<UI>
  <Dialog>

...snip...

    <Control Id="WebsiteName" Type="ComboBox" ComboList="yes" Sorted="yes" Property="IIS_WEBSITENAME" X="20" Y="73" Width="150" Height="17"/>

...snip...

    <!-- We want our custom action to fill in the WebsiteName ComboBox above
         however, if no ComboBox entries exist at compile time then the
         ComboBox table is not created in the MSI and we can't add to it in
         the custom action. So we have this hidden dummy list box to force
         the table to appear. -->
    <Control Id="DummyComboBox" Hidden="yes" Type="ComboBox" Sorted="yes" ComboList="yes" Property="DUMMYPROPERTY" X="65" Y="60" Width="150" Height="18">
      <ComboBox Property="DUMMYPROPERTY">
        <ListItem Text="Dummy" Value="Dummy"/>
      </ComboBox>
    </Control>
  </Dialog>
</UI>

<Property Id="DUMMYPROPERTY">Dummy</Property>
<Property Id="IIS_WEBSITENAME"/>
<CustomAction Id="FillWebsiteNameList" BinaryKey="WiXCustomAction.dll" DllEntry="FillWebsiteNameList" Execute="immediate" />
<InstallUISequence>
  <Custom Action="FillWebsiteNameList" After="CostFinalize"/>
</InstallUISequence>

My custom action code is:

[CustomAction]
public static ActionResult FillWebsiteNameList(Session xiSession)
{
  xiSession.Log("Begin FillWebsiteNameList");

  xiSession.Log("Opening view");

  View lView = xiSession.Database.OpenView("SELECT * FROM ComboBox");
  lView.Execute();

  xiSession.Log("Creating directory entry");

  DirectoryEntry lIis = new DirectoryEntry("IIS://localhost/w3svc");

  xiSession.Log("Checking each child entry");

  int lIndex = 1;
  foreach (DirectoryEntry lEntry in lIis.Children)
  {
    if (lEntry.SchemaClassName == "IIsWebServer")
    {
      xiSession.Log("Found web server entry: " + lEntry.Name);

      string lWebsiteName = (string)lEntry.Properties["ServerComment"].Value;
      xiSession.Log("Website name: " + lWebsiteName);

      xiSession.Log("Creating record");
      Record lRecord = xiSession.Database.CreateRecord(4);

      xiSession.Log("Setting record details");
      lRecord.SetString(1, "IIS_WEBSITENAME");
      lRecord.SetInteger(2, lIndex);
      lRecord.SetString(3, lEntry.Name); // Use lWebsiteName only if you want to look up the site by name.
      lRecord.SetString(4, lWebsiteName);

      xiSession.Log("Adding record");
      lView.Modify(ViewModifyMode.InsertTemporary, lRecord);

      ++lIndex;
    }
  }

  xiSession.Log("Closing view");

  lView.Close();

  xiSession.Log("Return success");

  return ActionResult.Success;
}

There used to be two problems:

1) The code above failed during the running of the custom action with "Function failed during execution. Database: Table(s) Update failed." - This was because of the indexing problem causing the code to try and write a string to an int column.

2) If I change the line

lRecord.SetString(2, lWebsiteName);

to

lRecord.SetString(2, lEntry.Name);

then looking at trace the action appears to succeed but when the installer run the combobox has no entries to chose from.

If I change the combobox to have hardcoded values everything works fine, even if I hardcode the equivalent of lWebsiteName.

Gymnasiarch answered 3/9, 2009 at 14:6 Comment(2)
so the installer must be run as a administrator? or how do i avoid the access denied when trying to get list of web sites?Soupandfish
It must be run as an administrator. Enjoy solving that problem.Phinney
R
3

I don't use DTF (all natural C++ CustomActions for me) but Record's are 1 based. Have you tried shifting all of your SetRecord() calls over by one index?

Also, the .wxs code above seems to suggest that you are using "DUMMYPROPERTY" as the control Property for the ComboBox not "IIS_WEBSITENAME" like the .cs code is using.

Roeder answered 4/9, 2009 at 18:30 Comment(2)
Thanks, I have tried 1 based at some point, but I've tried so many things I may not have done so with this code! I'll try again. I'm using DUMMYPROPERTY in the second, hidden combo control to ensure that the ComboBox table gets created, I'll try pointing it at the same property as the real control and see if that does anything.Gymnasiarch
It was the indexing (which explained both problems). Thanks again.Gymnasiarch
E
0

This one is pretty old, however I had similar issue and would like to share what I've found, maybe this saves someone's time.

to Make sure ComboBox table is created use EnsureTable, ensure CA doesn't overwrite defined value:

<EnsureTable Id="ComboBox"/>
<Property Id="RS_INSTANCES" Secure="yes"/>
<CustomAction Id="GetRSintances" BinaryKey="JSCommon" Return="ignore"
              JScriptCall="GetRSintances" Execute="immediate" />

<InstallUISequence>
  <Custom Action="GetRSintances" After="AppSearch">
    <![CDATA[NOT Installed AND NOT RS_INSTANCES]]>
  </Custom>
</InstallUISequence>

<InstallExecuteSequence>
  <Custom Action="GetRSintances" After="AppSearch">
    <![CDATA[NOT Installed AND NOT RS_INSTANCES]]>
  </Custom>
</InstallExecuteSequence>

 <!-- UI part -->
 <Control Id="ComboBox1" Type="ComboBox" X="20" Y="160" Width="100" Height="20" Property="RS_INSTANCES" Sorted="yes" >
    <ComboBox Property="RS_INSTANCES">
      <!-- dynamicly filled during installation -->
    </ComboBox>
  </Control>

I have a JavaScript function for filling ListItems: (yes, I know some of you don't like JS for custom actions, but it still is convenient enough)

// Add ListItem to ComboBox or ListView at install time
function AddListItemToMSI(Property, Order, Value, Text, Table) {
  try {
    var controlView = Session.Database.OpenView("SELECT * FROM " + Table);
    controlView.Execute();

    var record = Session.Installer.CreateRecord(4);
    record.StringData(1) = Property;
    record.IntegerData(2) = Order;
    record.StringData(3) = Value;
    record.StringData(4) = Text;

    controlView.Modify(7, record);
    controlView.Close();
  }
  catch (err) {
    ShowMessage('Couldn\'t add ListItem entry, error occured: ' + err.message, msiMessageTypeInfo);
  }

  return 1;
}

I call it from my other function (it is called as custom action) like this:

var ComboBoxProperty = 'RS_INSTANCES';
var InstanceFullName;
for (i = 0; i < Names.length; i++) {
    InstanceFullName = GetInstanceName(Names[i]); //this function looks up full name in the registry
    AddListItemToMSI(ComboBoxProperty, i, InstanceFullName, '', 'ComboBox');
    if (i == 0) {
      Session.Property(ComboBoxProperty) = InstanceFullName;
    }
}

NOTE: I removed non-relevant pieces of code from last function to make it readable. P.S. always (I mean ALWAYS) use null, zero length and error checking, try/catch and ensure logging with something like this:

function ShowMessage(text, options) {
    if (options == null) {
        var options = msiMessageTypeUser;
    }
    var oRecord = Session.Installer.CreateRecord(1);
    oRecord.StringData(1) = text;
    var response = Session.Message(options, oRecord);
    oRecord.ClearData();
    oRecord = null;
    response = null;
}
Erichericha answered 2/9, 2015 at 6:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.