Реализуем 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 просмотров]