WPF: Hosting WinForms Control from Another AppDomain

The Problem

How do you host a Windows Forms control in a WPF application? You use WindowsFormsHost class. But what if the Windows Forms control is in another app domain? It is still possible to host it, but it's not trivial. I could find bits and pieces of the solution on the Itnernet, but not a complete sample, so I am publishing one.

Download sample (40K)
Browse CrossDomainHost.cs (1.5K)

Why Different App Domains?

I needed to embed a Windows Forms application of medium complexity in a WPF application. Embedding a Form in another window is asking for trouble, so I converted most of the legacy application to a WinForms UserControl and tried to host it in my WPF application.

I wanted to run the legacy code in its own AppDomain for a number of reasons:

  1. Legacy code can use its own configuration file.
  2. It can be loaded and unloaded at will.
  3. There is a lower chance that its assemblies will conflict with my assemblie.s
  4. It's easier to separate its exceptions from my exceptions.
  5. And so on, and so forth...

Naive Approach Fails

Windows Forms controls are derived from MarshalByRefObject, so they can be accessed across app domains. I was able to create my winforms control via Activator.CreateInstanceAndUnwrap(), but when I tried to attach it as a child to my WPF WindowsFormsHost like this:

myWinFormsHost.Child = remoteWinFormControl;

I received the following exception:

RemotingException: Remoting cannot find field 'parent' on type 'System.Windows.Forms.Control'

Naive approach fails

What Went Wrong?

When MarshalByRefObjects are accessed across app domain boundaries, only public interfaces are marshaled. Any attempt to access private fields of the remote object will fail. Assigning to WindowsFormsHost.Child appears to require access to private field parent of class System.Windows.Forms.Control, and this means that the WPF host control must be in the same app domain as the hosted windows forms control, i.e. in the secondary app domain.

The trouble is, WPF controls are not derived from MarshalByRefObject, so they cannot be directly accessed from another app domain. I searched the Internet for an answer to this problem, and I found that one can use so called "AddIn API" to access WPF controls in other app domains indirectly, via so called "contract interfaces".

The Battle Plan

Here's the sequence of operations that will lead us to victory:

  1. Create a Windows Forms Control in another app domain.
  2. Create a WindowsFormsHost object in the same app domain.
  3. Convert WindowsFormsHost to a marshallable interface INativeHandleContract using FrameworkElementAdaptersViewToContract() method from the System.Addin.Contract assembly.
  4. Install interface pointer in a uniquely named app domain data slot. There may be more sofisticated ways to make data available across app domain boundaries, but this one works just as well.
  5. Read the interface pointer from the main app domain.
  6. Convert it back to WPF control (FrameworkElement to be exact) and embed in our WPF UI.
  7. Profit!

This approach is much more complicated than the original, but it works. Here's the diagram:

Working approach

Show Me The Code

The hosting code is exactly 42 lines long (hm...) and looks as follows:

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
36:
37:
38:
39:
40:
41:
42:
using System;
using System.AddIn.Contract;
using System.AddIn.Pipeline;
using System.IO;
using System.Windows;
using System.Windows.Forms.Integration;
 
namespace HostingLib
{
    public static class CrossDomainHost
    {
        const string SlotName = "Slot.01F1C93D-90F9-4D8A-86E4-44BE215E6CAE";
 
        public static FrameworkElement CreateHost()
        {
            var location = typeof(CrossDomainHost).Assembly.Location;
            var thisDir = Path.GetDirectoryName(location);
            var pluginsDir = Path.Combine(thisDir, "Plugins");
 
            var setup = new AppDomainSetup
            {
                ApplicationBase = thisDir,
                PrivateBinPath = pluginsDir,
                ConfigurationFile = Path.Combine(pluginsDir, "WinFormApp.exe.config"),
                AppDomainInitializer = InitDomain
            };
 
            var domain = AppDomain.CreateDomain("PluginsDomain"null, setup);
            var contract = (INativeHandleContract)domain.GetData(SlotName);
            var control = FrameworkElementAdapters.ContractToViewAdapter(contract);
            return control;
        }
 
        private static void InitDomain(string[] args)
        {
            var winFormControl = (System.Windows.Forms.Control)AppDomain.CurrentDomain.CreateInstanceAndUnwrap("WinFormApp""WinFormApp.MainControl");
            var host = new WindowsFormsHost() { Child = winFormControl };
            var contract = FrameworkElementAdapters.ViewToContractAdapter(host);
            AppDomain.CurrentDomain.SetData(SlotName, contract);
        }
    }
}

This code reference 10 (ten) external assemblies, all of them required. That's one assembly per about 4 lines of code, which I think is quite impressive.

How Hosting Code Works

It closely follows the battle plan outlined above. After some setup steps, it creates an app domain (line 28) where the win forms code will be executed.. One of the arguments of this call is an AppDomainSetup instance. Among other things, it contains a pointer to a domain initializer method, in our case InitDomain(). This method is executed in the context of the newly created app domain.

InitDomain() method proceeds to create windows forms control (line 36), then wraps it in a WindowsFormsHost (line 37), then converts it to a marshallable INativeHandleContract (line 38). On line 39 the contract refrence is put in a named data slot, which is no more than a glorified global variable.

Once the domain is initialized, the flow of execution continues on line 29. It retrieves the (marshalled) contract pointer from the slot, and converts it to a WPF control (line 30) which exists in the context of the main app domain. This control is then returned to the caller (line 31).

Conclusion

Hosting a Windows Forms control from another appdomain is a little bit troublesome, but is certainly possible, as my sample demonstrates. When I face an annoying problem like that, it is really nice to find a "grab-and-go" code, that gets things done. So, whenever I can, I return the favor.

Feedback

Questions? Comments? Feel free to
Leave feedback