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
TabItem
s in a TabControl
- Drag
TabItem
s from one TabControl
to another
- Drag
TreeViewItem
s to different locations within a TreeView
- Drag
TreeViewItem
leaves to a ListBox
- Drag
ListBoxItem
s to new locations within a ListBox
or to another ListBox
- Drag
ListBoxItem
s to a TreeView
- Drag
Button
s between ToolBar
s
- Drag
Button
s 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
TabItem
s, TreeViewItem
s and ListBoxItem
s 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 TabItem
s, TreeViewItem
s and ListBoxItem
s, 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.
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.Move |
DragDropEffects.Link |
DragDropEffects.None;
}
}
public override DataProviderActions DataProviderActions {
get {
return
DataProviderActions.QueryContinueDrag |
DataProviderActions.GiveFeedback |
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) {
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.
ListBoxDataProvider<ListBox, ListBoxItem> listBoxDataProvider =
new ListBoxDataProvider<ListBox, ListBoxItem>("ListBoxItemObject");
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.
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 |
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);
}
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 Unparent
ed 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.
ListBoxDataConsumer<ListBox, ListBoxItem> listBoxDataConsumer =
new ListBoxDataConsumer<ListBox, ListBoxItem>(new string[] { "ListBoxItemObject" });
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 Unparent
ed 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.
public override void SetData(ref DataObject data) {
System.Diagnostics.Debug.Assert
(data.GetDataPresent(this.SourceDataFormat) == false,
"Shouldn't set data more than once");
data.SetData(this.SourceDataFormat, this);
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 Unparent
s all data that implements the IDataProvider
interface.
public class TrashConsumer : DataConsumerBase, IDataConsumer
{
public TrashConsumer(string[] dataFormats)
: base(dataFormats)
{
}
public override DataConsumerActions DataConsumerActions {
get {
return
DataConsumerActions.DragOver |
DataConsumerActions.Drop |
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);
}
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 Unparent
ed 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:
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.
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;
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);
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;
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");
}
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