images

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.

Understanding Activity Designers

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.

images 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).

ActivityDesigner

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.

images 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:

images

ModelItem

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.

ExpressionTextBox

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:

images

ArgumentToExpressionConverter

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.

Understanding Expression Types

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 of In.
  • 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 of Out.
  • Set the UseLocationExpression property to True.

WorkflowItemPresenter and WorkflowItemsPresenter

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:

images

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:

images

images Note The WorkflowItemPresenter and WorkflowItemsPresenter classes are demonstrated later in the chapter.

Metadata Store and Designer Assignment

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 the MetadataStore 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.

The Custom Designer Workflow

In general, you should follow these steps when creating a new customer activity designer:

  1. Create a separate project to house activity designers.
  2. Create a new activity designer class that is derived from ActivityDesigner.
  3. If necessary, add support for multiple viewing modes (expanded and collapsed).
  4. Implement the visual elements and controls of the designer in Xaml.
  5. Add bindings for each ModelItem property that you want to maintain in the designer.
  6. Optionally, add a custom icon to the designer.
  7. Add entries to the metadata store that associate the designer with the activities that should use it.

Supporting Activity Properties

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.

Creating the Projects

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.

Implementing the CalcShipping Activity

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();
        }
    }
}

images 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.

Viewing the Default Design Experience

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.

images

Figure 15-1. CalcShipping with standard designer

Declaring a Custom Designer

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.

images 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>

images 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.

Associating the Activity with the 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.

images

Figure 15-2. CalcShipping with custom designer

Using the MetadataStore to Associate a Designer

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.

images 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.

Creating the Design Project

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.

images 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.

Adding the Metadata Class

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.

Removing the Design Attribute

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.

Testing the Designer

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

Adding an Icon

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:

  1. Add the image to the designer project.
  2. Set the Build Action option for the image to Resource.
  3. 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.

images 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.

Adding the Image to the Project

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:

  1. Locate the VS2010ImageLibrary.zip file that is deployed with Visual Studio. The file should be located in the Program 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.
  2. Unzip the VS2010ImageLibrary.zip file into a temporary directory.
  3. Locate the CalculatorHS.png file, which should be located in the VS2010ImageLibraryActionspng_formatOffice and VS relative folder after unzipping the image library.
  4. Copy the CalculatorHS.png file to the ActivityDesignerLibrary project folder.
  5. Add the CalculatorHS.png file to the project, and change the Build Action option for the file to Resource.
Adding the Image to the Designer

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>
Testing the Designer Icon

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.

images

Figure 15-3. CalcShipping with custom icon

Supporting Expanded and Collapsed Modes

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.

Declaring the Collapsible Designer

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>

Changing the Designer Attribute

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>

Testing the Collapsible Designer

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.

images

Figure 15-4. CalcShipping with collapsible designer

images

Figure 15-5. Collapsed CalcShipping

Supporting a Single Child Activity

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.

Implementing the MyWhile Activity

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.

images 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.

Declaring a Custom Designer

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>

images 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.

Testing the Designer

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.

images

Figure 15-6. MyWhile with custom designer

Supporting Multiple Child Activities

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.

Implementing the MySequence Activity

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.

Declaring a Custom 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>

Testing the Designer

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.

images

Figure 15-7. MySequence with custom designer

images

Figure 15-8. Collapsed MySequence

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.

Supporting the ActivityAction 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.

images 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.

Implementing the MyActivityWithAction Activity

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();
        }
    }
}

Declaring a Custom Designer

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>

Testing the Designer

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.

images

Figure 15-9. MyActivityWithAction

images Note Implementing a custom activity that uses an ActivityAction is one of the scenarios discussed in Chapter 16.

Understanding Validation

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.

Validation Attributes

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.

Validation Code

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.

Constraints

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.

images 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.

Using Validation Attributes

In this section of the chapter, you will use the RequiredArgument and OverloadGroup attributes to add basic validation to an activity.

Using the RequiredArgument Attribute

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.

images

Figure 15-10. CalcShipping with requirement arguments

Providing values for each property should eliminate the errors

Using the OverloadGroup Attribute

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.

images

Figure 15-11. TitleLookup properties

When you do this, you should see the error shown in Figure 15-12.

images

Figure 15-12. TitleLookup validation errors

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.

Adding Validation in Code

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.

images 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.

Adding an Error

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.

images

Figure 15-13. MySequenceWithValidation with error

Once you’ve added at least one child activity, the error should be cleared.

images 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.

Adding a Warning

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.

images

Figure 15-14. MySequenceWithValidation with warning

Using Constraints for Validation

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.

Implementing a Simple Constraint

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"),
                    }
                }
            };
        }
    }
}

images 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.

Validating Against Other Activities

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.

Checking the Children

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)
                            }
                        }
                    }
                }
            };
        }
    }
}
Checking the Parents

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)
                            }
                        }
                    }
                }
            };
        }
    }
}
Adding the Constraints

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());
        }
Testing the Constraints

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.

images

Figure 15-15. MySequenceWithConstraint with invalid child

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.

images

Figure 15-16. MySequenceWithConstraint with invalid grandchild

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.

images

Figure 15-17. MySequenceWithConstraint with invalid parent

Manually Executing Validation

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.

Implementing the Validation Tests

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);
            }
        }
    }
}

Executing the Validation Tests

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.

Implementing Activity Templates

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.

Implementing the Template

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"
                            }
                        }
                    }
                }
            };
        }
    }
}

Testing the Template

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.

images

Figure 15-18. MySequenceTemplate

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.119.162.49