Why can’t I change the TypeArgument of Switch<> or FlowSwitch<> using the property grid?

I don’t remember anyone ever actually asking me this, but I found myself wondering the question today while rustling up a quick rehosting app.

Foreach<> activity lets you change the TypeArgument dynamically via the property grid. But FlowSwitch<> doesn’t. Why?

image

In order to understand, let's investigate how the TypeArgument property works. As usual, I like investigating issues like this in a practical problem solving way: is it possible for us to ‘Add’ a TypeArgument property in the property grid for FlowSwitch<> in the same way that ForEach<> does?

By the way, the first thing to understand is that that the ForEach<> class doesn't actually have a CLR property called TypeArgument (link is to MSDN so you can check). So how come we can see it in the property grid?

If you do some disassembly diving you can see exactly what is going on. The activity designer class for FlowSwitch<T> calls into a bunch of of internal classes - like FeatureAttribute, UpdatableGenericArgumentsFeature and GenericArgumentUpdaterService.  And it is by using these classes that it adds the TypeArgument property to the ForEach<> activity's property grid. But if we keep assembly diving a little bit deeper, and look at how those classes work, we can also figure out that none of those internal classes really matters. They are just helper classes for using a regular public API: AttachedPropertiesService.

What is an AttachedPropertiesService? And how is it being used here?

I'm not going to do a full explanation here, because for general intro on AttachedPropertiesService, Matt’s venerable post is already a good reference. The executive summary is that it allows you to do is add extra, 'attached' properties on any Activity type being edited in WorfklowDesigner. This is a very cool feature, both flexibile and powerful. You want to add a new, fake property on a type you don't own, like a built-in framework type? Yes you can!

So how would we go about doing the same; how would we add a 'TypeArgument' property to FlowSwitch<>, using this API?

 

Step 1: Register the Attached Property

public static void MakeGenericArgumentUpdatable(EditingContext context)

{

    AttachedPropertiesService aps;

    AttachedProperty property;

           

    aps = context.Services.GetService<AttachedPropertiesService>();

    property = new AttachedProperty<Type>()

    {

        IsBrowsable = true,

        Name = "TypeArgument",

        Getter = GetTypeArgument,

        Setter = SetTypeArgument,

        OwnerType = typeof(FlowSwitch<>),

    };

    aps.AddProperty(property);

}

 

We passed in an EditingContext from the WorkflowDesigner where we want this attached property to be registered in. The generic type of our attached property is AttachedProperty<Type>, because the type of the virtual property we are creating should be System.Type.

[Note: In your rehosted designer application you can easily get the editing context like this:

WorfklowDesigner wd = new WorkflowDesigner();
EditingContext context = wd.Context;

But probably here you want to tie the attached properties to your Activity Designer class, and not your rehosted application, you would do it in a slightly different way - override ActivityDesigner.OnModelItemChanged(), and retrieve the ModelItem's editing context using the extension method GetEditingContext().]

Step 2: Implement the Getter

private static Type GetTypeArgument(ModelItem mi)

{

    object value = mi.GetCurrentValue();

    Type ret = value.GetType().GetGenericArguments()[0]; // Should be first generic argument of FlowSwitch<T>

    return ret;

}

 

Just pull apart the ModelItem assuming it is a FlowSwitch<X>, and figure out what X is.

 

Step 3: Implement the Setter

 

private static void SetTypeArgument(ModelItem oldModelItem, Type value)

{

           

    Type newItemType = typeof(FlowSwitch<>).MakeGenericType(value);

    object newItem = Activator.CreateInstance(newItemType);

    ModelItem newModelItem = WrapAsModelItem(oldModelItem.GetEditingContext(), newItem);

           

    // use the somewhat magical MorphHelper

    MorphHelper.MorphObject(oldModelItem, newModelItem);

    return;

}

 

private static ModelItem WrapAsModelItem(EditingContext context, object obj)

{

    return context.Services.GetService<ModelTreeManager>().CreateModelItem(null, obj);

}

 

This bit is the weirdest. MorphHelper.MorphObject is being used to replace one ModelItem in the Model Tree with another, and simultaneously updates all references to the old model item. This is a generally useful trick for ‘swapping in’ one object to replace another in the model tree.

There’s some remaining wrinkles… none of the properties of the old FlowSwitch<> got copied over the new one, ack! Also, the viewstate which tracks the FlowSwitch<>’s position in the designer is lost. Ideally we would also implement that copying behavior in our SetTypeArgument function, so that we don’t unexpected lose flowchart connections whenever someone changes their TypeArgument in the property grid…

You can use MorphHelper.MorphProperties as a best-effort implementation of automatic property cloning. But in this case it’s kind of hard not to lose data, because the type data on the FlowSwitch links is dependent on the type of FlowSwitch<>.

Which is probably the real reason that FlowSwitch<> doesn’t currently have a TypeArgument property.