Designers for custom activities

When writing a custom activity you’ll typically want to provide a custom designer which is used when rendering your activity on the design surface. Visual Studio will look for designers using a couple of methods – one old, one new. In this post I’ll show how to use both methods and explain the differences.

Visual Studio has had the concept of designers from the first version of the framework, and this method uses the [Designer] attribute. To associate a designer with an activity you could write the following code…

 [Designer(typeof(CustomWriteLineDesigner))]
public class CustomWriteLine : CodeActivity
{
    protected override void Execute(CodeActivityContext context)
    {
        // Your code here...
    }
}

Here the [Designer] attribute is using typeof to associate the designer class with the activity. The problem here is that there’s a hard-link between the activity class and its designer – usually you would create a designer in a separate assembly so that at runtime you would not need to load up the assembly that contains the designer(s).

In order to provide a soft-link you can use another override of the [Designer] attribute which takes a fully qualified type-name as in the following…

 [Designer("CustomActivities.Designers.CustomWriteLineDesigner, CustomActivities.Designers")]
public class CustomWriteLine : CodeActivity
{
    protected override void Execute(CodeActivityContext context)
    {
        // Your code here...
    }
}

This ensures that the activity assembly doesn’t need to add a reference to the designers assembly so all is good. Well, sort of.

The problem with the above is that the designer is more or less hard-coded, and if you wished to add a different designer you would have a hard job doing this.

The new way

If you look at any of the standard activities that come with Workflow 4 you’ll notice a distinct absence of any [Designer] attributes, so how does Visual Studio find the right designer? With WF 4 we now support the concept of registering a designer in another class – as in all things, every problem in computing can be solved by adding another level of indirection.

In this case it’s the IRegisterMetadata interface which is defined in the System.Activities.Presentation.Metadata namespace. This interface has one simple method – Register() which is called by Visual Studio under certain circumstances in order to register the designer (and indeed any other attributes) for your activity.

If you use Reflector you’ll find several classes that derive from this interface and they’re typically called "DesignerMetadata”. If you’re hosting the WF designer yourself you need to add in the following call to ensure that the designers are registered for all standard activities…

 new DesignerMetadata().Register();

Internally the method constructs an instance of the AttributeTableBuilder class, constructs a number of custom attributes, adds these to the builder and then calls AddAttributeTable on the MetadataStore class…

 public class DesignerMetadata : IRegisterMetadata
{
    public void Register()
    {
        AttributeTableBuilder builder = new AttributeTableBuilder();

        builder.AddCustomAttributes(typeof(CustomWriteLine), 
            new DesignerAttribute(typeof(CustomWriteLineDesigner)));

        MetadataStore.AddAttributeTable(builder.CreateTable());
    }
}

In effect this code is simply using the TypeDescriptor class to add on a custom type provider, which is then used by the designer to find the [Designer] attribute (and possibly others) for any object added to the design surface. So, rather than have a hard reference from the Activity to its designer class, now we’re using an indirect method to register the designer. We’re still using the same attribute, but applying it in a separate assembly.

The nice thing here is that it allows you to completely change the look and feel of the standard activities as you can add in your own designer for any of them. Instead of calling DesignerMetadata.Register you can do the job yourself, and replace any standard designers with your own. I’d urge caution here – you might end up writing a load of designer code but it’s there if you wish to use it.

Integration with Visual Studio

Having gone to all the bother of creating your own class that derives from IRegisterMetadata, how do you tell Visual Studio to use this?

In this instance you need to do two things (and both are very important)…

  • If your creating CustomActivities.dll, create another assembly called CustomActivities .Design.dll
  • Make sure that both assemblies reside in the same directory

The last point may well trip you up – especially if you’re developing both assemblies in the same solution and you want the registration to update as you’re working. Even if you’re test project contains a reference to both dll’s the designer won’t update on the user interface in Visual Studio *unless* both assemblies build to the same directory (or rather, the *.Design.dll assembly builds to the same directory as the activity assembly).

The best way to ensure that the second point is sorted is to update the properties for the design assembly in Visual Studio and change the output path to be the same directory as your activity assembly…

CustomActivities

Make sure you alter the output path for both the Debug and Release builds. And I love writing documentation so I always provide a documentation file to go along with my code. Yes – seriously!

One more tweak

There’s one more thing you can do to alter the design time behaviour of your activities and that’s to create another assembly that contains designers, but this time you’ll call it CustomActivities .VisualStudio.Design.dll.

If Visual Studio finds this assembly it will use this in addition to the .Design.dll. So, you could have one designer for mere mortals and another for developers. Maybe there are some properties that you only want developers to access – one example might be you wish to provide developers with an expression text box to define the value for an argument, but provide just a regular text box to less sophisticated users.

You can download a sample project from https://www.morganskinner.com/stuff/designtimeactivities.zip which shows off the above. Change from a Debug/Release build to the Developer build and then reload the Workflow1.xaml file to see a different designer.