Activity Validation
With the announcement today of Windows Workflow Foundation (WF), I thought I'd post some stuff I've been looking into as there's a paucity of information on the web about this new product, and it's cooler than a penguins chilly bits.
This article will discuss activity validation, and specifically how to create properties that behave like the ones in the activities provided by the framework. To follow along you'll need a copy of Visual Studio 2005 Beta 2, and have installed the workflow foundation Beta 1 bits. I'll digress a little and discuss how I have my virtual machines are setup, as this may save you some time.
I use Virtual PC for all my tinkering, as it doesn't mean I have to hose my work machines when something new shows up, and I can easily install some new stuff without worrying whether the stuff I need will work or not. So, I created a Windows XP base image, and installed IIS and Message Queueing into this base image. I don't put anything else on it as it's just that - a base image, and the other stuff I install typically changes fairly frequently.
Once I have my base image fully prepared, I use the precompactor tool (available in the Virtual Machine additions folder, typically C:\Program Files\Microsoft Virtual PC\Virtual Machine Additions\Virtual Disk Precompactor.iso). This gem zeroes out data on disk that is unused, so that you can then make your VM smaller.
Next, close down the VM and from the Virtual PC console choose 'Virtual Disk Wizard' from the 'File' menu. Here you choose the .vhd you created above and can compact it. This will save disk space which is always at a premium!
Next, I created a differencing drive (based on the XP vhd) and installed Whidbey Beta 2 into it. A differencing drive maintains only the changes between it and the base OS.
Last but not least I then created a third vhd, based on the XP SP2 With Whidbey differencing drive, and installed Windows Workflow Foundation into it. This is the way I like to set things up, as I can then install a new version of WF very easily, plus I can have other VM's based on XP SP2 and/or Whidbey Beta 2, and find I trash virtual machines a lot less frequently. Now on with the show.
Now create a workflow project and a separate activity project. I've created it this way as typically you would create activities in a library for reuse. Here's the activity I created...

No points for guessing what this activity will do. The next thing to do is add some properties - in this case I plumped for Source and Destination properties as shown below...

With that done, build your assemblies and you'll note that the toolbox updates to include your shiny new activity. If it doesn't, add a reference to the assembly containing your activities to the main workflow project, rebuild, and display the toolbox whilst editing the workflow. This is one cracking feature of Whidbey that will make everyones life easier - thanks development team!.

Now, the image isn't that elevating and where did that 3.0.0.0 come from?. I'll cover those in another article, for now lets concentrate on the design time behaviour of the new activity. Drag one from the toolbox into the workflow and you'll have something like the following...

This shows what you get without any tinkering, which whilst not too shabby it does leave a lot to be desired. In another article I'll show you how to change the visible aspects of the activity so that it looks more like the inbuilt ones and less like something you threw together. For now though we need to solve the problem that there's no validation of the source and destination filenames which is easy - we need an ActivityValidator object.
Create a new class that derives from ActivityValidator, such as that shown below...
|
using System;
using System.Workflow.ComponentModel.Compiler;
namespace CustomActivities
{
public class CopyFileValidator : ActivityValidator
{
public override ValidationErrorCollection Validate(ValidationManager manager, object obj)
{
ValidationErrorCollection errors = base.Validate(manager, obj);
CopyFile cf = obj as CopyFile;
if (null != cf)
{
if ( String.IsNullOrEmpty ( cf.Source ) )
errors.Add(new ValidationError("You have not entered a value for Source", 100));
if (String.IsNullOrEmpty(cf.Destination))
errors.Add(new ValidationError("You have not entered a value for Destination", 100));
}
return errors;
}
}
}
|
Once that's done you then need to associate the validator with the activity - as with many things in WF you use an attribute for this...
|
[Validator(typeof(CopyFileValidator))]
public partial class CopyFile : System.Workflow.ComponentModel.Activity
{
...
}
|
Now when you compile up you should see something like this in your workflow...

You'll also see two errors in the errors window that describe the problem too. Now, click on the error in the error list or select one of the errors from the smart tag. You'd expect it to go to the error within the property grid wouldn't you - as that's what the inbuilt activities do. Go on - try it. Less than impressed? - I know I was.
If you're not following along, when you click on an error raised from your validator it does nothing, other than select the node in the workflow that the error applies to. This isn't great, so I did some spelunking with my trusty copy of Reflector with the inbuilt validators and found that they do something extra within the validation function which hooks the error to the property which is at fault.
The error collection returned from the Validate() method contains instances of ValidationError objects, and these contain a collection of extra properties (it's a Hashtable). All you need to do is add the name of the property to this hashtable under a predefined key and voila - you'll get the behaviour you want. Add the following code to your Validate method...
|
ValidationError err = new ValidationError("You have not entered a value for Source", 100, false, "Source");
errors.Add(err);
|
With that addition you've made the link between the error and the property, so you'll see the following (and be able to click on the error to jump directly to the offending property)...

There are several overloads to the constructor for ValidationError - the one I'm using above defines the link between the error and
the property by naming the property in the last parameter. The false value indicates that this is an error instead of a warning.
What's happening under the covers is that there is a hashtable within the ValidationError object, and we add in an entry with a
well known GUID as the key, and the value is the property name.
If you would rather have an error that looks like ours, then use the static ValidationError.GetNotSetValidationError ( property ) method, which will load the error message from a resource file so it should be localised OK too. I'd use this over the example shown above when the value is null, but in the instance where the object has failed validation for some other reason you'll have to use the 4 parameter overload.