This is the third and last post regarding workflow driven wizard pages in a MVVM WPF application. Part 1 described the problem and solution and Part 2 showed the code used to communicated between the workflow and the view model.
This post will cover activities themselves. I created a base activity called NavigationActivity that extends NativeActivity. There is also a NavigationActivity<T> that extends NativeActivity<T> which looks exactly the same.
NavigationActivity
/// <summary>
/// A base class for activities in flow diagrams
/// </summary>
public abstract class NavigationActivity : NativeActivity
{
#region Fields
private readonly Variable<UserPrompt> _prompt = new Variable<UserPrompt>();
#endregion
#region Arguments
/// <summary>
/// Gets/sets wether to continue to the next step or not
/// </summary>
public OutArgument<bool> Continue { get; set; }
#endregion
#region Overrides of NativeActivity
/// <summary>
/// Creates and validates a description of the activity’s arguments, variables, child activities, and activity delegates.
/// </summary>
/// <param name="metadata">The activity’s metadata that encapsulates the activity’s arguments, variables, child activities, and activity delegates.</param>
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
base.CacheMetadata(metadata);
metadata.AddImplementationVariable(_prompt);
}
/// <summary>
/// When implemented in a derived class, runs the activity’s execution logic.
/// </summary>
/// <param name="context">The execution context in which the activity executes.</param>
protected override sealed void Execute(NativeActivityContext context)
{
if (context == null) throw new ArgumentNullException("context");
var prompt = new UserPrompt(context);
_prompt.Set(context, prompt);
OnExecute(context, prompt);
context.CreateBookmark(prompt.BookmarkName, Callback);
}
/// <summary>
/// Gets or sets a value that indicates whether the activity can cause the workflow to become idle.
/// </summary>
/// <returns>
/// true if the activity can cause the workflow to become idle, otherwise false. This value is false by default.
/// </returns>
protected override bool CanInduceIdle
{
get { return true; }
}
#endregion
#region Protected methods
/// <summary>
/// Called when the activity executes
/// </summary>
/// <param name="context"></param>
/// <param name="userPrompt"></param>
protected abstract void OnExecute(NativeActivityContext context, UserPrompt userPrompt);
/// <summary>
/// Called when the "Next" bookmark is resumed
/// </summary>
/// <param name="context"></param>
protected virtual void OnComplete(NativeActivityContext context)
{
}
#endregion
#region Private methods
private void Callback(NativeActivityContext context, Bookmark bookmark, object value)
{
var result = (UserPrompt) value;
if (result.Proceed)
OnComplete(context);
Continue.Set(context, result.Proceed);
}
#endregion
}
There are a few important things to note here, the first being how the UserPrompt created in the Execute method and passed to the derived class in OnExecute. The second is that the bookmark is created after OnExecute.
The Callback method is called when the workflow is resumed from its bookmark. The important bit here is that Continue is set according to what the view model indicated in UserPrompt.Proceed. This informs the workflow whether to coninue to the next step, or go back to the previous step.
Credit check example
Here is our credit check example that shows how this is used:
/// <summary>
/// Requests the user to perform a credit check
/// </summary>
public sealed class CreditCheck : NavigationActivity<CreditCheckResult>
{
[RequiredArgument]
public InArgument<Company> Company { get; set; }
#region Overrides of AddCompanyActivityBase<CreditCheckResult>
/// <summary>
/// Creates and validates a description of the activity’s arguments, variables, child activities, and activity delegates.
/// </summary>
/// <param name="metadata">The activity’s metadata that encapsulates the activity’s arguments, variables, child activities, and activity delegates.</param>
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
base.CacheMetadata(metadata);
metadata.RequireExtension<IEditAccountholder>();
}
/// <summary>
/// Called when the activity executes
/// </summary>
/// <param name="context"></param>
/// <param name="userPrompt">The user prompt to use when communicating with the observer/view model</param>
/// <returns></returns>
protected override void OnExecute(NativeActivityContext context, UserPrompt<CreditCheckResult> userPrompt)
{
IEditAccountholder editor = context.GetExtension<IEditAccountholder>();
editor.PerformCreditCheck(userPrompt, Company.Get(context));
}
#endregion
}
Conclusion
This was a tough problem for me to solve as there was no real guidance that I could find. I was hoping for some magic built-in functionality to do the work for me, but no such luck.