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.
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> publicinterface 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> publicinterface 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> publicinterface 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> publicclass DraggedData : IDraggedData { #region Instance fields
/// <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> publicobject 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.
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:
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:
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:
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:
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:
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:
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> publicabstractclass AdornerBase : Adorner { #region Instance fields
privatebool mIsDettached;
#endregion
#region Properties
publicbool IsDettached { get { return mIsDettached; } }
public AdornerLayer AdornerLayer { get; private set; }
/// <summary> /// Attach the adorner to the element's adorner layer /// </summary> publicvoid Attach() { AdornerLayer = AdornerLayer.GetAdornerLayer(AdornedElement); if (AdornerLayer != null) { AdornerLayer.Add(this); mIsDettached = false; } }
/// <summary> /// Dettach the adorner from the element's adorner layer /// </summary> publicvoid Dettach() { AdornerLayer = AdornerLayer ?? AdornerLayer.GetAdornerLayer(AdornedElement); if (AdornerLayer != null) { AdornerLayer.Remove(this); mIsDettached = true; } }
#endregion }
Comments
can you please give the code function performExecution?
Anonymous
Does someone have a sample View+ViewModel that illustrates the use of these classes for a drag-drop opeation?
Anonymous
thanks for your kind,sir. XD.
HelloWorld
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.
Siderite
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)"?
野老
yup.. works..my bad.
Narcis
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.
Siderite
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)
Narcis
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.
Siderite
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?
Anonymous
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.
Siderite
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
Peter Larsson
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
Siderite
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.
Comments
can you please give the code function performExecution?
AnonymousDoes someone have a sample View+ViewModel that illustrates the use of these classes for a drag-drop opeation?
Anonymousthanks for your kind,sir. XD.
HelloWorldThat 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.
Sideritehello 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)"?
野老yup.. works..my bad.
NarcisI 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.
SideriteI 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)
NarcisSorry 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.
SideriteI 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?
AnonymousI 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.
SideriteHi, 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
Peter LarssonIf 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
SideriteThank 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.
Alberto