Drag events swallowed by a hosted WPF control in its winforms parent

Question

I have a winforms control which needs to handle drag events in a particular way. This control contains a number of child controls - some of which are WPF embedded within ElementHost objects. It is the top-most winforms control that needs to handle the drag events.

I can easily get the drag behavior to work while the mouse is over winforms components within the parent. There is nothing special I need to do. However, the WPF controls are black holes - they seem to swallow the drag events, preventing the main winforms control from handling them properly.

I can hack around this with the following:

  1. set AllowDrop = true in the WPF control
  2. override OnDragEnter WPF control
  3. walk up the Control.Parent references recursively starting from the ElementHost until I find the top-level control I need
  4. call IDropTarget.OnDragEnter on the top-level control that was found

As you may imagine, this is rather ugly:

public MyWPFControl()
{
    InitializeComponent();
    _host = new Lazy<System.Windows.Forms.Control>(BuildHostControl);
    AllowDrop = true;
}

private System.Windows.Forms.Control BuildHostControl()
{
    return new System.Windows.Forms.Integration.ElementHost
    {
        Dock = System.Windows.Forms.DockStyle.Fill,
        Child = this,
    };
}

protected override void OnDragEnter(DragEventArgs e)
{
    base.OnDragEnter(e);
    if (e.Data.GetDataPresent(typeof(MyDragObject)))
    {
        var args = ConvertDragArgs(e);
        var control = GetControl(_host.Value)
        ((System.Windows.Forms.IDropTarget)control).OnDragEnter(args);
    }
}

private System.Windows.Forms.DragEventArgs ConvertDragArgs(DragEventArgs e)
{
    var dragObject = e.Data.GetData(typeof(MyDragObject)) as MyDragObject;
    var position = e.GetPosition(this);
    var data = new System.Windows.Forms.DataObject(dragObject);
    return new System.Windows.Forms.DragEventArgs(data, (int)e.KeyStates, (int)position.X, (int)position.Y, System.Windows.Forms.DragDropEffects.Copy, System.Windows.Forms.DragDropEffects.Copy);
}

private MyTopLevelControl GetControl(System.Windows.Forms.Control control)
{
    var cell = control as MyTopLevelControl;
    if (cell != null)
        return cell;

    return GetControl(control.Parent);
}

similar effort would have to be taken for OnDragDrop, but for brevity, I have omitted it

Certainly, I can inject a helper object that both the top-level control and the WPF control have references to so I can eliminate the control walking and clean it up somewhat.

However, I don't really want to handle the drag events in the WPF control if I can avoid it - I would rather the top-level control take charge and handle things like it does when I'm dragging over its winforms child controls. Is there a way to accomplish this?


Show source
| wpf   | winforms   | drag-and-drop   2016-12-27 18:12 1 Answers

Answers ( 1 )

  1. 2017-01-03 00:01

    What you describe is entirely normal. At breakneck speed: when you set the AllowDrop property to True then both Winforms and WPF call the RegisterDragDrop() COM function. You'll recognize the methods of the IDropTarget interface, they map directly to equivalent .NET events. Do note the HWND argument, that's where this behavior starts.

    In Winforms, every Control has its own Handle property. So nothing very special happens, the events are raised on the control that's being hovered/dropped. That doesn't work in WPF, controls don't have a handle, so they borrow the handle of the outer container. Typically the HWndSource of the Window object. The event must then be directed to the appropriate control, that's what routed events do. Key difference is that the events appear to "bubble" from the inner to the outer control. No such bubbling in either Winforms or the COM plumbing. So if the embedded WPF control doesn't have a use for the drag then that's where the buck stops, the user gets the "no drag" mouse cursor. Like what happens in any Winforms app.

    A Winforms programmer typically deals with this by giving the UI an obvious drag target. Something resembling a "drop here" glyph. That doesn't exactly hurt usability, drag+drop functionality tends to be pretty hard to discover. If that's not what you want then the only alternative you have is to bubble the events yourself. Much like you've been doing.

    Most of what you need is already there, you just didn't do this the optimal way. The most obvious approach is to do these bubbling in the ElementHost. The feature that all of these controls have in common. That's easy to do, 95% of all Winforms problems are solved by deriving your own class from the .NET class. Some code to play with:

    using System;
    using System.Windows.Forms;
    using System.Windows.Forms.Integration;
    
    class ElementHostEx : ElementHost {
        public ElementHostEx() {
            this.ChildChanged += ElementHostEx_ChildChanged;
        }
    
        private void ElementHostEx_ChildChanged(object sender, ChildChangedEventArgs e) {
            var prev = e.PreviousChild as System.Windows.UIElement;
            if (prev != null) {
                prev.DragEnter -= Child_DragEnter;
                prev.Drop -= Child_Drop;
    
            }
            if (this.Child != null) {
                this.Child.DragEnter += Child_DragEnter;
                this.Child.Drop += Child_Drop;
            }
        }
        // etc...
    }
    

    I didn't post the Child_Xxxx event handlers, you already have that code. Compile and drop the new control from the top of the toolbox, replacing the existing ElementHosts. Or, in code, create an ElementHostEx instead of an ElementHost.

◀ Go back