Advanced design-time support for your custom workflow blocks

In this article I will show you how you can improve the workflow designer experience by leveraging the power of the PropertyGrid control used by AgilePoint Envision to display and edit the properties of workflow activities.


Image 1: Screenshot of the AgilePoint Envision property grid.

Before we start, let me warn you that I will be assuming somewhat advanced knowledge of the .NET component model (see the System.ComponentModel namespace) as well as development of AgilePoint (and ADAM Workflow) building blocks.

Setting up the project

Using Visual Studio 2008, create a new class library project named Adam.Workflow.CustomActivities and add references to the following assemblies:

  • Adam.Core.dll
  • Adam.Tools.dll
  • Adam.Workflow.Core.dll
  • Adam.Workflow.Core.Design.dll
  • Adam.Workflow.AgilePoint.dll
  • Adam.Workflow.AgilePoint.Components.dll
  • Ascentn.Workflow.Share.dll
  • Ascentn.Workflow.WFBase.dll

Creating a RunMaintenanceJobs block

As an example, we will create a block that runs ADAM pending maintenance jobs using the MaintenanceManager class.

We start by creating the actual activity class, which is invoked by AgilePoint Server when executing the building block. This article focusses on the descriptor class of the building block, so we won't go into the details of actually calling the MaintenanceManager class and executing the maintenance jobs here. This is left as an exercise to the reader (comments!).

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/// <summary>vides a building block for running pending maintenance jobs.
/// </summary>
[AgilePart(AgilePartName)]
public sealed class RunMaintenanceJobs : AutomaticWorkItem
{
    /// <summary>
    /// Contains the unique name of the building block.
    /// </summary>
    public const string AgilePartName = "Adam.Workflow.CustomActivities.RunMaintenanceJobs";

    /// <summary>
    /// The entry point of the building block called by the workflow system when the building
    /// block needs to execute.
    /// </summary>
    /// <param name="processInstance"></param>
    /// <param name="workItem"></param>
    /// <param name="api"></param>
    /// <param name="parameters"></param>
    [AgilePartDescriptor(typeof(RunMaintenanceJobsDescriptor))]
    [Description("Runs all pending maintenance jobs.")]
    public void Main(WFProcessInstance processInstance, WFAutomaticWorkItem workItem, IWFAPI api, NameValue[] parameters)
    {
        // Call the base class execute method, which will parse the parameters
        // and initialize the ADAM context.
        Execute(processInstance, workItem, api, parameters);
    }

    /// <summary>
    /// Executes the building block.
    /// </summary>
    /// <returns></returns>
    protected override bool OnExecute()
    {
        // TODO: Call the MaintenanceManager.Execute() method here.

        return true;
    }
}

As you already know, the design-time properties for the building block are provided by the descriptor class, which is linked to the building block by decorating the entry-point method with the the AgilePartDecriptorAttribute attribute.

Now we can add the RunMaintenanceJobsDescriptor class to our project, which is the descriptor class for our building block. This class basically lists all available properties on the building block, and allows for serializing the values set by the workflow designer into the workflow XML template.

Behold the initial code for the RunMaintenanceJobsDescriptor class. Note that we're being a nice citizen in the .NET component model and we decorate our properties accordingly with the CategoryAttribute, DescriptionAttribute and DefaultAttribute attributes.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
/// <summary>
/// Provides the design-time properties for the <see cref="RunMaintenanceJobs"/> building block.
/// </summary>
public sealed class RunMaintenanceJobsDescriptor : AutomaticWorkItemDescriptor
{
    [Category("Behavior")]
    [Description("Specifies whether or not notification is enabled.")]
    [DefaultValue(true)]
    public bool NotificationEnabled
    {
        get { return GetPropertyValue("NotificationEnabled", true); }
        set { SetPropertyValue("NotificationEnabled", value); }
    }

    [Category("Behavior")]
    [Description("Specifies the threading mode used for the maintenance manager.")]
    [DefaultValue(ThreadingMode.SingleThreading)]
    public ThreadingMode ThreadingMode
    {
        get { return GetPropertyValue("ThreadingMode", ThreadingMode.SingleThreading); }
        set { SetPropertyValue("ThreadingMode", value); }
    }

    [Category("Behavior")]
    [Description("Specifies the number of threads used by the maintenance manager if multithreading is allowed.")]
    [DefaultValue(1)]
    public int ThreadCount
    {
        get { return GetPropertyValue("ThreadCount", 1); }
        set { SetPropertyValue("ThreadCount", value); }
    }

    [Category("In Parameters")]
    [DisplayName("Job Ids")]
    [Description("Specifies the collection of ids of maintenance jobs to execute.")]
    public string JobIds
    {
        get { return GetPropertyValue("JobIds", string.Empty); }
        set { SetPropertyValue("NotificationEnabled", value); }
    }
}

Adding some seasoning...

Now we have a plain dull descriptor class, which does little more than just giving the user text fields to bluntly type in values. Let's take this to the next level and add some user experience to this block!

We will allow the user to set the threading mode to either SingleThreading or MultiThreading, and only when MultiThreading is selected, we will allow the user to set the number of threads that need to be started. Furthermore, we will provide a list of default values for the ThreadCount property that suggests values from 1 up to and including the processor count.

In order to do this, we will need to develop a custom TypeDescriptor that will modify the way the PropertyGrid control looks at our descriptor class at runtime.

First things first, we also need a custom TypeDescriptionProvider in order to link our TypeDescriptor to our descriptor class using the TypeDescriptionProviderAttribute attribute.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public sealed class RunMaintenanceJobsTypeDescriptionProvider : TypeDescriptionProvider
{
    private static readonly TypeDescriptionProvider DefaultProvider = TypeDescriptor.GetProvider(typeof(RunMaintenanceJobsDescriptor));

    public RunMaintenanceJobsTypeDescriptionProvider()
        : base(DefaultProvider)
    {
    }

    public override ICustomTypeDescriptor GetTypeDescriptor(Type objectType, object instance)
    {
        ICustomTypeDescriptor baseTypeDescriptor = base.GetTypeDescriptor(objectType, instance);

        return instance == null ? baseTypeDescriptor : new RunMaintenanceJobsTypeDescriptor(baseTypeDescriptor, instance);
    }
}

And apply that to our descriptor class:

C#
1
2
3
4
5
[TypeDescriptionProvider(typeof(RunMaintenanceJobsTypeDescriptionProvider))]
public sealed class RunMaintenanceJobsDescriptor : AutomaticWorkItemDescriptor
{
    // ...
}

Now we will create the RunMaintenanceJobsTypeDescriptor class and implement the rules for displaying/hiding the ThreadCount property. We will inherit from the CustomTypeDescriptor class and provide an overridden version of the GetProperties() method, that will allow us to modify the attributes on our descriptor class' properties, so we can ultimately hide them at will.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public sealed class RunMaintenanceJobsTypeDescriptor : CustomTypeDescriptor
{
    public RunMaintenanceJobsTypeDescriptor(ICustomTypeDescriptor parent, object instance)
        : base(parent)
    {
    }

    public override PropertyDescriptorCollection GetProperties(Attribute[] attributes)
    {
        PropertyDescriptorCollection baseCollection = GetProperties();
        PropertyDescriptor[] propertyDescriptors = new PropertyDescriptor[baseCollection.Count];
        baseCollection.CopyTo(propertyDescriptors, 0);

        for (int index = 0; index < propertyDescriptors.Length; index++)
        {
            PropertyDescriptor propertyDescriptor = propertyDescriptors[index];
            object instance = GetPropertyOwner(propertyDescriptor);

            if (instance != null && instance is RunMaintenanceJobsDescriptor)
            {
                RunMaintenanceJobsDescriptor descriptor = (RunMaintenanceJobsDescriptor)instance;

                switch (propertyDescriptor.Name)
                {
                    case "ThreadCount":
                        List<Attribute> newAttributes = new List<Attribute>();

                        if (descriptor.ThreadingMode != ThreadingMode.MultiThreading)
                        {
                            // We're programatically adding a Browsable(false) attribute to the property descriptor 
                            // which will hide the property in the property grid.
                            newAttributes.Add(new BrowsableAttribute(false));
                        }

                        propertyDescriptors[index] = TypeDescriptor.CreateProperty(
                            instance.GetType(), 
                            propertyDescriptor, 
                            newAttributes.ToArray());
                        break;
                }
            }
        }

        return new PropertyDescriptorCollection(propertyDescriptors);
    }
}

The pièce de résistance

And now for our grand finale: we're going to show a drop-down list instead of the dull text box that comes with the ThreadCount property. Fortunately, we have done a lot of the work already, and we can just ad a TypeConverter deriving from Int32Converter and add it to our property descriptor.

By overriding the GetStandardValuesSupported, GetStandardValuesExclusive and GetStandardValues methods we can control which items are in the drop-down list and whether or not the user can provide custom values.

Please note that you cannot just add the TypeConverterAttribute property to the property in the descriptor class because of a limitation in AgilePoint Envision's PropertyGrid.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ThreadCountConverter : Int32Converter
{
    public override bool GetStandardValuesSupported(ITypeDescriptorContext context)
    {
        // We will provide a set of recommended default values.
        return true;
    }

    public override bool GetStandardValuesExclusive(ITypeDescriptorContext context)
    {
        // We will allow user values.
        return false;
    }

    public override StandardValuesCollection GetStandardValues(ITypeDescriptorContext context)
    {
        Collection<int> values = new Collection<int>();
        for (int index = 0; index < Environment.ProcessorCount; index++)
        {
            values.Add(index + 1);
        }

        return new StandardValuesCollection(values);
    }
}

Now we go back to our type descriptor and add the code to push our type converter to the ThreadCount property.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// ...

switch (propertyDescriptor.Name)
{
    case "ThreadCount":
        List<Attribute> newAttributes = new List<Attribute>();

        // We're going to add the TypeConverterAttribute programatically here, because the
        // AgilePoint Envision version of the PropertyGrid does not support adding this attribute
        // directly to the property in the descriptor class.
        newAttributes.Add(new TypeConverterAttribute(typeof (ThreadCountConverter)));

        if (descriptor.ThreadingMode != ThreadingMode.MultiThreading)
        {
        // We're programatically adding a Browsable(false) attribute to the property descriptor 
        // which will hide the property in the property grid.
            newAttributes.Add(new BrowsableAttribute(false));
        }

        propertyDescriptors[index] = TypeDescriptor.CreateProperty(
            instance.GetType(), 
            propertyDescriptor, 
            newAttributes.ToArray());
        break;
}

// ...

Further notes

Above all this, you can also add a UI editor to the properties using the EditorAttribute that comes with the System.ComponentModel namespace. While this will undoubtedly be the subject of another blog post, I will give you a short heads-up on how to use it. Fast forward to the RunMaintenanceJobDescriptor.JobIds property:

C#
1
2
3
4
5
6
7
8
9
[Category("In Parameters")]
[DisplayName("Job Ids")]
[Description("Specifies the collection of ids of maintenance jobs to execute.")]
[Editor(typeof(WindowsFormsEditor<GuidListEditorForm>), typeof(UITypeEditor))]
public string JobIds
{
    get { return GetPropertyValue("JobIds", string.Empty); }
    set { SetPropertyValue("JobIds", value); }
}

We have implemented a few common editor forms that you can reuse (like the one you see here), but you are free to build your own. More to come later!

That concludes our deep dive into building block development, hope you enjoyed it. Always looking forward to your comments, and as you know, you can download the source code below when you're logged on.

Happy coding!

Sample Code

The article contains sample code project(s).
You must be logged in to view or download sample code.
Sign in now

Comments

Wednesday, 25 August 2010Wouter Demuynck says

A side note for those who wonder where Adam.Workflow.Core.Design.dll is:

Due to a minor problem in the ADAM Workflow installer, this assembly is only placed in the GAC and not in the "Assemblies" folder under the installation directory. This will be fixed in a subsequent version. In the meanwhile, you can extract the DLL from the GAC using the copy command:

copy C:\WINDOWS\assembly\GAC_MSIL\Adam.Workflow.Core.Design\
1.8.0.0__3266306e8df4a2d3\Adam.Workflow.Core.Design.dll c:\temp

Thanks to Søren from our partner DXP for pointing this out!

Leave a comment
You must be logged in to post comments.
Sign in now
 
 
Technical
Business
rss feed