I want the user to be able to move items freely in a canvas.
My app is using Caliburn.Micro.
My MainViewModel has a collection if Items :
public BindableCollection<ItemViewModel> Items { get; set; }
That I display in a canvas through an ItemsControl :
<ItemsControl x:Name="Items">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas Background="#FFCADEEF" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<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>
<Border Background="{Binding Path=BackgroundColor}">
<Rectangle>
<Rectangle.Fill>
<VisualBrush Visual="{StaticResource appbar_cursor_move}" />
</Rectangle.Fill>
</Rectangle>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
I have successfully bound events (that do nothing for now) to the MouseLeftButtonDown, MouseLeftButtonUp and MouseMove but I have no idea how to get the cursor's position from the viewmodel.
I've used your behaviour and changed few things to make it more MVVM'y:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525" DataContext="{Binding RelativeSource={RelativeSource Self}, Path=ViewModel}">
<ItemsControl ItemsSource="{Binding Path=Shapes}">
<ItemsControl.Resources>
<DataTemplate DataType="{x:Type local:Rectangle}">
<Rectangle Canvas.Top="{Binding Top, Mode=TwoWay}" Canvas.Left="{Binding Left, Mode=TwoWay}" Width="{Binding Width}" Height="{Binding Height}" Fill="Red">
<i:Interaction.Behaviors>
<local:DragBehavior/>
</i:Interaction.Behaviors>
</Rectangle>
</DataTemplate>
<DataTemplate DataType="{x:Type local:Circle}">
<Ellipse Width="{Binding Radius}" Height="{Binding Radius}" Fill="Blue" Canvas.Top="{Binding Top, Mode=TwoWay}" Canvas.Left="{Binding Left, Mode=TwoWay}">
<i:Interaction.Behaviors>
<local:DragBehavior/>
</i:Interaction.Behaviors>
</Ellipse>
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding Path=Top, Mode=TwoWay}" />
<Setter Property="Canvas.Left" Value="{Binding Path=Left, Mode=TwoWay}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
</Window>
this is why I used a style for binding to Canvas.Top and Left.
This is my ViewModel. I used ReactiveUI for IPropertyChanged, but this doesn't matter really.
public class MainViewModel : ReactiveObject
{
private ReactiveList<IShape> _shapes;
public MainViewModel()
{
Shapes = new ReactiveList<IShape>();
Shapes.Add(new Rectangle { Top = 50, Left = 50, Height = 50, Width = 50 });
Shapes.Add(new Circle{Top = 100, Left = 100, Radius = 50});
}
public ReactiveList<IShape> Shapes
{
get { return _shapes; }
set { this.RaiseAndSetIfChanged(ref _shapes, value); }
}
}
public interface IShape
{
int Top { get; set; }
int Left { get; set; }
}
public abstract class Shape : ReactiveObject, IShape
{
private int _top;
private int _left;
public int Top
{
get { return _top; }
set { this.RaiseAndSetIfChanged(ref _top, value); }
}
public int Left
{
get { return _left; }
set { this.RaiseAndSetIfChanged(ref _left, value); }
}
}
public class Circle : Shape
{
private int _radius;
public int Radius
{
get { return _radius; }
set { this.RaiseAndSetIfChanged(ref _radius, value); }
}
}
public class Rectangle : Shape
{
private int _width;
private int _height;
public int Width
{
get { return _width; }
set { this.RaiseAndSetIfChanged(ref _width, value); }
}
public int Height
{
get { return _height; }
set { this.RaiseAndSetIfChanged(ref _height, value); }
}
}
I created classes for reactangles and circles, because the whole point of MVVM is to make distinction between layers. Holding UI controls in ViewModel is deffinetely against the idea.
Lastly, I had to change your MouseLeftButtonUp a little:
AssociatedObject.MouseLeftButtonUp += (sender, e) =>
{
AssociatedObject.ReleaseMouseCapture();
var diff = e.GetPosition(parent) - mouseStartPosition;
Canvas.SetTop(AssociatedObject, ElementStartPosition.Y + diff.Y);
Canvas.SetLeft(AssociatedObject, ElementStartPosition.X + diff.X);
transform.Y = 0;
transform.X = 0;
};
This takes changes from RenderTransform and writes them into object. Then, two way binding takes it down into our Rectangle class.
This is needed only,if you want to know where objects are, for example to check if they intersect in VM.
It works quite well, and is as MVVM as you can get. Maybe with exception of line var parent = Application.Current.MainWindow; - this should replaced I think with binding to public dependency property of your behaviour.
I have copied the behavior from another GitHub account :
public class DragBehavior
{
public readonly TranslateTransform Transform = new TranslateTransform();
private Point _elementStartPosition2;
private Point _mouseStartPosition2;
private static DragBehavior _instance = new DragBehavior();
public static DragBehavior Instance
{
get { return _instance; }
set { _instance = value; }
}
public static bool GetDrag(DependencyObject obj)
{
return (bool)obj.GetValue(IsDragProperty);
}
public static void SetDrag(DependencyObject obj, bool value)
{
obj.SetValue(IsDragProperty, value);
}
public static readonly DependencyProperty IsDragProperty =
DependencyProperty.RegisterAttached("Drag",
typeof(bool), typeof(DragBehavior),
new PropertyMetadata(false, OnDragChanged));
private static void OnDragChanged(object sender, DependencyPropertyChangedEventArgs e)
{
// ignoring error checking
var element = (UIElement)sender;
var isDrag = (bool)(e.NewValue);
Instance = new DragBehavior();
((UIElement)sender).RenderTransform = Instance.Transform;
if (isDrag)
{
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;
}
}
private void ElementOnMouseLeftButtonDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
var parent = Application.Current.MainWindow;
_mouseStartPosition2 = mouseButtonEventArgs.GetPosition(parent);
((UIElement)sender).CaptureMouse();
}
private void ElementOnMouseLeftButtonUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
{
((UIElement)sender).ReleaseMouseCapture();
_elementStartPosition2.X = Transform.X;
_elementStartPosition2.Y = Transform.Y;
}
private void ElementOnMouseMove(object sender, MouseEventArgs mouseEventArgs)
{
var parent = Application.Current.MainWindow;
var mousePos = mouseEventArgs.GetPosition(parent);
var diff = (mousePos - _mouseStartPosition2);
if (!((UIElement)sender).IsMouseCaptured) return;
Transform.X = _elementStartPosition2.X + diff.X;
Transform.Y = _elementStartPosition2.Y + diff.Y;
}
}
And simply attached it to the ItemsControl DataTemplate :
<DataTemplate>
<Border
Background="{Binding Path=BackgroundColor}"
behaviors:DragBehavior.Drag="True">
<!-- whatever -->
</Border>
</DataTemplate>
Now I need to find how to send a message from the behavior to the viewmodel when the user stops dragging (I'm assuming it involves a new behavior property).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With