3

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.

thomasb
  • 5,816
  • 10
  • 57
  • 92

4 Answers4

4

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.

Krzysztof Skowronek
  • 2,796
  • 1
  • 13
  • 29
2

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).

thomasb
  • 5,816
  • 10
  • 57
  • 92
1

If you're wondering how to do it in a nice way, then I would say that you're looking something a long of the lines:

<ItemsControl 
yourAttachedbehavior.MouseButtonMoved="{Binding YourViewModelCommand}"
x:Name="Items">

where MouseButtonMoved is an attached property (of type ICommand) that hooks itself into the ItemsControl MouseButtonMove event, and then executes the the command it is binded against, passing two double values (x, y) through the MouseButtonMoved.Execute(x, y).

If you want to see similiar implementations, check for gongdrop implementation. I'd use that as a solution, but if you want to rewrite it from zero, just read the source.

Erti-Chris Eelmaa
  • 25,338
  • 6
  • 61
  • 78
  • So you're saying I have to write a behavior ? I tried with Gong but I can't find how to make it work properly :/ (namely, the dragInfo parameter doesn't have an item attached) – thomasb Jan 12 '15 at 08:21
  • 1
    cosmo0; I actually implemented the exact same thing you're asking here, in commercial product few days ago, using `DragCanvas`. See if this helps you: https://github.com/jimgraham/WPF.JoshSmith/blob/master/WPF.JoshSmith/Panels/DragCanvas.cs In the root folder WPF.JoshSmith, there's an example how to use it. – Erti-Chris Eelmaa Jan 13 '15 at 10:34
1

I've got an open source library you can copy this functionality from.

DragablzItemsControl inherits from ItemsControl and DragablzItem inherits from ContentControl.

DragablzItem has a TemplatePart PART_Thumb which is of type Thumb. In OnApplyTemplate grab the thumb, subscribe to .DragDelta, and in there you can update the X,Y coordinates. It also manages sizing by additional thumbs around the borders.

Have a look at the source: http://github.com/ButchersBoy/Dragablz/blob/master/Dragablz/DragablzItem.cs

If you download the demo project from http://github.com/ButchersBoy/Dragablz you will see all of this in action

James Willock
  • 1,999
  • 14
  • 17