A site devoted to discussing techniques that promote quality and ethical practices in software development.

Thursday, July 30, 2009

Cross AppDomain WinForm implementation

In a .Net solution, one often finds the use of plug-in pattern to support extensibility. This design pattern is so prevalent that the BCL team even provides its own add in architecture, supported by a number of namespaces prefixed by System.AddIn in Fx3.5 and is known as the Add-in pipeline.

One of the reasons for supporting Add-in loaded into a separate AppDomain is to allow the container to constrain the add-in with some form of security policy. Another reason, is to be able to unload all the assemblies used by the Add-in and to recover resources if it is being loaded into a separate AppDomain.

When these add-ins are just computational devices then one needs not take too much special care in programming them other than to ensure that the class is remotable across AppDomain. This generally means that it should derive from System.MarshalByRefObject.

However, if these add-ins are UI-rich devices using say System.Windows.Forms.Form, then one needs to take special precaution in programming them as Form object is not always marshal-safe.

This blog is to concentrate on this very issue - how to write UI-rich assemblies using WinForm that can live in a separate AppDomain?

What is the problem? Isn't a System.Windows.Forms.Form is a System.Windows.Forms.Control, which is in turns derived from System.MarshalByRefObject, and hence it should be able to marshal across Domain boundary? While all these are true but it does not necessary follow that a Form can be marshaled across a domain boundary.

In a private correspondence with Microsoft, this is their comment on this issue:
Windows Forms only supports isolating top-level windows via app domains. It does not support parent-child relationships across domains. Many people have assumed that because Control ultimately derives from MarshalByRefObject that it can successfully be remoted; this is not true. Certain interfaces on a control can be remoted across domains, but the control’s API itself does not support remoting. When you see exceptions stating that the object cannot be remoted because it isn’t serializable, what you’re seeing is that someone has tried to cast the remote proxy to Control.

If you scan the internet you will frequently encounter question asking why they have SerializationException when they try to remote WinForm across AppDomain.

Therefore, just because a Control supports MarshalByRefObject, you should not simply pass a Control across AppDomain. Sometimes this 'works' but it may not works all the time.

What is the recommended way?

According to Microsoft, you should always create an interface to allow you to operate the remoted WinForm from one app domain rather than casting the object obtained from AppDomain.CreateInstanceAndUnwrap() to a Form object. From the above statement, it is clear from Microsoft that it is this casting into WinForm, that is causing the exception.

Here is a sample code from Microsoft to illustrate this concept:

public partial class Form1 : Form, IDialog
{
public Form1()
{
InitializeComponent();
}

private void button1_Click(object sender, EventArgs e)
{
AppDomain d = AppDomain.CreateDomain("Second Domain");

IDialog dialog = (IDialog)d.CreateInstanceAndUnwrap(typeof(Form1).Assembly.FullName,
typeof(Form1).FullName);
dialog.ShowDialog();
AppDomain.Unload(d);

}
}

public interface IDialog
{
DialogResult ShowDialog();
}

The idea is not to cast to WinForm as shown in the above highlighted line.

If you need to pass one's Form from one app domain to another so that it can establish the parent-and-child or owner-owned relationship, you need to, according to Microsoft:

Generally I would stay way from passing Windows Forms object across domains unless you know what the other side of the domain is doing with the form or control. The ShowDialog API requires an IWin32Window to provide the handle (that’s what the parameter type for owner is setup to be). You can implement this with your own marshal by ref object and have it return the form’s Handle property.

The above statement was clarified further with Microsoft and the recommendation is to create a carrier class which is derived from System.MarshalByRefObject and that it implements IWin32Window interface. Thus allowing the Control.Handle to be marshaled across the AppDomain.

The rationale according to Microsoft is to prevent any chance that Form.ShowDialog() may use some intelligent to detect if the given IWin32Window belongs a Control object, which is not marshal-safe across App Domain.

Furthermore, it is not recommended just to marshal a Control object's IWin32Window across App Domain as Control contains things not marshal-safe.

As a caveat: Control.FromHandle() cannot be used in another App Domain to regenerate a IWin32Window or Control from the Handle, which is a IntPtr.

The reason is that, according to Microsoft, Control.FromHandle() relies on static data, which is per-Domain specific. Hence when you pass a Handle, IntPtr, from one domain to another, when Control.FromHandle() is called by the recipient domain, it will produce a null reference object.

How to pass establish owner-relationship across AppDomain for Modeless form?

The above discussion is to handle modal form across AppDomain. But how to implement modeless form that lives in a separate AppDomain?

When a form has an owner (only top level form, ie System.Windows.Forms.Form.TopLevel == true, can have owner), it affects the z-order in relation to the owner. The owned form is always shown above the owner form.

If you simple want a Top level window, you do not have to define the owner.

If you want to define owner-owned form relationship, the technique discussed above cannot be used directly. This is because System.Windows.Forms.Form.Owner is of type System.Windows.Forms.Form and given the fact that Form is not safe to be marshaled across AppDomain, you need a different technique to establish the owner-owned relationship.

According to Microsoft's advice, you have to resort to using P/Invoke to get the job done. In according to the recommendation in previous section you continue to marhal an object that implements IWin32Window across the AppDomain. So you use this class to carry the owner's Handle across the AppDomain.

To establish the owner-owned relationship, you use the Win32 API, SetWindowLong() or SetWindowLongPtr(), for Win64.

Below is the C# code to establish the owner-owned relationship:

public sealed class OwnerWindowHelper
{
private OwnerWindowHelper() {}

[ DllImport( "user32.dll", EntryPoint="SetWindowLongA", SetLastError=true ) ]

private static extern IntPtr SetWindowLongPtr(HandleRef hWnd, int nIndex, HandleRef data);
const int GWLP_HWNDPARENT = -8;

public static void SetupOwnerWindow( HandleRef hwndOwner, HandleRef hwndOwned )
{
IntPtr result = IntPtr.Zero;
result = SetWindowLongPtr( hwndOwned, GWLP_HWNDPARENT, hwndOwner );
if( result == IntPtr.Zero )
{
int err = Marshal.GetLastWin32Error();

if( err != 0 )
throw new InvalidOperationException
( "Failed to establish owner-owned relation." );
}
}
}

That's it. If you doubt that one has to use P/Invoke to get the job done, try to have a look at the IL code in System.Windows.Forms.Form.Owner and trace the logic and you will arrive at similar conclusion.

No comments:

Blog Archive