Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
847 views
in Technique[技术] by (71.8m points)

wpf - Move items in a canvas using MVVM

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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.


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...