БлогNot. Реализуем Drag-and-drop в приложении WPF/MVVM

Реализуем Drag-and-drop в приложении WPF/MVVM

Этот пример показывает, как выполнять перетаскивание объектов мышью в приложении WPF, написанном в соответствии с паттерном MVVM, являющимся своего рода расширением классического MVC.

Конкретно в демке реализовано перетаскивание по форме Красного Квадрата с отслеживанием координат мыши.

Проект с именем Canvas1 типа "Приложение WPF (.NET Framework)" скомпилирован в актуальной сборке Visual Studio 2019.

Библиотека System.Windows.Interactivity, на которой раньше делалось управление поведением, устарела и должна заменяться на Microsoft.Xaml.Behaviors, что мы и сделали.

Убито полдня, но зато теперь решится проблема и в реальном проекте, а этот писался так:

Добавим классы для обработки событий и управления ими. Чтобы это сделать, в Обозревателе решений выберем папку с именем проекта (Canvas1) и обратимся к верхнему меню Проект - Добавить класс, где будем указывать имена классов с расширением .cs.

Файл EventArgs.cs
using System;

namespace Canvas1 {
 public class EventArgs<T> : EventArgs {
  public EventArgs (T value) {
   Value = value;
  }
    public T Value { get; private set; }
 }
}
Файл EventRaiser.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Canvas1 {
 public static class EventRaiser {
  public static void Raise (this EventHandler handler, object sender) {
   handler?.Invoke (sender, EventArgs.Empty); //выполняет делегат в потоке
  }

  public static void Raise<T> (this EventHandler<EventArgs<T>> handler, object sender, T value) {
   handler?.Invoke (sender, new EventArgs<T> (value));
  }

  public static void Raise<T> (this EventHandler<T> handler, object sender, T value) where T : EventArgs {
   handler?.Invoke (sender, value);
  }

  public static void Raise<T> (this EventHandler<EventArgs<T>> handler, object sender, EventArgs<T> value) {
   handler?.Invoke (sender, value);
  }
 }
}
Файл RelayCommand.cs
using System;
using System.Windows.Input; /* ICommand */

namespace Canvas1 {
 public class RelayCommand<T> : ICommand {
  private readonly Predicate<T> _canExecute;
  private readonly Action<T> _execute;

  public RelayCommand (Action<T> execute)
     : this (execute, null) {
   _execute = execute;
  }

  public RelayCommand (Action<T> execute, Predicate<T> canExecute) {
   if (execute == null)
    throw new ArgumentNullException (nameof (execute));
   _execute = execute;
   _canExecute = canExecute;
  }

  #region ICommand Members

  public bool CanExecute (object parameter) {
   return ( _canExecute == null ) || _canExecute ((T) parameter);
  }

  public void Execute (object parameter) {
   _execute ((T) parameter);
  }

  public event EventHandler CanExecuteChanged {
   add { CommandManager.RequerySuggested += value; }
   remove { CommandManager.RequerySuggested -= value; }
  }

  #endregion
 }

 public class RelayCommand : ICommand {
  private readonly Predicate<object> _canExecute;
  private readonly Action<object> _execute;

  public RelayCommand (Action<object> execute)
     : this (execute, null) {
   _execute = execute;
  }

  public RelayCommand (Action<object> execute, Predicate<object> canExecute) {
   if (execute == null)
    throw new ArgumentNullException ("execute");
   _execute = execute;
   _canExecute = canExecute;
  }

  #region ICommand Members

  public bool CanExecute (object parameter) {
   return ( _canExecute == null ) || _canExecute (parameter);
  }

  public void Execute (object parameter) {
   _execute (parameter);
  }

  // Запрашиваем все объекты RelayCommand
  public event EventHandler CanExecuteChanged {
   add {
    CommandManager.RequerySuggested += value;
    CanExecuteChangedInternal += value;
   }
   remove {
    CommandManager.RequerySuggested -= value;
    CanExecuteChangedInternal -= value;
   }
  }

  #endregion

  private event EventHandler CanExecuteChangedInternal;

  public void RaiseCanExecuteChanged () {
   CanExecuteChangedInternal.Raise (this);
  }
 }
}

Добавим ссылку на нужную для работу сборку, а именно, Microsoft.Xaml.Behaviors, которой мы заменим устаревшую System.Windows.Interactivity. Это позволит нам постоянно отслеживать положение мыши при её перемещении по экрану.

Щёлкните правой кнопкой мыши папку Ссылки в Обозревателе решений и выберите команду Добавить ссылку…

Если сборка не найдена в списках, её нужно установить через менеджер пакетов NuGet: правая кнопка мыши на папке "Ссылки", "Управление пакетами NuGet", ссылка "Обзор", ищем "Microsoft.Xaml.Behaviors.Wpf", устанавливаем.

Теперь добавим класс, содержащий свойства для получения координат мыши.

Файл MouseBehaviour.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using Microsoft.Xaml.Behaviors;

namespace Canvas1 {
 public class MouseBehaviour : Behavior<Panel> {
  public static readonly DependencyProperty MouseYProperty = DependencyProperty.Register (
     "MouseY", typeof (double), typeof (MouseBehaviour), new PropertyMetadata (default (double)));

  public static readonly DependencyProperty MouseXProperty = DependencyProperty.Register (
     "MouseX", typeof (double), typeof (MouseBehaviour), new PropertyMetadata (default (double)));

  public double MouseY {
   get { return (double) GetValue (MouseYProperty); }
   set { SetValue (MouseYProperty, value); }
  }

  public double MouseX {
   get { return (double) GetValue (MouseXProperty); }
   set { SetValue (MouseXProperty, value); }
  }

  protected override void OnAttached () {
   AssociatedObject.MouseMove += AssociatedObjectOnMouseMove;
  }

  protected override void OnDetaching () {
   AssociatedObject.MouseMove -= AssociatedObjectOnMouseMove;
  }

  private void AssociatedObjectOnMouseMove (object sender, MouseEventArgs mouseEventArgs) {
   var pos = mouseEventArgs.GetPosition (AssociatedObject);
   MouseX = pos.X;
   MouseY = pos.Y;
  }
 }
}

Добавим класс модели представления главного окна (MVVM).

Файл MainWindowViewModel.cs
using System;
using System.ComponentModel;
using System.Diagnostics;
using System.Windows.Input;

namespace Canvas1 {
 public class MainWindowViewModel : INotifyPropertyChanged {
  bool captured = false;

  public ICommand _leftButtonDownCommand;
  public ICommand _leftButtonUpCommand;
  public ICommand _previewMouseMove;
  public ICommand _leftMouseButtonUp;

  public MainWindowViewModel () {
   PanelX = 100;
   PanelY = 100;
   RectX = PanelX - 50;
   RectY = PanelY - 50;
  }

  public ICommand PreviewMouseMove {
   get {
    return _previewMouseMove ?? ( _previewMouseMove = new RelayCommand (
       x => {
        if (captured) {
         RectX = PanelX - 50;
         RectY = PanelY - 50;
        }
       }) );
   }
  }

  public ICommand LeftMouseButtonUp {
   get {
    return _leftMouseButtonUp ?? ( _leftMouseButtonUp = new RelayCommand (
       x => {
        captured = false;
       }) );
   }
  }

  public ICommand LeftMouseButtonDown {
   get {
    return _leftButtonDownCommand ?? ( _leftButtonDownCommand = new RelayCommand (
       x => {
        captured = true;
       }) );
   }
  }

  private int _panelX;
  private int _panelY;
  private int _rectX;
  private int _rectY;

  public int RectX {
   get { return _rectX; }
   set {
    if (value.Equals (_rectX)) return;
    _rectX = value;
    OnPropertyChanged ("RectX");
   }
  }

  public int RectY {
   get { return _rectY; }
   set {
    if (value.Equals (_rectY)) return;
    _rectY = value;
    OnPropertyChanged ("RectY");
   }
  }

  public int PanelX {
   get { return _panelX; }
   set {
    if (value.Equals (_panelX)) return;
    _panelX = value;
    OnPropertyChanged ("PanelX");
   }
  }

  public int PanelY {
   get { return _panelY; }
   set {
    if (value.Equals (_panelY)) return;
    _panelY = value;
    OnPropertyChanged ("PanelY");
   }
  }

  public event PropertyChangedEventHandler PropertyChanged;

  protected void OnPropertyChanged (string propertyName) {
   VerifyPropertyName (propertyName);
   var handler = PropertyChanged;
   handler?.Invoke (this, new PropertyChangedEventArgs (propertyName));
  }

  [Conditional ("DEBUG")]
  private void VerifyPropertyName (string propertyName) {
   if (TypeDescriptor.GetProperties (this) [propertyName] == null)
    throw new ArgumentNullException (GetType ().Name + " does not contain property: " + propertyName);
  }
 }
}

Наконец, заменим стандартное содержимое файла MainWindow.xaml этой разметкой:

Файл MainWindow.xaml
<Window x:Class="Canvas1.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:local="clr-namespace:Canvas1"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"       
        xmlns:i="clr-namespace:Microsoft.Xaml.Behaviors;assembly=Microsoft.Xaml.Behaviors" 
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="550">
    <Grid>
        <DockPanel>
            <StackPanel DockPanel.Dock="Top" Orientation="Horizontal">
                <TextBlock Text="{Binding Path=PanelX, StringFormat='x={0}'}" />
                <TextBlock Text="{Binding Path=PanelY, StringFormat=' y={0}'}" />
            </StackPanel>
            <Canvas x:Name="LayoutRoot" Background="White">
                <i:Interaction.Behaviors>
                    <local:MouseBehaviour
      MouseX="{Binding Path=PanelX, Mode=OneWayToSource}"
      MouseY="{Binding Path=PanelY, Mode=OneWayToSource}" />
                </i:Interaction.Behaviors>
                <Rectangle 
     x:Name="testSquare"
     Fill="Red" Stroke="Black" Width="100" Height="100"
     HorizontalAlignment="Left" VerticalAlignment="Bottom"
     Canvas.Left="{Binding Path=RectX, Mode=TwoWay}"
     Canvas.Top="{Binding Path=RectY, Mode=TwoWay}">
                    <i:Interaction.Triggers>
                        <i:EventTrigger EventName="PreviewMouseDown" >
                            <i:InvokeCommandAction 
        Command="{Binding ElementName=testSquare, Path=DataContext.LeftMouseButtonDown}"
        CommandParameter="{Binding}" />
                        </i:EventTrigger>
                        <i:EventTrigger EventName="PreviewMouseUp" >
                            <i:InvokeCommandAction 
        Command="{Binding ElementName=testSquare, Path=DataContext.LeftMouseButtonUp}"
        CommandParameter="{Binding}" />
                        </i:EventTrigger>
                        <i:EventTrigger EventName="PreviewMouseMove" >
                            <i:InvokeCommandAction 
         Command="{Binding ElementName=testSquare, Path=DataContext.PreviewMouseMove}"
         CommandParameter="{Binding}" />
                        </i:EventTrigger>
                    </i:Interaction.Triggers>
                </Rectangle>
            </Canvas>
        </DockPanel>
    </Grid>
</Window>

Не забудем изменить MainWindow.xaml.cs так, чтобы он создавал при запуске экземпляр модели:

Файл MainWindow.xaml.cs
using System.Windows;

namespace Canvas1 {
 public partial class MainWindow : Window {
  public MainWindow () {
   MainWindowViewModel M = new MainWindowViewModel ();
   InitializeComponent ();
   DataContext = M;
  }
 }
}

Приложение готово, можно запускать.

Проект в работе (скриншот)
Проект в работе (скриншот)

 Скачать этот проект VS 2019 в архиве .zip, развернуть в новую папку (13 Кб)

07.09.2024, 15:04 [96 просмотров]


теги: программирование учебное c# xml

К этой статье пока нет комментариев, Ваш будет первым