I was working on some code whereby the user of the application would drag data from Excel (or a compatible file) onto the application to import the data and I wanted to create a fairly generic method, in WPF, to handle this so that I could apply the code to various controls within the application at different points in the user’s workflow. I also wanted the user interface to change slightly to better inform the user when they were on a control which they could drop data onto. I’m not going to go into how we get the dropped data into the view model or other import mechanism, but will concentrate on the UI interactions.
Note: The UI element of this post was inspired by the GitHub for Windows user interface in that when you drag a URL onto it, the Window changes to indicate it’s in a sort of “drop” mode.
The Behavior
I decided to create a behavior which would handle the various drag and drop events on the associated control. A behavior is a nice abstraction allowing us to augment existing controls with extra functionality or the likes.
As the behavior was to be general to many control types, I chose to create the behavior to work with UIElement’s as these offered the lowest level for hooking into the various drag and drop events.
Let’s look at the initial code for such a behavior
You’ll need to reference System.Windows.Interactivity which can be found in the References | Assemblies | Framework.
public class UIElementDropBehavior : Behavior<UIElement> { protected override void OnAttached() { base.OnAttached(); AssociatedObject.AllowDrop = true; AssociatedObject.DragEnter += AssociatedObject_DragEnter; AssociatedObject.DragOver += AssociatedObject_DragOver; AssociatedObject.DragLeave += AssociatedObject_DragLeave; AssociatedObject.Drop += AssociatedObject_Drop; } private void AssociatedObject_Drop(object sender, DragEventArgs e) { e.Handled = true; } private void AssociatedObject_DragLeave(object sender, DragEventArgs e) { e.Handled = true; } private void AssociatedObject_DragOver(object sender, DragEventArgs e) { e.Handled = true; } private void AssociatedObject_DragEnter(object sender, DragEventArgs e) { e.Handled = true; } }
So the above code is pretty simple. Obviously to be a drop target we need to ensure the associated UIElement is in AllowDrop mode and then handle the events ourselves. We’ll start to implement the specific code next, but before we do let’s see how this would be used in XAML. So within the control declaration of the control we want the behavior associated with (in XAML) we would simply use the following
<i:Interaction.Behaviors> <behaviors:UIElementDropBehavior /> </i:Interaction.Behaviors>
Okay, at this point this doesn’t really do a lot.
So the idea is that when the user drags the data over the control a new UI is displayed which displays a nice bitmap and the text “Drop Items Here” or similar. When the user drops the items the UI should switch back to it’s previous state or when the user drags outside/leaves the control it should also switch back to it’s previous state. I decided to implement this UI as an Adorner. So let’s take a look at this code.
The Adorner
We’re going to use some drawing primitives to display our bitmap and text. Let’s jump straight in a look at the code
public class UIElementDropAdorner : Adorner { public UIElementDropAdorner(UIElement adornedElement) : base(adornedElement) { Focusable = false; IsHitTestVisible = false; } protected override void OnRender(DrawingContext drawingContext) { const int PEN_WIDTH = 8; var adornedRect = new Rect(AdornedElement.RenderSize); drawingContext.DrawRectangle(Brushes.White, new Pen(Brushes.LightGray, PEN_WIDTH), adornedRect); var image = new BitmapImage( new Uri("pack://application:,,,/SomeAssembly;component/Resources/drop.png", UriKind.Absolute)); var typeface = new Typeface( new FontFamily("Segoe UI"), FontStyles.Normal, FontWeights.Normal, FontStretches.Normal); var formattedText = new FormattedText( "Drop Items Here", CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeface, 24, Brushes.LightGray); var centre = new Point( AdornedElement.RenderSize.Width / 2, AdornedElement.RenderSize.Height / 2); var top = centre.Y - (image.Height + formattedText.Height) / 2; var textLocation = new Point( centre.X - formattedText.WidthIncludingTrailingWhitespace / 2, top + image.Height); drawingContext.DrawImage(image, new Rect(centre.X - image.Width / 2, top, image.Width, image.Height)); drawingContext.DrawText(formattedText, textLocation); } }
Don’t forget to change the Uri of the image to one within your project resources.
So the key areas in the code (above) are that we need to set IsHitTestVisible to false. If we don’t do this then when we implement the relevant code to tie the behavior and adorner together we’ll find that when the user drags over the control our behavior is associated with, the behavior will display the adorner and the adorner will then intercept the rest of the drag and drop events. This will have the effect of actually causing the control that we’re handle drag and drop on to get a DragLeave event and the behavior will then hide the adorner. Then it’ll get a DragEnter event and display the adorner again – this will causes the adorner to flicker on and off – not ideal. So we want the adorner to ignore events and pass them back to the behavior.
The code in the OnRender, simply displays a PNG with the text “Drop Items Here” below it, centred within the control we’re associating the drag and drop behavior with.
Now we need to tie the behavior and adorner together, i.e. we need the behavior to display the adorner and remove it when either a drop or drag leave event is received. We’re going to create a simple class to manage the interactions with the adorner.
The AdornerManager
So the AdornerManager class is going to be used to simply create an adorner when it’s requried, display it and hide and remove it when it’s not required. Here’s the code…
public class AdornerManager { private readonly AdornerLayer adornerLayer; private readonly Func<UIElement, Adorner> adornerFactory; private Adorner adorner; public AdornerManager( AdornerLayer adornerLayer, Func<UIElement, Adorner> adornerFactory) { this.adornerLayer = adornerLayer; this.adornerFactory = adornerFactory; } public void Update(UIElement adornedElement) { if (adorner == null || !adorner.AdornedElement.Equals(adornedElement)) { Remove(); adorner = adornerFactory(adornedElement); adornerLayer.Add(adorner); adornerLayer.Update(adornedElement); adorner.Visibility = Visibility.Visible; } } public void Remove() { if (adorner != null) { adorner.Visibility = Visibility.Collapsed; adornerLayer.Remove(adorner); adorner = null; } } }
So this code is pretty simple, we manager the creation of the adorner which is created using the factory method supplied in the constructor. When we create the adorner we associated with the adorner layer on the underlying control and display it. The Remove method simply tidies up – hiding the adorner then removing it.
We now need to revisit the UIElementDropBehavior and get the various events to call the AdornerManager.
Almost complete
The completed code for the UIElementDropBehavior class is listed below
public class UIElementDropBehavior : Behavior<UIElement> { private AdornerManager adornerManager; protected override void OnAttached() { base.OnAttached(); AssociatedObject.AllowDrop = true; AssociatedObject.DragEnter += AssociatedObject_DragEnter; AssociatedObject.DragOver += AssociatedObject_DragOver; AssociatedObject.DragLeave += AssociatedObject_DragLeave; AssociatedObject.Drop += AssociatedObject_Drop; } private void AssociatedObject_Drop(object sender, DragEventArgs e) { if (adornerManager != null) { adornerManager.Remove(); } e.Handled = true; } private void AssociatedObject_DragLeave(object sender, DragEventArgs e) { if (adornerManager != null) { var inputElement = sender as IInputElement; if (inputElement != null) { var pt = e.GetPosition(inputElement); var element = sender as UIElement; if (element != null) { if (!pt.Within(element.RenderSize)) { adornerManager.Remove(); } } } } e.Handled = true; } private void AssociatedObject_DragOver(object sender, DragEventArgs e) { if (adornerManager != null) { var element = sender as UIElement; if (element != null) { adornerManager.Update(element); } } e.Handled = true; } private void AssociatedObject_DragEnter(object sender, DragEventArgs e) { if (adornerManager == null) { var element = sender as UIElement; if (element != null) { adornerManager = new AdornerManager( AdornerLayer.GetAdornerLayer(element), adornedElement => new UIElementDropAdorner(adornedElement)); } } e.Handled = true; } }
In the above code we create the AdornerManager the first time the DragEnter event occurs and supply it the factory method as AdornerLayer for the control we intend to display the adorner over. In the Drop handler we simply remove the adorner using the AdornerManager and in the DragOver handler we update the adorner.
The DragLeave handler is a little more complex than just removing the adorner when the leave event occurs. This is because I found that the control I was handling drag and drop on appeared to cause the behavior’s DragLeave event to occur which would then cause the adorner to be removed even when the mouse was still over the control, so to stop this happening we check whether the the mouse is now outside of the control we’re handling drag and drop on before removing the adorner. This stops the flickering on and off of the adorner.
I’ve created a very simply extension method used in the code above (the Within method). Here’s the code
public static class PointExtensions { public static bool Within(this Point pt, Rect r) { return pt.X >= r.X && pt.Y >= r.Y && pt.X < r.Width && pt.Y < r.Height; } public static bool Within(this Point pt, Size s) { return pt.Within(new Rect(s)); } }
Sample Code