The source code of the sample application is available at GitHub. It has been strongly inspired by Gong WPF DragDrop and has started life as a copy of another “WPF behavior lab”.
The final goal of the application is to allow a user to freely drag an item in a canvas, and that the items “snap” among themselves, so that the user can align them easily. I’m taking it as an opportunity to learn more about WPF.
The first thing I had to learn was how to create and use custom behaviors. They allow to bind elements from the View into the ViewModel using custom properties.
In “pure” WPF (Blend has different behavior conventions), a behavior is a regular class, with a few conventions to declare a custom property. It is called a Dependency Property. It’s pretty easy to bind a simple value (boolean, integer…), examples are plentiful.
Now, in the view, I want to say “when the user moves this item, this method should be run”, because I want not only the item to move, but also the container to compare the item’s new position to its neighbors. A custom property can be anything a variable can be, but not a method. So, to circumvent this, the behavior must be aware of the ViewModel, but obviously we don’t want to strongly link the behavior with the ViewModel.
- The ViewModel will have to implement an interface
- The View will be bound to this ViewModel through {Binding}
- The Behavior will use this interface’s methods to send messages to the ViewModel
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 |
public class DragOnCanvasBehavior { // This declares a "DropHandler" property that we will be able to use in the view // The name (xxxProperty) and variable modifiers (public static reaonly) must follow the proper conventions public static readonly DependencyProperty DropHandlerProperty = DependencyProperty.RegisterAttached( "DropHandler", typeof(IDragDropHandler), typeof(DragOnCanvasBehavior), new PropertyMetadata(OnDropHandlerChanged)); // This will actually "link" to the ViewModel, which should implement IDragDropHandler private IDragDropHandler DropHandler { get; set; } // This is a convention-required method (Getxxx) // It will retrieve the property's value public static IDragDropHandler GetDropHandler(UIElement target) { return (IDragDropHandler)target.GetValue(DropHandlerProperty); } // This is a convention-required method (Setxxx) // It will set the property's value public static void SetDropHandler(UIElement target, IDragDropHandler value) { target.SetValue(DropHandlerProperty, value); } // This called using the callback of the xxxProperty (overload of DependencyProperty.RegisterAttached) // It will be run when the value is changed (meaning that Setxxx might be run multiple times with the same value) private static void OnDropHandlerChanged(object sender, DependencyPropertyChangedEventArgs e) { // cast the usable elements UIElement element = (UIElement)sender; IDragDropHandler handler = (IDragDropHandler)(e.NewValue); // we can now use the elements. // for instance, we can assign handlers to the UIElement: element.MouseLeftButtonDown += Whatever; } } |
The IDragDropHandler interface is very simple. The Moved method will notify the ViewModel that the item has been moved, and the Dropped method that the mouse button has been released.
1 2 3 4 5 |
public interface IDragDropHandler { void Dropped(); void Moved(double x, double y); } |
Now, the behavior will use these methods. To do that, we need to create mouse click and move handlers and assign them to the UIElement. Because the OnxxxChanged method is static, but the click/move handlers are not, we need a way to “memorize” the handlers. We’ll do that through a singleton instance:
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 |
public class DragOnCanvasBehavior { // we'll use this singleton instance of the behavior to access the DropHandler and the mouse events private static DragOnCanvasBehavior Instance = new DragOnCanvasBehavior(); // initialize the instance and attach events to the element private static void OnDropHandlerChanged(object sender, DependencyPropertyChangedEventArgs e) { UIElement element = (UIElement)sender; IDragDropHandler handler = (IDragDropHandler)(e.NewValue); // re-initialize the singleton (otherwise different items will have the same behavior) Instance = new DragOnCanvasBehavior(); // attach the handler to the singleton Instance.DropHandler = handler; // attach or detach the handler to the element // remember that the handler can be removed at run-time, so we need handle null values if (Instance.DropHandler != null) { element.MouseLeftButtonDown += Instance.ElementOnMouseLeftButtonDown; element.MouseLeftButtonUp += Instance.ElementOnMouseLeftButtonUp; element.MouseMove += Instance.ElementOnMouseMove; } else { element.MouseLeftButtonDown -= Instance.ElementOnMouseLeftButtonDown; element.MouseLeftButtonUp -= Instance.ElementOnMouseLeftButtonUp; element.MouseMove -= Instance.ElementOnMouseMove; } } // when the user starts dragging private void ElementOnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs) { // save the mouse position on button down // we only want a diff of the mouse position so we don't care much about which element we use as reference this.mouseStartPosition = this.GetMousePositionFromMainWindow(mouseButtonEventArgs); ((UIElement)sender).CaptureMouse(); } // when the user stops dragging private void ElementOnMouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs) { UIElement element = (UIElement)sender; element.ReleaseMouseCapture(); // Send a message to the viewmodel that the mouse button has been released if (this.DropHandler != null) { this.DropHandler.Dropped(); } } // while the user is dragging private void ElementOnMouseMove(object sender, MouseEventArgs mouseEventArgs) { // don't do anything if no button is clicked (or there is no handler) if (!((UIElement)sender).IsMouseCaptured || this.DropHandler == null) { return; } // calculate element movement Point mouseNewPos = this.GetMousePositionFromMainWindow(mouseEventArgs); Vector movement = (mouseNewPos - this.mouseStartPosition); // make sure the mouse has moved since last time we were here // (the MouseMove is run in loop while the button is clicked, even if the mouse isn't moving) if (movement.Length > 0) { // save current mouse position this.mouseStartPosition = mouseNewPos; // save the element movement Point elementNewPos = this.elementPosition + movement; this.elementPosition = elementNewPos; // notify the viewmodel that the element has been moved this.DropHandler.Moved(elementNewPos.X, elementNewPos.Y); } } // get the mouse position relative to the main window private Point GetMousePositionFromMainWindow(MouseEventArgs mouseEventArgs) { Window mainWindow = Application.Current.MainWindow; return mouseEventArgs.GetPosition(mainWindow); } } |
Now, the behavior is notifying the ViewModel that it is being moved. Let’s handle the movement:
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 |
// I'm using Fody.PropertyChanged to implement INotifyProperyChanged // I'm also using Caliburn.Micro as MVVM framwork [ImplementPropertyChanged] public class ItemViewModel : PropertyChangedBase, IDragDropHandler { private readonly IEventAggregator events; public ItemViewModel(IEventAggregator events) { this.events = events; this.events.Subscribe(this); this.Width = 100; this.Height = 100; this.X = 0; this.Y = 0; } public int Height { get; set; } public int Width { get; set; } public double X { get; set; } public double Y { get; set; } public void Dropped() { // TODO: do something (probably publish an event) } public void Moved(double x, double y) { this.X = x; this.Y = y; // Notify the container that the item is moved, so that it can calculate snapping this.events.PublishOnUIThread(new ItemMovedEvent(this.X, this.Y, this.Width, this.Height, this.ID)); } } |
Now that we have a ViewModel that know when it’s being moved, let’s actually allow the user to move it. First, let’s write a container for the items:
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 |
[ImplementPropertyChanged] public class MainViewModel : PropertyChangedBase, IHandle<ItemMovedEvent> { private readonly IEventAggregator events; public MainViewModel(IEventAggregator events) { this.events = events; this.events.Subscribe(this); this.Items = new BindableCollection<ItemViewModel>(); } // the items displayed on the canvas public BindableCollection<ItemViewModel> Items { get; set; } // adds an item to the collection public void AddItem() { this.Items.Add(new ItemViewModel(this.events)); } // fired when an item has been moved public void Handle(ItemMovedEvent message) { // TODO: handle snapping to other elements } } |
Then the view (I’m removing things like styling for brevity). Note that it follows Caliburn.Micro conventions on naming (among other things), so it automatically binds some things, like the ItemsControl items through its name.
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 |
<UserControl xmlns:cal="http://www.caliburnproject.org" xmlns:behaviors="clr-namespace:DragSnap.Behaviors"> <Grid> <Button Content="Add an item to the canvas" cal:Message.Attach="[Event Click] = [Action AddItem()]" /> <ItemsControl x:Name="Items"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemContainerStyle> <!-- This is what actually moves the items in the canvas --> <Style TargetType="ContentPresenter"> <Setter Property="Canvas.Left" Value="{Binding Path=X}" /> <Setter Property="Canvas.Top" Value="{Binding Path=Y}" /> <Setter Property="Width" Value="{Binding Path=Width}" /> <Setter Property="Height" Value="{Binding Path=Height}" /> </Style> </ItemsControl.ItemContainerStyle> <ItemsControl.ItemTemplate> <DataTemplate> <!-- The {Binding} binds the property's value to the ViewModel itself --> <Border behaviors:DragOnCanvasBehavior.DropHandler="{Binding}"> <!-- Whatever content you want --> </Border> </DataTemplate> </ItemsControl.ItemTemplate> </ItemsControl> </Grid> </UserControl> |
To summarize:
- The behavior is attached to the item’s viewmodel from the view
- The behavior memorizes the item instance through the IDragDropHandler interface, and assigns mouse events to the item
- The mouse events call the IDragDropHandler methods
- The item’s viewmodel implements these methods and handles coordinates changes itself (which are visually updated through binding in the view)
- The item viewmodel notifies the container viewmodel of its coordinates changes through Caliburn.Micro events