Drag&Drop по правилам WPF

alex-rudenkiy

Подскажите пожалуйста как сделать правильно drag&drop прибиндив его положение к MVVM, чтоб можно было бы его сохранить в файл просеарилизировав в JSON? Я знаю как это всё делается по "старинке", поставить событие на Mouse_Down, Mouse_Move и Mouse_Up, а когда происходит перетаскивание в Mouse_Move делать на подобии "canvas.setleft(uiobj, canvas.getleft(uiobj)+mouse.x)" (этот пример не будет работать я знаю), а вот как в mvvm всё это переделать я не знаю :-(

1 ответ

alex-rudenkiy

Смотрите. Давайте поделим всё на визуальную и модельную части.

Визуальная часть занимается перетаскиванием. При определении факта начала перетаскивания нужно отвязаться от VM (проще всего, наверное, скрыть элемент и показать на его месте другой, заодно можно немного поменять его вид, например, на полупрозрачный), обработать перетаскивание через MouseMove/MouseUp, определить, куда был дропнут элемент, отослать новые координаты в VM, и снова включить отображение элемента.

VM обновит данные, новые координаты вступят в силу через привязку.

Всё.

Написал простой пример MVVM-приложения с перетаскиванием. Приложение рисует набор из перетаскиваемых квадратов. Программа простая, поэтому я обхожусь без модели. Будет выглядеть так:

Начинаем с VM. Общий суперкласс, чтобы не имплементировать INPC каждый раз (если вы пользуетесь MVVM-фреймворком, у вас наверняка такой уже определён):

class VM : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

Теперь класс, представляющий собой фигуру. Мы выкладываем публичные свойства с текущим значением позиции, и командой для смены позиции. Если вы будете сериализировать этот объект, не забудьте отметить команду несериализируемой.

class SquareVM : VM
{
    public SquareVM()
    {
        RequestMove = new SimpleCommand<point>(MoveTo);
    }

    // стандартное свойство
    Point position;
    public Point Position
    {
        get { return position; }
        set { if (position != value) { position = value; NotifyPropertyChanged(); } }
    }

    // выставляем команду, которая занимается перемещением
    public ICommand RequestMove { get; }

    void MoveTo(Point newPosition)
    {
        // в реальности тут могут быть всякие проверки, конечно
        Position = newPosition;
    }
}
</point>

Я использую примитивнейший вариант команды:

class SimpleCommand<t> : ICommand
{
    readonly Action<t> onExecute;
    public SimpleCommand(Action<t> onExecute) { this.onExecute = onExecute; }

    public event EventHandler CanExecuteChanged;
    public bool CanExecute(object parameter) => true;
    public void Execute(object parameter) => onExecute((T)parameter);
}
</t></t></t>

Теперь, главная VM, ничего особенного:

class MainVM : VM
{
    public ObservableCollection<squarevm> Squares { get; } =
        new ObservableCollection<squarevm>()
        {
            new SquareVM() { Position = new Point( 30,  30) },
            new SquareVM() { Position = new Point(100,  70) },
            new SquareVM() { Position = new Point( 80,   0) },
            new SquareVM() { Position = new Point( 90, 180) },
            new SquareVM() { Position = new Point(200, 200) }
        };
}
</squarevm></squarevm>

На этом с VM-частью покончено.

Теперь, приложение. Стандартная заготовка для MVVM: App.xaml без StartupUri и переопределение OnStartup:

public partial class App : Application
{
    MainVM mainVM = new MainVM();

    protected override void OnStartup(StartupEventArgs e)
    {
        base.OnStartup(e);
        new MainWindow() { DataContext = mainVM }.Show();
    }
}

Переходим к интересной части: представление.

Окно отображает список элементов в Canvas'е. Для привязки списка элементов использован, как обычно, ItemsControl:

<window x:class="MvvmDraggable.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="clr-namespace:MvvmDraggable" title="Draggable squares" height="350" width="525">
    <grid>
        <itemscontrol itemssource="{Binding Squares}" width="300" height="300" background="Beige">
            <itemscontrol.itemspanel>
                <itemspaneltemplate>
                    <!-- хост списка элементов - канвас, чтобы можно было
                         произвольно устанавливать координаты -->
                    <canvas isitemshost="True">
                </canvas></itemspaneltemplate>
            </itemscontrol.itemspanel>
            <itemscontrol.itemtemplate>
                <datatemplate>
                    <!-- сам квадрат вынесем в отдельный UserControl -->
                    <local:draggablesquare>
                </local:draggablesquare></datatemplate>
            </itemscontrol.itemtemplate>
            <itemscontrol.itemcontainerstyle>
                
            </itemscontrol.itemcontainerstyle>
        </itemscontrol>
    </grid>
</window>

Сам контрол тоже несложен, код для обработки мышиных сообщений я честно стащил из ответа на вопрос «Как отследить перемещение одного окна над другим?» и выкинул всё ненужное.

<usercontrol x:class="MvvmDraggable.DraggableSquare" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" width="50" height="50" mousedown="OnMouseDown" *******="OnMouseUp" background="Green">
    </usercontrol>

Маленькая тонкость: чтобы не следить за командой в DataContext'е, я объявил DependencyProperty RequestMoveCommand, и установил Binding на него. Также мы подписались в XAML'е на MouseDown и MouseUp.

public partial class DraggableSquare : UserControl
{
    public DraggableSquare()
    {
        InitializeComponent();
        // устанавливаем Binding RequestMove из VM на свойство RequestMoveCommand:
        SetBinding(RequestMoveCommandProperty, new Binding("RequestMove"));
    }

    // стандартное DependencyProperty
    #region dp ICommand RequestMoveCommand
    public ICommand RequestMoveCommand
    {
        get { return (ICommand)GetValue(RequestMoveCommandProperty); }
        set { SetValue(RequestMoveCommandProperty, value); }
    }

    public static readonly DependencyProperty RequestMoveCommandProperty =
        DependencyProperty.Register("RequestMoveCommand", typeof(ICommand),
                                    typeof(DraggableSquare));
    #endregion

    Vector relativeMousePos; // смещение мыши от левого верхнего угла квадрата
    Canvas container;        // канвас-контейнер

    // по нажатию на левую клавишу начинаем следить за мышью
    void OnMouseDown(object sender, MouseButtonEventArgs e)
    {
        container = FindParent<canvas>();
        relativeMousePos = e.GetPosition(this) - new Point();
        MouseMove += OnDragMove;
        LostMouseCapture += OnLostCapture;
        Mouse.Capture(this);
    }

    // клавиша отпущена - завершаем процесс
    void OnMouseUp(object sender, MouseButtonEventArgs e)
    {
        FinishDrag(sender, e);
        Mouse.Capture(null);
    }

    // потеряли фокус (например, юзер переключился в другое окно) - завершаем тоже
    void OnLostCapture(object sender, MouseEventArgs e)
    {
        FinishDrag(sender, e);
    }

    void OnDragMove(object sender, MouseEventArgs e)
    {
        UpdatePosition(e);
    }

    void FinishDrag(object sender, MouseEventArgs e)
    {
        MouseMove -= OnDragMove;
        LostMouseCapture -= OnLostCapture;
        UpdatePosition(e);
    }

    // требуем у VM обновить позицию через команду
    void UpdatePosition(MouseEventArgs e)
    {
        var point = e.GetPosition(container);
        // не забываем проверку на null
        RequestMoveCommand?.Execute(point - relativeMousePos);
    }

    // это вспомогательная функция, ей место в общей библиотеке
    private T FindParent<t>() where T : FrameworkElement
    {
        FrameworkElement current = this;
        T t;
        do
        {
            t = current as T;
            current = (FrameworkElement)VisualTreeHelper.GetParent(current);
        }
        while (t == null && current != null);
        return t;
    }
}
</t></canvas>

Теперь совсем всё!

Вот тут модификация этого кода, чтобы вместо квадратов таскался их уменьшенный и полупрозрачный предпросмотр.

licensed under cc by-sa 3.0 with attribution.