Sep 14, 2009

Human Interaction Workflow

Windows Workflow Foundation 4.0 does not provide explicit framework classes or tools for workflow human interaction (e.g. waiting for a real actor of a workflow to make a decision how to continue).

Approach

Therefore we gave the following approach a try:

  1. Create a workflow with a SQL persistence extension that represent an order confirmation process
  2. Send email to actor, including a link to a ASP.NET application, a URL with request argument (the workflow instance id)
  3. Add a pick activity with two pickbranches, each including a aec.CreateNamedBookmark(<bm>)
  4. Create an ASP.NET application page to confirm a process, loading the workflow instance and calling ResumeNamedBookmark(<bm>)

Here a some details.

Bookmarks

To block the workflow and wait for human interaction we used the concept of “Bookmarks”. A bookmark is a named point in your workflow to unload and later resume. To create a bookmark and unload we created a simple custom activity:

public class CreateBookmark : NativeActivity
{
    public InArgument<string> Bookmark { get; set; }

    protected override void Execute(ActivityExecutionContext context)
    {
        context.CreateNamedBookmark(Bookmark.Get(context), new BookmarkCallback(BookmarkCallback));
    }

    private void BookmarkCallback(ActivityExecutionContext executionContext, Bookmark bookmark, object value)
    {
        executionContext.RemoveAllBookmarks();
    }
}

Sending Email

[Credits for this code to Alex]

Before we can unload our workflow we need to send an URL to the human. The link requests a page that resumes our workflow later on.

public class SendMailActivity : CodeActivity
{
    public InArgument<string> From { get; set; }
    public InArgument<string> To { get; set; }
    public InArgument<string> Subject { get; set; }
    public InArgument<string> Body { get; set; }

    protected override void Execute(CodeActivityContext context)
    {
        var fromAddress = new MailAddress(From.Get(context));
        var toAddress = new MailAddress(To.Get(context));

        var addresses = new MailAddressCollection();
        addresses.Add(toAddress);

        using (var message = new MailMessage(fromAddress, toAddress))
        {
            message.Subject = Subject.Get(context);
            message.Body = Body.Get(context);

            var mail = new SmtpClient("localhost", 25);
            mail.Send(message);
        }
    }
}

Get Workflow Instance Id

We did not find a simple way to get the workflow instance identifier in our workflow. To work around this we created another simple activity:

public class WorkflowIdActivity : CodeActivity<Guid>
{
    protected override void Execute(CodeActivityContext context)
    {
       Result.Set(context, context.WorkflowInstanceId);
    }
}

Pick and Pickbranch

To summarize: we have a workflow that sends a link to a human, persists and unloads. The question is now (the answer has cost me a beer) what WF4 construct to use to continue our workflow; resuming on two parallel branches. We used the pick and pickbranch activities and included a CreateBookmark trigger activity:

<p:Pick>
  <p:PickBranch>
    <p:PickBranch.Trigger>
      <w:CreateBookmark Bookmark="[‘Accepted’]" />
    </p:PickBranch.Trigger>
    <e:SendMailActivity Body="[‘Your order has been accepted’]" From="[‘goodboss@email.com’]" Subject="[‘Laptop order’]" To="[‘goodboss@email.com’]" />
  </p:PickBranch>
  <p:PickBranch>
    <p:PickBranch.Trigger>
      <w:CreateBookmark Bookmark="[‘Rejected’]" />
    </p:PickBranch.Trigger>
    <e:SendMailActivity Body="[‘Your order has been rejected’]" From="[‘badboss@email.com’]" Subject="[‘Laptop order’]" To="[‘badboss@email.com’]" />
  </p:PickBranch>
</p:Pick>

The Web Page

Last step is to build a simple web page, including a green and a red button. Note how we parsed the URL argument to get the workflow identifier.

public partial class OrderConfirmationForm : System.Web.UI.Page
{
    static SqlPersistenceProviderFactory persistenceProviderFactory;
    static AutoResetEvent instanceUnloaded = new AutoResetEvent(false);
    private Guid workflowId;

    protected void Page_Load(object sender, EventArgs e)
    {
       string id = this.Context.Request.Params["id"];
         workflowId = new Guid(id);
    }

    protected void btnAccept_Click(object sender, EventArgs e)
    {
        ResumeWorkflow("Accepted");
    }

   protected void btnDecline_Click(object sender, EventArgs e)
    {
        ResumeWorkflow("Rejected");
    }

    private void ResumeWorkflow(string bookmark)
    {
        SetupPersistence();

        PersistenceProvider persistenceProvider = persistenceProviderFactory.CreateProvider(workflowId);
        WorkflowInstance instance = WorkflowInstance.Load(new OrderProcessing(), persistenceProvider);
        instance.Extensions.Add(persistenceProvider);

        instance.OnUnloaded = () => instanceUnloaded.Set();

        instance.ResumeBookmark(bookmark, null);
        instanceUnloaded.WaitOne();

        ClosePersistence();
    }

    static void SetupPersistence()
    {
        persistenceProviderFactory = new SqlPersistenceProviderFactory(@"Database=Instances;Integrated Security=True", false, false, TimeSpan.FromSeconds(60));
        persistenceProviderFactory.Open();
    }

    static void ClosePersistence()
    {
        persistenceProviderFactory.Close();
    }
}

No comments:

Post a Comment