One of the responsibilities of an activity is to cooperate with the workflow designer in order to provide an appealing and productive design experience. This chapter focuses on the mechanisms provided with Windows Workflow Foundation (WF) that make the design-time experience easier to use and less error-prone. Rather than focus on a particular runtime feature of WF, this chapter is all about helping the developers who use your custom activities at design time.
This chapter begins with an overview of the classes that you will use to create custom activity designers. Several designer scenarios are then presented in a series of examples. The first example exposes the properties of an activity directly on the design surface. Subsequent examples demonstrate custom designers that allow you to add one or multiple children to an activity. Another designer targets activities that support the ActivityAction
class as a property.
Custom activity designers are really only half of the design-time story. The second half is activity validation. WF provides several ways that you can enforce validation logic for your activities. In a series of examples, validation attributes, code, and constraints are demonstrated. WF also allows you to manually perform activity validation outside the design experience. This is demonstrated in another example.
The chapter concludes with an example that creates an activity template. Activity templates allow you to create combinations of activities or preconfigured activities that are added to the design surface just like a normal activity.
The workflow designer is built upon Windows Presentation Foundation (WPF). Therefore, any custom activity designers that you develop are implemented as WPF controls. In a similar way to WF, WPF uses Xaml files to declaratively define the user interface. WPF is an incredibly rich and complex foundation that is comprised of a seemingly endless array of classes. Fortunately, WF provides a set of specialized classes that handle most of the heavy lifting for you. Before diving into the mechanics of creating a custom activity designer, this section acquaints you with the presentation-related WF classes that you will use along with a few designer concepts.
Note Helping you learn WPF is beyond the scope of this book. If you are new to WPF, you may want to refer to one of the many good references on WPF. I recommend Illustrated WPF (Apress, 2009).
The ActivityDesigner
class (found in the System.Activities.Presentation
namespace) is the base class to use when you create a new designer. This class, in turn, derives from the WorkflowViewElement
class. The WorkflowViewElement
class is the common base class for all editable UI elements that appear on the design canvas. Currently, the ActivityDesigner
is the only type derived from this class that is available for your use. The WorkflowViewElement
class is derived from the WPF ContentControl
class.
When you create a new designer, the ActivityDesigner
class is the root element of the Xaml file. The x:Class
attribute specifies the name of the resulting designer class that you are defining. This is similar to the way that workflow Xaml files are defined.
Note You are permitted to develop your own base class and to use it as the root of a designer Xaml file. You might want to do this if you need to develop several designers that share similar functionality. However, that base class must ultimately derive from the ActivityDesigner
class.
The ActivityDesigner
class supports a large number of properties and members (most of which are inherited from the WorkflowViewElement
class). Most of these members are related to their use as a WPF UI element and are used for event handling and management of the visual aspects of the control (location, size, style, resources, and so on). However, there are a few workflow-related properties that you will use. Here are the most important ones:
Within the ActivityDesigner
class, the ModelItem
property represents the activity that the designer is currently editing. This property is actually an instance of the ModelItem
class (found in the System.Activities.Presentation.Model
namespace) class. This object sits between the visual elements that present an activity to the developer and the in-memory representation of the activity. As you will soon see when you develop your first custom designer, the visual elements of the designer bind to properties of the ModelItem
object. When a change is made to an activity, the change is made via a WPF binding to the ModelItem
. The changes are eventually propagated to the source Xaml file when any pending changes are saved.
As you have already seen, expressions are pervasive in WF. It would be difficult (if not impossible) to declare a meaningful workflow without them. Because of this, WF includes a UI element that is specifically designed to support the entry of expressions. The ExpressionTextBox
class (found in the System.Activities.Presentation.View
namespace) is a WPF ContentControl
that you will use when you want to expose one or more activity properties on the design surface. Here are the most important properties of this class:
The ExpressionTextBox
is the visual UI element that allows a developer to enter an expression. It supports expression syntax checking, IntelliSense, and all of the other Visual Basic expression goodness. However, it requires the use of a separate value converter (ArgumentToExpressionConverter
) to actually convert the expression to a form that can be saved to the Xaml file. This converter (found in the System.Activities.Presentation.Converters
namespace) is typically added to the designer as a static resource. Once it is added as a resource, it is referenced in the WPF binding to the ExpressionTextBox.Expression
property.
When you use this converter to bind an expression, you must also provide a ConverterParameter
value that determines the type of conversion that should take place. A ConverterParameter
value of In
indicates that a value expression is being edited. A value of Out
indicates that an L-value expression is being edited. Additional information on these expression types is presented in the next section.
Expressions generally come in two flavors: L-value and value expressions. You can think of the “L” in L-value as referring to locator, location, or left. The type of expression that you are editing will determine the property values that you should use for the ExpressionTextBox
and the ArgumentToExpressionConverter
.
The best way to illustrate the difference between the two types of expressions is to review the Assign
activity. This activity supports two properties that are set via expressions in the designer: To
and Value
. The Assign.Value
property (an InArgument
) represents the value that you want to assign to the Assign.To
property. The Assign.Value
property is a value expression. It equates to a value that you could use on the right side of an assignment statement. The Assign.To
property (an OutArgument
) is an L-value expression. It represents a location in memory that you might use on the left side of an assignment statement. You can assign values to an L-value expression, but a value expression is read-only.
Using the Assign
activity as an example, the L-value expression is defined as an OutArgument
, and the value expression is an InArgument
. You will find that this is generally the case.
Follow these guidelines if you are binding an ExpressionTextBox
to a value expression (InArgument
):
- Set the
ConverterParameter
to a value ofIn
.- Set the
UseLocationExpression
property to False (the default).
Follow these guidelines if you are binding to an L-value expression (OutArgument
):
- Set the
ConverterParameter
to a value ofOut
.- Set the
UseLocationExpression
property to True.
The WorkflowItemPresenter
(found in the System.Activities.Presentation
namespace) is another WPF ContentControl
that is provided with WF. You use this UI element for activities that support a single child activity. Examples of activities that fall into this category are the While
and DoWhile
activities. This control supports the standard drag-and-drop interface that allows a developer to easily add an activity. Here are the most important properties of this UI element:
The WorkflowItemsPresenter
serves a similar purpose; however, it supports multiple child activities. The best known example of this type of activity is the Sequence
activity. It supports a similar set of properties:
Note The WorkflowItemPresenter
and WorkflowItemsPresenter
classes are demonstrated later in the chapter.
WF uses a metadata store to associate a particular designer with an activity. An instance of this in-memory store is automatically created when the workflow designer is loaded. The association between an activity and the designer that it should use can be added to the metadata store in two ways:
- The
System.ComponentModel.Designer
attribute can be added to the activity. This attribute specifies the designer that should be used each time the activity is shown on the design canvas.- The static
AddAttributeTable
method of theMetadataStore
class can be called to add the association.
The advantage to the first option (using the Designer
attribute) is that it is extremely simple to implement. However, the downside is that the designer is directly coupled to the activity at build time. Although this works well in many (if not most) situations, it doesn’t provide you with the flexibility that you might need if you choose to self-host the workflow designer in your own application. More specifically, it doesn’t allow you to dynamically swap out different designers based on the current editing context, the user’s security level, and so on.
The second option (using the MetadataStore
class) allows you to effectively perform late-binding of the designer with the activity. With this option, the activity is no longer hard-coded to always use a particular designer. You can choose the proper designer at runtime as the workflow designer is initialized.
In general, you should follow these steps when creating a new customer activity designer:
- Create a separate project to house activity designers.
- Create a new activity designer class that is derived from
ActivityDesigner
.- If necessary, add support for multiple viewing modes (expanded and collapsed).
- Implement the visual elements and controls of the designer in Xaml.
- Add bindings for each
ModelItem
property that you want to maintain in the designer.- Optionally, add a custom icon to the designer.
- Add entries to the metadata store that associate the designer with the activities that should use it.
A common use of a custom designer is to expose activity properties directly on the design surface. You’ve already seen this behavior in many of the standard activities. By exposing the most frequently used properties on the design surface, you improve the usability of the activity. Instead of switching to the Properties window, the developer can usually set property values more quickly on the design surface.
In this first example, you will develop a custom activity and a designer that supports the entry of property values.
To begin this example, create a new Activity Library workflow project. Name the project ActivityLibrary
, and add it to a new solution that is named for this chapter. All the projects in this chapter can be added to this same solution. The purpose of this project is to house the custom activities that you will develop in this chapter.
You can keep the Activity1.xaml
file that is created with the new project. At this point, add a Sequence
activity as the root of the Activity1.xaml
file, and save this change. To test each example, you need to actually drag and drop the custom activity onto the design surface. The Activity1.xaml
file can be used as a scratch pad for your testing. Once you finish with an example and are satisfied with the results, you can delete the individual activities that you added and prepare for the next example.
Create a second project, this time using the Activity Designer Library new project template. Name the project ActivityDesignerLibrary
, and add it to the solution. You can delete the ActivityDesigner1.xaml
file that is created with this project. This project will be used for all the custom designers that you will develop. You generally want to place the designers into a separate assembly instead of placing them in the same assembly as the activities. This allows maximum flexibility and keeps your options open when it comes to associating an activity with a designer. It also provides an opportunity to potentially reuse a designer for multiple activities in multiple assemblies.
The ActivityDesignerLibrary
project should already include the usual set of assembly references that are used for workflow projects (System.Activities
and so on). In addition, it should also reference these assemblies that are related to WPF and activity designers:
PresentationCore
PresentationFramework
System.Activities.Presentation
WindowsBase
Add this same list of additional assembly references to the ActivityLibrary
project. In addition, add a project reference to the ActivityDesignerLibrary
project. These additional references are needed because the ActivityLibrary
project will be referencing the custom designers directly via the Designer
attribute. Without these additional references, the ActivityLibrary
project will not successfully build.
This first example uses a slightly modified version of the CalcShipping
activity that you developed in Chapter 3. Add a new Code Activity to the ActivityLibrary
project, and name it CalcShipping
. Here is the code for this activity:
using System;
using System.Activities;
namespace ActivityLibrary
{
public sealed class CalcShipping : CodeActivity<Decimal>
{
public InArgument<Int32> Weight { get; set; }
public InArgument<Decimal> OrderTotal { get; set; }
public InArgument<String> ShipVia { get; set; }
protected override Decimal Execute(CodeActivityContext context)
{
throw new NotImplementedException();
}
}
}
Note You will quickly notice that I haven’t provided an implementation for the Execute
method. Since this chapter is all about the design experience, you won’t actually be executing any of these activities. So, there’s no need for a full implementation.
Before you go any further, build the solution to make sure that everything builds correctly. This also adds the new custom activity to the Visual Studio Toolbox.
As a point of reference, you can remind yourself what the default designer looks like before you implement a custom one. Open the Activity1.xaml
file in the ActivityLibrary
project, and add the CalcShipping
activity to the root Sequence
activity. Figure 15-1 shows the default designer for this activity.
To declare a custom designer, add a new Activity Designer item to the ActivityDesignLibrary
project. Name the new designer CalcShippingDesigner
. Custom designers have Xaml extensions since they are WPF markup.
Here is the default markup when you first create a new designer:
<sap:ActivityDesigner x:Class="ActivityDesignerLibrary.CalcShippingDesigner"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sap="clr-namespace:System.Activities.Presentation;
assembly=System.Activities.Presentation"
xmlns:sapv="clr-namespace:System.Activities.Presentation.View;
assembly=System.Activities.Presentation">
<Grid>
</Grid>
</sap:ActivityDesigner>
As you can see, the root element is ActivityDesigner
. The x:Class
attribute identifies the namespace and class name that will be assigned to the resulting type.
Note Each namespace in the designer markup must be entered on a single line. Because the length of many of these namespaces exceeds the maximum width allowed for this book, I’ve arbitrarily split the namespaces into multiple lines. When you enter them, make sure that the entire namespace is entered on a single line. This applies to all of the designer markup shown in this chapter.
For this example, the plan is to provide access to the four arguments of the activity directly on the design surface. Note, the fourth argument is the standard-named Result
argument.
The contents of this file should be replaced with the markup shown here. The first change that you might notice is the addition of two new namespaces. The xmlns:s
attribute references mscorlib
since you will need to reference types from the System
namespace. The xmlns:sapc
attribute references the System.Activities.Presentation.Converters
namespace. That namespace provides access to the ArgumentToExpressionConverter
.
Also please note that I am setting the Collapsible
property to False
. If this is set to True
, the expand/collapse indicator is shown in the upper-right corner of the activity. Since this first version of the designer does not support expand/collapse functionality, I wanted to avoid any confusion by hiding this indicator.
<sap:ActivityDesigner x:Class="ActivityDesignerLibrary.CalcShippingDesigner"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sap="clr-namespace:System.Activities.Presentation;
assembly=System.Activities.Presentation"
xmlns:sapv="clr-namespace:System.Activities.Presentation.View;
assembly=System.Activities.Presentation"
xmlns:sapc="clr-namespace:System.Activities.Presentation.Converters;
assembly=System.Activities.Presentation"
Collapsible="False" >
The ArgumentToExpressionConverter
is now added as a static resource. This is necessary in order to reference this converter later in the markup.
<sap:ActivityDesigner.Resources>
<ResourceDictionary>
<sapc:ArgumentToExpressionConverter
x:Key="ArgumentToExpressionConverter" />
</ResourceDictionary>
</sap:ActivityDesigner.Resources>
Next, a grid is declared with two columns and four rows. The first column is for the label that describes each property. The second column will be used for the ExpressionTextBox
for each property. And since there are four properties, there are four rows.
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
The markup is organized by row, with each row supporting a different property. The TextBlock
is used to display text that identifies the property by name. Following it, an ExpressionTextBox
is declared. The OwnerActivity
is bound to the ModelItem
property of the ActivityDesigner
. As I mentioned at the beginning of the chapter, the ModelItem
property provides a reference to the activity that is currently being edited.
The Expression
property is bound to the ModelItem.Weight
property. In this case, Weight
is the name of the property in the custom activity. The binding Mode
is set to TwoWay
. This allows the ExpressionTextBox
to be populated with an existing value as well as to enter a new value. The ArgumentToExpressionConverter
is then specified as the converter to use for the expression. The ConverterParameter
is set to In
. This indicates that this is a value expression. Finally, the ExpressionType
is set to the Int32
, which is the CLR type for this property.
<TextBlock Text="Weight" Grid.Row ="0" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="Total weight"
Grid.Row ="0" Grid.Column="1" MaxWidth="150" MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
Expression="{Binding Path=ModelItem.Weight, Mode=TwoWay,
Converter={StaticResource ArgumentToExpressionConverter},
ConverterParameter=In }"
ExpressionType="s:Int32" />
The next two properties are handled in a similar way. All these properties are value properties.
<TextBlock Text="OrderTotal" Grid.Row ="1" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="Order Total"
Grid.Row ="1" Grid.Column="1" MaxWidth="150" MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
Expression="{Binding Path=ModelItem.OrderTotal, Mode=TwoWay,
Converter={StaticResource ArgumentToExpressionConverter},
ConverterParameter=In }"
ExpressionType="s:Decimal" />
<TextBlock Text="ShipVia" Grid.Row ="2" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="Shipping Method"
Grid.Row ="2" Grid.Column="1" MaxWidth="150" MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
Expression="{Binding Path=ModelItem.ShipVia, Mode=TwoWay,
Converter={StaticResource ArgumentToExpressionConverter},
ConverterParameter=In }"
ExpressionType="s:String" />
The final property is the Result
property. This is an L-value expression since it represents a storage location in memory. The markup is similar to the previous properties, but it does have some subtle differences. The ConverterParameter
for this property is set to Out
. This is the direction that must be used for an L-value expression. Also, the UseLocationExpression
property is now included and is set to True
.
<TextBlock Text="Result" Grid.Row ="3" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="Result"
Grid.Row ="3" Grid.Column="1" MaxWidth="150" MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
Expression="{Binding Path=ModelItem.Result, Mode=TwoWay,
Converter={StaticResource ArgumentToExpressionConverter},
ConverterParameter=Out }"
UseLocationExpression="True"
ExpressionType="s:Decimal" />
</Grid>
</sap:ActivityDesigner>
Tip In this chapter, I list the markup for each designer since that is the most concise and accurate way to represent each designer. Although I actually prefer to work with the Xaml directly, you may prefer to set some of these properties using the Properties window of the WPF designer.
There are two ways to associate an activity with a designer. The easiest (and most direct) way to accomplish this is to add the System.ComponentModel.Designer
attribute to the activity. Here is the revised CalcShipping
activity class definition with the addition of the Designer
attribute:
using System;
using System.Activities;
using System.ComponentModel;
namespace ActivityLibrary
{
[Designer(typeof(ActivityDesignerLibrary.CalcShippingDesigner))]
public sealed class CalcShipping : CodeActivity<Decimal>
{
…
}
}
After rebuilding the solution, you should be able to test the new designer. Open the Activity1.xaml
file, and add an instance of the CalcShipping
activity to the Sequence
activity. If you saved the file after adding the previous CalcShipping
instance, there’s no need to add a new one. You can see my results in Figure 15-2. To test the ability to enter expressions, I’ve added a few workflow arguments of the correct types and used them in expressions. Feel free to test each property to see whether it correctly enters and saves each expression.
The second way to associate an activity to a designer is to use the MetadataStore
class. This has the advantage of decoupling the activity from the designer, allowing you to specify a different designer depending on the circumstances. This capability is more important if you are self-hosting the workflow designer. However, the disadvantage to this approach is that there are a number of steps involved, and they must all be followed correctly in order to achieve the correct results.
To demonstrate this approach, you will associate the same CalcShipping
activity to the custom designer that you developed.
Warning To complicate this process even more, there are subtle differences depending on whether you are using the activity within the Visual Studio environment or you are self-hosting the designer in your own application. The instructions given here assume that you are using Visual Studio. You can find information on self-hosting the workflow designer in Chapter 17. The instructions given in this section are important only if you want the designer to be self-discovered by Visual Studio.
Visual Studio uses a special naming convention for assemblies that contain activity designers. The assembly containing the designers must be in the form of [ActivityProjectName].VisualStudio.Design
or [ActivityProjectName].Design
. Replace [ActivityProjectName]
with the name of the assembly containing the activities. If you don’t place the activity designers in this specially named assembly, Visual Studio won’t find them. Visual Studio first searches for the [ActivityProjectName].Design
assembly and loads it if it is found. It then looks for the version with VisualStudio
in its name. An assembly containing VisualStudio
in its name overrides any metadata settings that might have already been loaded.
To illustrate this, create a new Activity Design Library project named ActivityLibrary.VisualStudio.Design
. Add this project to the solution for this chapter. Add a project reference to the ActivityLibrary
project.
Make a copy of the CalcShippingDesigner.xaml
file that is currently located in the ActivityDesignerLibrary
project. You also need to copy the CalcShippingDesigner.Xaml.cs
file. Add the copied files to the new ActivityLibrary.VisualStudio.Design
project. Make sure you open the Xaml and .cs
files for the designer and change the namespace to match the new project name.
Note If you are self-hosting the workflow designer, you need to omit the VisualStudio
node of the project name. For example, you would name the project containing the designer (and the metadata that is discussed in the next step) ActivityLibrary.Design
. This means that you may need to package your custom designers two different ways if they are used in both environments.
Once Visual Studio loads the correct assembly containing the designer, it looks for a class that implements the IRegisterMetadata
interface (found in the System.Activities.Presentation.Metadata
namespace). This interface defines a Register
method that is executed to add any necessary metadata entries.
To satisfy this requirement, add a new C# class named Metadata
to the ActivityLibrary.VisualStudio.Design
project. Here is the code that you need for this class:
using System;
using System.Activities.Presentation.Metadata;
using System.ComponentModel;
namespace ActivityLibrary.VisualStudio.Design
{
public class Metadata : IRegisterMetadata
{
public void Register()
{
AttributeTableBuilder atb =
new AttributeTableBuilder();
atb.AddCustomAttributes(typeof(CalcShipping),
new DesignerAttribute(typeof(CalcShippingDesigner)));
MetadataStore.AddAttributeTable(atb.CreateTable());
}
}
}
The code first creates an instance of the AttributeTableBuilder
class. Added to this class is a new Designer
attribute indicating the designer to use for the CalcShipping
activity. The CreateTable
method of the AttributeTableBuilder
is then executed, and the result is passed to the static AddAttributeTable
method of the MetadataStore
class.
To perform a meaningful test, you need to comment out the Designer
attribute from the CalcShipping
class (located in the ActivityLibrary
project). Otherwise, you are still relying on this attribute to designate the designer that will be used within Visual Studio. Here is the line that you should temporary comment out:
[Designer(typeof(ActivityDesignerLibrary.CalcShippingDesigner))]
You should rebuild the solution before proceeding to the next step.
To test the designer, create a new Workflow Console project named TestMetadata
. Add a reference to the ActivityLibrary
project.
For the custom designer to be located, the Activity.VisualStudio.Design.dll
must be in the same folder as the ActivityLibrary.dll
. Assuming that you are building for debug, copy this file from the indebug
folder of the project to the indebug
folder of the ActivityLibrary
project.
Open the Activity1.xaml
file in the new TestMetadata
project, and drop a CalcShipping
instance on the design canvas. If all goes well, you should see the custom designer. If not, the most likely cause is that the Activity.VisualStudio.Design.dll
file is not located in the correct folder.
Warning: Consistency Is Important
Since you are able to determine the placement of controls within the designer, you might expect to also be able to change the icon that is displayed on the designer surface. To accomplish this, you need to follow these steps:
- Add the image to the designer project.
- Set the Build Action option for the image to Resource.
- Add entries to the designer Xaml to reference the image.
To demonstrate this, you will now modify the CalcShippingDesigner
that is in the ActivityDesignerLibrary
project.
Note Throughout the remainder of this chapter, you will be working with designers in the ActivityDesignerLibrary
project once again. The ActivityLibrary.VisualStudio.Design
project was used only to demonstrate the use of the MetadataStore
and won’t be used again. If you commented out the Designer
attributed for the CalcShipping
activity, please uncomment it at this point.
Instead of designing a custom image (and demonstrating my total lack of artistic ability), I’ve chosen one of the sample images that ships with Visual Studio. Please follow these steps to add the CalculatorHS.png
image to the designer project:
- Locate the
VS2010ImageLibrary.zip
file that is deployed with Visual Studio. The file should be located in theProgram FilesMicrosoft Visual Studio 10.0Common7VS2010ImageLibrary1033
folder. The last node of the path (1033) may be different on your machine if you are using a non-English version of Windows.- Unzip the
VS2010ImageLibrary.zip
file into a temporary directory.- Locate the
CalculatorHS.png
file, which should be located in theVS2010ImageLibraryActionspng_formatOffice and VS
relative folder after unzipping the image library.- Copy the
CalculatorHS.png
file to theActivityDesignerLibrary
project folder.- Add the
CalculatorHS.png
file to the project, and change the Build Action option for the file to Resource.
Open the CalcShippingDesigner
(in the ActivityDesignerLibrary
project), and switch to the Xaml view. Add these entries to the markup to assign the new image to the Icon
property of the ActivityDesigner
. The new entries should be located as shown in this example markup:
<sap:ActivityDesigner>
<sap:ActivityDesigner.Resources>
…
</sap:ActivityDesigner.Resources>
<sap:ActivityDesigner.Icon>
<DrawingBrush>
<DrawingBrush.Drawing>
<ImageDrawing>
<ImageDrawing.Rect>
<Rect Location="0,0" Size="16,16" />
</ImageDrawing.Rect>
<ImageDrawing.ImageSource>
<BitmapImage UriSource="CalculatorHS.png" />
</ImageDrawing.ImageSource>
</ImageDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</sap:ActivityDesigner.Icon>
<Grid>
…
</Grid>
</sap:ActivityDesigner>
After rebuilding the solution, you should be able to test the revised designer. Open the Activity1.xaml
file in the ActivityLibrary
project, and add an instance of the CalcShipping
activity (or simply view the instance that is already there). Figure 15-3 shows my results after adding the icon.
Many of the designers for the standard activities support two viewing modes: expanded and collapsed. The expanded mode allows you to work with all the visual elements that you have added to the designer. For example, the CalcShippingDesigner
that you developed in the previous sections would look the same in the expanded view. You would be able to view and edit all of the properties that you have added to the designer. In collapsed mode, the activity shrinks to a much smaller single bar, hiding the properties.
In this example, you will create a new designer for the CalcShipping
activity that supports the expanded and collapsed modes.
A good portion of this designer will contain the same markup as the original CalcShippingDesigner
that you implemented in the previous sections. However, much of the markup has been moved around to accommodate the two viewing modes.
The easiest way to create this designer is to make a copy of the CalcShippingDesigner.xaml
and CalcShippingDesigner.xaml.cs
files (in the ActivityDesignerLibrary
project). Name the copied files CalcShippingCollapsibleDesigner, and add them it to the same project. Make sure that you rename the class name within the Xaml file and the CalcShippingCollapsibleDesigner.cs
file that is associated with it.
Here is the complete markup for this new designer:
<sap:ActivityDesigner x:Class="ActivityDesignerLibrary.CalcShippingCollapsibleDesigner"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sap="clr-namespace:System.Activities.Presentation;
assembly=System.Activities.Presentation"
xmlns:sapv="clr-namespace:System.Activities.Presentation.View;
assembly=System.Activities.Presentation"
xmlns:sapc="clr-namespace:System.Activities.Presentation.Converters;
assembly=System.Activities.Presentation"
This version of the designer sets the Collapsible
property to True
. This allows the collapse/expand icon to be shown in the upper-right corner of the designer.
Collapsible="True" >
In addition to the ArgumentToExpressionConverter
, the designer now requires several other resources. Two named data templates are defined. One is used to define the look of the designer when it is collapsed (named ShowAsCollapsed
), and the other defines the look when the designer is expanded (ShowAsExpanded
). Most of the markup from the previous version of the designer has been moved into the ShowAsExpanded
data template.
<sap:ActivityDesigner.Resources>
<sapc:ArgumentToExpressionConverter
x:Key="ArgumentToExpressionConverter" />
<DataTemplate x:Key="ShowAsCollapsed">
<TextBlock>Expand to edit properties</TextBlock>
</DataTemplate>
<DataTemplate x:Key="ShowAsExpanded">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Weight" Grid.Row ="0" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="Total weight"
Grid.Row ="0" Grid.Column="1" MaxWidth="150"
MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
Expression="{Binding Path=ModelItem.Weight, Mode=TwoWay,
Converter={StaticResource ArgumentToExpressionConverter},
ConverterParameter=In }"
ExpressionType="s:Int32" />
<TextBlock Text="OrderTotal" Grid.Row ="1" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="Order Total"
Grid.Row ="1" Grid.Column="1" MaxWidth="150"
MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
Expression="{Binding Path=ModelItem.OrderTotal, Mode=TwoWay,
Converter={StaticResource ArgumentToExpressionConverter},
ConverterParameter=In }"
ExpressionType="s:Decimal" />
<TextBlock Text="ShipVia" Grid.Row ="2" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="Shipping Method"
Grid.Row ="2" Grid.Column="1" MaxWidth="150"
MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
Expression="{Binding Path=ModelItem.ShipVia, Mode=TwoWay,
Converter={StaticResource ArgumentToExpressionConverter},
ConverterParameter=In }"
ExpressionType="s:String" />
<TextBlock Text="Result" Grid.Row ="3" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="Result"
Grid.Row ="3" Grid.Column="1" MaxWidth="150"
MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
Expression="{Binding Path=ModelItem.Result, Mode=TwoWay,
Converter={StaticResource ArgumentToExpressionConverter},
ConverterParameter=Out }"
UseLocationExpression="True"
ExpressionType="s:Decimal" />
</Grid>
</DataTemplate>
The designer uses a style (another resource) to determine which data template to show at any point in time. The style has a trigger that is bound to the ShowExpanded
property of the designer. When this property is False
, the ShowAsCollapsed
data template is shown. When it is True
, the ShowAsExpanded
data template is used.
<Style x:Key="StyleWithCollapse" TargetType="{x:Type ContentPresenter}">
<Setter Property="ContentTemplate"
Value="{DynamicResource ShowAsExpanded}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=ShowExpanded}" Value="False">
<Setter Property="ContentTemplate"
Value="{DynamicResource ShowAsCollapsed }"/>
</DataTrigger>
</Style.Triggers>
</Style>
</sap:ActivityDesigner.Resources>
<sap:ActivityDesigner.Icon>
<DrawingBrush>
<DrawingBrush.Drawing>
<ImageDrawing>
<ImageDrawing.Rect>
<Rect Location="0,0" Size="16,16" />
</ImageDrawing.Rect>
<ImageDrawing.ImageSource>
<BitmapImage UriSource="CalculatorHS.png" />
</ImageDrawing.ImageSource>
</ImageDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</sap:ActivityDesigner.Icon>
The remainder of the designer simply uses a WPF ContentPresenter
control that is associated with the style that was defined earlier. The style and the data templates do all of the heavy lifting to display the designer in the correct viewing mode.
<Grid>
<ContentPresenter Style="{DynamicResource StyleWithCollapse}"
Content="{Binding}" />
</Grid>
</sap:ActivityDesigner>
You need to change the Designer
attribute that is attached to the CalcShipping
activity to use the new designer. Here is the revised attribute:
[Designer(typeof(ActivityDesignerLibrary.CalcShippingCollapsibleDesigner))]
public sealed class CalcShipping : CodeActivity<Decimal>
You should now be able to rebuild the solution and test the revised designer. Open the Activity1.xaml
file in the designer, and add or view the CalcShipping
activity. Figure 15-4 shows the expanded designer, and Figure 15-5 shows the same designer in collapsed mode.
In this example, you will develop a designer for a custom activity that supports a single child activity. To accomplish this, the WorkflowItemPresenter
control will be used.
An example of a standard activity that supports a single child is the While
activity. So in honor of the While
activity, you will implement an activity with a similar look. As is the case with all the activities in this chapter, you won’t actually implement the logic needed to replicate the While
activity.
Add a new Code Activity to the ActivityLibrary
project, and name it MyWhile
. Here is the code for this activity:
using System;
using System.Activities;
using System.ComponentModel;
namespace ActivityLibrary
{
[Designer(typeof(ActivityDesignerLibrary.MyWhileDesigner))]
public sealed class MyWhile : NativeActivity
{
[Browsable(false)]
public Activity Body { get; set; }
public Activity<Boolean> Condition { get; set; }
protected override void Execute(NativeActivityContext context)
{
throw new NotImplementedException();
}
}
}
The activity has two properties. The Body
property is the child activity that will be maintained using the WorkflowItemPresenter
control. The Condition
property represents a Boolean condition that must be true in order to schedule execution of the Body
activity. Also note that the Body
property is decorated with the Browsable
attribute with a value of False
. This removes the property from the Properties window in Visual Studio.
Note To save a bit of time, I’ve already included the Designer
attribute that assigns the custom designer. Keep in mind that this code won’t actually build until you’ve implemented the designer in the next step.
Add a new Activity Designer to the ActivityDesignerLibrary
project. Name the new designer MyWhileDesigner
. Here is the markup for this new designer:
<sap:ActivityDesigner x:Class="ActivityDesignerLibrary.MyWhileDesigner"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sap="clr-namespace:System.Activities.Presentation;
assembly=System.Activities.Presentation"
xmlns:sapv="clr-namespace:System.Activities.Presentation.View;
assembly=System.Activities.Presentation"
xmlns:sapc="clr-namespace:System.Activities.Presentation.Converters;
assembly=System.Activities.Presentation"
Collapsible="False" >
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Condition" Grid.Row ="0" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="Enter a condition"
Grid.Row ="0" Grid.Column="1" MaxWidth="150" MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
ExpressionType="{x:Type TypeName=s:Boolean}"
Expression="{Binding Path=ModelItem.Condition, Mode=TwoWay}" />
The only new element to this designer is the inclusion of the WorkflowItemPresenter
. The Item
property of the WorkflowItemPresenter
is bound to the Body
property of the custom activity. As you can see, the binding for this control is actually much simpler than the ExpressionTextBox
. Although this is not a requirement, I decided to wrap this control in a thin WPF Border
. This draws attention to the drop area for the child activity.
<Border Grid.Row ="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="5"
MinHeight="40" BorderBrush="LightGray" BorderThickness="1" >
<sap:WorkflowItemPresenter HintText="Drop an activity here"
Item="{Binding Path=ModelItem.Body, Mode=TwoWay}" />
</Border>
</Grid>
</sap:ActivityDesigner>
Note This example also introduces a slight variation on the ExpressionTextBox
. You may notice that the ExpressionTextBox
that is bound to the Condition
property does not specify the ArgumentToExpressionConverter
. The converter is omitted because the control is bound to a property that is defined as Activity<Boolean>
, not to an InArgument<T>
or OutArgument<T>
. This works because the output from the ExpressionTextBox
is always a VisualBasicValue<T>
or VisualBasicReference<T>
, both of which are actually activities.
You should now be able to successfully rebuild the solution and try this new activity. Open the Activity1.xaml
file, and add an instance of the MyWhile
activity to the root Sequence
activity. Figure 15-6 shows the activity after I added a child Sequence
activity (with its own children) to the activity. I’ve also exercised the Condition
property using a variable that I added to the workflow.
The custom activity and designer you will develop in this example are similar to the previous example. However, in this example the custom activity supports a collection of child activities, similar in concept to the Sequence
activity. The designer uses the WorkflowItemsPresenter
control to permit the declaration of the child activities.
To implement the custom activity for this example, add a new Code Activity to the ActivityLibrary
project, and name it MySequence
. Here is the code for this activity:
using System;
using System.Activities;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Markup;
namespace ActivityLibrary
{
[Designer(typeof(ActivityDesignerLibrary.MySequenceDesigner))]
[ContentProperty("Activities")]
public class MySequence : NativeActivity
{
[Browsable(false)]
public Collection<Activity> Activities { get; set; }
public Activity<Boolean> Condition { get; set; }
[Browsable(false)]
public Collection<Variable> Variables { get; set; }
public MySequence()
{
Activities = new Collection<Activity>();
Variables = new Collection<Variable>();
}
protected override void Execute(NativeActivityContext context)
{
throw new NotImplementedException();
}
}
}
The properties of this activity are fairly straightforward and probably what you might expect to see. The Activities
property is a collection of Activity
objects and is the property used to add the child activities. As you saw in the previous example, this activity also includes a Condition
property. It also has a Variables
property that is typed as Collection<Variable>
. If you include a property with this exact name (Variables
) with this exact type, it is automatically supported by the variable editor of the designer.
As I did with the previous example, I’ve already included the Designer
attribute that references the custom designer. So, this code won’t build until you implement the designer.
Add a new Activity Designer to the ActivityDesignerLibrary
project, and name it MySequenceDesigner
. Since this activity might have a large number of children, the designer supports the expanded and collapsed viewing modes. Here is the markup for this designer:
<sap:ActivityDesigner x:Class="ActivityDesignerLibrary.MySequenceDesigner"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sap="clr-namespace:System.Activities.Presentation;
assembly=System.Activities.Presentation"
xmlns:sapv="clr-namespace:System.Activities.Presentation.View;
assembly=System.Activities.Presentation"
xmlns:sapc="clr-namespace:System.Activities.Presentation.Converters;
assembly=System.Activities.Presentation"
Collapsible="True" >
Just for fun, I decided to display the count of child activities when the designer is in collapsed mode. This is accomplished by binding the Text
property of the TextBlock
control to the Activities.Count
property of the activity.
<sap:ActivityDesigner.Resources>
<DataTemplate x:Key="ShowAsCollapsed">
<TextBlock Foreground="Gray">
<TextBlock.Text>
<MultiBinding StringFormat="Expand for {0} Activities">
<Binding Path="ModelItem.Activities.Count" />
</MultiBinding>
</TextBlock.Text>
</TextBlock>
</DataTemplate>
<DataTemplate x:Key="ShowAsExpanded">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Condition" Grid.Row ="0" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="Enter a condition"
Grid.Row ="0" Grid.Column="1"
MaxWidth="150" MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
ExpressionType="{x:Type TypeName=s:Boolean}"
Expression="{Binding Path=ModelItem.Condition, Mode=TwoWay}" />
As you might expect, the WorkflowItemsPresenter
control is bound to the Activities
property of the custom activity. However, unlike the WorkflowItemPresenter
, this control provides you with the ability to fine-tune additional aspects of its presentation. In this example, the markup displays a LightGray
rectangle as the spacer, which is shown between each child activity. The ItemsPanel
property defines the layout and orientation of the child activities. In this case, I went with the traditional top-down vertical orientation.
<sap:WorkflowItemsPresenter HintText="Drop activities here"
Grid.Row ="1" Grid.Column="0" Grid.ColumnSpan="2"
Margin="5" MinHeight="100"
Items="{Binding Path=ModelItem.Activities}">
<sap:WorkflowItemsPresenter.SpacerTemplate>
<DataTemplate>
<Rectangle Width="140" Height="3"
Fill="LightGray" Margin="7" />
</DataTemplate>
</sap:WorkflowItemsPresenter.SpacerTemplate>
<sap:WorkflowItemsPresenter.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</sap:WorkflowItemsPresenter.ItemsPanel>
</sap:WorkflowItemsPresenter>
</Grid>
</DataTemplate>
<Style x:Key="StyleWithCollapse" TargetType="{x:Type ContentPresenter}">
<Setter Property="ContentTemplate"
Value="{DynamicResource ShowAsExpanded}"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=ShowExpanded}" Value="False">
<Setter Property="ContentTemplate"
Value="{DynamicResource ShowAsCollapsed}"/>
</DataTrigger>
</Style.Triggers>
</Style>
</sap:ActivityDesigner.Resources>
<Grid>
<ContentPresenter Style="{DynamicResource StyleWithCollapse}"
Content="{Binding}"/>
</Grid>
</sap:ActivityDesigner>
After rebuilding the solution, you can open the Activity1.xaml
file and add an instance of the MySequence
activity. Once added, you should be able to add multiple child activities and set the Condition
property to a valid Boolean condition. Figure 15-7 shows the expanded view of the designer, and Figure 15-8 shows the designer collapsed. Note that the collapsed version correctly shows the count of child activities.
Although not shown in these figures, this activity does support the entry of variables. You should be able to open the variable editor and add one or more variables that are scoped by this activity.
The ActivityAction
activity is used for callback-like processing from an activity. It defines a planned hole that must be filled with an activity to execute. The ActivityAction
class is really a family of related generic classes designed to support a varying number of input arguments. For example, ActivityAction<T>
supports a single input argument, while ActivityAction<T1,T2>
supports two input arguments.
The ActivityAction
class presents an additional activity designer challenge since any arguments must be presented as arguments within the designer. To illustrate this, take a look at the standard ForEach<T>
activity. This activity iterates over the items in a collection, executing a single child activity for each item. To allow the child activity to access each item in the collection, an ActivityAction<T>
is used. The generic type assigned to this class defines the type of each item in the collection. But the real purpose of using the ActivityAction<T>
class is to provide an argument that represents the current item in the collection. This argument, with a default name of item
, is made available to the child activity so that it can reference each item.
Note You used an ActivityAction
for communication between the workflow and the host application in Chapter 8. You might want to refer to that chapter for more information on the ActivityAction
class. For more information on the ForEach<T>
activity, please refer to Chapter 6.
To demonstrate the additional designer requirements when an ActivityAction
is in the picture, you will implement an activity and designer that use an ActivityAction
.
Add a new Code Activity to the ActivityLibrary
project, and name it MyActivityWithAction
. Here is the code for this custom activity:
using System;
using System.Activities;
using System.Collections.Generic;
using System.ComponentModel;
namespace ActivityLibrary
{
[Designer(typeof(ActivityDesignerLibrary.MyActivityWithActionDesigner))]
public class MyActivityWithAction : NativeActivity
{
This activity defines an ActivityAction<String>
property named Notify
. In addition, it defines an input argument named Strings
, which, as you might guess, contains a collection of strings. The assumption is that this activity will iterate over the collection of strings and invoke the ActivityAction
for each string. This allows any activity that is assigned as the handler for the ActivityAction
to do something with each string.
[Browsable(false)]
public ActivityAction<String> Notify { get; set; }
[RequiredArgument]
public InArgument<List<String>> Strings { get; set; }
One crucial step to make this work is to initialize the ActivityAction
as shown in the constructor of this activity. This code creates a named DelegateInArgument
and assigns it to the Argument
property of the ActivityAction
. The Name
property that you assign here (message
in this example) is the name of the argument that is made available to the activity that is assigned to this ActivityAction
.
public MyActivityWithAction()
{
Notify = new ActivityAction<String>
{
Argument = new DelegateInArgument<String>
{
Name = "message"
}
};
}
In this example, the code overrides the CacheMetadata
method to manually define the properties of this activity. Notice that AddDelegate
is used for the Notify
property since it is an ActivityAction
.
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
metadata.AddDelegate(Notify);
metadata.AddArgument(new RuntimeArgument(
"Strings", typeof(List<String>), ArgumentDirection.In));
}
protected override void Execute(NativeActivityContext context)
{
throw new NotImplementedException();
}
}
}
Add a new Activity Designer to the ActivityDesignerLibrary
project, and name it MyActivityWithActionDesigner
. Here is the complete markup for this designer:
<sap:ActivityDesigner x:Class="ActivityDesignerLibrary.MyActivityWithActionDesigner"
xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sap="clr-namespace:System.Activities.Presentation;
assembly=System.Activities.Presentation"
xmlns:sapv="clr-namespace:System.Activities.Presentation.View;
assembly=System.Activities.Presentation"
xmlns:sapc="clr-namespace:System.Activities.Presentation.Converters;
assembly=System.Activities.Presentation"
Collapsible="False" >
<sap:ActivityDesigner.Resources>
<ResourceDictionary>
<sapc:ArgumentToExpressionConverter
x:Key="ArgumentToExpressionConverter" />
</ResourceDictionary>
</sap:ActivityDesigner.Resources>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<TextBlock Text="Strings" Grid.Row ="0" Grid.Column="0"
HorizontalAlignment="Left" VerticalAlignment="Center" />
<sapv:ExpressionTextBox HintText="List of Strings"
Grid.Row ="0" Grid.Column="1" MaxWidth="150" MinWidth="150" Margin="5"
OwnerActivity="{Binding Path=ModelItem}"
Expression="{Binding Path=ModelItem.Strings, Mode=TwoWay,
Converter={StaticResource ArgumentToExpressionConverter},
ConverterParameter=In }" />
A WorkflowItemPresenter
is used to allow the addition of an activity that will be executed by the ActivityAction
. Notice that this control is bound to the Notify.Handler
property, not to the Notify
property directly. The Handler
property of an ActivityAction
represents the real target activity to be executed.
<Border Grid.Row ="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="5"
MinHeight="40" BorderBrush="LightGray" BorderThickness="1" >
<sap:WorkflowItemPresenter HintText="Drop an activity action handler here"
Item="{Binding Path=ModelItem.Notify.Handler, Mode=TwoWay}" />
</Border>
</Grid>
</sap:ActivityDesigner>
To test the new custom activity and designer, rebuild the solution, and open the Activity1.xaml
file in the designer. Add an instance of the MyActivityWithAction
activity. Now add a Sequence
activity as the child of the MyActivityWithAction
activity, and then a WriteLine
as the child of the Sequence
. Start typing the argument name message
in the Text
property of the WriteLine
activity. As you type, you should see the message
argument in the IntelliSense list. This proves that this argument, which was created within the activity, is now available for your consumption within the designer. Figure 15-9 shows my test of this activity and designer.
Note Implementing a custom activity that uses an ActivityAction
is one of the scenarios discussed in Chapter 16.
In addition to developing a custom designer, you can also provide validation for your custom activities. The most common type of validation checks for missing arguments, but you can also implement additional kinds of validation. For example, if your activity supports child activities, you can add validation logic that limits the types of activities that are allowed as children. Or you can limit the type of parent activity that uses your activity as a child.
Regardless of the kind of validation that you implement, the goal of validation is to assist the developer at design time by identifying error and warning conditions. A custom activity that provides this type of validation enhances the design experience by providing cues to the proper use of the activity. The developer doesn’t have to wait until the workflow is executed to find out that a required argument was not provided. They are notified of problems at design time via visual designer cues.
If validation logic is directly associated with an activity, it is executed automatically each time an activity is used within the workflow designer. In addition, you can manually validate an activity using the ActivityValidationServices
class. The static Validate
method of this class allows you to execute the validation logic from within your own application, completely outside the designer environment. You might want to execution the validation logic if your application provides the end users with an opportunity to customize activities and workflows. If the activity definitions are outside of your application’s direct control, it’s a good idea to validate them before you attempt to execute them.
WF provides three mechanisms that you can use for activity validation:
- Validation attributes
- Validation code within the activity itself
- Constraints
Each of these validation mechanisms is briefly discussed in the sections that follow.
Using validation attributes is the easiest way to introduce basic validation to a custom activity. WF includes these two attributes (both found in the System.Activities
namespace):
RequiredArgument
OverloadGroup
Both of these attributes are designed to be applied to public properties of your activity. You have already seen the RequiredArgument
attributed used with some of the custom activities in previous chapters. Applying this attribute to a property indicates that it is required. When this attribute is applied, the developer must provide a value for the property; otherwise, the activity is flagged as failing validation.
A property that has the RequiredArgument
attribute applied is always required. In contrast with this, the OverloadGroup
attribute allows you to define multiple named groups of properties that must be supplied. The properties within a named group must be supplied, but other properties may be optional.
The OverloadGroup
attribute is best understand in the context of an example, so I’ll defer further discussion of it until it is used in an example later in this chapter.
You also have the option of implementing validation code within the activity itself. Any validation code is placed in an override of the CacheMetadata
virtual method. The purpose of this method is to create a complete description of the activity prior to its execution. The description includes any arguments, variables, delegates, and children that are referenced during execution.
WF also provides a mechanism to provide validation logic that resides outside the activity. This mechanism is called a constraint and is implemented by the Constraint<T>
class. This class is derived from the abstract Constraint
class, which ultimately derives from the base Activity
class. So, constraints are actually a specialized type of activity, and you compose them in code as you would other activities.
Note The Constraint<T>
class is sealed, so you can’t derive your own constraint class from it. Instead, all constraints are composed by assigning a tree of activities to their Body
property.
Once it is implemented, a constraint can be applied to an activity in two ways. First, you can add a constraint directly to an activity using its Constraints
property. This can be done in the constructor of the activity. Second, you can add constraints if you are using the ActivityValidationServices
class to manually validate an activity. An overload of the Validate
method of this class allows you to pass an instance of the ValidationSettings
class. This class supports an AdditionalConstraints
property that can be used to add constraints prior to validation.
In this section of the chapter, you will use the RequiredArgument
and OverloadGroup
attributes to add basic validation to an activity.
To see the RequiredArgument
attribute in action, you can revise the CalcShipping
activity that you used in the earlier examples in this chapter. This activity is located in the ActivityLibrary
project. Here is the revised code that adds this attribute to the input arguments:
using System;
using System.Activities;
using System.ComponentModel;
namespace ActivityLibrary
{
[Designer(typeof(ActivityDesignerLibrary.CalcShippingCollapsibleDesigner))]
public sealed class CalcShipping : CodeActivity<Decimal>
{
[RequiredArgument]
public InArgument<Int32> Weight { get; set; }
[RequiredArgument]
public InArgument<Decimal> OrderTotal { get; set; }
[RequiredArgument]
public InArgument<String> ShipVia { get; set; }
protected override Decimal Execute(CodeActivityContext context)
{
throw new NotImplementedException();
}
}
}
After rebuilding the solution, you should be able to open the Activity1.xaml
file and add a new instance of the CalcShipping
activity to the root Sequence
activity. As the activity is added, you should almost immediately see the errors shown in Figure 15-10.
Providing values for each property should eliminate the errors
To see the OverloadGroup
activity in action, you need to first implement a new example activity. To demonstrate this attribute, you need an activity that has multiple sets of mutually exclusive properties. Add a new Code Activity named TitleLookup
to the ActivityLibrary
project. Here is the code for this activity:
using System;
using System.Activities;
namespace ActivityLibrary
{
public sealed class TitleLookup : CodeActivity
{
[RequiredArgument]
[OverloadGroup("ByKeyword")]
[OverloadGroup("ByTitle")]
public InArgument<String> Category { get; set; }
[RequiredArgument]
[OverloadGroup("ByKeyword")]
public InArgument<String> Keyword { get; set; }
[RequiredArgument]
[OverloadGroup("ByTitle")]
public InArgument<String> Author { get; set; }
[RequiredArgument]
[OverloadGroup("ByTitle")]
public InArgument<String> Title { get; set; }
[RequiredArgument]
[OverloadGroup("ByISBN")]
public InArgument<String> ISBN { get; set; }
protected override void Execute(CodeActivityContext context)
{
throw new NotImplementedException();
}
}
}
The activity uses the OverloadGroup
attribute to organize the input arguments into three separate groups. If this contrived activity is used to find a book, it is completely reasonable that you might support several ways to perform the lookup. In this example, the OverloadGroup
attribute is used with three different named groups:
ByKeyword
ByTitle
ByISBN
The ByKeyword
group is assigned to the Category
and Keyword
properties. The RequiredArgument
attribute has also been applied to these properties. This combination of attributes means that one of the groups is required and that you cannot provide a value for properties in another group. Likewise, the ByTitle
group is assigned to the Category
, Author
, and Title
properties. Notice that the Category
property participates in both groups. Finally, the ByISBN
group is applied only to the ISBN
property. At design time, these groups are used to ensure that a valid combination of properties have values, as well as to prohibit values for properties that should not have them.
After rebuilding the solution, open the Activity1.xaml
file, and add an instance of this new activity to the root Sequence
activity. To test the validation, you can enter values for the Author
, Category
, and Keyword
properties, as shown in Figure 15-11.
When you do this, you should see the error shown in Figure 15-12.
By entering values for the Category
and Keyword
properties, you satisfy the requirements of the ByKeyword
OverloadGroup
. But by also including a value for the Author
property, the activity fails validation because this property is part of a different group. Clearing the value for the Author
property clears the error. Feel free to try other combinations of properties to further test the validation.
To demonstrate how to add validation logic within the activity itself, you will now revisit the MySequence
activity that was used earlier in the chapter. This activity supports multiple children, so it makes sense to validate that at least one child has been assigned to the Activities
property. One way to accomplish this is by adding validation code to the CacheMetadata
method of the activity.
Note In the section following this one, you will also implement a version of this activity that uses a constraint to perform the same type of validation. To make it clear that these are separate versions of the activity and are not built upon each other, I’ve chosen to make a copy of the previous MySequence
activity rather than to modify it.
Make a copy of the MySequence
activity (in the ActivityLibrary
project), and name the copy MySequenceWithValidation
. Here is the code that includes the validation logic:
using System;
using System.Activities;
using System.Activities.Validation;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Markup;
namespace ActivityLibrary
{
[Designer(typeof(ActivityDesignerLibrary.MySequenceDesigner))]
[ContentProperty("Activities")]
public class MySequenceWithValidation : NativeActivity
{
[Browsable(false)]
public Collection<Activity> Activities { get; set; }
public Activity<Boolean> Condition { get; set; }
[Browsable(false)]
public Collection<Variable> Variables { get; set; }
public MySequenceWithValidation()
{
Activities = new Collection<Activity>();
Variables = new Collection<Variable>();
}
After executing the base CacheMetadata
method, the code performs a simple check to determine whether the count of child activities is equal to zero. If it is, the AddValidationError
method of the metadata object is called to signal an error condition.
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
base.CacheMetadata(metadata);
if (Activities.Count == 0)
{
metadata.AddValidationError(
"At least one child activity must be added");
}
}
protected override void Execute(NativeActivityContext context)
{
throw new NotImplementedException();
}
}
}
Build the solution and add an instance of the new MySequenceWithValidation
activity to the Activity1.xaml
file. Figure 15-13 demonstrates the error that should appear when no children have been assigned to the activity.
Once you’ve added at least one child activity, the error should be cleared.
Warning Remember that you are validating the design-time properties of the activity. You do not have access to any of the runtime property values for the activity.
With just a slight modification to the code, you can turn this error into a warning instead. Instead of calling the version of the AddValidationError
method shown in the original code, you can use an override that accepts a ValidationError
object. When you construct this object, you can pass a Boolean value that determines whether the validation failure is considered an error or a warning. Here is the revised section of code:
protected override void CacheMetadata(NativeActivityMetadata metadata)
{
base.CacheMetadata(metadata);
if (Activities.Count == 0)
{
metadata.AddValidationError(
new ValidationError(
"At least one child activity must be added",
true, "Activities"));
}
}
After rebuilding the solution again, you should see that the previous error is now considered a warning, as shown in Figure 15-14.
To demonstrate the use of a constraint for validation, you will create yet another version of the MySequence
activity. You will develop a series of constraints that demonstrate various types of validation that can be implemented with constraints.
The first constraint that you will implement validates that at least one child activity has been added to the activity. This is the same validation logic that you implemented as imperative code within the activity in the previous example. Add a new C# class to the ActivityLibrary
project. Name the new class ChildActivityRequiredConstraint
. Constraints need to reference the activity type that they are constraining. For this reason, it makes sense to house the constraints in the same assembly as their activity type. Here is the code for this constraint class:
using System;
using System.Activities;
using System.Activities.Validation;
namespace ActivityLibrary
{
public static class ChildActivityRequiredConstraint
{
public static Constraint GetConstraint()
{
The Constraint<T>
class is sealed, so you can’t derive your own class from it. Instead, you need to construct a constraint using composition as demonstrated in this example. Note that each constraint must know the type of activity that it is designed to constrain (specified as the generic type). This is necessary in order to provide access to any properties that might be unique to the activity. Depending on your needs, you can compose constraints that narrowly target a single activity (such as this one) or more broadly constrain the design-time behavior of an entire related family of activities.
In this example, an ActivityAction
is assigned to the Body
property of the constraint. The ActivityAction
is defined to pass two arguments: the instance of the activity that you are validating (in this case a MySequenceWithConstraint
) and a ValidationContext
object. A DelegateInArgument
is assigned to the Argument1
property of the ActivityAction
. This provides access to the activity that is being validated.
An AssertValidation
activity is assigned to the Handler
property of the ActivityAction
. This is where the real work is of this constraint is accomplished. The Assertion
property is a Boolean InArgument
that asserts that the count of the Activities
property is greater than zero. If the assertion is true, the activity passes validation. If the assertion is false, the text that is provided for the Message
property is displayed as an error.
DelegateInArgument<MySequenceWithConstraint> element =
new DelegateInArgument<MySequenceWithConstraint>();
return new Constraint<MySequenceWithConstraint>
{
Body = new ActivityAction<MySequenceWithConstraint, ValidationContext>
{
Argument1 = element,
Handler = new AssertValidation
{
IsWarning = false,
Assertion = new InArgument<bool>(
env => (element.Get(env).Activities.Count > 0)),
Message = new InArgument<string>(
"At least one child activity must be added"),
}
}
};
}
}
}
Note The constraint won’t build at this point since it is referencing a new version of the MySequence
activity that you haven’t implemented yet. That is the next step in this example.
To create an activity that will use the constraint, make another copy of the original MySequence
activity (in the ActivityLibrary
project). Name the copy MySequenceWithConstraint
. Here is the code for this version of the activity:
using System;
using System.Activities;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Windows.Markup;
namespace ActivityLibrary
{
[Designer(typeof(ActivityDesignerLibrary.MySequenceDesigner))]
[ContentProperty("Activities")]
public class MySequenceWithConstraint : NativeActivity
{
[Browsable(false)]
public Collection<Activity> Activities { get; set; }
public Activity<Boolean> Condition { get; set; }
[Browsable(false)]
public Collection<Variable> Variables { get; set; }
public MySequenceWithConstraint()
{
Activities = new Collection<Activity>();
Variables = new Collection<Variable>();
The constraint is adding in the constructor of the activity. The static GetConstraint
method that was defined in the constraint class is invoked, and the result (an instance of the constraint) is passed to the Constraints.Add
method of the activity.
this.Constraints.Add(
ChildActivityRequiredConstraint.GetConstraint());
}
protected override void Execute(NativeActivityContext context)
{
throw new NotImplementedException();
}
}
}
Rebuild the solution, and test the new MySequenceWithConstraint
activity by adding it to the Activity1.xaml
file. Other than the difference in the display name of the activity, the results should be the same as you saw in Figure 15-13.
Constraints also allow you to execute validation logic against other activities. For example, you can validate that only certain types of activities are allowed (or forbidden) as children. Or, you can mandate that your activity must be the child of a specific parent (or not).
To help you to perform validation in these scenarios, WF includes a set of activities that you can use within a constraint:
GetParentChain
returns a collection of parents of the current activity.GetChildSubtree
returns all children of the current activity.GetWorkflowTree
returns the entire tree of activities in the current workflow.
All of these activities function in the same basic way. They all support a ValidationContext
property that must be set to the current ValidationContext
of the constraint. This object is provided as an argument to the ActivityAction
that you assign to the Constraint.Body
property. These activities return a collection of Activity
objects that represent the requested activities.
To demonstrate these activities, add another C# class to the ActivityLibrary
project, and name it LimitedChildActivitiesConstraint
. The purpose of this constraint is to restrict the type of activities that you can add as children to the MySequenceWithConstraint
activity. Here is the code for this constraint:
using System;
using System.Activities;
using System.Activities.Statements;
using System.Activities.Validation;
using System.Collections.Generic;
namespace ActivityLibrary
{
public static class LimitedChildActivitiesConstraint
{
public static Constraint GetConstraint()
{
A collection of allowed types is first populated. This list represents the activities that will be allowed as children of the activity. In this contrived example, only the Sequence
, WriteLine
, and Assign
activities are allowed. The code also defines two namespaces that are to be allowed. This was necessary if you want to allow expressions (which you most certainly do).
List<Type> allowedTypes = new List<Type>
{
typeof(Sequence),
typeof(WriteLine),
typeof(Assign)
};
List<String> allowedNamespaces = new List<String>
{
"Microsoft.VisualBasic.Activities",
"System.Activities.Expressions"
};
Variable<Boolean> result =
new Variable<Boolean>("result", true);
DelegateInArgument<MySequenceWithConstraint> element =
new DelegateInArgument<MySequenceWithConstraint>();
DelegateInArgument<ValidationContext> vc =
new DelegateInArgument<ValidationContext>();
DelegateInArgument<Activity> child =
new DelegateInArgument<Activity>();
return new Constraint<MySequenceWithConstraint>
{
Body = new ActivityAction
<MySequenceWithConstraint, ValidationContext>
{
Argument1 = element,
Argument2 = vc,
Handler = new Sequence
{
Variables = { result },
Activities =
{
The real work of this constraint takes place here in a ForEach<T>
activity. The collection of activities that the ForEach<T>
iterates over is retrieved from the GetChildSubtree
activity. Each activity in the returned collection is checked against the list of allowed types and namespaces. If it isn’t in one of those lists, a Boolean variable (named result
) is set to false.
An AssertValidation
activity is used to check the value of the result
variable. If it is false, the validation error is presented.
new ForEach<Activity>
{
Values = new GetChildSubtree
{
ValidationContext = vc
},
Body = new ActivityAction<Activity>
{
Argument = child,
Handler = new If()
{
Condition = new InArgument<Boolean>(ac =>
allowedTypes.Contains(
child.Get(ac).GetType()) ||
allowedNamespaces.Contains(
child.Get(ac).GetType().Namespace)),
Else = new Assign<Boolean>
{
To = result,
Value = false
}
}
}
},
new AssertValidation
{
Assertion = new InArgument<Boolean>(result),
Message = new InArgument<String>(
"Only Sequence, WriteLine, Assign allowed"),
PropertyName = new InArgument<String>(
(env) => element.Get(env).DisplayName)
}
}
}
}
};
}
}
}
Just to make this example more interesting, go ahead and add another constraint class to the ActivityLibrary
project. This time name the new class WhileParentConstraint
. The purpose of this constraint is to prevent the While
activity from being the parent of the constrained activity. Note that this particular constraint targets the base Activity
class. This allows the constraint to be used to constrain any activity. Here is the code for this constraint:
using System;
using System.Activities;
using System.Activities.Statements;
using System.Activities.Validation;
using System.Collections.Generic;
namespace ActivityLibrary
{
public static class WhileParentConstraint
{
public static Constraint GetConstraint()
{
List<Type> prohibitedParentTypes = new List<Type>
{
typeof(While),
};
Variable<Boolean> result =
new Variable<Boolean>("result", true);
DelegateInArgument<Activity> element =
new DelegateInArgument<Activity>();
DelegateInArgument<ValidationContext> vc =
new DelegateInArgument<ValidationContext>();
DelegateInArgument<Activity> child =
new DelegateInArgument<Activity>();
return new Constraint<Activity>
{
Body = new ActivityAction
<Activity, ValidationContext>
{
Argument1 = element,
Argument2 = vc,
Handler = new Sequence
{
Variables = { result },
Activities =
{
This constraint is similar in structure to the previous one. The major difference is that the GetParentChain
activity is used instead of GetChildSubtree
. This returns a collection of all parent activities instead of all children.
new ForEach<Activity>
{
Values = new GetParentChain
{
ValidationContext = vc
},
Body = new ActivityAction<Activity>
{
Argument = child,
Handler = new If()
{
Condition = new InArgument<Boolean>(ac =>
prohibitedParentTypes.Contains(
child.Get(ac).GetType())),
Then = new Assign<Boolean>
{
To = result,
Value = false
}
}
}
},
new AssertValidation
{
Assertion = new InArgument<Boolean>(result),
Message = new InArgument<String>(
"Parent While activity not allowed"),
PropertyName = new InArgument<String>(
(env) => element.Get(env).DisplayName)
}
}
}
}
};
}
}
}
To use these new constraints, you need to add them in the constructor of the MySequenceWithConstraint
activity. Here is the affected code:
public MySequenceWithConstraint()
{
Activities = new Collection<Activity>();
Variables = new Collection<Variable>();
this.Constraints.Add(
ChildActivityRequiredConstraint.GetConstraint());
this.Constraints.Add(
LimitedChildActivitiesConstraint.GetConstraint());
this.Constraints.Add(
WhileParentConstraint.GetConstraint());
}
After rebuilding the solution, you should be ready to test the new constraints. Open the Activity1.xaml
file and add a new instance of the MySequenceWithConstraint
activity. To test for valid children of this activity, add a WriteLine
(which is valid), followed by a Delay
activity as children. Set the Delay.Duration
property to clear the RequiredArgument
error due for that property. You should now see the error shown in Figure 15-15.
To prove that the entire tree of children is inspected, add a Sequence
activity (which is allowed) to the activity and then move the Delay
activity into it. The Delay
activity should continue to be flagged as a validation error, as shown in Figure 15-16.
Removing the Sequence
activity containing the Delay
activity should clear the error. To test the parent validation, add a While
activity to the root Sequence
of the Activity1.xaml
file, and move the MySequenceWithConstraint
activity into it as a child. Enter True
for the While.Condition
property to clear that particular validation error. You should now see the error shown in Figure 15-17.
Most of the time, you will use the validation logic that is already associated with a particular activity (attributes, code validation, or constraints). This is especially true for the standard activities that ship with WF, but this is also the case for custom activities that you develop. Generally, you bake the validation into the activity so that it is automatically executed each and every time the activity is used.
However, you can also choose to manually validate an activity. And when you do so, you have the option of associating additional constraints with activities that may not already have them. This is useful in an environment where you are self-hosting the workflow designer in your own application. If you are self-hosting, it is likely that you want to tightly control what a user can do with a particular workflow. You might want to place additional restrictions on the user as to what activities they can use and so on. Performing manual validation on an activity provides you with an opportunity to enforce any additional constraints that you want to add.
In this short example, you will manually execute validation on a series of activities that are constructed entirely in code. Some of the test activities are designed to pass validation while others deliberately fail.
Create a new Workflow Console project, and name it WorkflowValidation
. Add this new project to the solution for this chapter. Add a project reference to the ActivityLibrary
project. You can delete the Worklfow1.xaml
file since it won’t be used. All the code for this example goes into the Program.cs
file of the project. Here is the complete code that you need for this example:
using System;
using System.Activities;
using System.Activities.Statements;
using System.Activities.Validation;
using System.Collections.Generic;
using ActivityLibrary;
namespace WorkflowValidation
{
class Program
{
static void Main(string[] args)
{
The code executes a series of validation tests, with each test using a slightly different activity. The actual validation is accomplished by calling the static Validate
method of the ActivityValidationServices
class. Two different overloads of this method are used. One overload passes only the activity to be validated. This overload demonstrates the ability to manually execute the validation logic that has already been associated with an activity (or its children).
The second overload of the Validate
method allows you to pass an instance of the ValidationSettings
object. This class provides an AdditionalConstraints
property that can be used to inject additional constraints. The constraints that you add are used only during the call to the Validate
method; they are not permanently added to the activity.
Console.WriteLine("
MySequenceWithError");
ShowValidationResults(ActivityValidationServices.Validate(
MySequenceWithError()));
Console.WriteLine("
MySequenceNoError");
ShowValidationResults(ActivityValidationServices.Validate(
MySequenceNoError()));
This validation test adds the WhileParentConstraint
to the WriteLine
activity. You implemented this constraint earlier in the chapter. It prohibits the use of the While
activity as the parent of the activity with the constraint. Since this constraint is being added to the WriteLine
activity, the WriteLine
activity cannot be used as a child of the While
activity (at least for the duration of the call to the Validate
method).
Console.WriteLine("
WhileAndWriteLineError");
ValidationSettings settings = new ValidationSettings();
settings.AdditionalConstraints.Add(
typeof(WriteLine), new List<Constraint>
{
WhileParentConstraint.GetConstraint()
});
ShowValidationResults(ActivityValidationServices.Validate(
WhileAndWriteLine(), settings));
Console.WriteLine("
WhileAndWriteLineNoError");
ShowValidationResults(ActivityValidationServices.Validate(
WhileAndWriteLine()));
}
The MySequenceWithError
method constructs an activity that includes an instance of the MySequenceWithConstraint
activity. This custom activity was developed in the previous sections of this chapter. This activity defines a set of constraints that are added during construction. One of those is a requirement that at least one child be added to the activity. Therefore, the activity that is constructed by this method should fail validation.
private static Activity MySequenceWithError()
{
return new Sequence
{
Activities =
{
new MySequenceWithConstraint
{
Activities =
{
//no child activities is an error
}
}
}
};
}
The MySequenceNoError
method also constructs an activity that uses the MySequenceWithConstraint
activity. However, this activity does include the required child activity; therefore, it should pass validation.
private static Activity MySequenceNoError()
{
return new Sequence
{
Activities =
{
new MySequenceWithConstraint
{
Activities =
{
new WriteLine()
}
}
}
};
}
The WhileAndWriteLine
method constructs an activity that includes a Sequence
, a While
, and a Writeline
—all standard activities. The activity that is constructed by this method is used for validation tests that inject additional constraints that are used during validation.
private static Activity WhileAndWriteLine()
{
return new Sequence
{
Activities =
{
new While
{
Condition = true,
Body = new WriteLine()
}
}
};
}
private static void ShowValidationResults(ValidationResults vr)
{
Console.WriteLine("Total Errors: {0} - Warnings: {1}",
vr.Errors.Count, vr.Warnings.Count);
foreach (ValidationError error in vr.Errors)
{
Console.WriteLine(" Error: {0}", error.Message);
}
foreach (ValidationError warning in vr.Warnings)
{
Console.WriteLine(" Warning: {0}", warning.Message);
}
}
}
}
Build the WorkflowValidation
project, and run it without debugging (Ctrl-F5). Here are my results:
MySequenceWithError
Total Errors: 1 - Warnings: 0
Error: At least one child activity must be added
MySequenceNoError
Total Errors: 0 - Warnings: 0
WhileAndWriteLineError
Total Errors: 1 - Warnings: 0
Error: Parent While activity not allowed
WhileAndWriteLineNoError
Total Errors: 0 - Warnings: 0
Press any key to continue . . .
From these results, you can see that the tests that used the MySequenceWithConstraint
activity returned the expected results. When a child activity was present, the activity passed validation. When the child activity was missing, the validation error was generated.
The test that added the additional constraint to the WriteLine
activity also worked as expected. This demonstrates how you can add constraints to activities that don’t originally use them.
In addition to activities, WF also supports the concept of activity templates. An activity template is presented as a single activity in the Visual Studio Toolbox, but when added to the design surface, it may generate multiple activities. Or it may generate a single activity with property values that have been preconfigured for a given purpose.
Several of the standard activities that are supplied with WF are actually activity templates. For example, ReceiveAndSendReply
(used for WCF messaging) is actually an activity template. When added to the design surface, it generates instances of the Receive
and SendReply
activities that are preconfigured to work with each other. The SendAndReceiveReply
is also an activity template and works in a similar way.
Another example of a standard activity template is the ForEachWithBodyFactory<T>
class. This template is associated with the ForEach<T>
tool that you see in the standard WF Toolbox. When you drag and drop a ForEach<T>
activity to the workflow designer, you are actually adding this template. The need for this template arises because a ForEach<T>
activity also requires an ActivityAction<T>
instance to be added to allow you to add your own child activity. The activity template is the mechanism that is used to add the ForEach<T>
and an associated child ActivityAction<T>
. You might want to follow Microsoft’s example when you create custom activities that require an ActivityAction<T>
.
WF provides the IActivityTemplateFactory
interface (found in the System.Activities.Presentation
namespace), which you can use to develop your own activity templates. You can use this mechanism any time you need to combine multiple activities into a single package for use at design time. This mechanism can also be used to preconfigure activity property values. Although you can certainly compose multiple activities into a single custom activity, adding those activities with an activity template allows you to alter the structure at design time.
To implement an activity template, you simply need to create a class that implements the IActivityTemplateFactory
interface. This interface defines a single Create
method that returns a single Activity
. The Activity
that you return is the preconfigured activity or root of an activity tree containing the activities that you want to add to the design surface.
In this example, you will create an activity template that preconfigures an instance of the MySequenceWithConstraint
activity. For demonstration purposes, the template populates this activity with a child Sequence
activity. And the Sequence
activity contains a WriteLine
activity with a default message for its Text
property.
To create an activity template, add a new C# class to the ActivityLibrary
project, and name it MySequenceTemplate
. Here is the code that you need to implement this class:
using System;
using System.Activities;
using System.Activities.Presentation;
using System.Activities.Statements;
namespace ActivityLibrary
{
public class MySequenceTemplate : IActivityTemplateFactory
{
public Activity Create(System.Windows.DependencyObject target)
{
return new MySequenceWithConstraint
{
DisplayName = "MySequenceTemplate",
Activities =
{
new Sequence
{
Activities =
{
new WriteLine
{
Text = "Template generated text"
}
}
}
}
};
}
}
}
Rebuild the solution, and then open the Activity1.xaml
file in the ActivityLibrary
project. You should see that MySequenceTemplate
has been added to the Toolbox, just like all the other custom activities. When you drag and drop an instance of this template onto the designer surface, you should see the preconfigured collection of activities. Figure 15-18 shows the results that I see when I use this template.
This chapter focused on enhancing the design-time experience. WF provides you with the ability to create an appealing and productive environment using custom designers and activity validation.
The chapter began with an overview of the classes that are used to develop custom activity designers. Example activity designers were presented that expose the activity properties on the design surface, support dragging and dropping a single or multiple child activities, and support the special needs of an ActivityAction
property. The chapter also demonstrated how to construct an activity template.
Several examples were presented that demonstrated the three types of activity validation (attributes, code, and constraints). Manually executed validation and the injection of additional constraints was also demonstrated in another example.
In the next chapter, you will learn more about building advanced custom activities.
18.119.162.49