Validation






Validation

Compilers for languages like C# determine the validity of programs written in these languages. They establish that a program is legal by parsing the program's textual representation and checking that it conforms to the language grammar. This conformance check is somewhat analogous to ensuring that an XML document obeys the rules of a particular XML schema.

A WF program is a tree of activities. There is no WF grammar that restricts how activities can be arranged within a WF program, other than the rules that an activity must be composite in order to contain other activities, and an activity can only be contained by one other activity. The need for assessing the validity of a WF program, however, is arguably just as important as that for other kinds of programs. Many activities are meant to be used within WF programs in specific ways.

The fundamental (and liberating) difference between the WF programming model and traditional program validation is that in WF the activities themselves define relevant validation logic. There is no grammar in WF, but you have the freedom (as an activity author) to decide exactly how your activities can be used within WF programs. You can write activities that impose few, if any, restrictions on their usage. You can also write more restrictive activities; a set of such activities might implement a "grammatically closed" domain-specific language.

To this end, one of the components that can optionally be associated with any activity is a validator. The purpose of a validator component is to ensure that the companion activity is correctly used within a WF program. For example, a validator component will complain if the properties of the activity do not have values (at the time of validation) that are collectively an acceptable basis for that activity's participation in the WF program. The set of validators for the activities in a WF program together ensure the validity of the program.

Earlier in this chapter, we learned about the difference between activity metadata and instance data. Validation often is limited to metadata; the values of metadata properties must be supplied when the WF program is being designed, because metadata cannot be changed once an instance of the WF program is running. Activity metadata includes properties of Activity such as Name and Enabled, plus any custom activity properties that are registered as metadata properties. A composite activity's set of child activities (to be precise, a composite activity's containment relationship with each child activity) is also metadata because it is established as part of the definition of the program. A validator component for an activity should in general validate all of the metadata for that activity.

Values for normal properties of an activity are not generally required at design-time. The distinguishing feature of a normal property (compared to a metadata property) is that its value can be different across different instances of the same WF program. The Text property of WriteLine, for example, can be set dynamically (at runtime) just before a WriteLine activity executes. It may be supplied statically at design-time but if WriteLine required this, it would be a far less useful activity. A validator component for an activity should usually not require the values of normal properties to be set (because they are expected to be set at runtime); however, if values for such properties are supplied statically, they may become subject to validation at the discretion of the activity author.

A validator is associated with an activity by applying ActivityValidatorAttribute to the activity class definition. Listing 7.10 defines a custom activity, called Simple, and an associated validator, called SimpleValidator. The Simple activity defines a metadata property named Amount, of type integer. The SimpleValidator casts the obj parameter of the Validate method to a Simple object, and checks to make sure that the value of the Amount property is between 50 and 100. If it is not, a validation error is created and added to the collection of errors that is returned by the Validate method.

Custom Activity with a Validator Component

using System;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;

namespace EssentialWF.Activities
{
  [ActivityValidator(typeof(SimpleValidator))]
  public class Simple : Activity
  {
    public static readonly DependencyProperty AmountProperty =
      DependencyProperty.Register("Amount",
        typeof(int),
        typeof(Simple),
        new PropertyMetadata(DependencyPropertyOptions.Metadata)
      );

    public int Amount
    {
      get { return (int)base.GetValue(AmountProperty); }
      set { base.SetValue(AmountProperty, value); }
    }

    ...
  }

  public class SimpleValidator : ActivityValidator
  {
    public override ValidationErrorCollection Validate(
      ValidationManager manager, object obj)
    {
      ValidationErrorCollection errors =
        base.Validate(manager, obj);

      Simple simple = obj as Simple;

      if (simple.Amount > 100 || simple.Amount < 50)
      {
        ValidationError error = new ValidationError(
          "Simple.Amount must be between 50 and 100", 7000);
        errors.Add(error);
      }
      CompositeActivity parent = simple.Parent;
      Sequence seq = parent as Sequence;

      if (parent == null || seq == null)
      {
        ValidationError error = new ValidationError(
          "Simple must be a child activity of a Sequence", 7001);
        errors.Add(error);
      }

      return errors;
    }
  }
}

The validator for the Simple activity also performs a second validation check, which ensures that the Simple activity is always part of a WF program in which a Sequence activity is its parent; Sequence, of course, has no knowledge of the Simple activity. Although this example is contrived, it illustrates an important principle: All activities can participate in the validation of the containment relationships in an activity tree.

Because a validator component can inspect the entire WF program in which its companion activity is located, more sophisticated validation logic can be written. This allows for the development of sets of activities that have meaningful relationships above and beyond what is conveyed in the tree structure of a WF program. To take one example, if you are modeling a request-response operation with two distinct activities (say, Request and Response), these activities can perform special validation: You should not be able to use a Response in a WF program without a corresponding Request; and, if the activities are used within a Sequence, a Request should occur before its Response.

Activity developers can write whatever validation logic is necessary to ensure that a given activity is appropriately related to other activities within the WF programthe context for performing validation is a fully instantiated tree of activities. One common example is a validation check in which a composite activity validates that all of its child activities are of some specific type or types (or, perhaps, that all child activities implement some interface). Similarly, an activity may validate that its parent activity is always of a particular type (as we demonstrated with the Simple activity). An activity may validate that it is always at the root of a WF program by checking that its Parent property is null.

Turning to Listing 7.10, you can see that the SimpleValidator class derives from ActivityValidator, which is the base class for all activity validator components. ActivityValidator is a derivative of DependencyObjectValidator, which in turn derives from Validator. The chain of base classes is depicted in Figure. As you might guess, not all validators need to be associated with activities; more on this later.

4. Validator base classes


The Validator class, which (along with the other validator base classes shown in Figure) is part of the System.Workflow.ComponentModel.Compiler namespace, is shown in Listing 7.11.

Listing 7.11. Validator

namespace System.Workflow.ComponentModel.Compiler
{
  public class Validator
  {
    public virtual ValidationErrorCollection Validate(
      ValidationManager manager, object obj);

    public virtual ValidationErrorCollection ValidateProperties(
      ValidationManager manager, object obj);

    /* *** other methods *** */
  }
}

The method relevant to all activity validators is Validate, which returns a collection of validation errors produced in the analysis of the object being validated, the obj parameter. The ValidationErrorCollection and ValidationError classes are shown in Listings 7.12 and 7.13, respectively.

Listing 7.12. ValidationErrorCollection

namespace System.Workflow.ComponentModel.Compiler
{
  public sealed class ValidationErrorCollection :
    Collection<ValidationError>
  {
    public ValidationErrorCollection();
    public ValidationErrorCollection(
      ValidationErrorCollection errors);
    public ValidationErrorCollection(
      IEnumerable<ValidationError> errors);

    public void AddRange(IEnumerable<ValidationError> errors);
    public ValidationError[] ToArray();

    protected override void InsertItem(int index,
      ValidationError item);
    protected override void SetItem(int index,
      ValidationError item);

    public bool HasErrors { get; }
    public bool HasWarnings { get; }
  }
}

A validation error is identified by a number, ErrorNumber, and carries a string property, ErrorText, that described the error. A ValidationError object can be marked as an error (this is the default) or as a warning. The IsWarning property is used to distinguish between validation issues that must be corrected and those that are not vital to fix (or, situations where the validator cannot be sure of an error). The ToString method override returns a formatted string containing the text and number of the error, and also an indication of whether or not the error is a warning.

Listing 7.13. ValidationError

namespace System.Workflow.ComponentModel.Compiler
{

  public sealed class ValidationError
  {
    public ValidationError(string errorText, int errorNumber);
    public ValidationError(string errorText, int errorNumber,
      bool isWarning);

    public int ErrorNumber { get; }
    public string ErrorText { get; }
    public bool IsWarning { get; }
    public IDictionary UserData { get; }

    public override string ToString();

    /* *** other members *** */
  }
}

ValidationError is a sealed class, but you may use the UserData dictionary to store any custom data that you need to associate with a validation error.

Now let's look at how to perform validation of a WF program (actually, any tree of activity objects). The console application in Listing 7.14 performs validation of two different activity treesone tree is an Interleave activity containing a freshly constructed Simple activity; the other tree is a Sequence activity containing a Simple activity that has had several of its properties set.

Validation of WF Programs

using System;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;
using EssentialWF.Activities;

namespace Chapter7
{
  class Program
  {
    static void Main()
    {
      Simple simple1 = new Simple();
      Interleave i = new Interleave();
      i.Activities.Add(simple1);
      Console.WriteLine("- Validating Interleave containing a Simple activity -");
      Validate(i);

      Simple simple2 = new Simple();
      simple2.Amount = 70;
      simple2.Name = "$simple";
      Sequence seq = new Sequence();
      seq.Activities.Add(simple2);
      Console.WriteLine("\n- Validating Sequence containing a Simple activity -");
      Validate(seq);
    }

    static void Validate(Activity activity)
    {
      System.ComponentModel.Design.ServiceContainer container =
        new System.ComponentModel.Design.ServiceContainer();

      ValidationManager manager = new ValidationManager(container);

      foreach (Validator validator in
        manager.GetValidators(activity.GetType()))
      {
        ValidationErrorCollection errors =
          validator.Validate(manager, activity);

        foreach (ValidationError error in errors)
          Console.WriteLine(error.ToString());
      }
    }
  }
}

We use the ValidationManager type to obtain the validators associated with the root activity in an activity tree, and then call the Validate method of each. There typically will be just a single validator for the root activity in a WF program, which, as part of its validation logic, will recursively propagate the call to Validate down the hierarchy of activities in the tree.

When we run the program in Listing 7.14, we see the following result:

- Validating Interleave containing a Simple -
error 7000: Simple.Amount must be between 50 and 100
error 7001: Simple must be a child activity of a Sequence

- Validating Sequence containing a Simple -
error 281: Property 'Name' has invalid value. The identifier '$simple' is not a
valid name.

Validation of the first activity tree yields two errors, both of which arise, as expected, due to the validator component we associated with the Simple activity. The second activity tree yields neither of these errors because the Simple activity in this case is a child activity of a Sequence, and also has its Amount metadata property set to a value deemed appropriate. However, validation of the second activity tree does produce one validation error. You may have noticed that the value of the Name property of the simple2 object is set to a value of "$simple" in the console application before the validation of the second activity tree occurs. It is the call to base.Validate within the Validate method of SimpleValidator that checks the value of the Name property and produces this validation error. All validator components should derive from an appropriate base class and call base.Validate as part of their implementation of the Validate method.

ActivityValidator

ActivityValidator is the base class for all validators associated with activities. The validation that is performed by ActivityValidator ensures the following:

  • If the activity is the root activity in the activity tree, Enabled is TRue.

  • The value of the Name property is a legal identifier.

  • The value of the QualifiedName property is unique within the activity tree.

  • Standard validation (based upon validation options, discussed later) of all metadata properties takes place.

  • Invocation of the associated validator occurs for all properties (metadata and instance) whose type has a custom Validator component.

Validation of properties entails a call to the ValidateProperties method defined on the Validator class. We will see a bit later how the default property validation logic utilizes the ValidationOption enumeration in the validation of metadata properties.

Composite Activity Validation

As we have seen, composite activities are analogous in purpose to control flow constructs found in familiar programming languages (though given the degrees of freedom in the WF programming model, not all composite activities will have familiar analogs in languages like C#). As one example, the Sequence composite activity maps closely in meaning to a C# { } program statement block. The Interleave composite activity, on the other hand, doesn't have an analog in the grammar of a language like C#.

Without validation logic that ensures the legitimacy of the containment relationships it has with its child activities, a composite activity cannot claim to represent a well-defined control flow construction. Let's look at a familiar and simple example. Consider the while construction in the C# language:

while (boolean-expression)
    embedded-statement

The while statement is an element of the C# language grammar. The compiler for the C# language enforces the correctness of any usage of the while keyword; a C# boolean-expression (which governs the looping) is required along with exactly one C# embedded-statement (which can be a compound statement such as a {} program statement block).

Because the WF runtime has no special knowledge of any control flow constructs, each composite activity is responsible for enforcing its semantics by providing a validator component. Consider the While composite activity and its associated validator, shown in Listing 7.15.

Listing 7.15. Custom While Activity and Associated Validator

using System;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Compiler;

namespace EssentialWF.Activities
{
  [ActivityValidator(typeof(WhileValidator))]
  public class While : CompositeActivity
  {
    public static readonly DependencyProperty ConditionProperty
      = DependencyProperty.Register("Condition",
        typeof(ActivityCondition), typeof(While),
        new PropertyMetadata(
          DependencyPropertyOptions.Metadata,
          new Attribute[] { new ValidationOptionAttribute(
            ValidationOption.Required) }
        )
        );

    public ActivityCondition Condition
    {
      get
      {
        return GetValue(While.ConditionProperty)
          as ActivityCondition;
      }
      set
      {
        SetValue(While.ConditionProperty, value);
      }
    }

    ...

  }

  public class WhileValidator : CompositeActivityValidator
  {
    public override ValidationErrorCollection Validate(
      ValidationManager manager, object obj)
    {
      ValidationErrorCollection errors = base.Validate(manager, obj);
      While loop = obj as While;

      if (loop.EnabledActivities.Count != 1)
      {
        errors.Add(new ValidationError(
          "While must have exactly one child activity", 7002));
      }

      return errors;
    }
  }
}

We are using the ActivityCondition type here, which is an abstract class defined in the System.Workflow.ComponentModel namespace. ActivityCondition was briefly discussed in Chapter 4, where we wrote a simple derivative of it. In Chapter 8, we will dig into the details of rules and conditions; for now it is enough to understand that the runtime evaluation of an object of type ActivityCondition will return a boolean value. This is just what we will need to drive the execution logic of While.

The WhileValidator must enforce that our While composite activity has a single child activity. We do not want the user of While to arbitrarily add child activities; we want exactly one, in order to exactly match the familiar C# language construction. The job of While is to iterate; it should not also provide sequential (or other) execution of a set of child activities.

Clearly a composite activity may decide to raise validation errors based upon the number and type of child activities it is given. But it is not necessary to always write validation logic. For instance, the Sequence composite activity permits an empty Activities collection. This is effectively equivalent to allowing an empty {} statement block as a legal construction in C#.

As expected, WhileValidator derives from CompositeActivityValidator, and calls base.Validate as part of its validation logic. The validation that is performed by CompositeActivityValidator, which derives from ActivityValidator, ensures the following:

  • Recursive validation of all enabled child activities.

  • At most one child CancellationHandler activity is present.

  • At most one child FaultHandlers activity is present.

  • At most one child CompensationHandler activity is present.

CancellationHandler, FaultHandlers, and CompensationHandler are special modeling constructs that were discussed in Chapter 4.

Validation Options

You may have noticed that in the registration of the ConditionProperty dependency property in the While class, an extra piece of informationa validation optionwas specified as part of the PropertyMetadata:

public static readonly DependencyProperty ConditionProperty
  = DependencyProperty.Register("Condition",
    typeof(ActivityCondition),
    typeof(While),
    new PropertyMetadata(
      DependencyPropertyOptions.Metadata,
      new Attribute[] { new ValidationOptionAttribute(
        ValidationOption.Required)}
  );

The validation option that is specified as part of the PropertyMetadata object is utilized by the validation logic of ActivityValidator. This reduces the need for writing custom validation logic. The ValidationOption enumeration is shown in Listing 7.16. If no validation option is used in the registration of a metadata property, a value of ValidationOption.Optional is assumed.

Listing 7.16. ValidationOption

public enum ValidationOption
{
  None,
  Optional,
  Required
}

The ValidationOptionAttribute is utilized (by validator base classes) only in the validation of metadata properties. A value of ValidationOption.Optional indicates that a value for the associated metadata property does not need to be specified, but if a value is specified and its type has an associated validator component, then validation will occur. We will see an example of this soon when we discuss validation of databinding. A value of ValidationOption.Required means that a value must be specified, and if the type of the property has an associated validator component, that validation will, of course, also occur. A value of ValidationOption.None means that validation is disabled; even if a value is present, and the type of the property has an associated validator, that validator will not be given a chance to validate the property value.

We can demonstrate how ValidationOption is used by base class validation logic with an example that validates an activity tree with a While activity at the root. The following code snippet can be added to the console application we developed earlier (refer to Listing 7.14):

 While loop = new While();
 loop.Activities.Add(new Sequence());

 Console.WriteLine("\n- Validating While -");
 Validate(loop);

The result of validation of the While is, as expected:

- Validating While -
error 278: Property 'Condition' is not set.

Because the registration of the Condition property by While (refer to Listing 7.15) used ValidationOption.Required, we did not need to write any custom validation logic to ensure that the Condition property must be set.



 Python   SQL   Java   php   Perl 
 game development   web development   internet   *nix   graphics   hardware 
 telecommunications   C++ 
 Flash   Active Directory   Windows