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
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 is to pair the Activity with an Extension. It works like this.
This is the right way for dealing with CLR events and callbacks.
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); } }
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); } }
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