ISWIX, LLC View Christopher Painter's profile on LinkedIn profile for Christopher Painter at Stack Overflow, Q&A for professional and enthusiast programmers

Friday, October 28, 2011

Beam Me Up: Using JSON to serialize CustomActionData

Data driven custom actions require two custom actions.  The first custom action queries custom tables and evaluates business rules and then passes CustomActionData to the second custom action which then performs the operations needed.   Windows Installer calls this script generation and script execution but I call it "brains" and "brawn".   That is the first action has access to all the information needed and can think about what needs to be done yet it doesn't have the access rights to do the work.   The second action has all the rights to do the work but doesn't have enough access to teh MSI handle to be able to decide what needs to be done.

So just how does the brain communicate with the muscle?  Through the CustomActionData property. 

Today I want to demonstrate a lightweight technique of transporting complex instructions through this property using JavaScript Object Notation aka JSON.

Let's say I wanted a custom action that could create local user groups and I wanted it to be data driven.  First I'd start off with defining custom tables that can express the
 requirements:

<CustomTable Id="Groups">
<Column Id="Group" Type="string" PrimaryKey="yes" Category="Identifier" Description="Unique Identifier of Group" />
<Column Id="Component_" Type="string" Width="72" Modularize="Column" KeyTable="Component" KeyColumn="1"/>
<Column Id="Name" Type="string" Description="Name of Group"/>
<Column Id="Description" Type="string" Width="255" Description="Description of Group" />
<Row>
<Data Column="Group">groupRockers</Data>
<Data Column="Component_">SomeComponent</Data>
<Data Column="Name">Rockers</Data>
<Data Column="Description">People who rock!</Data>
</Row>
</CustomTable>

This table basically says let's create a group called Rockers when SomeComponent is being installed.  Now let's write some code.   The brain needs to be able to communicate with the bronze so let's start with creating something they can both understand.  A POCO class ( Plain Old C# Object ) that describes a Group:

class Group
{
public string Name { get; set; }
public string Description { get; set; }
}

Now let's write the brains:

[CustomAction]
public static ActionResult CostGroups(Session session)
{
var cad = new CustomActionData();
var groups = new List<Group>();
using (View view = session.Database.OpenView("SELECT `Component_`, `Name`, `Description` FROM `Groups`"))
{
view.Execute();
foreach (var record in view)
using (record)
if (session.Components[record.GetString(1)].RequestState.Equals(InstallState.Local))
groups.Add(new Group() { Name = record.GetString(2), Description = record.GetString(3) });
}
 
cad.Add("Groups", JsonConvert.SerializeObject(groups));
session["CreateGroups"] = cad.ToString();
return ActionResult.Success;

We query the Groups table and then iterate through the rows.  During this we evaluate if the component is being installed and if it is we generate a Group class and add it to the Groups list. Finally we serialize the List of Groups to JSON and put it in the CustomActionData collection as the Groups KeyValuePair.

So what does this CustomActionData string end up looking like?  Let's look at the Windows Installer log:

MSI (s) (70!E4) [09:47:15:735]: PROPERTY CHANGE:
Adding CreateGroups  property. Its value is:

Groups=[{"Name":"Rockers","Description":"People who rock!"}]


Now it's time for the brawn to flex some muscle:

[CustomAction]
public static ActionResult CreateGroups(Session session)
{
try
{
var groups = JsonConvert.DeserializeObject<List<Group>>(session.CustomActionData["Groups"]);
foreach (var group in groups)
{
var ad = new DirectoryEntry("WinNT://" + Environment.MachineName + ",computer");
DirectoryEntry newGroup = ad.Children.Add(group.Name, "group");
newGroup.Invoke("Put", new object[] { "Description", group.Description });
newGroup.CommitChanges();
}
}
catch (Exception ex)
{
session.Log(ex.Message);
}
return ActionResult.Success;
}

We grab the value of the Groups KeyValuePair from the CustomActionData property and then deserialize it back into a List of Group.  Then we iterate through the list and call the API needed to create the group(s).

So there you have it.  Beam me up!  A simple contextual example of how to use JSON in your installer.  It turns out JSON isn't just for the web guys.

Disclaimer: This code was thrown together in about 30 minutes and has not been thoroughly tested. I'll be improving it over time and if you'd like a copy you can contact me via email.