Pages

Friday, 2 October 2009

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"

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);
}
}

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.

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).

License and Disclaimer

In practice, this means that you can take any code you find here and freely use it in your own projects, commercial or otherwise. The only requirement is that you include an attribution link in your source code back to this blog.

The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.