简体   繁体   中英

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?

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

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

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 .

GetFullPath seems to be outputting a wrong value. What you want to drag/drop is the Header and you can get it directly from item . Also bear in mind that the method below is associated with the MouseMove Event of the TreeView .

private void TreeView_MouseMove(object sender, MouseButtonEventArgs e)
{
    if (e.LeftButton != MouseButtonState.Pressed) return;
    TreeViewItem item = e.Source as TreeViewItem;
    if (item != null)
    {
        DataObject dataObject = new DataObject();
        dataObject.SetData(DataFormats.StringFormat, item.Header);
        DragDrop.DoDragDrop(item, dataObject, DragDropEffects.Move);
    }
}

I did create the drop part based on text rather than on the TreeViewItem ( e.Data.GetData(typeof(string)).ToString() ) but the most surprising thing is that it isn't even required. If you open a new C# WPF project, put a TreeView and a TextBox on it (, update the XAML part) and copy the code above, you can drop text from the TreeView into the TextBox without doing anything else!! The text is copied into the TextBox without accounting for the Drop handling .

Am I using the correct events?: I think you are using the correct events, but I think you have several problems in your code. I assume you have set the DataContext of your treeview to the real items and you use binding.

  1. How do I access the DataObject in textbox_Drop ? --> For getting the DataObject you have to get the real item by recursion (other solutions possible)

     DependencyObject k = VisualTreeHelper.HitTest(tv_treeView, DagEventArgs.GetPosition(lv_treeView)).VisualHit; while (k != null) { if (k is TreeViewItem) { TreeViewItem treeNode = k as TreeViewItem; // Check if the context is your desired type if (treeNode.DataContext is YourType) { // save the item targetTreeViewItem = treeNode; return; } } else if (k == tv_treeview) { Console.WriteLine("Found treeview instance"); return; } // Get the parent item if no item from YourType was found k = VisualTreeHelper.GetParent(k); } 
  2. Why is the text being pasted in the WPF version? --> The Header is displayed because (I assume) it is like the tostring method on your items. If for a complex item the binding is not specified, the ToString Method is executed. Try not to set the Text directly in the handler of the drop event. Set the data context to your item (to the item you found in 1. point) and then specify the binding path via XAML. (for displaying)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM