Monday, 19 September 2011

Workflow Wizards – Part 3

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.

Thursday, 15 September 2011

Workflow Wizards – Part 2

As promised, the follow-up to the previous post. The biggest challenge in communicating with a view model from the workflow is to indicate to the workflow what result is expected. The second challenge was to allow the view model to resume the workflow without having to hardcode bookmark names. Both of these problems were solved with the UserPrompt class.

UserPrompt and UserPrompt<T>

	/// <summary>
/// Contains the result of prompting a user for input
/// </summary>
/// <remarks>This is used to pass back information to the workflow</remarks>
public class UserPrompt
{
/// <summary>
/// Initializes a new <see cref="UserPrompt"/>
/// </summary>
public UserPrompt(ActivityContext context)
{
if (context == null) throw new ArgumentNullException("context");
Proceed = true;
BookmarkName = context.ActivityInstanceId;
}

/// <summary>
/// Gets/sets whether to proceed to the next node
/// </summary>
/// <remarks>If <see langword="true"/> the workflow will proceed to the next node.
/// If <see langword="false"/>, it will return to the previous node requiring user input.</remarks>
public bool Proceed { get; set; }

/// <summary>
/// Gets/sets the name of the bookmark to resume
/// </summary>
public string BookmarkName { get; private set; }
}

/// <summary>
/// Contains the result of prompting a user for input
/// </summary>
/// <remarks>This is used to pass back information to the workflow</remarks>
/// <typeparam name="T"></typeparam>
public class UserPrompt<T> : UserPrompt
{
/// <summary>
/// Initializes a new <see cref="UserPrompt"/>
/// </summary>
/// <param name="context"></param>
public UserPrompt(ActivityContext context)
: base(context)
{
}

/// <summary>
/// gets/sets the result of the user input.
/// </summary>
public T Result { get; set; }
}

There are two versions of the class, the first being the basic one used when the workflow doesn’t expect a result. It contains two properties – the bookmark name and a boolean called Proceed. The bookmark name is used to resume the bookmark (obviously!) and the Proceed property is set by the view model to indicate whether the user pressed “Next” or “Previous”.


The second version of UserPrompt – UserPrompt<T> is used when the current activity expects a result. The view model should set the Result property. Our credit check example shows clearly how this is used:

void PerformCreditCheck(UserPrompt<CreditCheckResult> userPrompt, Company company);

This prompts the view model to perform a credit check and indicates that the result is a CreditCheckResult object.


Resuming the bookmark


It is relatively easy to resume a workflow bookmark, but to make sure that the developers writing the view models know what to do, I wrote the following extension method for WorkflowApplication :

/// <summary>
/// Resumes the current bookmark with the result
/// </summary>
/// <param name="app"></param>
/// <param name="result">The result of the user input</param>
public static void ResumeBookmark(this WorkflowApplication app, UserPrompt result)
{
if (app == null) throw new ArgumentNullException("app");
if (result == null) throw new ArgumentNullException("result");
app.ResumeBookmark(result.BookmarkName, result);
}

 


Conclusion


The user prompt class is the glue that holds everything together, but there is still something missing: The activities. In the next post, I’ll dig into them.

Wednesday, 14 September 2011

Workflow Wizards – Part 1

We are building an application using WPF, Prism and Windows Workflow Foundation (WF). The team had already defined the business processes as Visio flow charts and we wanted to use the new Flowchart workflows to implement them. This meant driving the user interface from the workflows.

This is the first in a series of posts to describe how one would implement a set of wizard pages that are driven by a Flowchart based workflow.

In implementing the user interface, we faced two major challenges:

  1. How would the workflow request user input and receive a result?
  2. How would the user indicate to go to the next step or the previous step?
  3. How would the implementer of the view model know what type of result to return?

The eventual solution to both of these were relatively simple, but it took us a while to get there. The solution consists of the following components:

  1. NavigationActivity: This is a base class for custom activities that will request interaction with the user. It inherits from NativeActivity.
  2. UserPrompt: A class that will be used to pass information from the user interface to the workflow. It also contains the name of a bookmark – more on this later.
  3. The actual flowchart workflow, that consists of built-in activities, WCF service calls and of course the activities derived from NavigationActivity to request information from the user.
  4. Workflow interface: Each workflow will have an interface that should be implemented by the view model. The interface defines the methods the activities will call when requesting user interaction. Each interface method will have at least one parameter: A UserPrompt.

      The workflow

      image

      This is part of our EditAccountHolder workflow. The CreditCheck activity is a custom, NavigationActivity derived activity. It prompts the user to perform a credit check and expects a result in the form of an attached file. Each navigation activity has a boolean result that indicates whether the user clicked Next or Previous. This is evaluated in the Decision Activity. If Next is selected, the workflow continues. If Previous is selected, we either move back to the previous user interaction, or set an overall result if we are already at the beginning of the workflow.

      Workflow interface

      The workflow’s interface looks like this:

      	public interface IEditAccountholder
      {
      /// <summary>
      /// Prompts the user to perform a credit check.
      /// </summary>
      /// <param name="userPrompt">Set <see cref="UserPrompt{T}.Result"/> to the credit check result</param>
      /// <param name="company">The company to perform the credit check for</param>
      void PerformCreditCheck(UserPrompt<CreditCheckResult> userPrompt, Company company);

      }

      User interaction


      The credit check activity calls IEditAccountHolder.PerformCreditCheck and then sets a bookmark to wait for the response. It is the view model’s responsibility to prompt the user for input and collect results. Once this is done, the results are stored in the UserPrompt and the workflow is resumed, passing the UserPrompt as the value parameter of ResumeBookmark.


      Conclusion


      It is not really obvious from the above how our three problems are solved. Part 2 will contain the code for UserPrompt and NavigationActivity which will hopefully answer your questions.


      Friday, 26 August 2011

      Exchange 2010: Setting “Send As” permissions

      Unlike user mailboxes, setting “Send As” permission can only be performed from Powershell for a distribution list. The following script will prompt you for the user and group and then set the send as permission. It will then display what the result was.

      param ([string]$User = $(Read-Host -prompt "User"), [string]$Group = $(Read-Host -prompt "Group") )
      
      Add-ADPermission -Identity "mydomain.local/$Group" -User "$User" -ExtendedRights "Send As"
      
      Get-AdPermission -Identity "mydomain.local/$Group" | where-object {$_.extendedrights -like 'send-as'} | select user,extendedrights
      

       


      Remember to replace “mydomain.local” with your domain name. I normally specify the group name as the full path to the group, including the AD containers to make sure I get the correct one. You can change that to –Identity $group and just specify the group name if you want to.


      The following reverses the process:

      param ([string]$User = $(Read-Host -prompt "User"), [string]$Group = $(Read-Host -prompt "Group") )
      
      Remove-ADPermission -Identity "mydomain.local/$Group" -User "$User" -ExtendedRights "Send As" -Confirm:$False
      
      Get-AdPermission -Identity "mydomain.local/$Group" | where-object {$_.extendedrights -like 'send-as'} | select user,extendedrights
      

      South African postal codes

      Since the demise of Dotnet.org.za, I haven’t had a blog. So I decided to start again. So lets start with something simple that I had to do today.

      It sound like it should be simple to get an electronic list of postal codes for South Africa to import into your app, but its surprisingly difficult. I decided to write a PowerShell script to retrieve it from SA Web. They have a page for each alphabet letter that contains the suburbs starting with that letter. The script downloads each of the 26 pages and parses each page for the list of suburbs and their codes.

      I decided to use the InternetExplorer.Application COM object that allows me to access the HTML DOM.

      $alph=@()
      97..122|%{$alph+=[char]$_}
      $urls=@()
      $alph | % {$urls+="http://www.saweb.co.za/pcodes/"+$_+".html"}
      $ie= new-object -com InternetExplorer.Application
      $ie.visible = $true
      $codes = @()
      $urls | % {
      $ie.Navigate($_);
      while($ie.ReadyState -ne 4) {start-sleep -m 100}
      $doc = $ie.Document;
      $doc.getElementsByTagName("P") | % {$codes += $_.innerText }
      }
      $ie.Quit()
      $results = @()
      $regex = [regex]"^(?<suburb>[\w\s]+)\s-\s(?<code>\d+)\s-.*$"
      $codes | % {
      $regex.Matches($_) | %{
      if($_.Success -eq $True) {
      $suburb = $_.Groups[1].Value
      $code = $_.Groups[2].Value.PadLeft(4, '0')
      $results += New-Object Object | Add-Member NoteProperty Suburb $suburb -PassThru | Add-Member NoteProperty Code $code -PassThru
      }
      }
      }
      $results| Export-Csv "postalcodes.csv"


      The results are exported as a CSV.