Traversing Activities in Hosted Workflow Designer

August 11th 2014 Workflow Foundation

One of the more attractive parts of Windows Workflow Foundation is its workflow designer and the possibility of rehosting it in your own application with a minimum amount of code. Often simple rehosting of the designer is enough, but sometimes you will want to do some additional background processing of the workflow as the user is designing it. Here are a couple of ideas for such processing:

  • Ensure that each activity has a unique display name. In case of a duplicate display name append a sequential number to it (e.g. Sequence, Sequence 1, Sequence 2, etc.).
  • Some custom activities require the designer to provide them with additional information, such as a list of valid values for a property in the current context. For this purpose they can expose a property, which the designer will set to a proxy data access class.

All such features require a traversal of all activities in the workflow. In this article I'm going to present a way for achieving that, implementing a simplified version of the first idea in the process: set the display name of each newly added activity to a random Guid value.

We first need to attach a handler to the designer's ModelChanged event:

private void AddDesigner()
{
  //Create an instance of WorkflowDesigner class.
  this.wd = new WorkflowDesigner();

  //Place the designer canvas in the middle column of the grid.
  Grid.SetColumn(this.wd.View, 1);

  //Load a new Sequence as default.
  this.wd.Load(new Sequence());

  //Add the designer canvas to the grid.
  grid1.Children.Add(this.wd.View);

  //Attach the event handler
  var modelService = wd.Context.Services.GetService<ModelService>();
  modelService.ModelChanged += OnModelChanged;
}

The above code is based on the method from MSDN's designer hosting tutorial.

In .NET 4.5 the only property of ModelChangedEventArgs that should be used, is ModelChangeInfo. In .NET 4 separate ItemsAdded, ItemsRemoved and PropertiesChanged properties are available instead, which are now marked as obsolete. The key to processing the changes is ModelChangeInfo.ModelChangeType. Based on it different types of changes can be handled in a different way.

In our scenario we only need to traverse newly added activities, therefore the only ModelChangeTypes of interest to us are CollectionItemAdded and PropertyChanged:

private void OnModelChanged(object sender, ModelChangedEventArgs e)
{
  switch (e.ModelChangeInfo.ModelChangeType)
  {
    case ModelChangeType.CollectionItemAdded:
    case ModelChangeType.PropertyChanged:
      RandomizeDisplayName(e.ModelChangeInfo.Value);
      break;
  }
}

In a naive first attempt we will search for the DisplayName property of the changed item and set it to the desired value:

private void RandomizeDisplayName(ModelItem item)
{
  if (item == null)
  {
    return;
  }

  var displayNameProperty = item.Properties
    .SingleOrDefault(property => property.Name == "DisplayName");

  if (displayNameProperty != null)
  {
    displayNameProperty.SetValue(Guid.NewGuid().ToString());
  }
}

This would work as long as the user would only add individual activities from the toolbox. Though, the designer also supports pasting of composite activities, e.g. a Sequence with multiple activities inside it. The above code would only set the display name of the root activity in such a case. To also process all its sub-activities, we need to recursively traverse the activity tree:

private void RandomizeDisplayName(ModelItem rootItem)
{
  if (rootItem == null)
  {
    return;
  }

  var displayNameProperty = rootItem.Properties
    .SingleOrDefault(property => property.Name == "DisplayName");

  if (displayNameProperty != null)
  {
    displayNameProperty.SetValue(Guid.NewGuid().ToString());
  }

  foreach (var modelProperty in rootItem.Properties)
  {
    if (typeof(Activity).IsAssignableFrom(modelProperty.PropertyType) ||
      typeof(FlowNode).IsAssignableFrom(modelProperty.PropertyType))
    {
      RandomizeDisplayName(modelProperty.Value);
    }
    else if (modelProperty.PropertyType.IsGenericType &&
      modelProperty.PropertyType.GetGenericTypeDefinition() == typeof(Collection<>) &&
      modelProperty.Collection != null)
    {
      foreach (var activityModel in modelProperty.Collection)
      {
        RandomizeDisplayName(activityModel);
      }
    }
  }
}

If you take a closer look at the above foreach loop, you can see that it only processes the Activity (e.g. branches of If activity) and FlowNode (a node inside a FlowChart) properties, and enumerates all collection properties (e.g. FlowChart.Nodes or Sequence.Activities).

This is already enough for our sample scenario, but since activity traversal will probably be a common operation in the hosted designer, we can refactor the above code for activity enumeration so that it can be used for any activity processing:

private void ProcessActivities(ModelItem rootItem, Action<ModelItem> action)
{
  if (rootItem == null)
  {
    return;
  }

  action(rootItem);

  foreach (var modelProperty in rootItem.Properties)
  {
    if (typeof(Activity).IsAssignableFrom(modelProperty.PropertyType) ||
      typeof(FlowNode).IsAssignableFrom(modelProperty.PropertyType))
    {
      ProcessActivities(modelProperty.Value, action);
    }
    else if (modelProperty.PropertyType.IsGenericType &&
      modelProperty.PropertyType.GetGenericTypeDefinition() == typeof(Collection<>) &&
      modelProperty.Collection != null)
    {
      foreach (var activityModel in modelProperty.Collection)
      {
        ProcessActivities(activityModel, action);
      }
    }
  }
}

The action to be performed on each activity can now be implemented in a separate method:

private void RandomizeDisplayName(ModelItem item)
{
  var displayNameProperty = item.Properties
    .SingleOrDefault(property => property.Name == "DisplayName");

  if (displayNameProperty != null)
  {
    displayNameProperty.SetValue(Guid.NewGuid().ToString());
  }
}

To do the processing, we can call the common traversal method, passing it the above implemented action:

private void OnModelChanged(object sender, ModelChangedEventArgs e)
{
  switch (e.ModelChangeInfo.ModelChangeType)
  {
    case ModelChangeType.CollectionItemAdded:
    case ModelChangeType.PropertyChanged:
      ProcessActivities(e.ModelChangeInfo.Value, RandomizeDisplayName);
      break;
  }
}

We now only have to implement a different action and pass it to ProcessActivities, to process them in a different way. No need to worry, how to actually enumerate all the sub-activities.

Copyright
Creative Commons License