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:
- Create a workflow with a SQL persistence extension that represent an order confirmation process
- Send email to actor, including a link to a ASP.NET application, a URL with request argument (the workflow instance id)
- Add a pick activity with two pickbranches, each including a aec.CreateNamedBookmark(<bm>)
- 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