.NET (296) administrative (41) Ajax (42) AngularJS (2) ASP.NET (144) bicycle (2) books (180) browser (8) C# (134) cars (1) chess (28) CodePlex (10) Coma (8) database (47) deployment (3) Entity Framework (2) essay (112) flash/shockwave (2) flex (1) food (3) friend (2) game (20) idea (5) IIS (8) javascript (82) LInQ (2) Linux (6) management (4) manga (42) misc (672) mobile (1) movies (91) MsAccess (1) murder (2) music (64) mysql (1) news (100) permanent (1) personal (68) PHP (1) physics (2) picture (307) places (12) politics (13) programming (504) rant (120) religion (3) science (43) Sharepoint (3) software (58) space (1) T4 (2) technology (11) Test Driven Development (4) translation (2) VB (2) video (97) Visual Studio (44) web design (46) Windows API (8) Windows Forms (3) Windows Server (5) WPF/Silverlight (63) XML (11)

Tuesday, January 04, 2011

MVVM Drag and Drop

When I first tried to do drag and drop in WPF I thought it would be something easy like DragAndDrop.IsDraggable="True". Boy,was I wrong! The mechanism for this has remained almost unchanged from the Windows Forms version. It is completely event driven and has nothing in the way of actual graphical feedback of what is going on except making the mouse cursor look different. If you want to do it using MVVM, you have another thing coming.

Disclaimer:Now, I have found a good solution for all the problems above, but the drag and drop behaviour is part of a larger framework that I have been working on and creating a separate project just for it might prove difficult. I mean, you want to show MVVM drag and drop, you should also have Views and ViewModels and base classes and helpers and everything. The solution, I guess, is to make separate articles for the main features in the framework, then present the project as a whole in the end. The framework itself is work in progress and untested in a real life project, so this might have to wait, as well. I will make sure, though, to put much code directly in this post.

First a bit of thanks to the people that inspired me to work on that. I have Sascha Barber to thank for his comprehensive articles on WPF, especially the ones about his Cinch framework. Then Lee Roth and Bea Stollnitz/Costa for the ideas about using adorners to show the drag and drop items. Finally Jason Young, for writing about MVVM Drag and Drop, but without the graphic part.

Ok then, let's define the requirements of this system. We need:
  • A drag item
  • A drop target
  • Showing the target is being dragged
  • Showing the target can or cannot be dropped
  • Showing the item being dragged
  • Changing the appearance of the original dragged element while dragging
  • Changing the appearance of the drop target while dragging something over
  • Allowing for drag and drop between Windows
  • Allowing for drag and drop between applications
  • Changing an application that works but has no drag and drop in an easy and maintainable way
  • Using as simple a system as possible
  • Doing everything using the Model-View-ViewModel pattern

From these requirements we can form a basic idea of the way we would like this to work. First of all, we need the ability to mark any element as a drag item. Also, we need a container that can be marked as a drop target. We can do this using boolean IsDragSource and IsDropTarget Attached Properties; once set they will force a bind of the drag and drop events to some special handlers that would then direct decisions to ICommands.

As we are doing it in MVVM, we don't use the elements directly, but the data they represent, so we work with dragging and dropping commands using data objects. The classes responsible with the decisions for the drop permissions and actions should be in the ViewModel. We could, of course, link all events to commands, but that would be very cumbersome to use. Besides, we want it simple, we don't really want the user of the system to care about the drag and drop inner workings. Therefore, the solution is to change the IsDragSource and IsDropTarget to DragSource and DropTarget properties that accept objects of type IDragSource and IDropTarget containing all the methods needed for the events in question:

/// <summary>
/// Holds the data of a dragged object in a drag-and-drop operation.
/// </summary>
public interface IDraggedData
{
/// <summary>
/// A dictionary with the format as the key and the data in that format in the value
/// </summary>
IDictionary<string, object> Values
{
get;
}

/// <summary>
/// Optional object for additional information
/// </summary>
object Tag
{
get;
}
}

/// <summary>
/// Business end of the drag source
/// </summary>
public interface IDragSource
{
/// <summary>
/// Gets the supported drop effects.
/// </summary>
/// <param name="dataContext">The data context.</param>
/// <returns></returns>
DragEffects GetDragEffects(object dataContext);

/// <summary>
/// Gets the data.
/// </summary>
/// <param name="dataContext">The data context.</param>
/// <returns></returns>
object GetData(object dataContext);
}

/// <summary>
/// Defines the handler object of a drop operation
/// </summary>
public interface IDropTarget
{
/// <summary>
/// Gets the effects.
/// </summary>
/// <param name="dataObject">The data object.</param>
/// <returns></returns>
DragEffects GetDropEffects(IDraggedData dataObject);

/// <summary>
/// Drops the specified data object
/// </summary>
/// <param name="dataObject">The data object.</param>
void Drop(IDraggedData dataObject);
}
We need the methods for the effects to instruct the system about the types of operations that are allowed during drag and drop: None, Copy, Move, Scroll, All.

If you are going for the purist approach, you should use your own DragEffects enumeration, as above, since the DragDropEffects enumeration is in the System.Windows assembly, which in theory should have nothing to do with the ViewModel part of the application (one could want to use the ViewModel in a web environment, for example, or in a console application).

You will notice that the GetDropEffects method receives an IDraggedData object. This is also because the IDataObject interface and the DataObject class used in Windows drag and drop operations are also in the System.Windows assembly.

The IDraggedData interface is basically a dictionary that uses the data format as the key and the dragged data object stored in that format as the value. An important fact is that, when trying to drag and drop between applications, you need that the dragged data object be binary serializable. If not, you will only get the expected result when dragging to the same application. Here is an implementation of the interface, complete with a totally lazy way of getting the data based on which type is more "important":

/// <summary>
/// Holds data for a drag-and-drop operation
/// </summary>
public class DraggedData : IDraggedData
{
#region Instance fields

private Dictionary<string, Exception> mExceptions;
private Dictionary<string, object> mValues;

#endregion

#region Properties

/// <summary>
/// A dictionary with the format as the key and the data in that format in the value
/// </summary>
/// <value></value>
public Dictionary<string, object> Values
{
get
{
if (mValues == null)
{
mValues = new Dictionary<string, object>();
}
return mValues;
}
}

/// <summary>
/// A dictionary with the format as the key and the data in that format in the value
/// </summary>
/// <value></value>
IDictionary<string, object> IDraggedData.Values
{
get
{
return Values;
}
}

/// <summary>
/// Optional object for additional information
/// </summary>
/// <value></value>
public object Tag
{
get;
set;
}

/// <summary>
/// A dictionary for exceptions when retrieving the data in a specified format
/// </summary>
/// <value>The exceptions.</value>
public Dictionary<string, Exception> Exceptions
{
get
{
if (mExceptions == null)
{
mExceptions = new Dictionary<string, Exception>();
}
return mExceptions;
}
}

#endregion
}
In the IDragSource interface we need the GetData method to extract the data object associated with a dragged object, since the Windows drag and drop mechanism encapsulates the objects in an application agnostic way, so one can perform drag and drop between applications or to/from the operating system. Finally, we need the Drop method in the IDropTarget interface to handle in the ViewModel what happends when an item is dropped.

Let's get to the juicy part: a DragService static class that will register the attached properties that we need. Besides the DragSource and DropTarget properties we need status properties like DragOverStatus (for the target), DraggedStatus and IsDragged (for the original dragged item) as well as two properties called BringIntoViewOnDrag and ActivateOnDrag which would bring an item completely into view or activate it (if a Window) when a valid drop target. This one is long. It also contains some extension methods that would be explained later.

Click to expand/collapse

/// <summary>
/// Holds attached properties related to drag-and-drop operations
/// </summary>
public static class DragService
{
#region Static Instance fields

/// <summary>
/// Property for the behaviour to activate a window when something is dragged over it
/// </summary>
public static readonly DependencyProperty ActivateOnDragProperty
= DependencyProperty.RegisterAttached(
"ActivateOnDrag",
typeof (bool), typeof (DragService),
new FrameworkPropertyMetadata(
false)
);

/// <summary>
/// Property for the behaviour to bring into view an element when something is dragged over it
/// </summary>
public static readonly DependencyProperty BringIntoViewOnDragProperty
= DependencyProperty.RegisterAttached(
"BringIntoViewOnDrag",
typeof (bool), typeof (DragService),
new FrameworkPropertyMetadata(
false)
);

/// <summary>
/// Property for the status of a drag-over operation
/// </summary>
public static readonly DependencyProperty DragOverStatusProperty
= DependencyProperty.RegisterAttached(
"DragOverStatus",
typeof (DragEffects), typeof (DragService),
new FrameworkPropertyMetadata(DragEffects.None)
);

/// <summary>
/// Property for the handler of drag-source operations
/// </summary>
public static readonly DependencyProperty DragSourceProperty
= DependencyProperty.RegisterAttached(
"DragSource",
typeof (IDragSource), typeof (DragService),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(dragSourceChanged))
);

/// <summary>
/// Property for the drag status of a drag source
/// </summary>
public static readonly DependencyProperty DragStatusProperty
= DependencyProperty.RegisterAttached(
"DragStatus",
typeof (DragEffects), typeof (DragService),
new FrameworkPropertyMetadata(DragEffects.None)
);

/// <summary>
/// Property for the handler of drop-target operations
/// </summary>
public static readonly DependencyProperty DropTargetProperty
= DependencyProperty.RegisterAttached(
"DropTarget",
typeof (IDropTarget), typeof (DragService),
new FrameworkPropertyMetadata(null, new PropertyChangedCallback(dropTargetChanged)));

/// <summary>
/// True if element is part of the content displayed while dragging
/// </summary>
public static readonly DependencyProperty IsDraggedProperty
= DependencyProperty.RegisterAttached(
"IsDragged",
typeof (bool), typeof (DragService),
new FrameworkPropertyMetadata(false)
);

private static DependencyObject sDraggedDependencyObject;
private static Point? sStartPoint;

#endregion

#region Static Public Methods

///<summary>
/// Attached getter method for DragOverStatus
///</summary>
///<param name="element"></param>
///<returns></returns>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[AttachedPropertyBrowsableForType(typeof (UIElement))]
public static DragEffects GetDragOverStatus(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (DragEffects) element.GetValue(DragOverStatusProperty);
}

///<summary>
/// Attached setter method for DragOverStatus
///</summary>
///<param name="element"></param>
///<param name="value"></param>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public static void SetDragOverStatus(DependencyObject element, DragEffects value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(DragOverStatusProperty, value);
}

///<summary>
/// Attached getter method for DragStatus
///</summary>
///<param name="element"></param>
///<returns></returns>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[AttachedPropertyBrowsableForType(typeof (UIElement))]
public static DragEffects GetDragStatus(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (DragEffects) element.GetValue(DragStatusProperty);
}

///<summary>
/// Attached setter method for DragStatus
///</summary>
///<param name="element"></param>
///<param name="value"></param>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public static void SetDragStatus(DependencyObject element, DragEffects value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(DragStatusProperty, value);
}

///<summary>
/// Attached getter method for IsDragged
///</summary>
///<param name="element"></param>
///<returns></returns>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[AttachedPropertyBrowsableForType(typeof (UIElement))]
public static bool GetIsDragged(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (bool) element.GetValue(IsDraggedProperty);
}

///<summary>
/// Attached setter method for IsDragged
///</summary>
///<param name="element"></param>
///<param name="value"></param>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public static void SetIsDragged(DependencyObject element, bool value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(IsDraggedProperty, value);
}

/// <summary>
/// Try to get the dragged data in its most qualified format:
/// something not null and not string, string, anything else, null
/// </summary>
/// <param name="e"></param>
/// <returns></returns>
public static object GetBestDraggedDataObject(DragEventArgs e)
{
DraggedData draggedData = getData(e);
object data = null;
foreach (KeyValuePair<string, object> pair in draggedData.Values)
{
object value = pair.Value;
if (value == null)
{
continue;
}
if (data == null)
{
data = value;
continue;
}
if ((data is string) && !(value is string))
{
data = value;
break;
}
}
return data;
}

///<summary>
/// Attached getter method for DropTarget
///</summary>
///<param name="element"></param>
///<returns></returns>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[AttachedPropertyBrowsableForType(typeof (UIElement))]
public static IDropTarget GetDropTarget(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (IDropTarget) element.GetValue(DropTargetProperty);
}

///<summary>
/// Attached setter method for DropTarget
///</summary>
///<param name="element"></param>
///<param name="value"></param>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public static void SetDropTarget(DependencyObject element, IDropTarget value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(DropTargetProperty, value);
}

///<summary>
/// Attached getter method for DragSource
///</summary>
///<param name="element"></param>
///<returns></returns>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[AttachedPropertyBrowsableForType(typeof (UIElement))]
public static IDragSource GetDragSource(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (IDragSource) element.GetValue(DragSourceProperty);
}

///<summary>
/// Attached setter method for DragSource
///</summary>
///<param name="element"></param>
///<param name="value"></param>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public static void SetDragSource(DependencyObject element, IDragSource value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(DragSourceProperty, value);
}

///<summary>
/// Attached getter method for BringIntoViewOnDrag
///</summary>
///<param name="element"></param>
///<returns></returns>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[AttachedPropertyBrowsableForType(typeof (FrameworkElement))]
public static bool GetBringIntoViewOnDrag(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (bool) element.GetValue(BringIntoViewOnDragProperty);
}

///<summary>
/// Attached setter method for BringIntoViewOnDrag
///</summary>
///<param name="element"></param>
///<param name="value"></param>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public static void SetBringIntoViewOnDrag(DependencyObject element, bool value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(BringIntoViewOnDragProperty, value);
}

///<summary>
/// Attached getter method for ActivateOnDrag
///</summary>
///<param name="element"></param>
///<returns></returns>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[AttachedPropertyBrowsableForType(typeof (UIElement))]
public static bool GetActivateOnDrag(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return (bool) element.GetValue(ActivateOnDragProperty);
}

///<summary>
/// Attached setter method for ActivateOnDrag
///</summary>
///<param name="element"></param>
///<param name="value"></param>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public static void SetActivateOnDrag(DependencyObject element, bool value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(ActivateOnDragProperty, value);
}

#endregion

#region Static Private Methods

private static void dropTargetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement) d;

if (e.NewValue != null)
{
registerDropTarget(element);
}
else
{
unregisterDropTarget(element);
}
}

private static void unregisterDropTarget(UIElement element)
{
element.DragOver -= dragOver;
element.DragLeave -= elementDragLeave;
element.Drop -= drop;
element.AllowDrop = false;
}

private static void registerDropTarget(UIElement element)
{
element.DragOver += dragOver;
element.DragLeave += elementDragLeave;
element.Drop += drop;
element.AllowDrop = true;
element.ExecuteWhenUnloaded(() => unregisterDropTarget(element));
}

private static void elementDragLeave(object sender, DragEventArgs e)
{
DependencyObject dependencyObject = (DependencyObject) sender;
SetDragOverStatus(dependencyObject, DragEffects.None);
}

private static void drop(object sender, DragEventArgs e)
{
DependencyObject dependencyObject = (DependencyObject) sender;
IDropTarget dropTarget = GetDropTarget(dependencyObject);
DraggedData data = getData(e);
dropTarget.Drop(data);
SetDragOverStatus(dependencyObject, DragEffects.None);
e.Handled = true;
}

private static DraggedData getData(DragEventArgs e)
{
DraggedData data = new DraggedData();
string[] formats = e.Data.GetFormats(false);
foreach (string format in formats)
{
try
{
data.Values[format] = e.Data.GetData(format);
}
catch (Exception ex)
{
data.Values[format] = null;
data.Exceptions[format] = ex;
}
}
return data;
}

private static void dragOver(object sender, DragEventArgs e)
{
DependencyObject dependencyObject = (DependencyObject) sender;
IDropTarget dropTarget = GetDropTarget(dependencyObject);

DraggedData data = getData(e);
DragEffects dragEffects = dropTarget.GetDropEffects(data);
e.Effects = getEffects(dragEffects);
if (sDraggedDependencyObject != null)
{
SetDragStatus(sDraggedDependencyObject, dragEffects);
}
SetDragOverStatus(dependencyObject, dragEffects);

e.Handled = true;
if (GetActivateOnDrag(dependencyObject))
{
Window window = Window.GetWindow(dependencyObject);
if (window != null && !window.IsActive)
{
window.Activate();
}
}
if (GetBringIntoViewOnDrag(dependencyObject))
{
FrameworkElement element = dependencyObject as FrameworkElement;
if (element != null)
{
element.BringIntoView();
}
}
}

private static DragDropEffects getEffects(DragEffects effects)
{
DragDropEffects result;
return Enum.TryParse(effects.ToString(), out result)
? result
: (DragDropEffects) (int) effects;
}

private static void dragSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UIElement element = (UIElement) d;
if (e.NewValue != null)
{
registerDragItem(element);
}
else
{
unregisterDragItem(element);
}
}

private static void unregisterDragItem(UIElement element)
{
element.PreviewMouseLeftButtonDown -= previewMouseLeftButtonDown;
element.PreviewMouseMove -= previewMouseMove;
element.MouseLeave -= mouseLeave;
element.QueryContinueDrag -= elementQueryContinueDrag;
element.GiveFeedback -= elementGiveFeedback;
}

private static void registerDragItem(UIElement element)
{
element.PreviewMouseLeftButtonDown += previewMouseLeftButtonDown;
element.PreviewMouseMove += previewMouseMove;
element.MouseLeave += mouseLeave;
element.QueryContinueDrag += elementQueryContinueDrag;
element.GiveFeedback += elementGiveFeedback;
element.ExecuteWhenUnloaded(() => unregisterDragItem(element));
}

private static void elementGiveFeedback(object sender, GiveFeedbackEventArgs e)
{
if (sDraggedDependencyObject != null)
{
DragEffects dragDropEffects = getDragDropEffects(e.Effects);
SetDragStatus(sDraggedDependencyObject, dragDropEffects);
}
}

private static DragEffects getDragDropEffects(DragDropEffects effects)
{
DragEffects result;
return Enum.TryParse(effects.ToString(), out result)
? result
: (DragEffects) (int) effects;
}

private static void elementQueryContinueDrag(object sender, QueryContinueDragEventArgs e)
{
if (!e.KeyStates.HasFlag(DragDropKeyStates.LeftMouseButton))
{
DependencyObject dependencyObject = sender as DependencyObject ?? sDraggedDependencyObject;
endDrag(dependencyObject);
}
}

private static void endDrag(DependencyObject dependencyObject)
{
if (dependencyObject == null)
{
return;
}
SetIsDragged(dependencyObject, false);
if (sDraggedDependencyObject != null)
{
SetDragStatus(sDraggedDependencyObject, DragEffects.None);
}
sDraggedDependencyObject = null;
}

private static void mouseLeave(object sender, MouseEventArgs e)
{
sStartPoint = null;
}

private static void previewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || sStartPoint == null)
{
return;
}

if (!hasMouseMovedFarEnough(e))
{
return;
}

FrameworkElement dependencyObject = (FrameworkElement) sender;
object dataContext = dependencyObject.GetValue(FrameworkElement.DataContextProperty);
IDragSource dragSource = GetDragSource(dependencyObject);

DragDropEffects dragDropEffects = getEffects(dragSource.GetDragEffects(dataContext));
if (dragDropEffects == DragDropEffects.None)
{
return;
}
startDrag(dependencyObject);
DragDrop.DoDragDrop(dependencyObject,
getDraggedData(dragSource, dataContext),
dragDropEffects);
}

private static void startDrag(DependencyObject dependencyObject)
{
SetIsDragged(dependencyObject, true);
sDraggedDependencyObject = dependencyObject;
}

private static object getDraggedData(IDragSource dragSource, object dataContext)
{
object data = dragSource.GetData(dataContext);
DataObject dataObject = new DataObject();
if (data != null)
{
if (!SerializationHelper.IsBinarySerializable(data))
{
DebugHelper.Warn(
"Trying to drag a DataItem that cannot be binary serialized. It will not work across applications");
}
dataObject.SetData(DataFormats.Text, data.ToString());
string typeName = data.GetType().FullName;
if (typeName != null)
{
DataFormat format = DataFormats.GetDataFormat(typeName);
dataObject.SetData(format.Name, data);
}
}
return dataObject;
}

private static void previewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
sStartPoint = e.GetPosition(null);
}

private static bool hasMouseMovedFarEnough(MouseEventArgs e)
{
if (sStartPoint == null)
{
return false;
}
Vector delta = sStartPoint.GetValueOrDefault() - e.GetPosition(null);

return Math.Abs(delta.X) > SystemParameters.MinimumHorizontalDragDistance ||
Math.Abs(delta.Y) > SystemParameters.MinimumVerticalDragDistance;
}

#endregion
}
Most of the code here is self explanatory: you have attached property that bind to the element drag and drop events and handle them either through direct code or by delegating to the values set. There are some extension methods like ExecuteWhenLoaded and ExecuteWhenUnloaded which execute an action when the element has finished loading or when it is unloading. An important aspect here is that a window closing does not trigger the Unloaded event for its child elements, so you need to bind to the Dispatcher.StartedShutdown event as well:

/// <summary>
/// Execute an action when an element is unloaded
/// </summary>
/// <param name="element"></param>
/// <param name="action"></param>
public static void ExecuteWhenUnloaded(this UIElement element, Action action)
{
if (element == null)
{
throw new ArgumentNullException("element",
Resources.
UIExtensions_ExecuteWhenUnloaded_Element_cannot_be_null);
}
RoutedEventHandler elementOnUnloaded = null;
EventHandler dispatcherOnShutdownStarted = null;
// ReSharper disable AccessToModifiedClosure
elementOnUnloaded = (sender, args) =>
{
performExecution(element,
dispatcherOnShutdownStarted,
elementOnUnloaded,
action);
};
dispatcherOnShutdownStarted = (sender, args) =>
{
performExecution(element,
dispatcherOnShutdownStarted,
elementOnUnloaded, action);
};
// ReSharper restore AccessToModifiedClosure
FrameworkElement frameworkElement = element as FrameworkElement;
if (frameworkElement != null)
{
frameworkElement.Unloaded += elementOnUnloaded;
}
element.Dispatcher.ShutdownStarted += dispatcherOnShutdownStarted;
}

That is pretty much it for the drag and drop itself. You can implement IDropTarget on the ViewModel directly, but IDragSource needs to be binary serializable if you intend to drag and drop across application domains, so you usually implement it into a separate class that is a property of the dragged item view model or DataContext.

Because the DragService sets the status properties for each element, you can manipulate the appearance and behaviour of both drag source and drop target based on them. However, at this point the mouse will be the only indication that you are dragging something. You might want to actually drag something, especially since in a move operation the original element would be hidden during the drag. The problem here is that the drag source element cannot control the display of the dragged item in other applications, nor should it in its application, since it is not its responsibility. The solution: decorate an element over which you would drag something (like the root element of the entire window) with something that knows how to display dragged items:
Click to expand/collapse

/// <summary>
/// Decorator for an element that would respons visually to a drag-over operation
/// </summary>
public class DragAndDropAdornerDecorator : ContentControl
{
#region Instance fields

private DragAndDropAdorner mAdorner;

#endregion

#region Properties

/// <summary>
/// Visual content for the dragged data
/// </summary>
public FrameworkElement AdornerContent
{
get
{
return (FrameworkElement) GetValue(AdornerContentProperty);
}
set
{
SetValue(AdornerContentProperty, value);
}
}

/// <summary>
/// The data being dragged
/// </summary>
public object DraggedData
{
get
{
return GetValue(DraggedDataProperty);
}
set
{
SetValue(DraggedDataProperty, value);
}
}

/// <summary>
/// The position of the drag point in the decorator content
/// </summary>
public Point Offset
{
get
{
return (Point) GetValue(OffsetProperty);
}
set
{
SetValue(OffsetProperty, value);
}
}

/// <summary>
/// Represents the point in which a dragged item has first entered the drag zone.
/// Use it to create animations when a drop operation did not succeed.
/// </summary>
public Point? EntryPoint
{
get
{
return (Point?) GetValue(EntryPointProperty);
}
private set
{
SetValue(EntryPointPropertyKey, value);
}
}

#endregion

#region Constructors

/// <summary>
/// Initializes a new instance of the <see cref="DragAndDropAdornerDecorator"/> class.
/// </summary>
public DragAndDropAdornerDecorator()
{
Focusable = false; // By default don't want 'AdornedControl' to be focusable.

DataContextChanged += dragAndDropAdornerDecoratorDataContextChanged;
updateAdornerDataContext();
}

#endregion

#region Protected Methods

/// <summary>
/// Called when the <see cref="T:System.Windows.Media.VisualCollection"/> of the visual object is modified.
/// </summary>
/// <param name="visualAdded">The <see cref="T:System.Windows.Media.Visual"/> that was added to the collection</param><param name="visualRemoved">The <see cref="T:System.Windows.Media.Visual"/> that was removed from the collection</param>
protected override void OnVisualChildrenChanged(DependencyObject visualAdded,
DependencyObject visualRemoved)
{
UIElement element = visualRemoved as UIElement;
if (element != null)
{
dettachAdorner(element);
}
base.OnVisualChildrenChanged(visualAdded, visualRemoved);
element = visualAdded as UIElement;
if (element != null)
{
attachAdorner(element);
this.ExecuteWhenUnloaded(() => dettachAdorner(element));
}
}

#endregion

#region Private Methods

private void dragAndDropAdornerDecoratorDataContextChanged(object sender,
DependencyPropertyChangedEventArgs e)
{
updateAdornerDataContext();
}

/// <summary>
/// Update the DataContext of the adorner from the adorned control.
/// </summary>
private void updateAdornerDataContext()
{
if (AdornerContent != null)
{
AdornerContent.DataContext = DataContext;
}
}

private void attachAdorner(UIElement element)
{
mAdorner = new DragAndDropAdorner(element, AdornerContent)
{
Decorator = this
};
mAdorner.Attach();
AddLogicalChild(mAdorner.AdornerLayer);
element.AddHandler(DragEnterEvent, new DragEventHandler(elementDragEnter), true);
element.AddHandler(DragOverEvent, new DragEventHandler(elementDragOver), true);
element.AddHandler(DragLeaveEvent, new DragEventHandler(elementDragLeave), true);
element.AddHandler(QueryContinueDragEvent,
new QueryContinueDragEventHandler(elementQueryContinueDrag), true);
element.AddHandler(DropEvent,
new DragEventHandler(elementDrop), true);
hideAdorner();
}

private void elementDragEnter(object sender, DragEventArgs e)
{
EntryPoint = e.GetPosition(this);
}

private void elementDrop(object sender, DragEventArgs e)
{
hideAdorner();
}

private void elementQueryContinueDrag(object sender, QueryContinueDragEventArgs e)
{
if (!e.KeyStates.HasFlag(DragDropKeyStates.LeftMouseButton))
{
hideAdorner();
}
}

private void elementDragLeave(object sender, DragEventArgs e)
{
hideAdorner();
}

private void hideAdorner()
{
DraggedData = null;
EntryPoint = null;
if (mAdorner == null)
{
return;
}
mAdorner.Hide();
}

private void elementDragOver(object sender, DragEventArgs e)
{
DraggedData = DragService.GetBestDraggedDataObject(e);
showAdorner(e.GetPosition(this));
}

private void showAdorner(Point position)
{
if (mAdorner == null)
{
return;
}
mAdorner.Show(position);
}

private void dettachAdorner(UIElement element)
{
if (mAdorner == null)
{
return;
}
RemoveLogicalChild(mAdorner.AdornerLayer);
mAdorner.DisconnectChild();
mAdorner.Dettach();
mAdorner = null;
element.RemoveHandler(DragEnterEvent, new DragEventHandler(elementDragEnter));
element.RemoveHandler(DragOverEvent, new DragEventHandler(elementDragOver));
element.RemoveHandler(DragLeaveEvent, new DragEventHandler(elementDragLeave));
element.RemoveHandler(QueryContinueDragEvent,
new QueryContinueDragEventHandler(elementQueryContinueDrag));
element.RemoveHandler(DropEvent, new DragEventHandler(elementDrop));
}

#endregion

#region Static Instance fields

public static readonly DependencyProperty AdornerContentProperty
= DependencyProperty.Register("AdornerContent",
typeof (FrameworkElement),
typeof (DragAndDropAdornerDecorator),
new FrameworkPropertyMetadata(null));

public static readonly DependencyProperty DraggedDataProperty
= DependencyProperty.RegisterAttached(
"DraggedData",
typeof (object), typeof (DragAndDropAdornerDecorator),
new FrameworkPropertyMetadata(null,
FrameworkPropertyMetadataOptions.
OverridesInheritanceBehavior
| FrameworkPropertyMetadataOptions.Inherits)
);

public static readonly DependencyPropertyKey EntryPointPropertyKey
= DependencyProperty.RegisterReadOnly("EntryPoint",
typeof (Point?), typeof (DragAndDropAdornerDecorator),
new FrameworkPropertyMetadata(default(Point?)));

public static readonly DependencyProperty EntryPointProperty =
EntryPointPropertyKey.DependencyProperty;


public static readonly DependencyProperty OffsetProperty
= DependencyProperty.Register("Offset",
typeof (Point), typeof (DragAndDropAdornerDecorator),
new FrameworkPropertyMetadata(new Point()));

#endregion

#region Static Public Methods

///<summary>
/// Attached getter method for DraggedData
///</summary>
///<param name="element"></param>
///<returns></returns>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
[AttachedPropertyBrowsableForType(typeof (UIElement))]
public static object GetDraggedData(DependencyObject element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
return element.GetValue(DraggedDataProperty);
}

///<summary>
/// Attached setter method for DraggedData
///</summary>
///<param name="element"></param>
///<param name="value"></param>
///<exception cref="ArgumentNullException"></exception>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public static void SetDraggedData(DependencyObject element, object value)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
element.SetValue(DraggedDataProperty, value);
}

#endregion
}
This code uses an adorner to display the dragged item over it. Nothing fancy, since the actual template for the dragged data is defined in the decorator as the content. This is not the place to discuss the adorner, though, so here is just the code:
Click to expand/collapse

/// <summary>
/// Adorner for the drag-and-drop operations: see DragHelper and DragAndDropDecorator
/// </summary>
public class DragAndDropAdorner : AdornerBase
{
#region Instance fields

private readonly FrameworkElement mChild;
private Point mPosition;

#endregion

#region Properties

/// <summary>
/// Gets the number of visual child elements within this element.
/// </summary>
/// <returns>
/// The number of visual child elements for this element.
/// </returns>
protected override int VisualChildrenCount
{
get
{
return 1;
}
}

/// <summary>
/// Gets an enumerator for logical child elements of this element.
/// </summary>
/// <returns>
/// An enumerator for logical child elements of this element.
/// </returns>
protected override IEnumerator LogicalChildren
{
get
{
yield return mChild;
}
}

/// <summary>
/// Reference to the decorator that instantiates the adorner
/// </summary>
public DragAndDropAdornerDecorator Decorator
{
get;
set;
}

#endregion

#region Constructors

/// <summary>
/// Instantiate an adorner for an element over which to show the dragged content
/// </summary>
/// <param name="adornedElement"></param>
/// <param name="adornerContent"></param>
public DragAndDropAdorner(UIElement adornedElement, FrameworkElement adornerContent)
: base(adornedElement)
{
IsHitTestVisible = false;
Focusable = false;
mChild = adornerContent;
connectChild();
}

#endregion

#region Public Methods

/// <summary>
/// Disconnect the child element from the visual tree so that it may be reused later.
/// </summary>
public void DisconnectChild()
{
RemoveLogicalChild(mChild);
RemoveVisualChild(mChild);
}

/// <summary>
/// Hide the adorner content
/// </summary>
public void Hide()
{
Opacity = 0.0;
Visibility = Visibility.Collapsed;
}

/// <summary>
/// Show the adorner content at a certain position
/// </summary>
/// <param name="point"></param>
public void Show(Point point)
{
mPosition = point;
Opacity = 1.0;
Visibility = Visibility.Visible;
InvalidateArrange();
}

#endregion

#region Protected Methods

/// <summary>
/// Implements any custom measuring behavior for the adorner.
/// </summary>
/// <returns>
/// A <see cref="T:System.Windows.Size"/> object representing the amount of layout space needed by the adorner.
/// </returns>
/// <param name="constraint">A size to constrain the adorner to.</param>
protected override Size MeasureOverride(Size constraint)
{
mChild.Measure(constraint);
return mChild.DesiredSize;
}

/// <summary>
/// When overridden in a derived class, positions child elements and determines a size for a <see cref="T:System.Windows.FrameworkElement"/> derived class.
/// </summary>
/// <returns>
/// The actual size used.
/// </returns>
/// <param name="finalSize">The final area within the parent that this element should use to arrange itself and its children.</param>
protected override Size ArrangeOverride(Size finalSize)
{
double adornerWidth = mChild.DesiredSize.Width;
double adornerHeight = mChild.DesiredSize.Height;
double offsetX=0;
double offsetY=0;
if (Decorator!=null)
{
offsetX = -Decorator.Offset.X;
offsetY = -Decorator.Offset.Y;
}
mChild.Arrange(new Rect(mPosition.X + offsetX, mPosition.Y + offsetY, adornerWidth, adornerHeight));
return finalSize;
}

/// <summary>
/// Overrides <see cref="M:System.Windows.Media.Visual.GetVisualChild(System.Int32)"/>, and returns a child at the specified index from a collection of child elements.
/// </summary>
/// <returns>
/// The requested child element. This should not return null; if the provided index is out of range, an exception is thrown.
/// </returns>
/// <param name="index">The zero-based index of the requested child element in the collection.</param>
protected override Visual GetVisualChild(int index)
{
return mChild;
}

#endregion

#region Private Methods

private void connectChild()
{
AddLogicalChild(mChild);
AddVisualChild(mChild);
}

#endregion
}
You would only need to decorate a view and then define the AdornerContent property for the decorator and, optionally, the grab point offset for the dragged element.

All that is left here is to show some usage examples. Let's assume we need a View over which we can drag and drop items:
<UserControl x:Class="BestPractices.Views.SecondaryView"
[...]
UIUtils:DragService.DropTarget="{Binding .}"
UIUtils:DragService.BringIntoViewOnDrag="True"
UIUtils:DragService.ActivateOnDrag="True"
UIUtils:DragService.DragOverStatus="{Binding DragOverStatus,Mode=OneWayToSource}">
Here you have a user control view which has the drop target set to its own view model and it is set to update the DragOverStatus property of the ViewModel when its attached DragOverStatus property is changed. The status properties are inheritable, so all the children of the view have them set. It is easy to define a Button style that has its text bolded when a copy operation is allowed for a drop item:
<Style TargetType="{x:Type Button}" BasedOn="{StaticResource {x:Type Button}}" >  <Style.Triggers>
<DataTrigger Binding="{Binding DragOverStatus}" Value="{x:Static Utils:DragEffects.Copy}">
<Setter Property="FontWeight" Value="Bold"/>
</DataTrigger>
</Style.Triggers
></Style>
Its content is more interesting:
<UIUtils:DragAndDropAdornerDecorator Offset="40,40">
<UIUtils:DragAndDropAdornerDecorator.AdornerContent>
<Controls:ContentItem
DataContext="{Binding Path=(UIUtils:DragAndDropAdornerDecorator.DraggedData),
RelativeSource={RelativeSource Self}}"

Opacity="0.7"/>
</UIUtils:DragAndDropAdornerDecorator.AdornerContent>
<DockPanel Background="{Binding Background,
RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
>
<Button Command="{m:CommandBinding AddItemCommand}"
DockPanel.Dock="Top"
>Add item</Button>
<Button Command="{m:CommandBinding RemoveItemCommand}"
DockPanel.Dock="Top"
>Remove item</Button>
<WrapPanel x:Name="ContainerPanel" >
</WrapPanel>
</DockPanel>
</UIUtils:DragAndDropAdornerDecorator>
The container for the items is a simple WrapPanel and it is placed in a dock panel together with add and remove item buttons. This dock panel is decorated as a drag visual container, and the content that is dragged is set to a custom control called ContentItem, with a drag point set to 40,40. The DataContext property of the item is set to the DraggedData property so that it expresses the actual dragged object.

Now we have set up a container to be a drop target for items. It displays the items as they are dragged over it. All we have left is to set up the items, the ContentItem control, to be a DragSource:

<Style TargetType="{x:Type Controls:ContentItem}">
<Setter Property="UIUtils:DragService.DragSource" Value="{Binding DragSource}"/>
<Setter Property="UIUtils:DragService.IsDragged" Value="{Binding IsDragged,Mode=OneWayToSource}"/>
<Setter Property="UIUtils:DragService.DragStatus" Value="{Binding DragStatus,Mode=OneWayToSource}"/>
<Setter Property="Background" Value="LightBlue"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Controls:ContentItem}">
<Border BorderThickness="1" BorderBrush="Blue" CornerRadius="2" Background="{TemplateBinding Background}" Width="75" Height="60">
<TextBlock Text="{Binding Id}" HorizontalAlignment="Center" VerticalAlignment="Center" FontWeight="Bold" FontSize="22"/>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding IsDragged}" Value="True">
<Setter Property="Background" Value="LightPink"/>
</DataTrigger>
<DataTrigger Binding="{Binding DragStatus}" Value="{x:Static Utils:DragEffects.Copy}">
<Setter Property="Background" Value="Lime"/>
</DataTrigger>
</Style.Triggers>
</Style>
The control style defines as a drag source the DragSource property of the data context of the item and synchronizes with the data context the properties of IsDragged and DragStatus. Triggers then make is pinkish when dragged and greenish when it can be dropped. Notice that this applies to the original item, while its representation is dragged, so you have a feedback of what is going on with the item right at the source.

I won't put here the ViewModels or the data items, since they are pretty much part of the business context, not the drag and drop. Just return DragEffects. All on the effects methods and you can drag anything anywhere, for example.

That's it, folks: drag and drop completely MVVM, without as much as writing an event handler or caring about the actual elements in the viewmodel. It would be even easier if you would allow references to WPF assemblies in the ViewModels, since you could also get the source elements and do stuff with them, but that wouldn't be much of an MVVM pattern, would it?

And here is the AdornerBase class, just a simple helper class:

/// <summary>
/// Basic adorner class that exposes simple Attach and Dettach methods
/// </summary>
public abstract class AdornerBase : Adorner
{
#region Instance fields

private bool mIsDettached;

#endregion

#region Properties

public bool IsDettached
{
get
{
return mIsDettached;
}
}

public AdornerLayer AdornerLayer
{
get;
private set;
}

#endregion

#region Constructors

protected AdornerBase(UIElement adornedElement)
: base(adornedElement)
{
mIsDettached = true;
}

#endregion

#region Public Methods

/// <summary>
/// Attach the adorner to the element's adorner layer
/// </summary>
public void Attach()
{
AdornerLayer = AdornerLayer.GetAdornerLayer(AdornedElement);
if (AdornerLayer != null)
{
AdornerLayer.Add(this);
mIsDettached = false;
}
}

/// <summary>
/// Dettach the adorner from the element's adorner layer
/// </summary>
public void Dettach()
{
AdornerLayer = AdornerLayer ?? AdornerLayer.GetAdornerLayer(AdornedElement);
if (AdornerLayer != null)
{
AdornerLayer.Remove(this);
mIsDettached = true;
}
}

#endregion
}

14 comments:

Alberto said...

Thank you a lot!! It worked for me, but now I need a little additional behavior, not in the dragged item but in the target container in the application, I mean: I want to drag a treeViewItem object from a treeView and drop it in another place of the application (it could be a Canvas, Grid, StackPanel), but when the mouse is over the target pane, we would show a border in the target container pane.

What could be the best approach to achieve this? I really don't know where indicate to the target pane that the border thikness have to change preserving the MVVM pattern.

Thanks again.

Siderite said...

If you want to show a border around the target, all you have to do is place it in a Grid together with a Border and bind the BorderThickness property of the border element to the DragService.DraggedOverStatus property. When something is dragged over, the border will show.

If you want a border around the item you are dragging, you need to change the AdornerContent

Peter Larsson said...

Hi, thank you for a great article explaining the issues with implementing DnD behavior in MVVM.

I was wondering If you have the code as a downloadable file?

/Peter

Siderite said...

I do, but there are a lot of custom changes I had to make on it because of the different issues that arose. I intended to complete the entry and also post the project somewhere, but I lack the time. So, no, I cannot give you a downloadable item for this post.

Anonymous said...

I cannot get this to compile. The problem seems to be the use of the AdornerBase class, from which you derive the DragAndDropAdorner class. What resource do I need to reference in order to use this class?

Siderite said...

Sorry about that. Here is the class now. I need to remind you, though, that this is a learning example, don't expect it to be perfect.

Narcis said...

I throws exception: Specified element is already the logical child of another element. Disconnect it first.

in DragAndDropAdornerDecorator:attachAdorner

Why is the adorner attached with OnVisualChildrenChanged ?(fired multiple times)

Siderite said...

I assure you this code worked, however, it had some issues that were resolved afterwards. I always wanted to remake this project in order to be "perfect", but I was out of time and now I am not working on WPF anymore.

Try to get the gist of the project and create your own, better version.

Narcis said...

yup.. works..my bad.

野老 said...

hello siderite,thanks for your blog.
i am a beginner about wpf and c#.
Excuse me, could you tell me where I can find a

"SerializationHelper. IsBinarySerializable (data)"?

Siderite said...

That is a simple method to determine if an object is binary serializable. It looks like this:
/// <summary>
/// Return true if it can be binary serialized
/// Warning: it actually serializes, maybe it should be a TrySerializeBinary...
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static bool IsBinarySerializable(object obj)
{
using (MemoryStream mem = new MemoryStream())
{
BinaryFormatter bin = new BinaryFormatter();
try
{
bin.Serialize(mem, obj);
return true;
}
catch (Exception)
{
return false;
}
}
}

It is not very elegant either, so I left it to the skill of the dev using the code.

HelloWorld said...

thanks for your kind,sir. XD.

Anonymous said...

Does someone have a sample View+ViewModel that illustrates the use of these classes for a drag-drop opeation?

Anonymous said...

can you please give the code function performExecution?