or: best practices registering Workflow Activity Designers

Getting your first custom Activity Designer to show up is not hard. There are plenty of samples to find. However, something mostly under-explained in such samples is that the way you get started with Activity Designers is not the best way to structure your code long term. For some of the same reasons, if you are an Activity vendor it’s also not the way we would recommend you would ship custom activities to a customer. What am I talking about?

Here is the typical Activity Designer getting started sample.

CustomActivities.csproj:    <Project …>
- CustomActivityDesigner.xaml:  <sap:ActivityDesigner x:Class=”CustomActivityDesigner” …>
   -CustomActivityDesigner.xaml.cs: public partial class CustomActivityDesigner {…}
- CustomActivity.cs:   [DesignerAttribute(typeof(CustomActivityDesigner))] public class CustomActivity : CodeActivity {…}

Is there anything wrong with this picture?

Layering

When designing a large software system, layering is a useful concept, and more than a concept – it is a great tool for keeping things manageable.

Manageable? It’s kind of a hard concept to appreciate, unless you’ve experienced the inverse scenario, which is day-to-day development in a huge and unmanageably complex software system. The enormous C++ solution with 40 projects where compiling takes forever because of header file importing header file importing header file. The systems where the user interaction code is riddled with calls into the bowels of every other system. The systems where there are too many threads and too many locks, and nobody can really tell you why it deadlocked. The bloated operating system where every new feature depends on old cruft from the previous version that depends on yet more old cruft, and booting into your shell takes 3 GB of RAM. The DLLs with circular dependencies that break when loaded in the wrong order. The app that is impossible to patch by patching one DLL - instead you have to ship a 200MB ‘update the world’ patch.  The systems where there are no rules and guidelines about who can call what. With a thousand colored links from every component to every other component, viewed from afar it appears to be a big, brown, smelly ball of mud.

Once you’ve experienced the unmanageable you will find some idea of manageableness – or manageability. Being a sort of general quality which will make the world simpler and help attain some other qualities that you desire. Debugability. Reliability. Performance. Testability. Maintainability. Reusability.

Layers are a useful tool for keeping things comprehensible and manageable. They let you limit dependencies and interactions to small, analyzable sets. But specifically, why might it be good to split the canonical activity library example into two layers? Because there are some ‘pro’s to having the two layers being two assemblies.

Two Assemblies, Two Layers

Think about that assembly structure we start off with. We have one assembly with two classes. Great! We’ve already got a layer, sort of, if we think of the classes as little layers. We can see two bodies of code which are separated according to their purpose and usage:

  • Activity code: The main purpose of the activity class is to do some work at runtime.
  • Designer code: The main purpose of the designer class is to make your activity good-looking and easy to configure.

We can also try to think of this in terms of when each body of code is needed.

  • Runtime: only the activity class is needed. There is no advantage to loading a designer class just in order for your activity to ‘wake up’, do a small unit of work, and go back to sleep.
  • Design time: primarily the designer class is needed. It’s possible to imagine a workflow designer system which allows a designer class that works in the absence of an activity being loaded. But specifically in WF4, the activity class will also be loaded at runtime. It just won’t be doing a whole lot, mostly property getting, a little property setting, and maybe having its CacheMetadata() function invoked.

We can also notice that each body of code will have different dependencies.

  • Activity code: Depends on the System.Activities assembly. Probably also depends on some assemblies specific for your activity’s runtime purpose, such as System.Data, System.ServiceModel, etc. Also there’s a bunch of other assemblies which System.Activities tends to load like System.Xaml and System.ComponentModel.
  • Designer code: Depends on System.Activities.Presentation, and therefore also depends on System.Activities, and all the assemblies needed for WPF: WindowsBase, PresentationCore, PresentationFoundation. Also could maybe depend on a few other things related to UI like System.Windows.Forms.

Assembly size and assembly dependency sets mainly affect two aspects of performance – memory usage, and thereby load time. First, if we have a lot of Activity Designer classes in the same assembly as our Activities, then the Activity assembly is larger overall. And we will almost certainly require more memory in order to load that merged assembly, than a split assembly with activity runtime types only. Secondly if we let our Activity class depend on a lot of WPF assemblies*, then all those assemblies may need to be loaded before the workflow can fully run. Which leads to probably an even larger runtime memory bloat impact.

With that second consideration in mind, you could choose to create this rule: “For performance reasons, Activity runtime code will not refer to presentation assemblies”.

Once we have a rule like this, it’s quite logical for you to choose to enforce the rule by splitting your Activity classes and your Activity Designer classes into two sets, and packaging each set in a separate assembly. This has advantages in terms of the first consideration too (size of assembly needed for runtime).

Are there any costs to this decision? Well, of course! (Any economist will tell you there is no such thing as a free lunch!)

  • Two assemblies is a little bit more work to build and package.
  • Activity classes can no longer do cool things to the UI… Wait, I’m joking - that shouldn’t be a cost! You could do be doing whatever it is from a designer class instead.
  • Your code can no longer be written the same convenient way, with [DesignerAttribute(typeof(ActivityDesigner))] used to associate activities and designers.

Are there any other possible advantages to the decision? Sure, maybe. Less code to deploy for runtime could sometimes be an advantage. Ability to ship activities to a customer (as part of product X) without shipping designers (which you just used to build X)? It’s pretty situational whether that kind of flexibility is useful.

Anyway, let’s explore the how-to.

The IRegisterMetadata Interface

There are two major scenarios that the WF team had when developing the workflow designer. Visual Studio, and everything else (rehosted).

In the Visual Studio scenario, the layered design above is enabled by you implementing the IRegisterMetadata interface. However, it has to be done in a specific way, which works not by hardcoding or configuration, but by a plug-in scheme/convention:

The IRegisterMetadata interface is called on implementations of IRegisterMetadata from an assembly XXXYYY.Design.dll found located in the same physical folder as XXXYYY.dll. It is called whenever XXXYYY.dll is loaded into the workflow designer. (The workflow designer is loaded in a separate AppDomain from the rest of VisualStudio.)

In the everything else scenario (i.e. your custom app that rehosts Workflow Designer) it’s up to you the app writer as to how you want to call IRegisterMetadata or whether you want to call it IRegisterMetadata. Your app can implement a similar convention, perhaps based on handling Assembly load events, perhaps based on something else, like assemblies in a particular folder you choose.

By the way, in order to possibly influence your decision: I think it would be pretty cool if all rehosted apps followed the same convention, so that your IRegisterMetadata interface works in every rehosted app, thereby making Activity Designer Libraries easier to reuse throughout the world. Just something to think about. Smile

As for what to put in IRegisterMetadata, there are quite a few resources that help with explaining that, I’ll link them below. (My job today was the why.)

    class Metadata : IRegisterMetadata

    {

        public void Register()

        {

            AttributeTableBuilder builder = new AttributeTableBuilder();

            // Register Designers.

            builder.AddCustomAttributes(typeof(Prompt), new DesignerAttribute(typeof(CustomActivities.Design.BasicDesignerWithStyling)));

            // Apply the metadata

            MetadataStore.AddAttributeTable(builder.CreateTable());

        }

    }

 

[Note: there is nothing to stop you from using this knowledge to make other things happen when your activities are loaded into Visual Studio (like modifying global designer state), but the effect can only occur once your custom activity’s assembly is loaded.]

Further Reading:

(And references mostly covering the same info but with less about the why)