(WF4,VS) WorkflowDesigner Extensions in Visual Studio 2012

(Intro: Sometimes as part of testing I go to silly lengths to try to point out what I think is going to be a bug in a piece of code I have seen. And frequently when I do this, I am basically wrong, for any of various reasons – it’s a bug according to some world view which is not quite aligned with reality, or whatever. Overall I think I tend to frustrate some people when I go through this exercise, but an end result is often that I learn something...)

I just emerged from the testing vaults with a lost feature to share. Smile

In VS 2010 there wasn’t a good customization story for the workflow designer in Visual Studio. What do I mean customization story? Let me try to think of some motivating examples.

Example #1: You want to customize Workflow Designer inside VS, by registering a new custom property editor for a system or custom Type. For sake of example e.g. Nullable<T>.

Example #2 You want VS to provide a new Service to your custom designers via EditingContext.Services – but the service will actually be hosted in Visual Studio’s main appdomain, and its data will persist throughout a VS session, and be shared with all WorkflowDesigner instances.

These scenarios are not supported in VS 2010. However, what I found out so far is that Example #1 and Example #2 appear to be supported in VS 2012. Now I’m calling this a lost feature because I couldn’t find any official docs for this, so these are what I picked up from talking to developer Tony, and disassembling certain assemblies.

The Tip

Here’s the basic hot tip that set me off on this hunt and summarizes the feature. Apparently there’s a directory you can place workflow designer extension DLLs in, that Visual Studio 2012 will find them somehow. On 64-bit windows OS, you’ll see it’s typically here:

C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\Extensions\WorkflowDesigner

Tony also sent me an example code which is just an ordinary C# class with a MEF exported interface:

[Export(typeof(IEntityInfoService))] // note: IEntityInfoService - this is just some custom type for example, what type really doesn’t matter

Aside from this, his sample had two features which let me know the class might be hosted in the main VS app domain:

1) it imports DTE, which is the core Visual Studio programming interface. A DTE object gives you access to many things in VS such as the VS solution tree

[Import]
public DTE Dte { get; set; }

2) it inherits MarshalByRefObject – this means we can hold object references to the object from another domain, and that it will inherit stuff you need for .Net remoting lifetime management: GetLifetimeService, etc.

Another interesting find, while trying to understand the tip

In Microsoft.VisualStudio.Activities.AddIn.WorkflowDesignerAddIn, there is code which uses an internal class (AddInWorkflowDesignerExtensionManager) to discover extensions which implement IRegisterMetadata. You know, that interface for registering custom designer stuff. For each such extension found, the designer add-in calls Register() straight away.

Last find
Further in the ‘how it might work’ category – there’s another internal class that VS uses called WorkflowDesignerExtensionManager, which appears to be there for allowing Services (as in EditingContext.Services) to be discovered by the WorkflowDesigner or custom activity designers. It leverages the MEF API (and DirectoryCatalog in particular) to discover extensions from the directory Tony mentioned! Note, I heard a long time back that MEF was also planned to be key part of the extensibility story for Visual Studio 2012… looks like this sort of fits into that strategy.

Trying stuff…

Pieces of the puzzle are falling into place nicely. So it’s time to try some stuff out!

1) Create a custom dll, implement and Export (MEF) the IRegisterMetadata interface, and see if I can get called by Visual Studio. I’m guessing I don’t need MarshalByRefObject in this scenario, since the MetadataStore is generally in the WorkflowDesigner’s AppDomain.

2) Create  a custom dll, implement and Export a custom interface, IFooService to a MarshalByRef object. Get and call the service from a custom activity designer, which will be loaded in one of the usual ways.

 

Since Visual Studio 2010 doesn’t (in my experience) do a very good job of debugging Visual Studio 2012, I’ll be creating my test projects in Visual Studio 2012.

namespace MetadataExtension

{

    [Export(typeof(IRegisterMetadata))]

    internal class RegisterMetadata : IRegisterMetadata

    {

        public RegisterMetadata()

        {

        }

 

        public void Register()

        {

            MessageBox.Show("Hello... and goodbye sequence designer");

            var builder = new AttributeTableBuilder();

            builder.AddCustomAttributes(typeof(Sequence),

                new DesignerAttribute(typeof(ActivityDesigner)));

            MetadataStore.AddAttributeTable(builder.CreateTable());

        }

    }

}

I copy the debug output dll to C:\Program Files (x86)\Microsoft Visual Studio 11.0\Common7\IDE\Extensions\WorkflowDesigner, and launch a new instance of Visual Studio 2012. I create a new workflow activity library project, and … success!

I have just succeeded in pranking myself – the Sequence activity is now totally unusable:

image

Of course while you could use this trick to prank your WF-using office mates, I don’t recommend it. We’d better think of some more constructive use for that feature. Smile
Here’s the call stack when the metadata extension gets called.

    METADATAEXTENSION.dll!MetadataExtension.RegisterMetadata.Register() Line 24    C#
     Microsoft.VisualStudio.Activities.Addin.dll!Microsoft.VisualStudio.Activities.AddIn.WorkflowDesignerAddIn.ExecuteRegisterMetadataExtensions() + 0x82 bytes   
     Microsoft.VisualStudio.Activities.AddinAdapter.dll!Microsoft.VisualStudio.Activities.AddInAdapter.IDesignerContractToViewAddInAdapter.ExecuteRegisterMetadataExtensions() + 0xc bytes   
     mscorlib.dll!System.Runtime.Remoting.Messaging.StackBuilderSink.SyncProcessMessage(System.Runtime.Remoting.Messaging.IMessage msg) + 0x1e7 bytes   
     […more remoting stuff…]
     mscorlib.dll!System.Runtime.Remoting.Proxies.RemotingProxy.InternalInvoke(System.Runtime.Remoting.Messaging.IMethodCallMessage reqMcmMsg, bool useDispatchMessage, int callType) + 0x1cc bytes   
     mscorlib.dll!System.Runtime.Remoting.Proxies.RemotingProxy.Invoke(System.Runtime.Remoting.Messaging.IMessage reqMsg) + 0x66 bytes   
     mscorlib.dll!System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(ref System.Runtime.Remoting.Proxies.MessageData msgData, int type) + 0xea bytes   
     Microsoft.VisualStudio.Activities.HostAdapter.dll!Microsoft.VisualStudio.Activities.HostAdapter.IDesignerViewToContractHostAdapter.ExecuteRegisterMetadataExtensions() + 0xc bytes   
     Microsoft.VisualStudio.Activities.dll!Microsoft.VisualStudio.Activities.EditorPane.CreateWorkflowDesignerInIsolatedMode() + 0x2cb bytes   
     Microsoft.VisualStudio.Activities.dll!Microsoft.VisualStudio.Activities.EditorPane.Microsoft.VisualStudio.Shell.Interop.IPersistFileFormat.Load(string fileName, uint formatMode, int readOnly) + 0xda bytes   
     Microsoft.VisualStudio.Activities.dll!Microsoft.VisualStudio.Activities.EditorPane.Microsoft.VisualStudio.Shell.Interop.IVsPersistDocData.LoadDocData(string documentName) + 0xd bytes   
    
From this stack we can notice that
1) our RegisterMetadata extension being called is very early indeed in the process of creating and setting up the Workflow Designer
2) it’s triggered by Visual Studio which is specifically calling for such extensions to be loaded, so it doesn’t apply for rehosted apps. Which is fine. In a rehosted app we already are able to control and hook into the workflow designer creation process.

The second example is just slightly more work.

Part #1: Define a contract

    public interface ICustomContract

    {

        void Hello();

    }

Part #2: Define an extension

    [Export(typeof(ICustomContract))]

    public class CustomContractExtension : MarshalByRefObject, ICustomContract

    {

        public CustomContractExtension()

        {

        }

 

        public void Hello()

        {

            MessageBox.Show("Hello");

            MessageBox.Show("Got DTE? " + (this.Dte != null));

        }

 

        [Import]

        public DTE Dte { get; set; }

    }

Now we can place our extension dll in the Extensions\WorkflowDesigner directory, fire up a new instance of VS 2012, and see if anything happens.

This time, nothing happens when we create a new class library. We need a couple more steps.

1) Add a new code activity

2) Add a new activity designer

3) Associate them (today I’ll do it the quickest way, using DesignerAttribute)

    [Designer(typeof(ActivityDesigner1))]

    public sealed class CodeActivity1 : CodeActivity

    {

        protected override void Execute(CodeActivityContext context)

        {

        }

    }

4) Add a reference to the DLL where we defined ICustomContract – we can browse it from the Extensions folder

5) Modify our activity designer slightly, in the .xaml.cs file:

    public partial class ActivityDesigner1

    {

        public ActivityDesigner1()

        {

            InitializeComponent();

        }

 

        protected override void OnModelItemChanged(object newItem)

        {

            base.OnModelItemChanged(newItem);

            if (newItem is ModelItem)

            {

                var cc = (newItem as ModelItem).GetEditingContext().Services.GetService<ICustomContract>();

                if (cc != null)

                {

                    cc.Hello();

                }

            }

        }

    }

 

The important point here is that we will call EditingContext.Services.GetService<ICustomContract>() in order to get the custom service created above.

When we try it out, we will see our popup dialogs:

image

image

Interesting and important to note - if you set breakpoints in the constructor of CustomContractExtension, then you will see that it is being created lazily, upon demand.

Also interesting and important to note – the CustomContractExtension is indeed created in the main Visual Studio App Domain. So calls to it will work calling via .Net Remoting. Now it happens that there is no reference to that object in the main VS AppDomain, which leaves it subject to potential garbage collection. Ideally WorkflowDesigner or the EditingContext would create a remoting Sponsor in order to extend the life time of the object to match the lifetime of the EditingContext.Services object. However, in practice this does not happen. Leaving Visual Studio for idle for a few minutes, you can come back, try to use your custom activity again, and get errors such as:

image

Unfortunately I haven’t figured out the right way to stop Garbage collection blowing this up yet, but I think there are a couple approaches that could work: 1) sponsorship as mentioned above, 2) forced  reachability to the service itself by ensuring references from a GC root in its home app domain.