Recently David K contacted me through my blog and shared with me a solution that he came up with for a particular problem that he was able to solve using IWorkflowInstanceExtension and said it would be great if I could do an endpoint.tv episode about it.  David already solved his problem but he wanted to spare you the pain in case you ever have to do this.

Download WF4 Activity Callbacks and Events Example from MSDN Code Gallery

The Problem

Let’s imagine that you have a class library that exposes a type called Gizmo.  This Gizmo type exposes an event that is fired from time to time for some reason (maybe it detects some input from a hardware device) and you want to create an activity that will wait for the Gizmo.Fire event and return the level that was fired.

public class Gizmo

{
public event EventHandler<GizmoEventArgs> Fire;

public void FireGizmo(int level)
{
if (Fire != null)
{
Fire(this, new GizmoEventArgs {Level = level});
}
}
}

Activities cannot just sit around waiting for an event and blocking the workflow thread so we need a way to add an event handler that can handle the event and resume the workflow once it is fired. Also we need to be sure that this works correctly even if the workflow host has a persistence enabled. Because there is no way for an activity to re-establish connections with event handlers when being loaded from persistence we have to avoid being persisted while we have a connection to the Gizmo.Fire event.

The Solution

The solution is to pair the Activity with an Extension.  It works like this.

  1. Create an activity called WaitForGizmo and an extension called WaitForGizmoExtension which implements IWorkflowInstanceExtension
  2. When the Activity is executed it will enter a no-persist zone and let the extension know when it needs to handle the event and set a bookmark
  3. The WaitForGizmoExtension will handle the event and resume the bookmark
  4. When the bookmark is resumed the activity will obtain the out argument values and exit the no-persist scope

This is the right way for dealing with CLR events and callbacks.

The WaitForGizmo activity

public sealed class WaitForGizmo : NativeActivity<int>

{
internal const string BookmarkName = "WaitingForGizmoToBeFired";
private readonly Variable<NoPersistHandle> _noPersistHandle = new Variable<NoPersistHandle>();
private BookmarkCallback _gizmoBookmarkCallback;

public InArgument<Gizmo> TheGizmo { get; set; }

public BookmarkCallback GizmoBookmarkCallback
{
get
{
return _gizmoBookmarkCallback ??
(_gizmoBookmarkCallback = new BookmarkCallback(OnGizmoCallback));
}
}

protected override bool CanInduceIdle
{
get { return true; }
}

protected override void CacheMetadata(NativeActivityMetadata metadata)
{
// Tell the runtime that we need this extension
metadata.RequireExtension(typeof (WaitForGizmoExtension));

// Provide a Func<T> to create the extension if it does not already exist
metadata.AddDefaultExtensionProvider(() => new WaitForGizmoExtension());

metadata.AddArgument(new RuntimeArgument("TheGizmo", typeof (Gizmo), ArgumentDirection.In, true));
metadata.AddArgument(new RuntimeArgument("Result", typeof (int), ArgumentDirection.Out, false));
metadata.AddImplementationVariable(_noPersistHandle);
}

protected override void Execute(NativeActivityContext context)
{
// Enter a no persist zone to pin this activity to memory since we are setting up a delegate to receive a callback
var handle = _noPersistHandle.Get(context);
handle.Enter(context);

// Get (which may create) the extension
var gizmoExtension = context.GetExtension<WaitForGizmoExtension>();

// Add the callback
gizmoExtension.AddGizmoCallback(TheGizmo.Get(context));

// Set a bookmark - the extension will resume when the Gizmo is fired
context.CreateBookmark(BookmarkName, GizmoBookmarkCallback);
}

internal void OnGizmoCallback(NativeActivityContext context, Bookmark bookmark, Object value)
{
// Store the result
Result.Set(context, (int) value);

// Exit the no persist zone
var handle = _noPersistHandle.Get(context);
handle.Exit(context);
}
}

The WaitForGizmoExtension

internal class WaitForGizmoExtension : IWorkflowInstanceExtension

{
private static bool _addedCallback;
private WorkflowInstanceProxy _instance;

#region IWorkflowInstanceExtension Members

public IEnumerable<object> GetAdditionalExtensions()
{
return null;
}

public void SetInstance(WorkflowInstanceProxy instance)
{
_instance = instance;
}

#endregion

internal void AddGizmoCallback(Gizmo gizmo)
{
if (!_addedCallback)
{
_addedCallback = true;
gizmo.Fire += OnGizmoFired;
}
}

internal void OnGizmoFired(object sender, GizmoEventArgs args)
{
// Gizmo was fired, resume the bookmark
_instance.BeginResumeBookmark(
new Bookmark(WaitForGizmo.BookmarkName),
args.Level,
(asr) => _instance.EndResumeBookmark(asr),
null);
}
}

Handling Timeouts

What if Gizmo does not fire? You might want to have a solution where if Gizmo doesn't fire within some timeout period you do something else. The best way to deal with this is at the workflow level by using a Pick activity with a branch that contains a delay. This ensures that either a Gizmo.Fire event is detected or the Timeout occurs