Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Porting WinForms drag and drop to WPF drag and drop

I am porting my program from WinForms to WPF and have ran into some issues with the drag and drop. It should allow for dragging from a TreeView (it is like a file explorer) to a textbox which opens the file. However, the WPF version acts like a copy-and-paste of the TreeViewItem's header text automatically. I think I just have something mixed up? Possibly the DataObject stuff.

The fully functional, relevant WinForms code:

private void treeView1_MouseMove(object sender, MouseEventArgs e)
{
    if (e.Button != MouseButtons.Left) return;
    TreeNode node = treeView1.GetNodeAt(e.Location);
    if (node != null) treeView1.DoDragDrop(node, DragDropEffects.Move);
}

textbox[i].DragDrop += (o, ee) =>
{
     if (ee.Data.GetDataPresent(typeof(TreeNode)))
     {
         TreeNode node = (TreeNode)ee.Data.GetData(typeof(TreeNode));   
         ((Textbox)o).Text = File.ReadAllLines(pathRoot + node.Parent.FullPath);
         ...

The WPF code that should do the same thing:

private void TreeView_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    TreeViewItem item = e.Source as TreeViewItem;
    if (item != null)
    {
        DataObject dataObject = new DataObject();
        dataObject.SetData(DataFormats.StringFormat, GetFullPath(item));
        DragDrop.DoDragDrop(item, dataObject, DragDropEffects.Move);
    }
}

//textbox[i].PreviewDrop += textbox_Drop;
private void textbox_Drop(object sender, DragEventArgs e)
{
    TreeViewItem node = (TreeViewItem)e.Data.GetData(typeof(TreeViewItem)); //null?
    ((Textbox)sender).Text = ""; 
    //this is being executed BUT then the node's header text is being pasted
    //also, how do I access the DataObject I passed?
}

Problem: In my WPF version, I am setting the textbox's text to empty (as a test), which occurs, but afterwards the TreeViewItem's header text is being pasted which is not what I want.

Questions: What is the correct way to port this WinForms code to WPF? Why is the text being pasted in the WPF version? How do I prevent that? Am I using the correct events? How do I access the DataObject in textbox_Drop so that I can open the file like I did in the WinForms version? Why is TreeViewItem node always null in the WPF version?

like image 849
Austin Henley Avatar asked Jul 16 '13 04:07

Austin Henley


3 Answers

Ah, what the heck, I'll expand my comment to an answer:

The link to read, as mentioned, is this: http://msdn.microsoft.com/en-us/library/hh144798.aspx

Short story, the TextBox-derived controls already implement most of the "guts" for basic drag/drop operations, and it is recommended that you extend that rather than provide explicit DragEnter/DragOver/Drop handlers.

Assuming a tree "data" structure like:

public class TreeThing
{
   public string Description { get; set; }
   public string Path { get; set; }
}

The handlers might look something like this:

this.tb.AddHandler(UIElement.DragOverEvent, new DragEventHandler((sender, e) =>
    {
        e.Effects = !e.Data.GetDataPresent("treeThing") ? 
            DragDropEffects.None : 
            DragDropEffects.Copy;                    
    }), true);

this.tb.AddHandler(UIElement.DropEvent, new DragEventHandler((sender, e) =>
{
    if (e.Data.GetDataPresent("treeThing"))
    {
        var item = e.Data.GetData("treeThing") as TreeThing;
        if (item != null)
        {
            tb.Text = item.Path;
            // TODO: Actually open up the file here
        }
    }
}), true);

And just for giggles, here's a quick-and-dirty test app that is pure showboating in it's use of the Reactive Extensions (Rx) for the drag start stuff:

XAML:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <TreeView x:Name="tree" Grid.Column="0" ItemsSource="{Binding TreeStuff}" DisplayMemberPath="Description"/>
        <TextBox x:Name="tb" Grid.Column="1" AllowDrop="True" Text="Drop here" Height="30"/>
    </Grid>
</Window>

Nasty code-behind (too lazy to MVVM this):

using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Runtime.CompilerServices;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace WpfApplication1
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window, INotifyPropertyChanged
    {
        public MainWindow()
        {
            InitializeComponent();
            TreeStuff = new ObservableCollection<TreeThing>()
                {
                    new TreeThing() { Description="file 1",  Path = @"c:\temp\test.txt" },
                    new TreeThing() { Description="file 2", Path = @"c:\temp\test2.txt" },
                    new TreeThing() { Description="file 3", Path = @"c:\temp\test3.txt" },
                };

            var dragStart = 
                from mouseDown in 
                    Observable.FromEventPattern<MouseButtonEventHandler, MouseEventArgs>(
                        h => tree.PreviewMouseDown += h, 
                        h => tree.PreviewMouseDown -= h)
                let startPosition = mouseDown.EventArgs.GetPosition(null)
                from mouseMove in 
                    Observable.FromEventPattern<MouseEventHandler, MouseEventArgs>(
                        h => tree.MouseMove += h, 
                        h => tree.MouseMove -= h)
                let mousePosition = mouseMove.EventArgs.GetPosition(null)
                let dragDiff = startPosition - mousePosition
                where mouseMove.EventArgs.LeftButton == MouseButtonState.Pressed && 
                    (Math.Abs(dragDiff.X) > SystemParameters.MinimumHorizontalDragDistance ||
                    Math.Abs(dragDiff.Y) > SystemParameters.MinimumVerticalDragDistance)
                select mouseMove;

            dragStart.ObserveOnDispatcher().Subscribe(start =>
                {
                    var nodeSource = this.FindAncestor<TreeViewItem>(
                        (DependencyObject)start.EventArgs.OriginalSource);
                    var source = start.Sender as TreeView;
                    if (nodeSource == null || source == null)
                    {
                        return;
                    }
                    var data = (TreeThing)source
                        .ItemContainerGenerator
                        .ItemFromContainer(nodeSource);
                    DragDrop.DoDragDrop(nodeSource, new DataObject("treeThing", data), DragDropEffects.All);
                });

            this.tb.AddHandler(UIElement.DragOverEvent, new DragEventHandler((sender, e) =>
                {
                    e.Effects = !e.Data.GetDataPresent("treeThing") ? 
                        DragDropEffects.None : 
                        DragDropEffects.Copy;                    
                }), true);

            this.tb.AddHandler(UIElement.DropEvent, new DragEventHandler((sender, e) =>
            {
                if (e.Data.GetDataPresent("treeThing"))
                {
                    var item = e.Data.GetData("treeThing") as TreeThing;
                    if (item != null)
                    {
                        tb.Text = item.Path;
                       // TODO: Actually open up the file here
                    }
                }
            }), true);
            this.DataContext = this;
        }


        private T FindAncestor<T>(DependencyObject current)
            where T:DependencyObject
        {
            do
            {
                if (current is T)
                {
                    return (T)current;
                }
                current = VisualTreeHelper.GetParent(current);
            }
            while (current != null);
            return null;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public ObservableCollection<TreeThing> TreeStuff { get; set; }

        protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }

    public class TreeThing
    {
        public string Description { get; set; }
        public string Path { get; set; }
    }
}
like image 116
JerKimball Avatar answered Nov 12 '22 19:11

JerKimball


You've got more than one problem, enough to make this difficult. First issue is that you got the drag object wrong, you are dragging a string but still checking for a TreeViewItem. Just use the same approach as you used in Winforms, dragging the node. Second problem is that TextBox already implements D+D support and that gets in the way of your code. And the reason you saw the text show up after the drop.

Let's tackle the start of the drag first. You'll need to do a bit of extra work since the way you started the drag interferes with the normal usage of the TreeView, it gets very hard to select a node. Only start the drag when the mouse was moved far enough:

    private Point MouseDownPos;

    private void treeView1_PreviewMouseDown(object sender, MouseButtonEventArgs e) {
        MouseDownPos = e.GetPosition(treeView1);
    }

    private void treeView1_PreviewMouseMove(object sender, MouseEventArgs e) {
        if (e.LeftButton == MouseButtonState.Released) return;
        var pos = e.GetPosition(treeView1);
        if (Math.Abs(pos.X - MouseDownPos.X) >= SystemParameters.MinimumHorizontalDragDistance ||
            Math.Abs(pos.Y - MouseDownPos.Y) >= SystemParameters.MinimumVerticalDragDistance) {
            TreeViewItem item = e.Source as TreeViewItem;
            if (item != null) DragDrop.DoDragDrop(item, item, DragDropEffects.Copy);
        }
    }

Now the drop, you will need to implement the DragEnter, DragOver and Drop event handlers to avoid the default D+D support built into TextBox from getting in the way. Setting the e.Handled property to true is necessary:

    private void textBox1_PreviewDragEnter(object sender, DragEventArgs e) {
        if (e.Data.GetDataPresent(typeof(TreeViewItem))) e.Effects = e.AllowedEffects;
        e.Handled = true;
    }

    private void textBox1_PreviewDrop(object sender, DragEventArgs e) {
        var item = (TreeViewItem)e.Data.GetData(typeof(TreeViewItem));
        textBox1.Text = item.Header.ToString();   // Replace this with your own code
        e.Handled = true;
    }

    private void textBox1_PreviewDragOver(object sender, DragEventArgs e) {
        e.Handled = true;
    }
like image 3
Hans Passant Avatar answered Nov 12 '22 20:11

Hans Passant


Problem: In my WPF version, I am setting the textbox's text to empty (as a test), which occurs, but afterwards the TreeViewItem's header text is being pasted which is not what I want.

I think a parent UI element is handling (and therefore overriding) the Drop event so you're not getting the results you expect. As a matter of fact, when trying to recreate your issue, I couldn't even get my TextBox.Drop event to fire. However, using the TextBox's PreviewDrop event, I was able to get what (I think) is your expected result. Try this:

    private void textBox1_PreviewDrop(object sender, DragEventArgs e)
    {
        TextBox tb = sender as TextBox;
        if (tb != null)
        {
            // If the DataObject contains string data, extract it.
            if (e.Data.GetDataPresent(DataFormats.StringFormat))
            {
                string fileName = e.Data.GetData(DataFormats.StringFormat) as string;
                using (StreamReader s = File.OpenText(fileName))
                {
                    ((TextBox)sender).Text = s.ReadToEnd();
                }
            }
        }
        e.Handled = true; //be sure to set this to true
    }

I think that code snippet should answer most of the questions you posed except for this one:

Why is TreeViewItem node always null in the WPF version?

The DataObject you are passing in the DragDrop event does not support passing a TreeViewItem. In your code (and mine) we specify that the data format will be DataFormats.StringFormat which cannot be cast to a TreeViewItem.

like image 1
dtesenair Avatar answered Nov 12 '22 20:11

dtesenair