In my article, "Empowering Workflow Services Using Custom Activities" (December 2009), I introduced how to empower workflow services by creating custom activities to define and encapsulate domain-specific logic. In addition to defining logic for runtime use, workflows can also specify constraints on the design that help ensure the workflow is properly designed, that activities have their required properties and arguments correctly filled, and in general give the workflow author a greater confidence that the workflow designed will run as desired. Within Visual Studio 2010, issues discovered during a validation pass (often referred to as an episode) are surfaced as errors or warnings—both above the activity on the design surface and listed in the Errors Window. In this article, I will show how you can create your own validation logic, defining it either in code or in XAML as best suits your needs.
What Can Be Validated
Validation can be used to validate an individual activity’s properties that an InArgument has a source configured (e.g., that its values are not empty and have a variable or Visual Basic Expression defined), or that an expected OutArgument is properly configured (e.g., with the name of a variable that will receive the value).
Beyond checking the configuration of an activity, validation enables structural verification. There are three main checks you can perform here. Given an individual activity, you can examine the tree of its ancestor activities (e.g., to ensure that an activity has an expected parent), its descendant activities (e.g., to restrict the types of children the activity should have), or you can look at all the activities in the workflow irrespective of their relative location (e.g., to verify the activity’s configuration doesn’t conflict with another related activity).
Where to Validate
Validation logic can be defined by the activity itself or injected by the workflow designer host. In the self-describing case, an activity can specify the validation logic imperatively or declaratively. The imperative approach defines validation logic with the C# or VB code of a CodeActivity or NativeActivty. The declarative approach can define similar logic, though it’s specified within the XAML of an Activity.
The activity isn’t the only place to define validation logic—both the workflow designer host and an Activity Designer can inject validation attributes (introduced later) during metadata registration. In addition, the host can explicitly invoke a validation episode, and in doing so specify validation that should occur in addition to that provided by the activities. In the sections that follow, we will examine the variety of options that exist using imperative and declarative approaches to defining activity-provided validation.
From code, there are three ways that a code-based activity (specifically CodeActivity or NativeActivty) can provide validation logic. The three approaches are: using validation attributes, defining logic in CacheMetdata, or adding constraints within the constructor. Let’s examine each of these in more detail.
There are two attributes that can be applied to an Activity’s properties that are evaluated during a validation episode: RequiredArgumentAttribute and OverloadGroupAttribute.
RequiredArgument works as you would expect. If a value is not supplied for this argument, a validation error is created. Figure 1 illustrates the error that appears in Visual Studio 2010 when the RequiredArgument validation is not satisfied on a custom activity. On the workflow designer’s surface, observe that the ValidatingActivity has an error glyph, that the tooltip indicates the specific error, and that the containing sequence indicates an error as well (specifically, it indicates that one or more children have validation errors or warnings). This ensures that the validation errors and warnings are visible even if the offending portions are obscured because their parents are collapsed. Also note that the Errors List window includes the same error. Double clicking this entry will select the offending activity in the designer, much like a compile-time error would bring you to a line of source code.
Figure 2 shows how the RequiredArgument attribute is applied to the custom activity shown previously. An important point to note is that the RequiredArgument only applies properties of an Argument type. While it can be applied to both Arguments and CLR Properties, only Arguments will trigger validation. In the figure, notice that the CLR property MyProp is decorated with RequiredArgument; however, no validation error will ever appear on the design surface when it is empty.
The OverloadGroup attribute lets you group properties together by name and specify within that group which properties are required. It also lets you define properties on an activity that are mutually exclusive and should never be configured with a value at the same time. Let's enhance our sample activity with a few properties to show how to apply this. Say our activity was used to perform some image manipulation and we wanted properties for specifying a background that could be either a solid fill or an image. In addition, if an image fill was specified, we might also allow the user to optionally specify a color within the image to treat as transparent. Obviously, it is an error if the workflow author provides both a background color and an image path or a background color and a transparent color. Let’s apply the overload groups to the activity as shown in Figure 3.
Figure 4 shows the resulting validation error when we have satisfied only the original Text RequiredArgument (by giving it the value “Hello World”, but not any of the OverloadGroups. If we fill in a background color (e.g., red), then the SolidColorFill group has values for all of its required arguments, so there are no longer any validation errors.
However, if we then fill in the TransparentColor, we will again have a validation error because TransparentColor is not a member of the SolidColorFill group—we have configured them to be mutually exclusive. Figure 5 shows the validation error that results in this case.
Finally, if we clear out BackgroundColor, TransparentColor, and specify a value for BackgroundImagePath only, then all validation errors go away because we have satisfied all of the required arguments for the ImageFill overload group. Note that TransparentColor is optional once BackgroundImagePath has been specified because it is not decorated with RequiredArgument.
Validating in CacheMetdata
The CacheMetdata method is generally overridden to allow an activity to specify dynamic arguments, variables, and delegates and is called by the designer as it builds the workflow tree. It can also be used to execute validation logic and explicitly add validation errors as they are detected. Doing so is very straightforward. Building upon our sample activity, let’s say we wanted to remind the workflow author of best practices by reminding him or her that the DisplayName property should always have a value. In this case, we don’t necessarily want an error and will display the message as a warning. Figure 6 shows the method we need to add to the custom activity code.
When the DisplayName property is blank, this validation error will appear with warning icons in both the designer and in the error list, as Figure 7 shows.
Validating with Constraints
Constraints take a unique approach to validation. They allow you to define the validation logic for your activity using other activities. These workflows are executed during a validation episode against your current activity, and any errors or warnings appear just as we have seen with the other approaches. Adding a constraint to your activity consists of two steps:
- Create a Constraint out of activities that will perform the validation.
- Within the constructor of your activity, add the constraint to the constraint collection.
Let’s add another best-practice validation to our sample activity that illustrates using a constraint. In this case, let’s suggest that this activity always be used within a TryCatch activity. Figure 8 shows the validation warning that appears if/when the activity is not contained within a TryCatch.
Defining the Constraint is done by building the activity tree and defining any needed delegate arguments or variables used by the validation workflow during validation. Note that there is no UI experience for designing constraints within Visual Studio. Figure 9 shows the code required to build up a constraint. All constraints will contain at least one AssertValidation activity whose Assertion member is configured with the result of the validation. If true, no validation error is created; if false, a validation error is created with the following information: level (warning or error) and property name (if applicable).
In Figure 9, we build a sequence as follows:
- ForEach. Loop over each parent activity. When a TryCatch is encounterd, change the value of the IsWithinTry variable to true using an Assign Activity.
- AssertValidation. Examines the value of the IsWithinTry variable, and adds a warning validation error if it is false.
In order to provide the ForEach collection of parent activities, we make use of the GetParentChain activity. This is one of a set of three validation activities that help you navigate the structure. The other two, GetChildSubtree (which returns the collection of child activities) and GetWorkflowTree (which returns all activities in the workflow) are used in the same way: they require the ValidationContext that is always acquired from Argument2 of the root ActivityAction.
To actually make use of the constraint, we need to add it to the activity’s constraint collection. This is done within the constructor, as shown below.
Declarative Activity Provided Validation
The validation provided by the RequiredArgumentAttribute, OverloadGroupAttribute, and by adding constraints can also be defined within the XAML of an activity. To show this, we will rebuild our ValidatingActivity deriving from Activity in XAML (using the Activity template within Visual Studio). Recall that we can define all arguments and the CLR properties for an activity within the workflow designer’s Arguments window, and we have done so for this activity, as Figure 10 shows.
Declarative Validation Attributes Applying the RequiredArgument attribute is easy, and can be applied by checking the checkbox within the IsRequired field in the property grid for an argument selected in the Arguments window. (Note that IsRequired is disabled for Arguments with a direction of type Property, as you would expect.)
While there is no UI support for specifying OverloadGroup attributes, the XAML generated by the designer gives a good hint as to how to approach it—just add the OverloadAttribute element within the Property.Attributes element for each property that should have it. Checking IsRequired for every argument on our activity will result in a new Property.Attributes element that contains a RequiredAttribute element for each Property defined in the XAML
Declarative Constraints There is also no UI for building the constraints workflow in XAML, so one must add an Activity.Constraints section in XAML within the root Activity element. To perform the same validation that checks if the activity is within a TryCatch as we did in code, we need to add the constraint definition shown in Figure 11 beneath the mva:VisualBasicSettings element.
Five Steps Toward Better Configuration This article walked through five approaches you can take to make your activities more vocal and helpful to the workflow author to ensure that they are properly configured. I showed that in the imperative approach, you can use the RequiredArgument and OverloadGroup Attributes, override CacheMetadata, and define constraints. I also showed in the declarative case how you can define the same attributes and constraints in XAML.