Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF Drag-and-Drop Smorgasbord

0.00/5 (No votes)
10 Jun 2009 2  
This article presents a framework that supports Drag-and-Drop between TreeView, TabControl, ListBox, ToolBar and Canvas controls with custom cursors and adorners.
Drag-and-Drop Smorgasbord Screenshot

Introduction

While investigating drag-and-drop using Windows Presentation Foundation, I found that most of the examples dealt with a single control and data type. In this article, I present an intra-application drag-and-drop framework in C# that supports multiple controls and data formats. Custom cursors and adorners are also supported.

The following functionality is covered in this article and supported by the framework.

  • Rearrange TabItems in a TabControl
  • Drag TabItems from one TabControl to another
  • Drag TreeViewItems to different locations within a TreeView
  • Drag TreeViewItem leaves to a ListBox
  • Drag ListBoxItems to new locations within a ListBox or to another ListBox
  • Drag ListBoxItems to a TreeView
  • Drag Buttons between ToolBars
  • Drag Buttons from a Canvas to a ToolBar or from a ToolBar to a Canvas
  • Move TextBlock, Rectangle and Button elements within a Canvas or to another Canvas
  • Create a TextBlock by dragging highlighted text in a RichTextBox to a Canvas
  • Insert text in a RichTextBox by dragging an element from a Canvas to a RichTextBox
  • Create TabItems, TreeViewItems and ListBoxItems by dragging a file or files from Windows Explorer
  • Drag any of the above items to a "trash" area to delete them (except Explorer files)
  • Custom cursors
  • Default adorner

Background

A drag-and-drop operation can be viewed as a data transfer from a data provider (the drag source) to a data consumer (the drop target). The data itself is passed between the data provider and data consumer using a mechanism similar to that of the Windows clipboard.

I'm not going to cover drag-and-drop fundamentals as I feel this topic has been sufficiently covered elsewhere. Here are a few links you may wish to investigate.

Framework Overview

The goal of the framework is to encapsulate the common intricacies of drag-and-drop so you just need to focus on the data you are dragging and dropping. The framework comprises base implementations of a data provider and data consumer, a drag manager, a drop manager, a default adorner and a few utility methods. You'll find the framework files in the DragDropFramework subdirectory of the project.

Data Providers and Consumers

Data providers are most often defined as a source container and a source object, and data consumers are most often defined as a destination container and a drop target. Consider the following list of data providers.

  • TabControl container/TabItem object
  • ListBox container/ListBoxItem object
  • TreeView container/TreeViewItem object
  • Canvas container/Button object
  • Canvas container/TextBlock object

Now consider the following list of data consumers.

  • TabControl container/TabItem object
  • ListBox container/ListBoxItem object
  • TreeView container/TreeViewItem object
  • Canvas container/Button object
  • Canvas container/TextBlock object

Most of the time, controls such as the TabControl, TreeView and ListBox will only provide and consume TabItems, TreeViewItems and ListBoxItems, respectively. However, in this article, the Canvas control provides and accepts several different object types.

The data provider and consumer files are kept in the DragDropFrameworkData subdirectory of the project.

Drag Manager

It is the framework user's responsibility to create an instance of the drag manager by specifying the container to be monitored (e.g. TabControl, Canvas, etc.) and the data to be dragged. The data to be dragged is defined by a class that extends the DataProviderBase class.

A drag operation begins when the user clicks on and drags an object defined by a data provider class. The drag manager automatically deals with housekeeping chores such as hooking events, displaying the correct cursor and showing the adorner.

Drop Manager

It is the framework user's responsibility to create an instance of the drop manager by specifying the container to be monitored (e.g. TabControl, Canvas, etc.) and the data to accept. The data to accept is defined by a class that extends the DataConsumerBase class.

When a user drags an object over a drop container, there are four events that can be triggered, as defined by the WPF drag-and-drop implementation. Here is a list of these events:

  • Drag Enter
  • Drag Over
  • Drag Leave
  • Drop

Each of these events provides the framework user with the opportunity to give feedback to the user by returning the appropriate DragDropEffects value. This value is used in the drag manager to display the appropriate cursor.

In the case of the Drop event, the returned DragDropEffects value indicates the operation that was finally performed (e.g. Move, Link or Copy).

We'll examine the data consumer in more depth later on in the article.

ListBox Data Provider

We'll start by looking at the ListBoxItem data provider. The following code shows the completed ListBoxDataProvider class.

/// <summary>
/// This Data Provider represents ListBoxItems.
/// </summary>
/// <typeparam name="TContainer">Drag source container type</typeparam>
/// <typeparam name="TObject">Drag source object type</typeparam>
public class ListBoxDataProvider<TContainer, TObject> : 
	DataProviderBase<TContainer, TObject>, IDataProvider
    where TContainer : ItemsControl
    where TObject : FrameworkElement
{

    public ListBoxDataProvider(string dataFormatString) :
        base(dataFormatString)
    {
    }

    public override DragDropEffects AllowedEffects {
        get {
            return
                //DragDropEffects.Copy |
                //DragDropEffects.Scroll |
                DragDropEffects.Move |	// Move ListBoxItem
                DragDropEffects.Link |  	// Needed for moving ListBoxItem 
					//as a TreeView sibling
                DragDropEffects.None;
        }
    }

    public override DataProviderActions DataProviderActions {
        get {
            return
                DataProviderActions.QueryContinueDrag | // Need Shift key info 
						//(for TreeView)
                DataProviderActions.GiveFeedback |
                //DragDropDataProviderActions.DoDragDrop_Done |

                DataProviderActions.None;
        }
    }

    public override void DragSource_GiveFeedback
		(object sender, GiveFeedbackEventArgs e) {
        if(e.Effects == DragDropEffects.Move) {
            e.UseDefaultCursors = true;
            e.Handled = true;
        }
        else if(e.Effects == DragDropEffects.Link) {    // ... when Shift key is pressed
            e.UseDefaultCursors = true;
            e.Handled = true;
        }
    }

    public override void Unparent() {
        TObject item = this.SourceObject as TObject;
        TContainer container = this.SourceContainer as TContainer;

        Debug.Assert(item != null, "Unparent expects a non-null item");
        Debug.Assert(container != null, "Unparent expects a non-null container");

        if((container != null) && (item != null))
            container.Items.Remove(item);
    }
}

A data provider in most cases extends the DataProviderBase class and must always implement the interface IDataProvider. A minimum data provider class must implement three methods. First, the constructor must pass the name given to the data to the base constructor. Second, AllowedEffects must return which of the four effects will be used. In most cases, WPF/Windows ANDs the effects returned by drop manager's events with the value of AllowedEffects. Third, DataProviderActions returns which methods to call in the data provider implementation.

AllowedEffects

The DragDropEffects Move, Copy and Link help determine which cursor should be displayed during a drag-and-drop operation. The standard cursors for these operations are displayed when dragging files within Windows Explorer, and the Shift, Ctrl and Alt keys are used to modify the drag operation.

DataProviderActions

The QueryContinueDrag event is specified by the WPF Drag-and-Drop implementation. However, it is encapsulated by the drag-and-drop framework implementation. By defining the QueryContinueDrag DataProviderAction, DataProviderBase makes available the state of the Shift, Ctrl, Alt and Esc keys during a drag operation through the KeyStates and EscapedPressed properties, respectively.

The GiveFeedback event is also specified by the WPF Drag-and-Drop implementation. However, it too is encapsulated by the drag-and-drop framework implementation. By defining the GiveFeedback DataProviderAction and writing the DragSource_GiveFeedback method, you can control the cursor that is displayed while a drag is in progress.

Unparent Method

In order for a ListBoxItem to be inserted into another Items collection, it must first be removed from its current Items collection. After the user does a drop, Unparent is called from a method in the data consumer to remove the dropped item from its old collection so it can be added to a new parent.

Creating the Drag Manager and ListBox Data Provider

The following code segment shows how the ListBoxDataProvider is created and passed to the drag manager along with the ListBox to monitor.

Note how ListBox and ListBoxItem are used as type parameters TContainer and TObject, respectively, when creating the data provider instance.

// Data Provider
ListBoxDataProvider<ListBox, ListBoxItem> listBoxDataProvider =
    new ListBoxDataProvider<ListBox, ListBoxItem>("ListBoxItemObject");

// Drag Manager
DragManager dragHelperListBox0 = new DragManager(this.listBox, listBoxDataProvider);

In order for a drag operation to begin, the user must click on an item of type ListBoxItem contained in a ListBox, as defined by the ListBoxDataProvider constructor. When the user drags such an object, its data is named "ListBoxItemObject," as passed to the constructor, and listBoxDataProvider is the data.

Note that the drag data is retrieved by the data consumer using its name, in this case "ListBoxItemObject." It's important to realize that the data object name used when creating the data provider class instance must match the data object name used when creating the data consumer.

Also note that the class instance can only be used by the program that created that data as the pointers would be invalid for any other program.

ListBox Data Consumer

We'll continue by looking at the ListBoxItem data consumer. The following code shows the completed ListBoxDataConsumer class.

/// <summary>
/// This data consumer looks for ListBoxItems.
/// The ListBoxItem is inserted before the
/// target ListBoxItem or at the end of the
/// list if dropped on empty space.
/// </summary>
/// <typeparam name="TContainer">Drag source and drop destination container type
/// </typeparam>
/// <typeparam name="TObject">Drag source and drop destination object type</typeparam>
public class ListBoxDataConsumer<TContainer, TObject> : DataConsumerBase, IDataConsumer
    where TContainer : ItemsControl
    where TObject : ListBoxItem
{
    public ListBoxDataConsumer(string[] dataFormats)
        : base(dataFormats)
    {
    }

    public override DataConsumerActions DataConsumerActions {
        get {
            return
                DataConsumerActions.DragEnter |
                DataConsumerActions.DragOver |
                DataConsumerActions.Drop |
                //DragDropDataConsumerActions.DragLeave |

                DataConsumerActions.None;
        }
    }

    public override void DropTarget_DragEnter(object sender, DragEventArgs e) {
        this.DragOverOrDrop(false, sender, e);
    }

    public override void DropTarget_DragOver(object sender, DragEventArgs e) {
        this.DragOverOrDrop(false, sender, e);
    }

    public override void DropTarget_Drop(object sender, DragEventArgs e) {
        this.DragOverOrDrop(true, sender, e);
    }

    /// <summary>
    /// First determine whether the drag data is supported.
    /// Finally handle the actual drop when <code>bDrop</code> is true.
    /// Insert the item before the drop target.  When there is no drop
    /// target (dropped on empty space), add to the end of the items.
    /// </summary>
    /// <param name="bDrop">True to perform an actual drop, 
    /// otherwise just return e.Effects</param>
    /// <param name="sender">DragDrop event <code>sender</code></param>
    /// <param name="e">DragDrop event arguments</param>
    private void DragOverOrDrop(bool bDrop, object sender, DragEventArgs e) {
        ListBoxDataProvider<TContainer, TObject> dataProvider = 
		this.GetData(e) as ListBoxDataProvider<TContainer, TObject>;
        if(dataProvider != null) {
            TContainer dragSourceContainer = dataProvider.SourceContainer as TContainer;
            TObject dragSourceObject = dataProvider.SourceObject as TObject;
            Debug.Assert(dragSourceObject != null);
            Debug.Assert(dragSourceContainer != null);

            TContainer dropContainer = Utilities.FindParentControlIncludingMe
					<TContainer>(e.Source as DependencyObject);
            TObject dropTarget = e.Source as TObject;

            if(dropContainer != null) {
                if(bDrop) {
                    dataProvider.Unparent();
                    if(dropTarget == null)
                        dropContainer.Items.Add(dragSourceObject);
                    else
                        dropContainer.Items.Insert
			(dropContainer.Items.IndexOf(dropTarget), dragSourceObject);

                    dragSourceObject.IsSelected = true;
                    dragSourceObject.BringIntoView();
                }
                e.Effects = DragDropEffects.Move;
                e.Handled = true;
            }
            else {
                e.Effects = DragDropEffects.None;
                e.Handled = true;
            }
        }
    }
}

A data consumer in most cases extends the DataConsumerBase class and must always implement the interface IDataConsumer. A minimum data consumer class must implement three methods. First, the constructor must pass the names given to the data to the base constructor. Second, DataConsumerActions returns which methods to call in the data consumer implementation. Third, in order to complete a drop, the method DropTarget_Drop must be implemented to perform the actions associated with the drop.

The work is done in the DragOverOrDrop method, which is called from DropTarget_DragEnter, DropTarget_DragOver and DropTarget_Drop. Normally all data consumer classes are written this way.

DragOverOrDrop

The first step is to retrieve the data being dragged. If dataProvider is not null, it is an instance of ListBoxDataProvider. The source container and source object are available using properties defined by the interface IDataProvider. Next get the drop container and drop object. If the source object is being dragged over an empty area of the list box, dropTarget will be null.

bDrop is true when the object is dropped; in other words the user has released the left mouse button. After a drop has happened, the source object is Unparented and either added to the drop container's collection (when dropTarget is null) or inserted before the drop target.

DropTarget_DragEnter and DropTarget_DragLeave

DropTarget_DragEnter is called when an object is dragged into a drop container and DropTarget_DragLeave is called when the object is dragged out of the drop container. You may wish to highlight the border of a ListBox when an object is dragged into the ListBox and return the border to a normal color when the object is dragged out of the ListBox. The DragEnter and DragLeave methods would be a good choice for implementing this kind of behavior.

In order for the correct cursor to be displayed by DragSource_GiveFeedback, e.Effects must be set to the proper value and e.Handled must be set to true. These are requirements of the WPF Drag-and-Drop implementation.

Note that the e.Effects value returned by DropTarget_dragEnter is masked by the value returned by the data provider's AllowedEffects. Furthermore, the e.Effects value returned by DropTarget_DragLeave is the value passed to DropTarget_DragEnter in both e.Effects and e.AllowedEffects and is not masked by the data provider's AllowedEffects. This behavior is defined by the WPF Drag-and-Drop implementation.

DropTarget_DragOver

DropTarget_DragOver is called many times as an object is dragged over a drop container. In order for the correct cursor to be displayed by DragSource_GiveFeedback, e.Effects must be set to the proper value and e.Handled must be set to true. These are requirements of the WPF Drag-and-Drop implementation.

DropTarget_Drop

When the user drops an object, DropTarget_Drop is called. Like the three DropTarget_* methods before, e.Effects must be set to the proper value and e.Handled must be set to true.

The e.Effects value returned by DropTarget_Drop is passed to the data provider's DoDragDrop_Done method, if it is provided. When moving a file, for example, the file would be copied to its destination by DropTarget_Drop and the original file would be deleted by DoDragDrop_Done after a successful copy.

Creating the Drop Manager

The following code segment shows how the ListBoxDataConsumer is created and passed to the drop manager along with the ListBox instance to monitor.

// Data Consumer
ListBoxDataConsumer<ListBox, ListBoxItem> listBoxDataConsumer =
    new ListBoxDataConsumer<ListBox, ListBoxItem>(new string[] { "ListBoxItemObject" });

// Drop Manager
DropManager dropHelperListBox = new DropManager(this.listBox,
    new IDataConsumer[] {
        listBoxDataConsumer,
    });

Remember how ListBox and ListBoxItem were used as type parameters when creating the data provider instance? The same two types must be used to create the data consumer instance. Note that the data format name passed to the ListBoxDataConsumer constructor is the same as the one passed to the ListBoxDataProvider. These are requirements for the ListBoxDataConsumer to consume data provided by the ListBoxDataProvider.

Quick Recap

To establish the overall flow, let's quickly recap what we've covered so far.

Data Provider

A data provider class is written which handles the source container type (ListBox) and the source object type (ListBoxItem). An instance of the data provider class is created which defines the data object's name ("ListBoxItemObject").

Drag Manager

A drag manager instance is created, passing the source container to monitor (ListBox instance) and an instance of the data provider class.

Data Consumer

A data consumer class is written which handles the drop container type (ListBox) and drop target type (ListBoxItem) to monitor. An instance of the data consumer class is created which defines the data object's name ("ListBoxItemObject").

Drop Manager

A drop manager instance is created, passing the drop container to monitor (ListBox instance) and an instance of the data consumer class.

The Flow

The drag manager detects when an object starts to be dragged and checks its list of data providers. If a match is found, it uses the class of the matching data provider as the drag data and initiates a drag operation by calling the WPF method DoDragDrop.

When an object is dragged into a container monitored by a drop manager, the appropriate method is called (DropTarget_Enter, then DropTarget_DragOver multiple times) which ends up calling DragOverOrDrop. DragOverOrDrop looks for a data provider it recognizes, then returns the appropriate value in e.Effects so the correct cursor is displayed by the data provider's DragSource_GiveFeedback method.

When the object is dropped, DragOverOrDrop is called a final time with bDrop set to true so the source object is Unparented and either inserted or added to the drop container's Items list.

Different Data Formats

Similar to working with clipboard data, the more data formats provided during a drag operation, the better. By default the drag manager sets one data format, which is the DataProvider class. The CanvasDataProvider overrides the default SetData method, shown below, so it can add a string data format.

/// <summary>
/// Not only add the DataProvider class, also add a string
/// </summary>
public override void SetData(ref DataObject data) {
    // Set default data
    System.Diagnostics.Debug.Assert
	(data.GetDataPresent(this.SourceDataFormat) == false, 
	"Shouldn't set data more than once");
    data.SetData(this.SourceDataFormat, this);

    // Look for a System.String
    string textString = null;

    if(this.SourceObject is Rectangle) {
        Rectangle rect = (Rectangle)this.SourceObject;
        if(rect.Fill != null)
            textString = rect.Fill.ToString();
    }
    else if(this.SourceObject is TextBlock) {
        TextBlock textBlock = (TextBlock)this.SourceObject;
        textString = textBlock.Text;
    }
    else if(this.SourceObject is Button) {
        Button button = (Button)this.SourceObject;
        if(button.ToolTip != null)
            textString = button.ToolTip.ToString();
    }

    if(textString != null)
        data.SetData(textString);
}

By adding the string format, Rectangle, TextBlock and Button objects can be dragged from the canvas to the rich text box to insert text. Note how the first call to SetData, which adds the default data, is called with the SourceDataFormat string and a reference to the DataProvider class.

The second call to SetData is made to set the string data as long as textString isn't null.

Trash Data Consumer

The trash data consumer is the simplest data consumer implementation. To delete an object, as shown below, it simply Unparents all data that implements the IDataProvider interface.

/// <summary>
/// This data consumer looks for all data formats specified in the constructor.
/// When dropped, erase (Unparent) the source object.
/// </summary>
public class TrashConsumer : DataConsumerBase, IDataConsumer
{
    public TrashConsumer(string[] dataFormats)
        : base(dataFormats)
    {
    }

    public override DataConsumerActions DataConsumerActions {
        get {
            return
                //DragDropDataConsumerActions.DragEnter |
                DataConsumerActions.DragOver |
                DataConsumerActions.Drop |
                //DragDropDataConsumerActions.DragLeave |

                DataConsumerActions.None;
        }
    }

    public override void DropTarget_DragOver(object sender, DragEventArgs e) {
        this.DragOverOrDrop(false, sender, e);
    }

    public override void DropTarget_Drop(object sender, DragEventArgs e) {
        this.DragOverOrDrop(true, sender, e);
    }

    /// <summary>
    /// First determine whether the drag data is supported.
    /// Finally erase (Unparent) the source object when <code>bDrop</code> is true.
    /// </summary>
    /// <param name="bDrop">True to perform an actual drop, 
    /// otherwise just return e.Effects</param>
    /// <param name="sender">DragDrop event <code>sender</code></param>
    /// <param name="e">DragDrop event arguments</param>
    private void DragOverOrDrop(bool bDrop, object sender, DragEventArgs e) {
        IDataProvider dataProvider = this.GetData(e) as IDataProvider;
        if(dataProvider != null) {
            if(bDrop) {
                dataProvider.Unparent();
            }
            e.Effects = DragDropEffects.Move;
            e.Handled = true;
        }
    }
}

Other Data Providers and Data Consumers

There is a total of twelve DataProvider/DataConsumer files in the project's DragDropFrameworkData directory. I'll take a little time and point out features that are unique to each implementation.

CanvasButtonConsumer.cs

This data consumer implementation is attached to tool bars and consumes buttons from the canvas. It's interesting that the button cannot simply be Unparented from the canvas and moved to the tool bar; a new copy of the button must be made. Try using the same button and you'll see that a selection box is drawn around the button once it's moved to the tool bar.

When a button is dragged on top of another button in the tool bar, a link cursor is displayed. The link cursor indicates that the button will be inserted before the target button. When a button is dragged over a tool bar's empty space, a regular non-link cursor is displayed; when dropped it will be the last button of the tool bar.

CanvasData.cs

We already looked at how the canvas data provider adds a string data format so that when an object is dragged from the canvas to the rich text box, text is inserted.

In the CanvasDataProvider implementation, the AddAdorner method is overridden and returns true so that objects dragged from the canvas have the default adorner.

Another unique feature of the canvas data provider and consumer is that dragged objects are placed at specific coordinates on the canvas when dropped. When an object is dragged, the point where the left mouse was clicked, relative to the object to be dragged, is saved in the StartPosition property. Later when the object is dropped, the StartPosition is subtracted from the point on the canvas where the left mouse button was released so the relationship of the mouse pointer to the object is maintained.

Note that data provider and data consumer instances are created for each object type (TextBlock, Rectangle and Button).

FileDropConsumer.cs

When a single file is dragged from Windows Explorer, its type is FileNameW and when multiple files are dragged, the type used is FileDrop. The actual data format in both cases is a string array. See in the following example how an instance of the FileDropConsumer is created:

// Used by TabControl, TreeView and ListBox.
// This data consumer allows items to be created
// from a file or files dragged from Windows Explorer.
FileDropConsumer fileDropDataConsumer =
    new FileDropConsumer(new string[] {
        "FileDrop",
        "FileNameW",
    });

The above code shows a FileDropConsumer instance that consumes data formats FileDrop and FileNameW (as passed to the constructor). When an object is dragged over a target container, the search for supported formats is done in the order the formats are specified in the constructor. In this case FileDrop is searched for before FileNameW.

FileDropConsumer was written to be used with a TabControl, ListBox and TreeView. The following code shows the implementation of the DragOverOrDrop method.

/// <summary>
/// First determine whether the drag data is supported.
/// Second determine what type the container is.
/// Third determine what operation to do (only copy is supported).
/// And finally handle the actual drop when <code>bDrop</code> is true.
/// </summary>
/// <param name="bDrop">True to perform an actual drop,
/// otherwise just return e.Effects</param>
/// <param name="sender">DragDrop event <code>sender</code></param>
/// <param name="e">DragDrop event arguments</param>
private void DragOverOrDrop(bool bDrop, object sender, DragEventArgs e) {
    string[] files = this.GetData(e) as string[];
    if(files != null) {
        e.Effects = DragDropEffects.None;
        ItemsControl dstItemsControl = sender as ItemsControl;  // 'sender' is used 
						// when dropped in an empty area
        if(dstItemsControl != null) {
            foreach(string file in files) {
                if(sender is TabControl) {
                    if(bDrop) {
                        TabItem item = new TabItem();
                        item.Header = System.IO.Path.GetFileName(file);
                        item.ToolTip = file;
                        dstItemsControl.Items.Insert(0, item);
                        item.IsSelected = true;
                    }
                    e.Effects = DragDropEffects.Copy;
                }
                else if(sender is ListBox) {
                    if(bDrop) {
                        ListBoxItem dstItem = 
			Utilities.FindParentControlIncludingMe<ListBoxItem>
			(e.Source as DependencyObject);
                        ListBoxItem item = new ListBoxItem();
                        item.Content = System.IO.Path.GetFileName(file);
                        item.ToolTip = file;
                        if(dstItem == null)
                            dstItemsControl.Items.Add(item);    // ... if dropped on 
							// an empty area
                        else
                            dstItemsControl.Items.Insert
			(dstItemsControl.Items.IndexOf(dstItem), item);

                        item.IsSelected = true;
                        item.BringIntoView();
                    }
                    e.Effects = DragDropEffects.Copy;
                }
                else if(sender is TreeView) {
                    if(bDrop) {
                        if(e.Source is ItemsControl)
                            dstItemsControl = e.Source as ItemsControl; // Dropped on 
							     // a TreeViewItem
                        TreeViewItem item = new TreeViewItem();
                        item.Header = System.IO.Path.GetFileName(file);
                        item.ToolTip = file;
                        dstItemsControl.Items.Add(item);
                        item.IsSelected = true;
                        item.BringIntoView();
                    }
                    e.Effects = DragDropEffects.Copy;
                }
                else {
                    throw new NotSupportedException("The item type is not implemented");
                }
                // No need to loop through multiple
                // files if we're not dropping them
                if(!bDrop)
                    break;
            }
        }
        e.Handled = true;
    }
}

Notice how the type of the sender is checked for one of the supported controls. When a drop is performed, an item corresponding to the sender's type is created and either inserted or added to the target control.

ListBoxData.cs

The ListBox data provider and consumer are generic in nature and were used as examples earlier in the article.

ListBoxToTreeView.cs

An object is dropped on a TreeView in one of three ways. The drop can occur over an empty area of the TreeView; in other words the object isn't dropped on a TreeView item. In this case, an item is added to the end of the TreeView. When an object is dropped on a TreeView item, the shift key can be used to modify the drop behavior. When the shift key is pressed, the object is added as a sibling of the drop target; otherwise it is added to the drop target as a child.

Note that a new TreeViewItem is created and the ListBoxItem's content is copied to it.

StringToCanvasTextBlock.cs

A text selection dragged from the rich text box has System.String as one of the available data formats. A StringToCanvasTextBlock instance is created that looks for that data type and adds a TextBlock to the canvas at the point where the left mouse button is released.

TabControlData.cs

The TabControl data provider and consumer allow tabs to be rearranged within the same control and tabs to be moved from one TabControl to another. When tabs within the same control are being rearranged and the widths of the source and target tabs are different, there is a tendency for the two tabs to oscillate back and forth during the move. The TabControlDataConsumer examines these widths and moves the tabs after there is no chance of oscillation.

When there is insufficient width to display tabs side-by-side, the TabControl will stack the tabs. This data consumer implementation does not address the oscillation that can occur when tabs are stacked.

The TabControl data provider demonstrates the use of custom cursors. When a tab is being moved from one control to another, a 'page' cursor is displayed. When the destination is forbidden, a 'page-not' cursor is displayed. The normal arrow cursor is used when tabs are being rearranged within the same control.

ToolbarButtonToCanvasButton.cs

Similar to the CanvasButtonConsumer discussed earlier, a copy of the tool bar button is made before being placed on the canvas. The reason a copy is made is because if the tool bar button were reused, there wouldn't be a border around the button once placed on the canvas. Plus a reused button would display a tool bar style selection box around the 'button' when the mouse hovers over it.

ToolBarData.cs

As described in the CanvasButtonConsumer, the cursor changes depending on whether a button object is over another button or the tool bar's empty space. When the drag cursor displays a link, the button will be inserted before the drop target, otherwise the button is added as the last button of the tool bar.

TrashConsumer.cs

The TrashConsumer was discussed earlier in the article.

TreeViewData.cs

As discussed above in ListBoxToTreeView, an object is added to a TreeView in one of three ways. An object dropped on empty space is added to the end of the TreeView. When dropped on a TreeViewItem, the object is added as a child of the TreeViewItem. However, when the shift key is being pressed, the object is added as a sibling of the drop target.

TreeViewToListBox.cs

Similar to the discussion above, when a TreeViewItem is dropped in a ListBox, the relevant information is copied from the TreeViewItem to a new ListBoxItem and then either inserted or added to the control.

Points of Interest

Boundary Conditions

As programmers, we know the importance of testing boundary conditions. As an example, let's look at both the ListBox and the TreeView.

Click on a ListBoxItem in the middle of a list a couple of pixels from either the top or bottom border. Now drag toward that border and into the neighboring ListBoxItem. Notice that the neighboring ListBoxItem becomes selected.

Repeat the same exercise, however this time selecting a TreeViewItem instead.

You should note that unlike the neighboring ListBoxItem that was selected, the neighboring TreeViewItem is not selected. There is a special case in the drag manager that saves the ListBox source object in the PreviewMouseMove event, as the source object can change between the PreviewMouseLeftButtonDown event and the PreviewMouseMove event when dealing with a ListBox.

Another boundary condition to test is clicking on a source object a couple of pixels from the container's edge and dragging outside of the container's boundary.

COMException crossed a native/managed boundary

Be prepared to experience an exception while running a debug version if you drag outside of the test application or Visual Studio. The exception can be silenced by unchecking 'Break when exceptions cross AppDomain or managed/native boundaries' found in Tools | Options... | Debugging | General.

Quick Adorners for All

You can quickly enable adorners for everything by changing AddAdorner to return true in DataProviderBase.cs.

PRINT2BUFFER and PRINT2OUTPUT Conditional Compile Debug Constants

There are two conditional compile constants used to provide debug information. When PRINT2BUFFER is defined, a character is appended to buf0 on entry to an event, and a character is appended to buf1 upon exit from the event. After the drag-and-drop operation is complete, the two buffers are compared and the results are written to Visual Studio's Output window. If the two buffers differ, that indicates a reentrancy issue. You must keep your code short and efficient to avoid such issues.

When PRINT2OUTPUT is defined, events provide more verbose debug information in Visual Studio's Output window.

Conclusion

Upon completing this framework, I asked myself whether there was a net gain using the drag-and-drop framework interface as opposed to directly using Microsoft's drag-and-drop interface. After multiple projects I decided that there indeed was an advantage to using this framework over WPF's native drag-and-drop. There is ramp up involved no matter which choice you make. However, by encapsulating the WPF nuances once and for all within the drag-and-drop framework, I feel I'm able to more easily concentrate on what needs to be dragged and dropped.

History

  • June 2009
    • Initial release

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here