This is part 3 of the WPF drag & drop exploration. Part 2 can be found here.
Let’s recap what we have right now. We have a sample application that allow us to freely drag items across an area. Now, since there might be multiple items stacking on top of each other, we want the user to be sure which item will be moved. For that, we want to add a visual indication on the item. This kind of stuff is called an adorner in WPF.
Now, you will see many samples of code-driven adorners implementation. But we don’t want to break the MVVM pattern, and to do that, we want the adorner to be implemented in the view.
So we’ll create an Adorner that allows us to use a DataTemplate. For that, I’ll just copy some existing code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class TemplateAdorner : Adorner { private readonly FrameworkElement frameworkElementAdorner; public TemplateAdorner(UIElement adornedElement, FrameworkElement frameworkElementAdorner) : base(adornedElement) { this.frameworkElementAdorner = frameworkElementAdorner; this.AddVisualChild(frameworkElementAdorner); this.AddLogicalChild(frameworkElementAdorner); } protected override int VisualChildrenCount { get { return 1; } } protected override Size ArrangeOverride(Size finalSize) { this.frameworkElementAdorner.Arrange(new Rect(new Point(0, 0), finalSize)); return finalSize; } protected override Visual GetVisualChild(int index) { return this.frameworkElementAdorner; } protected override Size MeasureOverride(Size constraint) { this.frameworkElementAdorner.Width = constraint.Width; this.frameworkElementAdorner.Height = constraint.Height; return constraint; } } |
We can now use this adorner in the behavior.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
public class DragOnCanvasBehavior : Behavior<DependencyObject> { // the mouse over adorner template property public static readonly DependencyProperty MouseOverAdornerTemplateProperty = DependencyProperty.Register( "MouseOverAdornerTemplate", typeof(DataTemplate), typeof(DragOnCanvasBehavior), new PropertyMetadata(new PropertyChangedCallback((d, o) => { if (((DragOnCanvasBehavior)d).mouseOverAdornerControl != null) { ((DragOnCanvasBehavior)d).mouseOverAdornerControl.ContentTemplate = (DataTemplate)o.NewValue; } }))); // the template public DataTemplate MouseOverAdornerTemplate { get { return (DataTemplate)this.GetValue(MouseOverAdornerTemplateProperty); } set { this.SetValue(MouseOverAdornerTemplateProperty, value); } } // stores the adorner instance private TemplateAdorner mouseOverAdorner; // stores the visual control private ContentControl mouseOverAdornerControl; // whether the adorner is initialized private bool mouseOverAdornerInitialized = false; // whether the adorner is shown private bool mouseOverAdornerShown = false; // the commands to hide/show the adorner public ICommand HideMouseOverAdorner { get; private set; } public ICommand ShowMouseOverAdorner { get; private set; } // binds the commands with the methods public DragOnCanvasBehavior() { // [...] this.ShowMouseOverAdorner = new RelayCommand((o) => { this.OnShowMouseOverAdorner(); }); this.HideMouseOverAdorner = new RelayCommand((o) => { this.OnHideMouseOverAdorner(); }); } // shows the adorner private void OnShowMouseOverAdorner() { if (this.mouseOverAdornerShown) { return; } this.InitializeAdorner(this.MouseOverAdornerTemplate, ref this.mouseOverAdornerControl, ref this.mouseOverAdorner, ref this.mouseOverAdornerInitialized); this.mouseOverAdornerControl.Visibility = Visibility.Visible; this.mouseOverAdornerShown = true; } // hides the adorner private void OnHideMouseOverAdorner() { this.mouseOverAdornerControl.Visibility = Visibility.Collapsed; this.mouseOverAdornerShown = false; } // initializes an adorner (we'll need to reuse this later on) private void InitializeAdorner(DataTemplate template, ref ContentControl control, ref TemplateAdorner adorner, ref bool initialized) { if (initialized) { return; } if (template != null) { AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(this.AssociatedObject as UIElement); if (adornerLayer == null) { throw new NullReferenceException(string.Format("No adorner found in attached object: {0}", this.AssociatedObject)); } control = new ContentControl(); adornerLayer.Add(adorner = new TemplateAdorner(this.AssociatedObject as UIElement, control)); control.Content = template.LoadContent(); Binding bindingMargin = new Binding("AdornerMargin"); bindingMargin.Source = this; BindingOperations.SetBinding(adorner, ContentControl.MarginProperty, bindingMargin); } var dataContext = (this.AssociatedObject as FrameworkElement).DataContext; if (control.DataContext == null) { control.DataContext = dataContext; } control.Visibility = Visibility.Collapsed; initialized = true; } } |
Now, we just need to create the adorner’s DataTemplate, and wire up the events. We just have to add an AdornerDecorator in the controls tree, otherwise the adorner may get a random behavior (it would be attached to the window), or not work at all.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
<ItemsControl.ItemTemplate> <DataTemplate> <AdornerDecorator> <Border x:Name="DraggableBorder"> <!-- contents --> <i:Interaction.Behaviors> <behaviors:DragOnCanvasBehavior DraggableItem="{Binding}"> <behaviors:DragOnCanvasBehavior.MouseOverAdornerTemplate> <DataTemplate> <Border DataContext="DraggableBorder" BorderBrush="Black" BorderThickness="3" Width="{Binding Path=Width}" Height="{Binding Path=Height}"></Border> </DataTemplate> </behaviors:DragOnCanvasBehavior.MouseOverAdornerTemplate> <i:Interaction.Triggers> <!-- Dragging events --> <i:EventTrigger EventName="MouseEnter"> <i:InvokeCommandAction CommandName="ShowMouseOverAdorner" /> </i:EventTrigger> <i:EventTrigger EventName="MouseLeave"> <i:InvokeCommandAction CommandName="HideMouseOverAdorner" /> </i:EventTrigger> </i:Interaction.Triggers> </behaviors:DragOnCanvasBehavior> </i:Interaction.Behaviors> </Border> </AdornerDecorator> </DataTemplate> </ItemsControl.ItemTemplate> |
Note that from the adorner’s DataTemplate, we set the DataContext property, so that we can bind the adorner’s properties directly to the ones of the item view model.
Now, on mouse-over, a black border will appear, and will disappear when the mouse leaves. There are a few problems with this method: most notably, the adorner flickers when the mouse stays over it. But we’ll try to tackle them later on.