(WF4) Hosting the WorkflowDesigner (or other WPF stuff) in a separate AppDomain


A little while I begin a new rehosted designer project, and this time I want to get a little bit more creative with it, mixing in some of the stuff I have done earlier with XAML compilation. The more functionality like that I add, the closer to the Visual Studio scenario the scenarios it is capable of would become. And unlike the standard rehosted designer sample, Visual Studio hosts the Workflow Designer in its own app domain. There are a reasons for this, and also a few consequences of this design.

The main reason Visual Studio hosts WorkflowDesigner in a new AppDomain is that the assemblies it loads are subject to change. OK that’s only half a reason. The other half of the reason is that you can’t unload assemblies without unloading a whole AppDomain. Subject to change as in, you’re going to hit compile, and suddenly the assemblies your workflow depends on transformed themselves. Kewl.

Visual Studio hosts the WorkflowDesigner in a new AppDomain following the Visual Studio AddIn model (which apparently dates back to VS 2008, and since became included in the .NET Framework as System.AddIn). The System.AddIn way of doing things is quite well documented here on MSDN. The way I found out all this today however is by coming across an old blog post all about hosting WPF controls isolated across AppDomain boundaries… here:

AppDomain Isolated WPF Add-Ins [Jesse Kaplan]

The site has a link to a codeplex sample, so I tried out, and found it to be a pretty sweet demo – it worked for me. (Except for some security exception related to the fact it is hosting with partial trust . Not a big deal, I’m happy to do full-trust if I’m going to do something like this…)

And so I started looking into really doing this, and figuring out what is going on…

And I eventually realized that really, following the System.AddIn model is just a pain in the ass if I’m not actually building an extensible app. (Why? Well, you need not just a contract, but you need to have a special folder structure, you have to load AddIns using Tokens, which are got by an automated API which you tell to grok your folder structure, only there is no indication of whether you are doing anything right or not, and so your app doesn’t work unless you read and follow all the documentation very carefully, and in order to do that you’re going to be doing a LOT of reading, and as far as I can tell creating a lot of types and assemblies just to folow the AddIn model and for no other really good reason…???)

Yes, extensibility is not actually a goal for me, all I want to do is host the workflow designer in a second AppDomain so that I can Unload() it at will.

So… does that mean I have to research creating a HWndHost in order to wrap up my WorkflowDesigner? I’m feeling a wee bit sad at the prospect… but wait… I just wrote all this code to try out the AddIn thing that doesn’t work, but what if… what if I just create the AppDomain and instantiate the AddIn object myself, will that work?? Oh…. it does???? Hmmm…

Wait, this is good!

“What is!?”, you ask? Well, let’s rewind.

If you skim read the MSDN documentation on creating Isolated WPF AddIns following the System.AddIn model… (read it if you want…)

…then you (like me) might just walk away with the impression that there is this magical class FrameworkElementAdapters which will solve all your WPF+AppDomain hosting problems, if only you follow the System.AddIn way of creating your AppDomain. Since that is what this document is all about.

But come on, look at the call stacks when we run – this is just garden variety remoting! What’s it doing? (Quote)

At the highest level, as we’ve seen, WPF enables .NET Framework add-ins to implement UIs (that derive directly or indirectly from FrameworkElement) using INativeHandleContract, ViewToContractAdapter and ContractToViewAdapter. The result is that the host application is returned a FrameworkElement that is displayed from UI in the host application.

For simple UI add-in scenarios, this is as much detail as a developer needs. For more complex scenarios, particularly those that try to utilize additional WPF services such as layout, resources, and data binding, more detailed knowledge of how WPF extends the .NET Framework add-in model with UI support is required to understand its benefits and limitations.

Fundamentally, WPF doesn’t pass a UI from an add-in to a host application; instead, WPF passes the Win32 window handle for the UI by using WPF interoperability. As such, when a UI from an add-in is passed to a host application, the following occurs:

  • On the add-in side, WPF acquires a window handle for the UI that will be displayed by the host application. The window handle is encapsulated by an internal WPF class that derives from HwndSource and implements INativeHandleContract. An instance of this class is returned by ViewToContractAdapter and is marshaled from the add-in’s application domain to the host application’s application domain.

  • On the host application side, WPF repackages the HwndSource as an internal WPF class that derives from HwndHost and consumes INativeHandleContract. An instance of this class is returned by ContractToViewAdapter to the host application.

HwndHost exists to display UIs, identified by window handles, from WPF UIs. For more information, see WPF and Win32 Interoperation.

In summary, INativeHandleContract, ViewToContractAdapter, and ContractToViewAdapter exist to allow the window handle for a WPF UI to be passed from an add-in to a host application, where it is encapsulated by a HwndHost and displayed the host application’s UI.

NoteNote

Because the host application gets an HwndHost, the host application cannot convert the object that is returned by ContractToViewAdapter to the type it is implemented as by the add-in (for example, a UserControl).

By its nature, HwndHost has certain limitations that affect how host applications can use them. However, WPF extends HwndHost with several capabilities for add-in scenarios. These benefits and limitations are described below.

OK… that’s gibberish mate, show me the code! Basically, here is how we will add a WorkflowDesigner, in a secondary AppDomain, to a grid in our primary AppDomain:

private void LoadDesignAppDomain()

{

    AppDomainSetup adSetup = new AppDomainSetup

    {

        LoaderOptimization = LoaderOptimization.MultiDomainHost,

    };

    AppDomain childDomain = AppDomain.CreateDomain("DesignerDomain", null, adSetup);

    this.designerAdapter = new DesignerHostAdapter((IDesignerContract)childDomain.CreateInstanceAndUnwrap(typeof(DesignerAddIn).Assembly.FullName, typeof(DesignerAddIn).FullName));

 

    FrameworkElement designSurface = this.designerAdapter.GetDesignView();

    Grid.SetColumn(designSurface, 1);

    appGrid.Children.Add(designSurface);

}

Where’s the FrameworkElementAdapters magic? It’s hidden behind a thin curtain.

In the overall design here it’s similar to the System.AddIn style where there is a ‘HostAdapter’ class to provide a ‘host-friendly’ API wrapping up the actual remoting contract, in a little class just barely doing enough to pull it’s own weight:

class DesignerHostAdapter

{

    IDesignerContract proxy;

 

    public DesignerHostAdapter(IDesignerContract proxy)

    {

        this.proxy = proxy;

    }

 

    public FrameworkElement GetDesignView()

    {

        return FrameworkElementAdapters.ContractToViewAdapter(this.proxy.GetDesignView());

    }

 

    public FrameworkElement GetPropertyGrid()

    {

        return FrameworkElementAdapters.ContractToViewAdapter(this.proxy.GetPropertyGrid());

    }

 

    public void Load(string file)

    {

        this.proxy.Load(file);

    }

 

    public void Save(string file)

    {

        this.proxy.Save(file);

    }

}

IDesignerContract is an interface, with the obvious methods:

 

interface IDesignerContract : /*IContract*/

{

    INativeHandleContract GetDesignView();

    INativeHandleContract GetPropertyGrid();

    void Load(string file);

    void Save(string file);

}

Note that while using FrameworkElementAdapters I haven’t yet found any actual need to do the System.AddIn magic invocations of inheriting ‘IContract’ and using ‘AddInContract’ attribute. And inheriting MarshalByRefObject instead of ContractBase appears to work also.

 

public class DesignerAddIn : MarshalByRefObject, /*ContractBase,*/ IDesignerContract

{

    WorkflowDesigner designer;

    DesignSurfaceHost designerHost;

 

    public DesignerAddIn()

    {

        new DesignerMetadata().Register();

        this.designer = new WorkflowDesigner();

        this.designerHost = new DesignSurfaceHost(designer);

    }

 

    public System.AddIn.Contract.INativeHandleContract GetDesignView()

    {

        return FrameworkElementAdapters.ViewToContractAdapter(this.designerHost);

    }

 

    public System.AddIn.Contract.INativeHandleContract GetPropertyGrid()

    {

        return FrameworkElementAdapters.ViewToContractAdapter((FrameworkElement)this.designer.PropertyInspectorView);

    }

 

    public void Load(string file)

    {

        this.designer.Load(file);

    }

 

    public void Save(string file)

    {

        this.designer.Save(file);

    }

}

DesignSurfaceHost is just a simple <Page x:Class> class which will inherit FrameworkElement (so that it’s legal to pass to FrameworkElementAdapters), and hold the WorkflowDesigner.View as content. And there we have it, rehosting the designer in its own AppDomain, in far less code than I had imagined it would take. With thanks to whoever invented the FrameworkElementAdapters class!

(And note, of course you have to do DesignerMetadata().Register in the WorkflowDesigner app domain, not the primary app domain. Second note, I’m still pretty early along with this, no idea yet what bugs are waiting to be found in above code.)

Comments (4)

  1. Notre says:

    Hi Tim,

    I totally agree that the System.AddIn model is overkill if you're not building an extensible app; it was very confusing to follow when I tried to understand the vanilla VS editor's hosting of the workflow designer.

    Another reason to load WF designer in a separate app domain is because there is actually a memory leak right now in the WF designer, and the only workaround is to load the designer in a separate app domain.  Please see:

    social.msdn.microsoft.com/…/6e69566f-5b46-4d17-80b8-c2e73e5207a2

    Thanks

  2. Olly Brown says:

    Hi Tim, i found your article very helpfull. I did however come up against a stumbling block that i was wondering you could maybe help with? Its around the Validation service associated with my workflow designer. When the designer is hosted in a seperate App domain as you have shown i am unable to associate the validation service and subsequently update the UI (a simple list box) with the outstanding errors in my main app domain. The errors i am getting is because the binding objects are not serializable and subsequently cannot be passed accross the API domain.

    Any help or if you need a better explanation of my problem please ask.

    Thanks in advance,

    Olly

  3. tilovell09 says:

    Hi Olly,

    I think you will probably need to host your validation service and other designer services in the designer app domain, since the service contracts weren't designed with remoting-compatibility in mind.

    Tim

  4. SupaDupa says:

    I love you! You just fixed the issue I was trying to solve for a couple of days now!!!!!

Skip to main content