Custom Activities


Thus far, you have used activities that are defined within the System.Workflow.Activities namespace. In this section, you’ll learn how you can create custom activities and extend these activities to provide a good user experience at both design time and runtime.

To begin, you’ll create a WriteLineActivity that can be used to output a line of text to the console. Although this is a trivial example, it will be expanded to show the full gamut of options available for custom activities using this example. When creating custom activities, you can simply construct a class within a workflow project; however, it is preferable to construct your custom activities inside a separate assembly, as the Visual Studio design time environment (and specifically workflow projects) will load activities from your assemblies and can lock the assembly that you are trying to update. For that reason, you should create a simple class library project to construct your custom activities within.

A simple activity such as the WriteLineActivity will be derived directly from the Activity base class. The following code shows a constructed activity class, and defines a Message property that is displayed when the Execute method is called:

  using System; using System.ComponentModel;  using System.Workflow.ComponentModel; namespace SimpleActivity {   /// <summary>   /// A simple activity that displays a message to the console when it executes   /// </summary>   public class WriteLineActivity : Activity   {     /// <summary>     /// Execute the activity - display the message on screen     /// </summary>     /// <param name="executionContext"></param>     /// <returns></returns>     protected override ActivityExecutionStatus Execute      (ActivityExecutionContext executionContext)     {       Console.WriteLine(Message);       return ActivityExecutionStatus.Closed;     }     /// <summary>     /// Get/Set the message displayed to the user     /// </summary>     [Description("The message to display")]     [Category("Parameters")]     public string Message     {       get { return _message; }       set { _message = value; }     }     /// <summary>     /// Store the message displayed to the user     /// </summary>     private string _message;   } } 

Within the Execute method, you can write the message to the console and then return a status of Closed to notify that runtime that the activity has completed.

You can also defined attributes on the Message property so that a description and category are defined for that property - this is used in the property grid within Visual Studio, as shown in Figure 41-8.

image from book
Figure 41-8

The code for the activities created in this section is in the 03 CustomActivities solution. If you compile that solution, you can then add the custom activities to the toolbox within Visual Studio by choosing the Choose Items menu item from the context menu on the toolbox and navigating to the folder where the assembly containing the activities resides. All activities within the assembly will be added to the toolbox.

As it stands the activity is perfectly usable; however, there are several areas that should be addressed to make this more user-friendly. As you saw with the CodeActivity earlier in the chapter, it has some mandatory properties that, when not defined, produce an error glyph on the design surface. To get the same behavior from your activity, you need to construct a class that derives from ActivityValidator and associate this class with your activity.

Activity Validation

When an activity is placed onto the design surface, the Workflow Designer looks for an attribute on that activity that defines a class that performs validation on that activity. To validate your activity, you need to check if the Message property has been set.

A custom validator is passed the activity instance, and from this you can then determine which mandatory properties (if any) have not been defined, and add an error to the ValidationErrorCollection used by the Designer. This collection is then read by the Workflow Designer, and any errors found in the collection will cause a glyph to be added to the activity and optionally link each error to the property that needs attention.

  using System; using System.Workflow.ComponentModel.Compiler; namespace SimpleActivity {   public class WriteLineValidator : ActivityValidator   {     public override ValidationErrorCollection Validate       (ValidationManager manager, object obj)     {       if (null == manager)         throw new ArgumentNullException("manager");       if (null == obj)         throw new ArgumentNullException("obj");           ValidationErrorCollection errors = base.Validate(manager, obj);           // Coerce to a WriteLineActivity       WriteLineActivity act = obj as WriteLineActivity;           if (null != act)       {          if (null != act.Parent)          {            // Check the Message property            if (string.IsNullOrEmpty(act.Message))              errors.Add(ValidationError.GetNotSetValidationError("Message"));          }       }       return errors;     }   } } 

The Validate method is called by the Designer when any part of the activity is updated and also when the activity is dropped onto the design surface. The Designer calls the Validate method and passes through the activity as the untyped obj parameter.

In this method, first validate the arguments passed in, and then call the base class Validate method to obtain a ValidationErrorCollection. Although this is not strictly necessary here, if you are deriving from an activity that has a number of properties that also need to be validated then calling the base class method will ensure that these are also checked.

Coerce the passed obj parameter into a WriteLineActivity instance, and check if the activity has a parent. This test is necessary because the Validate function is called during compilation of the activity (if the activity is within a workflow project or activity library), and at this point no parent activity has been defined. Without this check, you cannot actually build the assembly that contains the activity and the validator. This extra step is not needed if the project type is class library.

The last step is to check that the Message property has been set to a value other than an empty string - this uses a static method of the ValidationError class, which constructs an error that specifies that the property has not been defined.

To add validation support to your WriteLineActivity, the last step is to add the ActivityValidation attribute to the activity, as shown in the snippet below:

 [ActivityValidator(typeof(WriteLineValidator))] public class WriteLineActivity : Activity {   ... }

If you compile the application and then drop a WriteLineActivity onto the workflow, you should see a validation error, as shown in Figure 41-9; clicking on this error will take you to that property within the property grid.

image from book
Figure 41-9

If you enter some text for the Message property, the validation error will be removed, and you can then compile and run the application.

Now that you have completed the activity validation, the next thing to do is to change the rendering behavior of the activity to add a fill color to that activity. To do this, you need to define both an ActivityDesigner class and an ActivityDesignerTheme class, as described in the next section.

Themes & Designers

The onscreen rendering of an activity is performed using an ActivityDesigner class, and this can also use an ActivityDesignerTheme.

The theme class is used to make simple changes to the rendering behavior of the activity within the Workflow Designer.

  public class WriteLineTheme : ActivityDesignerTheme {   /// <summary>   /// Construct the theme and set some defaults   /// </summary>   /// <param name="theme"></param>   public WriteLineTheme(WorkflowTheme theme)     : base(theme)   {     this.BackColorStart = Color.Yellow;     this.BackColorEnd = Color.Orange;     this.BackgroundStyle = LinearGradientMode.ForwardDiagonal;   } } 

A theme is derived from ActivityDesignerTheme, which has a constructor that is passed a WorkflowTheme argument. Within the constructor, set the start and end colors for the activity, and then define a linear gradient brush, which is used when painting the background.

The Designer class is used to override the rendering behavior of the activity - in this case, no override is necessary, so the following code will suffice:

  [ActivityDesignerTheme(typeof(WriteLineTheme))] public class WriteLineDesigner : ActivityDesigner { } 

Note that the theme has been associated with the Designer by using the ActivityDesignerTheme attribute.

The last step is to adorn the activity with the Designer attribute:

 [ActivityValidator(typeof(WriteLineValidator))] [Designer(typeof(WriteLineDesigner))] public class WriteLineActivity : Activity {   ... }

With this in place, the activity is rendered as shown in Figure 41-10.

image from book
Figure 41-10

With the addition of the Designer and the theme, the activity now looks much more professional. There are a number of other properties available on the theme - such as the pen used to render the border, the color of the border, and the border style.

By overriding the OnPaint method of the ActivityDesigner class, you can have complete control over the rendering of the activity. I’d suggest exercising restraint here, as you could get carried away and create an activity that doesn’t resemble any of the other activities in the toolbox.

One other useful override on the ActivityDesigner class is the Verbs property. This allows you to add menu items on the context menu for the activity, and is used by the Designer of the ParallelActivity to insert the Add Branch menu item into the activities context menu and also the Workflow menu. You can also alter the list of properties exposed for an activity by overriding the PreFilterProperties method of the Designer - this is how the method parameters for the CallExternalMethodActivity are surfaced into the property grid. If you need to do this type of extension to your Designer, you should run Lutz Roeder’s Reflector and load the workflow assemblies into it to see how Microsoft has defined some of these extended properties.

This activity is nearly done, but now you need to define the icon used when rendering the activity and also the toolbox item to associate with the activity.

ActivityToolboxItem and Icons

To complete your custom activity, you need to add an icon and can optionally create a class deriving from ActivityToolboxItem, which is used when displaying the activity in the toolbox within Visual Studio.

To define an icon for an activity, create a 16*16 pixel image and include it into your project, and when it has been included, set the build action for the image to be Embedded Resource. This will include the image in the manifest resources for the assembly. You can add a folder to your project called Resources, as shown in Figure 41-11.

image from book
Figure 41-11

Once you have added the image file and set its build action to Embedded Resource, you can then attribute the activity as shown in the following snippet:

 [ActivityValidator(typeof(WriteLineValidator))] [Designer(typeof(WriteLineDesigner))] [ToolboxBitmap(typeof(WriteLineActivity),"Resources.WriteLine.png")] public class WriteLineActivity : Activity {   ... }

The ToolboxBitmap attribute has a number of constructors defined, and the one being used here takes a type defined in the activity assembly and the name of the resource. When you add a resource to a folder its name is constructed from the namespace of the assembly and the name of the folder that the image resides within - so the fully qualified name for my resource is CustomActivities.Resources. WriteLine.png. The constructor used with the ToolboxBitmap attribute appends the namespace that the type parameter resides within to the string passed as the second argument, so this will resolve to the appropriate resource when loaded by Visual Studio.

The last class you need to create is derived from ActivityToolboxItem. This class is used when the activity is loaded into the Visual Studio toolbox. A typical use of this class is to change the displayed name of the activity on the toolbox - all of the built-in activities have their names changed to remove the word “Activity” from the type. In your class, you can do the same by setting the DisplayName property to “WriteLine.”

  [Serializable] public class WriteLineToolboxItem : ActivityToolboxItem {     /// <summary>     /// Set the display name to WriteLine - i.e. trim off the 'Activity' string     /// </summary>     /// <param name="t"></param>     public WriteLineToolboxItem(Type t)         : base(t)     {         base.DisplayName = "WriteLine";     }     /// <summary>     /// Necessary for the Visual Studio design time environment      /// </summary>     /// <param name="info"></param>     /// <param name="context"></param>     private WriteLineToolboxItem(SerializationInfo info, StreamingContext context)     {         this.Deserialize(info, context);     } } 

The class is derived from ActivityToolboxItem and overrides the constructor to change the display name; it also provides a serialization constructor that is used by the toolbox when the item is loaded into the toolbox. Without this constructor, you will receive an error when you attempt to add the activity to the toolbox. Note that the class is also marked as [Serializable].

The toolbox item is added to the activity by using the ToolboxItem attribute as shown:

 [ActivityValidator(typeof(WriteLineValidator))] [Designer(typeof(WriteLineDesigner))] [ToolboxBitmap(typeof(WriteLineActivity),"Resources.WriteLine.png")] [ToolboxItem(typeof(WriteLineToolboxItem))] public class WriteLineActivity : Activity {   ... }

With all of these changes in place, you can compile the assembly and then create a new workflow project. To add the activity to the toolbox, open a workflow and then display the context menu for the toolbox and click Choose Items.

You can then browse for the assembly containing your activity and when you have added it to the toolbox it will look something like Figure 41-12. The icon is somewhat less than perfect, but it’s close enough.

image from book
Figure 41-12

You’ll revisit the ActivityToolboxItem in the next section on custom composite activities, as there are some extra facilities available with that class that are only necessary when adding composite activities to the design surface.

Custom Composite Activities

There are two main types of activity - those that derive from Activity can be thought of as callable functions from the workflow. Activities that derive from CompositeActivity (such as ParallelActivity, IfElseActivity, and the ListenActivity) are containers for other activities, and their design-time behavior is considerably different from simple activities in that they present an area on the Designer where child activities can be dropped.

In this section, you’ll create an activity that you can call the DaysOfWeekActivity. This activity can be used to execute different parts of a workflow based on the current date. You might, for instance, need to execute a different path in the workflow for orders that arrive over the weekend than for those that arrive during the week. In this example, you will learn about a number of advanced workflow topics, and by the end of this section you should have a good understanding of how to extend the system with your own composite activities. The code for this example is also available in the 03 CustomActivities solution.

To begin, you will create a custom activity that has a property that will default to the current date/time, and allow that property to be set to another value that could come from another activity in the workflow or a parameter that is passed to the workflow when it executes. This composite activity will contain a number of branches - these will be user defined. Each of these branches will contain an enumerated constant that defines which day(s) that branch will execute. The example below defines the activity and two branches:

  DaysOfWeekActivity   SequenceActivty: Monday, Tuesday, Wednesday, Thursday, Friday     <other activites as appropriate>   SequenceActivity: Saturday, Sunday     <other activites as appropriate> 

For this example, you need an enumeration that defines the days of the week - this will include the [Flags] attribute (so you can’t use the built-in DayOfWeek enum defined within the System namespace, as this doesn’t include the [Flags] attribute).

  [Flags] [Editor(typeof(FlagsEnumEditor), typeof(UITypeEditor))] public enum WeekdayEnum : byte {     None = 0x00,     Sunday = 0x01,     Monday = 0x02,     Tuesday = 0x04,     Wednesday = 0x08,     Thursday = 0x10,     Friday = 0x20,     Saturday = 0x40 } 

Also included is a custom editor for this type, which will allow you to choose enum values based on check boxes - this code is available in the download.

With the enumerated type defined, you can take an initial stab at the activity itself. Custom composite activities are typically derived from the CompositeActivity class, as this defines among other things an Activities property, which is a collection of all subordinate activities. The DateTime property, which is used when executing the activity, is also.

  public class DaysOfWeekActivity : CompositeActivity {     /// <summary>     /// Get/Set the day of week property     /// </summary>     [Browsable(true)]     [Category("Behavior")]     [Description("Bind to a DateTime property, set a specific date time,                   or leave blank for DateTime.Now")]     [DefaultValue(typeof(DateTime),"")]     public DateTime Date     {         get { return (DateTime)base.GetValue(DaysOfWeekActivity.DateProperty); }         set { base.SetValue(DaysOfWeekActivity.DateProperty, value); }     }     /// <summary>     /// Register the DayOfWeek property     /// </summary>     public static DependencyProperty DateProperty =         DependencyProperty.Register("Date", typeof(DateTime),             typeof(DaysOfWeekActivity)); } 

The Date property provides the regular getter and setter, and I've also added a number of standard attributes so that it displays correctly within the property browser. The code though looks somewhat different than a normal .NET property, as the getter and setter are not using a standard field to store their values, but instead are using what's called a DependencyProperty.

The Activity class (and therefore this class, as it's ultimately derived from Activity) is derived from the DependencyObject class, and this defines a dictionary of values keyed on a DependencyProperty. This indirection of getting/setting property values is used by WF to support binding; that is, linking a property of one activity to a property of another. As an example, it is common to pass parameters around in code, sometimes by value, sometimes by reference. WF uses binding to link property values together - so in this example you might have a DateTime property defined on the workflow, and this activity might need to be bound to that value at runtime. I'll show an example of binding later in the chapter.

If you build this activity, it won't do much - indeed it will not even allow child activities to be dropped into it, as you haven't defined a Designer class for the activity.

Adding a Designer

As you saw with the WriteLineActivity earlier in the chapter, each activity can have an associated Designer class, which is used to change the design-time behavior of that activity. You saw a blank Designer in the WriteLineActivity, but for the composite activity you need to override a couple of methods to add some special case processing.

  public class DaysOfWeekDesigner : ParallelActivityDesigner {     public override bool CanInsertActivities         (HitTestInfo insertLocation, ReadOnlyCollection<Activity> activities)     {         foreach (Activity act in activities)         {             if (!(act is SequenceActivity))                 return false;         }         return base.CanInsertActivities(insertLocation, activitiesToInsert);     }     protected override CompositeActivity OnCreateNewBranch()     {         return new SequenceActivity();     } } 

This Designer derives from ParallalActivityDesigner, which provides you with good design-time behavior when adding child activities. You will need to override CanInsertActivities to return false if any of the dropped activities is not a SequenceActivity. If all activities are of the appropriate type, then you can call the base class method, which makes some further checks on the activity types permitted within your custom activity.

You should also override the OnCreateNewBranch method that is called when the user chooses the Add Branch menu item. The Designer is associated with the activity by using the [Designer] attribute, as shown below:

 [Designer(typeof(DaysOfWeekDesigner))] public class DaysOfWeekActivity : CompositeActivity { }

The design-time behavior is nearly complete; however, you also need to add a class that is derived from ActivityToolboxItem to this activity, as that defines what happens when an instance of that activity is dragged from the toolbox. The default behavior is simply to construct a new activity; however, in the example you also want to create two default branches. The code shows the toolbox item class in its entirety:

  [Serializable] public class DaysOfWeekToolboxItem : ActivityToolboxItem {     public DaysOfWeekToolboxItem(Type t)         : base(t)     {         this.DisplayName = "DaysOfWeek";     }     private DaysOfWeekToolboxItem(SerializationInfo info, StreamingContext context)     {         this.Deserialize(info, context);     }     protected override IComponent[] CreateComponentsCore(IDesignerHost host)     {         CompositeActivity parent = new DaysOfWeekActivity();         parent.Activities.Add(new SequenceActivity());         parent.Activities.Add(new SequenceActivity());         return new IComponent[] { parent };     } } 

As shown in the code, the display name of the activity was changed, a serialization constructor was implemented, and the CreateComponentsCore method was overridden.

This method is called at the end of the drag-and-drop operation, and it is where you construct an instance of the DaysOfWeekActivity. In the code, you are also constructing two child sequence activities, as this gives the user of the activity a better design-time experience. Several of the built-in activities do this, too - when you drop an IfElseActivity onto the design surface, its toolbox item class adds two branches. A similar thing happens when you add a ParallelActivity to your workflow.

The serialization constructor and the [Serializable] attribute are necessary for all classes derived from ActivityToolboxItem.

The last thing to do is associate this toolbox item class with the activity:

 [Designer(typeof(DaysOfWeekDesigner))] [ToolboxItem(typeof(DaysOfWeekToolboxItem))] public class DaysOfWeekActivity : CompositeActivity { }

With that in place, the UI of your activity is almost complete, as can be seen in Figure 41-13.

image from book
Figure 41-13

Now, you need to define a property on each of the sequence activities shown in Figure 41-13, so that the user can define which day(s) the branch will execute. There are two ways to do this in Windows Workflow: you can create a subclass of SequenceActivity and define it there, or you could use another feature of dependency properties called Attached Properties.

I’ll use the latter method, as this means that you don’t have to subclass but instead can effectively extend the sequence activity without needing the source code of that activity.

Attached Properties

When registering dependency properties, you can call the RegisterAttached method to create an attached property. An attached property is one that is defined on one class but is displayed on another - so here you define a property on the DaysOfWeekActivity but that property is actually displayed in the UI as attached to a sequential activity.

The code in the snippet below shows a property called Weekday of type WeekdayEnum, which will be added to the sequence activities that reside within your composite activity.

  public static DependencyProperty WeekdayProperty =     DependencyProperty.RegisterAttached("Weekday",         typeof(WeekdayEnum), typeof(DaysOfWeekActivity),         new PropertyMetadata(DependencyPropertyOptions.Metadata)); 

The final line allows you to specify extra information about a property - in this instance, it is specifying that it is a Metadata property.

Metadata properties differ from normal properties in that they are effectively read only at runtime. You can think of a Metadata property as similar to a constant declaration within C#. You cannot alter constants while the program is executing, and you cannot change Metadata properties while a workflow is executing.

In this example, you wish to define the days that the activity will execute, so you could in the Designer set this field to “Saturday, Sunday”. In the code emitted for the workflow, you would see a declaration as follows (I have reformatted the code to fit the confines of the page).

  this.sequenceActivity1.SetValue   (DaysOfWeekActivity.WeekdayProperty,   ((WeekdayEnum)((WeekdayEnum.Sunday | WeekdayEnum.Saturday)))); 

In addition to defining the dependency property, you will need methods to get and set this value on an arbitrary activity. These are typically defined as static methods on the composite activity and are shown in the code below:

  public static void SetWeekday(Activity activity, object value) {     if (null == activity)         throw new ArgumentNullException("activity");     if (null == value)         throw new ArgumentNullException("value");     activity.SetValue(DaysOfWeekActivity.WeekdayProperty, value); } public static object GetWeekday(Activity activity) {     if (null == activity)         throw new ArgumentNullException("activity");     return activity.GetValue(DaysOfWeekActivity.WeekdayProperty); } 

There are two other changes you need to make in order for this extra property to show up attached to a SequenceActivity. The first is to create an extender provider, which tells Visual Studio to include the extra property in the sequence activity, and the second is to register this provider, which is done by overriding the Initialize method of the Activity Designer and adding the following code to it:

  protected override void Initialize(Activity activity) {     base.Initialize(activity);     IExtenderListService iels = base.GetService(typeof(IExtenderListService))         as IExtenderListService;     if (null != iels)     {         bool extenderExists = false;         foreach (IExtenderProvider provider in iels.GetExtenderProviders())         {             if (provider.GetType() == typeof(WeekdayExtenderProvider))             {                 extenderExists = true;                 break;             }         }         if (!extenderExists)         {             IExtenderProviderService ieps =                 base.GetService(typeof(IExtenderProviderService))                     as IExtenderProviderService;             if (null != ieps)                 ieps.AddExtenderProvider(new WeekdayExtenderProvider());         }     } } 

The calls to GetService in the code above to allow the custom Designer to query for services proffered by the host (in this case Visual Studio). You query Visual Studio for the IextenderListService, which provides a way to enumerate all available extender providers, and if no instance of the WeekdayExtenderProvider service is found, then query for the IExtenderProviderService and add a new provider.

The code for the extender provider is shown below:

  [ProvideProperty("Weekday", typeof(SequenceActivity))] public class WeekdayExtenderProvider : IExtenderProvider {     bool IExtenderProvider.CanExtend(object extendee)     {         bool canExtend = false;         if ((this != extendee) && (extendee is SequenceActivity))         {             Activity parent = ((Activity)extendee).Parent;             if (null != parent)                 canExtend = parent is DaysOfWeekActivity;         }         return canExtend;     }     public WeekdayEnum GetWeekday(Activity activity)     {         WeekdayEnum weekday = WeekdayEnum.None;         Activity parent = activity.Parent;         if ((null != parent) && (parent is DaysOfWeekActivity))             weekday = (WeekdayEnum)DaysOfWeekActivity.GetWeekday(activity);         return weekday;     }     public void SetWeekday(Activity activity, WeekdayEnum weekday)     {         Activity parent = activity.Parent;         if ((null != parent) && (parent is DaysOfWeekActivity))            DaysOfWeekActivity.SetWeekday(activity, weekday);     } } 

An extender provider is attributed with the properties that it provides, and for each of these properties it must provide a public Get<Property> and Set<Property> method. The names of these methods must match the name of the property with the appropriate Get or Set prefix.

With the above changes made to the Designer and the addition of the extender provider, when you click on a sequence activity within the Designer, you will see the properties in Figure 41-14 within Visual Studio.

image from book
Figure 41-14

Extender providers are used for other features in .NET - one common one is to add tooltips to controls in a Windows Forms project. When you add a tooltip control to a form, this registers an extender and adds a Tooltip property to each control on the form.




Professional C# 2005 with .NET 3.0
Professional C# 2005 with .NET 3.0
ISBN: 470124725
EAN: N/A
Year: 2007
Pages: 427

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net