Advanced design-time support for your custom workflow blocks - Part 2

In the previous article I've shown how to improve the design-time experience of your custom workflow blocks by creating a TypeDescriptor class and take advandage of the PropertyGrid control used by AgilePoint Envision to display and edit the properties of workflow blocks. I promised to continue on this topic and elaborate on how to add custom editors for your properties. This article is all about that.

Before we start, let me warn you again 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. This time, I also expect you to have some System.Windows.Forms awareness.

Setting up the project

For this article, I'm going to continue with the project we created last time. First off, we need to add an extra reference to our project:

  • System.Windows.Forms.dll
  • System.Drawing.Design.dll

Starting easy

As I mentioned at the end of the previous article, the ADAM Workflow API provides an easy shortcut for adding custom form editors to workflow block properties. A few default forms are also available, but you can roll your own as well.

We start by adding a new form named GuidCollectionEditorForm to the project, and have it derive from the WindowsFormsEditorForm class.


Image 1: Project context menu for adding a new Windows Form.

When switching back to the form designer after changing the base class, you should see that the basic controls for this type of form are already present:


Image 2: Screenshot of GuidCollectionEditorForm form designer in Visual Studio 2008.

You can design your form inside the panel in this form. I will add a listbox, a few labels and such, until I get roughly the following layout:


Image 3: Layout sketch of the GuidCollectionEditorForm form.

Now we will override two methods from the base class that we need for passing around the value of the target property: The LoadFormValue and SaveFormValue methods. My implementation here is quite basic, but you have the freedom to do whatever you want (like checking whether the provided string is an AgilePoint variable reference for instance) in here as long as the values being passed around are correct for the target properties.

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 override void LoadFormValue(object value)
{
    if (value == null)
    {
        return;
    }

    if (value.GetType() == typeof(string))
    {
        listBox.Items.AddRange(Array.ConvertAll(
            ((string)value).Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries),
            item => (object)GuidConverter.Parse(item)));
    }
}

public override object SaveFormValue()
{
    List<string> items = new List<string>();
    foreach (object item in listBox.Items)
    {
        items.Add(item.ToString());
    }

    return string.Join(";", items.ToArray());
}

We can use this editor form in AgilePoint Envision after decorating the JobIds property with the EditorAttribute attribute properly:

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<GuidCollectionEditorForm>), typeof(UITypeEditor))]
public string JobIds
{
    get { return GetPropertyValue("JobIds", string.Empty); }
    set { SetPropertyValue("JobIds", value); }
}

As a side note, I've mentioned before that a few designer forms are availbable out of the box. Below is a comprehensive list of those and when to use them (you can find them in the Adam.Workflow.AgilePoint.Components.Design namespace):

Class NameUsage
GuidListRefEditorFormCollection of unique identifiers (semicolon-separated), or a reference to a variable containing such a collection
GuidRefEditorForm Single unique identifier, or a reference to a variable containing a unique identifier.
StringEditorForm A single line string.
StringListRefEditorFormCollection of strings (semicolon-separated), or a reference to a variable containing such a collection.
StringRefEditorFormA single line string, or a reference to a variable containing such a string.
TextEditorForm Multiline text.

Table 1: Overview of all available editor forms.

Creating custom drop-down editors

In the previous article, we added a drop-down list with suggested values for the ThreadCount property. A more convenient way of allowing the user to select this would be through a slider control (see the TrackBar control) that will give the user more visual interaction.

To do this, we will start by creating a user control called SliderEditorControl with a single TrackBar control by right-clicking and clicking the Add\New User Control context menu item.


Image 4: Project context menu for adding a new User Control.

Then we drag in a TrackBar control from the toolbox and resize it so it will fit in our control. It's recommended to set the Anchor property of the control so it will resize nicely and you can add some other visuals if you like to. I ended up with this:


Image 5: Screenshot of the SliderEditorControl control designer in Visual Studio 2008.

Now we'll leave this control for now, and jump to the plumbing between our ThreadCount property and this editor we're creating. In order to tell the PropertyGrid it needs to show this editor instead of the default one, we need to use the EditorAttribute attribute on our property. This attribute will tell which type defines the editor for the target property, and which base type it uses.

However, we cannot simple hand over our user control's type to the editor, since it must derive from the UITypeEditor class. So first, we need to create a companion class for our SliderEditorControl control that inherits from the UITypeEditor class. Enter the SliderEditor class:

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
47
48
49
50
51
52
53
54
55
56
57
58
59
public sealed class SliderEditor : UITypeEditor
{
    private IWindowsFormsEditorService _EditorService;

    public override bool IsDropDownResizable
    {
        get
        {
            return false;
        }
    }

    public override UITypeEditorEditStyle GetEditStyle(ITypeDescriptorContext context)
    {
        // This tells the property grid to show the control as a drop-down item.
        return UITypeEditorEditStyle.DropDown;
    }

    public override object EditValue(ITypeDescriptorContext context, IServiceProvider provider, object value)
    {
        if (context != null)
        {
            if (context.PropertyDescriptor.PropertyType != typeof (int))
            {
                throw new InvalidOperationException(
                    "The SliderEditor editor can only be used on System.Int32 properties.");
            }
        }

        if (provider != null)
        {
            // Find the editor service on the provider.
            _EditorService = provider.GetService(typeof(IWindowsFormsEditorService)) as IWindowsFormsEditorService;
        }

        if (_EditorService != null)
        {
            SliderEditorControl control = new SliderEditorControl();

            if (context != null)
            {
                // We have defined a range custom attribute that specifies the
                // slider's range.
                RangeAttribute range = context.PropertyDescriptor.Attributes[typeof(RangeAttribute)] as RangeAttribute;
                if (range != null)
                {
                    control.Minimum = range.Minimum;
                    control.Maximum = range.Maximum;
                }
            }

            control.Value = (int)value;
            _EditorService.DropDownControl(control);
            value = control.Value;
        }

        return value;
    }
}

I have also added a RangeAttibute attribute that helps to set the range of the slider in our control. The source code for this attribute can be found in the attached project.

Now we can decorate our ThreadCount property with the EditorAttribute attribute like shown here:

C#
1
2
3
4
5
6
7
8
9
10
[Category("Behavior")]
[Description("Specifies the number of threads used by the maintenance manager if multithreading is allowed.")]
[DefaultValue(1)]
[Range(1, 20)]
[Editor(typeof(SliderEditor), typeof(UITypeEditor))]
public int ThreadCount
{
    get { return GetPropertyValue("ThreadCount", 1); }
    set { SetPropertyValue("ThreadCount", value); }
}

Now, after removing the custom TypeConverter from the previous article in our custom TypeDescriptor, behold the SliderEditor in all its glory:


Image 6: Screenshot of the SliderEditor in action in AgilePoint Envision.

Further notes

Most of what we've achieved during this article series is done using the plain old System.ComponentModel and System.Windows.Forms namespaces. There's much more that can be explored than we can do blog posts on. If you're interested in the matter, here's the one-stop source for all information you need: http://msdn.microsoft.com/en-us/library/37899azc.aspx.

So from now on, you can improve the design-time support for your workflow blocks, and by extension for all controls you write (most of this information also applies to Windows controls and Web controls).

Live long, and prosper.

Sample Code

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

Comments

Thursday, 23 September 2010Wouter Demuynck says
As noted by Livien from our partner StyleLabs, the base class to derive from when implementing custom WinForms editors is not WindowsFormsEditorBase, but WindowsFormsEditorForm. Sorry for the inconvenience. I have modified the original blog article.
Leave a comment
You must be logged in to post comments.
Sign in now
 
 
Technical
Business
rss feed