Friday, 2 October 2009

Work around for WPF Bug: Drag and Drop does not work when executing a WPF application in a non-default AppDomain

Today's task was implementing Drag/Drop to move items into folders. My estimate of yesterday was 6 hours to get the job done. That should have been plenty: then I discovered the bug.

We're creating an Excel Addin using Addin-Express (which is very like VSTO). All the UI for our add-in is built with WPF (naturally). When Addin Express hosts Add-ins for Excel, it isolates them each in their own App-Domain. Which is why today I came to add myself to the list of people who have reproduced this issue on the Connect website: "Drag and Drop does not work when executing a WPF application in a non-default AppDomain"

Googling around the problem, I found that Tao Wen had also encountered the problem, and had managed to find something of a work-around.

Having Reflectored around the WPF source-code a little, I can see that when WPF is hosted in a non-default App Domain, the check that the HwndSource class (which manages the Win32 Window object on behalf of the WPF Window class) does to ensure the code has the UnmanagedCode permission fails, so Windows do not register themselves as OLE Drop Targets (see the last few lines of the HwndSource.Initialize method)

Tao Wen's solution is to use reflection to directly call the internal DragDrop.RegisterDropTarget method. What his solution doesn't take into account is that, when the Window is closed it must unregister itself as a drop target.

Fortunately, by incrementing the _registeredDropTargetCount field in the HwndSource class we can ensure that HwndSource will itself call DropDrop.RevokeDropTarget when it's disposed (see about half-way down the HwndSource.Dispose method).

For anybody else who is blocked by the same issue, I've created a little class that will implement the hack on a Window class. Be warned: it uses evil reflection code to access private members of WPF classes, and it might break at any time. Use it at your own risk.

/// <summary>
/// Contains a helper method to enable a window as a drop target.
/// </summary>
public static class DropTargetEnabler
{
    /// <summary>
    /// Enables the window as drop target. Should only be used to work around the bug that
    /// occurs when WPF is hosted in a non-default App Domain
    /// </summary>
    /// <param name="window">The window to enable.</param>
    /// <remarks>
    /// This is a hack, so should be used with caution. It might stop working in future versions of .Net.
    /// The original wpf bug report is here: http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=422485
    /// </remarks>
    // This code was inspired by a post made by Tao Wen on the Add-in Express forum:
    // http://www.add-in-express.com/forum/read.php?FID=5&TID=2436
    public static void EnableWindowAsDropTarget(Window window)
    {
        window.Loaded += HandleWindowLoaded;
    }

    private static void HandleWindowLoaded(object sender, RoutedEventArgs e)
    {
        // See the HwndSource.Initialize method to see how it sets up a window as a Drop Target
        // also see the HwndSource.Dispose for how the window is removed as a drop target
        var window = sender as Window;

        IntPtr windowHandle = new WindowInteropHelper(window).Handle;

        // invoking RegisterDropTarget calls Ole32 RegisterDragDrop
        InvokeRegisterDropTarget(windowHandle);

        // to make sure RevokeDragDrop gets called we have to increment the private field
        // _registeredDropTargetCount on the HwndSource instance that the Window is attached to
        EnsureRevokeDropTargetGetsCalled(windowHandle);
    }

    private static void InvokeRegisterDropTarget(IntPtr windowHandle)
    {
        var registerDropTargetMethod = typeof(System.Windows.DragDrop)
            .GetMethod("RegisterDropTarget", BindingFlags.Static | BindingFlags.NonPublic);
        if (registerDropTargetMethod == null)
        {
            throw new InvalidOperationException("The EnableWindowAsDropTarget Hack no longer works!");
        }

        registerDropTargetMethod
            .Invoke(null, new object[] { windowHandle });
    }

    private static void EnsureRevokeDropTargetGetsCalled(IntPtr windowHandle)
    {
        var hwndSource = HwndSource.FromHwnd(windowHandle);
        var fieldInfo = typeof(HwndSource).GetField("_registeredDropTargetCount", BindingFlags.NonPublic | BindingFlags.Instance);
        if (fieldInfo == null)
        {
            throw new InvalidOperationException("The EnableWindowAsDropTarget hack no longer works!");
        }

        var currentValue = (int) fieldInfo.GetValue(hwndSource);
        fieldInfo.SetValue(hwndSource, currentValue + 1);
    }
}

Use it like this:

public partial class MyWindow
 {
        public MyWindow()
        {
            InitializeComponent();
            DropTargetEnabler.EnableWindowAsDropTarget(this);
        }
}

6 comments:

stefaan said...

Hello Sam,

thanks for the post, this will certainly help some people who run into this problem !

On a side note, could you share with us why Addin-Express was picked for this project instead of VSTO ?

best greetings,

Stefaan Rillaert (a Silverlight/WPF developer who also has to write an Excel plugin)

Sam said...

Stefaan,
The main reason we went with Add-in Express is that it allows us to create Application-wide custom Task Panes for Excel 2003. VSTO only allows one to create Task Panes for Workbook customisations in Excel 2003. It also has some nice designers for Toolbars and Ribbons.

Hope that helps.

Stefaan Rillaert said...

Thanks Sam,

I'll keep an eye on Add-in Express. Currently our addin only has to support Excel 2007 and (at least with the VSTO that ships with Visual Studio 2008) it is possible to create Application-wide custom Task Panes for 2007. An article by Andrew Whitechapel got me started with this part (http://msdn.microsoft.com/en-us/magazine/cc163292.aspx).

Have fun coding, Stefaan

rrss said...

Thanks very much for posting this workaround. It was exactly what I needed.

Anonymous said...

Thank you so much, saved my life!

Anna said...

if you have the the same problem with WinForms application, that contains wpf elements:



public partial class WpfHostControl : System.Windows.Forms.UserControl

{

public UIElement WpfControl

{

get;

set;

}

private ElementHost ctrlHost;





public WpfHostControl(UIElement wpfControl)

{

WpfControl = null;

InitializeComponent();

Dock = DockStyle.Fill;

WpfControl = wpfControl;

}





private void ParentControl_Load(object sender, EventArgs e)

{

ctrlHost = new ElementHost { Dock = DockStyle.Fill };

panel.Controls.Add(ctrlHost);

ctrlHost.Child = WpfControl;

ctrlHost.AllowDrop = true;

PropertyInfo pi = ctrlHost.HostContainer.GetType().GetProperty("Sink");

if (pi == null)

return;

RegisterDropTarget((HwndSource)pi.GetValue(ctrlHost.HostContainer, null));

}

private static void RegisterDropTarget(HwndSource source)

{

try

{

typeof(DragDrop).GetMethod("RegisterDropTarget", BindingFlags.Static | BindingFlags.NonPublic).

Invoke(null, new object[] { source.Handle });

var fieldInfo = typeof(HwndSource).GetField("_registeredDropTargetCount",

BindingFlags.NonPublic | BindingFlags.Instance);

var currentValue = (int)fieldInfo.GetValue(source);

fieldInfo.SetValue(source, currentValue + 1);

}

catch { }

}

}



where WpfHostControl - is a wrapper for your wpf control

Post a Comment