доработана winUI реализация

This commit is contained in:
2026-01-25 06:29:37 +03:00
parent 0e050b452a
commit a902474345
8 changed files with 1594 additions and 452 deletions

View File

@@ -1,7 +1,8 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Abstractions;
using Lattice.UI.DragDrop.Behaviors;
using Lattice.UI.DragDrop.WinUI.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
@@ -13,29 +14,54 @@ namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Реализация поведения источника перетаскивания для элементов WinUI.
/// Наследуется от <see cref="DragSourceBehaviorBase{FrameworkElement}"/> для использования
/// общей логики управления операциями перетаскивания и интеграции с системой <see cref="Core.DragDrop"/>.
/// </summary>
/// <remarks>
/// <para>
/// Этот класс обрабатывает события мыши/тач и преобразует их в операции перетаскивания.
/// Он реализует интерфейс <see cref="IDragSource"/> для интеграции с системой перетаскивания.
/// Этот класс предоставляет конкретную реализацию для платформы WinUI, обрабатывая события
/// указателя (мышь, тач, перо) и преобразуя их в операции перетаскивания через центральный
/// сервис <see cref="IDragDropService"/>.
/// </para>
/// <para>
/// Поведение автоматически отслеживает начало перемещения мыши, проверяет порог
/// перетаскивания и инициирует операцию через <see cref="IDragDropService"/>.
/// Основные функции:
/// <list type="bullet">
/// <item>Обработка событий PointerPressed, PointerMoved, PointerReleased</item>
/// <item>Автоматическое отслеживание порога начала перетаскивания</item>
/// <item>Создание информации о перетаскивании на основе данных элемента</item>
/// <item>Интеграция с визуальной обратной связью через <see cref="WinUIDragDropHost"/></item>
/// <item>Преобразование координат между локальной системой элемента и экранными координатами</item>
/// </list>
/// </para>
/// <para>
/// Для использования необходимо:
/// <list type="number">
/// <item>Создать экземпляр поведения через фабрику <see cref="Factories.WinUIDragDropFactory.CreateDragSourceBehavior"/></item>
/// <item>Прикрепить к элементу с помощью метода <see cref="Attach"/></item>
/// <item>Указать данные для перетаскивания (опционально, по умолчанию используется DataContext)</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание поведения
/// var behavior = WinUIDragDropFactory.CreateDragSourceBehavior(dragDropService, host);
///
/// // Прикрепление к элементу
/// behavior.Attach(myElement, myData);
///
/// // Или через attached properties
/// &lt;Border x:Name="DragElement"
/// local:DragDropProperties.IsDragSource="True"
/// local:DragDropProperties.DragData="{Binding MyData}" /&gt;
/// </code>
/// </example>
/// </remarks>
public sealed class WinUIDragSourceBehavior : IDragSource
public sealed class WinUIDragSourceBehavior : DragSourceBehaviorBase<FrameworkElement>
{
#region Поля
private readonly IDragDropService _dragDropService;
private readonly WinUIDragDropHost _host;
private FrameworkElement? _element;
private readonly IDragDropHost _host;
private object? _dragData;
private Point _dragStartPosition;
private bool _isDragging;
private CancellationTokenSource? _cancellationTokenSource;
#endregion
@@ -44,14 +70,27 @@ public sealed class WinUIDragSourceBehavior : IDragSource
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDragSourceBehavior"/>.
/// </summary>
/// <param name="dragDropService">Сервис для управления операциями перетаскивания.</param>
/// <param name="host">Хост для отображения визуальных элементов.</param>
/// <param name="dragDropService">
/// Сервис управления операциями перетаскивания. Используется для координации
/// между источниками и целями перетаскивания.
/// </param>
/// <param name="host">
/// Хост для управления визуальными элементами перетаскивания. Обеспечивает
/// отображение визуальной обратной связи во время операции.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если любой из параметров равен null.
/// Выбрасывается, если <paramref name="dragDropService"/> или <paramref name="host"/>
/// равны null.
/// </exception>
public WinUIDragSourceBehavior(IDragDropService dragDropService, WinUIDragDropHost host)
/// <remarks>
/// Конструктор инициализирует базовый класс <see cref="DragSourceBehaviorBase{FrameworkElement}"/>
/// и сохраняет ссылки на необходимые сервисы для последующего использования.
/// </remarks>
public WinUIDragSourceBehavior(
Core.DragDrop.Services.IDragDropService dragDropService,
WinUIDragDropHost host)
: base(dragDropService)
{
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
_host = host ?? throw new ArgumentNullException(nameof(host));
}
@@ -60,207 +99,202 @@ public sealed class WinUIDragSourceBehavior : IDragSource
#region Публичные методы
/// <summary>
/// Прикрепляет поведение к указанному элементу.
/// Прикрепляет поведение к указанному элементу WinUI.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <param name="dragData">Данные для перетаскивания. Если не указано, используются
/// DataContext или Tag элемента.</param>
/// <param name="element">
/// Элемент <see cref="FrameworkElement"/>, который должен стать источником перетаскивания.
/// Не может быть null.
/// </param>
/// <param name="dragData">
/// Данные, которые будут перетаскиваться. Может быть null.
/// Если не указано, используется <see cref="FrameworkElement.DataContext"/> или
/// <see cref="FrameworkElement.Tag"/> элемента.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// После вызова этого метода элемент начинает обрабатывать события перетаскивания.
/// <para>
/// После вызова этого метода:
/// <list type="bullet">
/// <item>Элементу автоматически подписываются обработчики событий указателя</item>
/// <item>Поведение начинает отслеживать взаимодействия с элементом</item>
/// <item>При превышении порога перетаскивания инициируется операция через <see cref="Core.DragDrop.Services.IDragDropService"/></item>
/// </list>
/// </para>
/// <para>
/// Для открепления поведения используйте метод <see cref="Detach"/>.
/// </para>
/// </remarks>
public void Attach(FrameworkElement element, object? dragData = null)
{
Detach();
if (element == null)
throw new ArgumentNullException(nameof(element));
_element = element ?? throw new ArgumentNullException(nameof(element));
_dragData = dragData ?? element.DataContext ?? element.Tag;
SubscribeToEvents();
AssociatedElement = element;
}
/// <summary>
/// Открепляет поведение от элемента.
/// Открепляет поведение от текущего элемента.
/// </summary>
public void Detach()
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Отписывается от всех событий элемента</item>
/// <item>Сбрасывает внутреннее состояние</item>
/// <item>Освобождает ссылки на связанные объекты</item>
/// </list>
/// </para>
/// <para>
/// Если в момент вызова активна операция перетаскивания, она будет автоматически отменена.
/// </para>
/// <para>
/// После вызова этого метода поведение может быть повторно прикреплено к другому элементу.
/// </para>
/// </remarks>
public new void Detach()
{
if (_element == null) return;
UnsubscribeFromEvents();
if (_isDragging)
{
_dragDropService.CancelDragAsync().ConfigureAwait(false);
}
_element = null;
base.Detach();
_dragData = null;
_isDragging = false;
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
#endregion
#region Обработчики событий
#region Реализация абстрактных методов DragSourceBehaviorBase<FrameworkElement>
private void SubscribeToEvents()
/// <inheritdoc/>
protected override void SubscribeToEvents(FrameworkElement element)
{
if (_element == null) return;
if (element == null) return;
_element.PointerPressed += OnPointerPressed;
_element.PointerMoved += OnPointerMoved;
_element.PointerReleased += OnPointerReleased;
_element.PointerCanceled += OnPointerCanceled;
_element.PointerCaptureLost += OnPointerCaptureLost;
element.PointerPressed += OnPointerPressed;
element.PointerMoved += OnPointerMoved;
element.PointerReleased += OnPointerReleased;
element.PointerCanceled += OnPointerCanceled;
element.PointerCaptureLost += OnPointerCaptureLost;
}
private void UnsubscribeFromEvents()
/// <inheritdoc/>
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (_element == null) return;
if (element == null) return;
_element.PointerPressed -= OnPointerPressed;
_element.PointerMoved -= OnPointerMoved;
_element.PointerReleased -= OnPointerReleased;
_element.PointerCanceled -= OnPointerCanceled;
_element.PointerCaptureLost -= OnPointerCaptureLost;
element.PointerPressed -= OnPointerPressed;
element.PointerMoved -= OnPointerMoved;
element.PointerReleased -= OnPointerReleased;
element.PointerCanceled -= OnPointerCanceled;
element.PointerCaptureLost -= OnPointerCaptureLost;
}
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
/// <inheritdoc/>
protected override Point ConvertToScreenCoordinates(Point point)
{
if (_element == null || _isDragging) return;
var point = e.GetCurrentPoint(_element);
_dragStartPosition = new Point(point.Position.X, point.Position.Y);
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
}
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_element == null || _isDragging || _cancellationTokenSource?.IsCancellationRequested == true)
return;
var point = e.GetCurrentPoint(_element);
var currentPosition = new Point(point.Position.X, point.Position.Y);
var distance = CalculateDistance(_dragStartPosition, currentPosition);
if (distance > _dragDropService.DragStartThreshold)
{
await StartDragAsync(currentPosition);
}
}
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
ResetState();
}
private void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
{
ResetState();
}
private void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
ResetState();
}
private double CalculateDistance(Point p1, Point p2)
{
var dx = p2.X - p1.X;
var dy = p2.Y - p1.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
/// <summary>
/// Преобразует координаты элемента в экранные координаты.
/// </summary>
/// <param name="point">Точка в координатах элемента.</param>
/// <returns>Точка в экранных координатах.</returns>
private Point ConvertToScreenCoordinates(Point point)
{
if (_element == null) return point;
try
{
var transform = _element.TransformToVisual(Window.Current.Content);
var screenPoint = transform.TransformPoint(new Windows.Foundation.Point(point.X, point.Y));
return new Point(screenPoint.X, screenPoint.Y);
}
catch
{
if (AssociatedElement == null)
return point;
}
}
private async Task StartDragAsync(Point position)
{
if (_element == null || _dragData == null || _isDragging) return;
try
{
var screenPosition = ConvertToScreenCoordinates(position);
var started = await _dragDropService.StartDragAsync(this, screenPosition);
_isDragging = started;
}
catch
{
ResetState();
}
}
private void ResetState()
{
_isDragging = false;
_dragStartPosition = default;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
return WinUIWindowHelper.ConvertToScreenCoordinates(AssociatedElement, point);
}
#endregion
#region IDragSource Implementation
#region Реализация интерфейса IDragSource
public async Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
/// <inheritdoc/>
public override async Task<DragInfo?> TryStartDragAsync(
Point startPosition,
CancellationToken cancellationToken = default)
{
if (_element == null || _dragData == null)
if (AssociatedElement == null || _dragData == null)
return null;
try
{
// Создаем информацию о перетаскивании
var dragInfo = new DragInfo(
data: _dragData,
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy |
Core.DragDrop.Enums.DragDropEffects.Move,
Core.DragDrop.Enums.DragDropEffects.Move |
Core.DragDrop.Enums.DragDropEffects.Link,
startPosition: startPosition,
source: this
);
return dragInfo;
// Добавляем дополнительные параметры
dragInfo.SetParameter("SourceElement", AssociatedElement);
dragInfo.SetParameter("SourceType", AssociatedElement.GetType().Name);
return await Task.FromResult(dragInfo);
}
catch
catch (Exception ex)
{
// Логирование ошибки создания информации о перетаскивании
System.Diagnostics.Debug.WriteLine(
$"Ошибка создания DragInfo: {ex.Message}");
return null;
}
}
public Task OnDragCompletedAsync(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects, CancellationToken cancellationToken = default)
#endregion
#region Обработчики событий WinUI
private async void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
_isDragging = false;
return Task.CompletedTask;
if (AssociatedElement == null) return;
var point = e.GetCurrentPoint(AssociatedElement);
var position = new Point(point.Position.X, point.Position.Y);
await OnInteractionStarted(position);
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
_isDragging = false;
return Task.CompletedTask;
if (AssociatedElement == null) return;
var point = e.GetCurrentPoint(AssociatedElement);
var position = new Point(point.Position.X, point.Position.Y);
await OnInteractionMoved(position);
}
private async void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
await OnInteractionEnded();
}
private async void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
{
await OnInteractionCancelled();
}
private async void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
await OnInteractionCancelled();
}
#endregion
#region Переопределение виртуальных методов
/// <inheritdoc/>
protected override void OnDragCompleted(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
{
base.OnDragCompleted(dragInfo, effects);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, обновление состояния элемента после успешного перетаскивания
}
/// <inheritdoc/>
protected override void OnDragCancelled(DragInfo dragInfo)
{
base.OnDragCancelled(dragInfo);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, восстановление визуального состояния элемента после отмены
}
#endregion

View File

@@ -1,9 +1,11 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.WinUI.Services;
using Lattice.UI.DragDrop.Abstractions;
using Lattice.UI.DragDrop.Behaviors;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -11,27 +13,52 @@ namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Реализация поведения цели сброса для элементов WinUI.
/// Наследуется от <see cref="DropTargetBehaviorBase{FrameworkElement}"/> для использования
/// общей логики регистрации целей и обработки операций сброса.
/// </summary>
/// <remarks>
/// <para>
/// Этот класс обрабатывает события перетаскивания WinUI и преобразует их в вызовы
/// методов интерфейса <see cref="IDropTarget"/>.
/// Этот класс предоставляет конкретную реализацию для платформы WinUI, обрабатывая события
/// перетаскивания WinUI и преобразуя их в вызовы методов интерфейса <see cref="Core.DragDrop.Abstractions.IDropTarget"/>.
/// </para>
/// <para>
/// Поведение автоматически регистрирует элемент в системе перетаскивания,
/// обновляет его границы при изменении размера и обрабатывает все этапы операции сброса.
/// Основные функции:
/// <list type="bullet">
/// <item>Автоматическая регистрация в <see cref="IDragDropService"/> при прикреплении к элементу</item>
/// <item>Обработка событий DragEnter, DragOver, DragLeave, Drop</item>
/// <item>Автоматическое обновление границ элемента при изменении размера или позиции</item>
/// <item>Поддержка фильтрации принимаемых типов данных</item>
/// <item>Интеграция с визуальной обратной связью</item>
/// </list>
/// </para>
/// <para>
/// Для использования необходимо:
/// <list type="number">
/// <item>Создать экземпляр поведения через фабрику <see cref="Factories.WinUIDragDropFactory.CreateDropTargetBehavior"/></item>
/// <item>Прикрепить к элементу с помощью метода <see cref="Attach"/></item>
/// <item>Настроить фильтры принимаемых данных (опционально)</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание поведения с фильтрацией типов
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior(dragDropService, host);
/// behavior.AcceptTypes(typeof(MyDataModel), typeof(string));
/// behavior.Attach(myDropArea);
///
/// // Или через attached properties
/// &lt;Border x:Name="DropArea"
/// local:DragDropProperties.IsDropTarget="True" /&gt;
/// </code>
/// </example>
/// </remarks>
public sealed class WinUIDropTargetBehavior : IDropTarget
public sealed class WinUIDropTargetBehavior : DropTargetBehaviorBase<FrameworkElement>
{
#region Поля
private readonly Lattice.Core.DragDrop.Services.IDragDropService _dragDropService;
private readonly WinUIDragDropHost _host;
private FrameworkElement? _element;
private string? _registrationId;
private Rect _currentBounds;
private readonly IDragDropHost _host;
private readonly List<Type> _acceptedTypes = new();
private readonly HashSet<string> _acceptedFormats = new();
#endregion
@@ -40,14 +67,28 @@ public sealed class WinUIDropTargetBehavior : IDropTarget
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <param name="dragDropService">Сервис для управления операциями перетаскивания.</param>
/// <param name="host">Хост для отображения визуальных элементов.</param>
/// <param name="dragDropService">
/// Сервис управления операциями перетаскивания. Используется для регистрации
/// цели и координации операций сброса.
/// </param>
/// <param name="host">
/// Хост для управления визуальной обратной связью. Обеспечивает отображение
/// индикаторов возможности сброса.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если любой из параметров равен null.
/// Выбрасывается, если <paramref name="dragDropService"/> или <paramref name="host"/>
/// равны null.
/// </exception>
public WinUIDropTargetBehavior(Lattice.Core.DragDrop.Services.IDragDropService dragDropService, WinUIDragDropHost host)
/// <remarks>
/// Конструктор инициализирует базовый класс и сохраняет ссылки на сервисы.
/// По умолчанию цель принимает все типы данных. Для настройки фильтрации
/// используйте методы <see cref="AcceptTypes"/> и <see cref="AcceptFormats"/>.
/// </remarks>
public WinUIDropTargetBehavior(
Core.DragDrop.Services.IDragDropService dragDropService,
IDragDropHost host)
: base(dragDropService)
{
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
_host = host ?? throw new ArgumentNullException(nameof(host));
}
@@ -56,106 +97,301 @@ public sealed class WinUIDropTargetBehavior : IDropTarget
#region Публичные методы
/// <summary>
/// Прикрепляет поведение к указанному элементу.
/// Прикрепляет поведение к указанному элементу WinUI.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <param name="element">
/// Элемент <see cref="FrameworkElement"/>, который должен стать целью сброса.
/// Не может быть null.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// После вызова этого метода:
/// 1. Элементу устанавливается <see cref="UIElement.AllowDrop"/> = true
/// 2. Поведение подписывается на события перетаскивания
/// 3. Элемент регистрируется в системе перетаскивания
/// <list type="bullet">
/// <item>Элементу устанавливается свойство <see cref="UIElement.AllowDrop"/> = true</item>
/// <item>Поведение подписывается на события перетаскивания WinUI</item>
/// <item>Элемент регистрируется в системе перетаскивания с текущими границами</item>
/// <item>Начинается отслеживание изменений размера и позиции элемента</item>
/// </list>
/// </para>
/// <para>
/// Для открепления поведения используйте метод <see cref="Detach"/>.
/// </para>
/// </remarks>
public void Attach(FrameworkElement element)
{
Detach();
if (element == null)
throw new ArgumentNullException(nameof(element));
_element = element ?? throw new ArgumentNullException(nameof(element));
element.AllowDrop = true;
SubscribeToEvents();
UpdateBounds();
RegisterToService();
AssociatedElement = element;
}
/// <summary>
/// Открепляет поведение от элемента.
/// Настраивает поведение для приема только указанных типов данных.
/// </summary>
public void Detach()
/// <param name="types">
/// Типы данных, которые может принимать цель. Если пусто, принимаются все типы.
/// </param>
/// <remarks>
/// <para>
/// Этот метод позволяет ограничить типы данных, которые могут быть сброшены на цель.
/// Проверка выполняется в методе <see cref="CanAcceptDropAsync"/> путем сравнения
/// типа сбрасываемых данных с указанными типами.
/// </para>
/// <para>
/// Если метод не вызывался или передан пустой список, цель будет принимать данные любого типа.
/// </para>
/// <example>
/// <code>
/// // Принимать только строки и объекты MyModel
/// behavior.AcceptTypes(typeof(string), typeof(MyModel));
/// </code>
/// </example>
/// </remarks>
public void AcceptTypes(params Type[] types)
{
if (_element == null) return;
_acceptedTypes.Clear();
if (types != null && types.Length > 0)
{
_acceptedTypes.AddRange(types);
}
}
UnsubscribeFromEvents();
UnregisterFromService();
/// <summary>
/// Настраивает поведение для приема только указанных форматов данных.
/// </summary>
/// <param name="formats">
/// Форматы данных (например, "Text", "Bitmap", "FileDrop"), которые может принимать цель.
/// Если пусто, формат не проверяется.
/// </param>
/// <remarks>
/// <para>
/// Этот метод позволяет ограничить форматы данных, которые могут быть сброшены на цель.
/// Актуально для межпроцессного перетаскивания или работы с системными форматами.
/// </para>
/// <para>
/// Если метод не вызывался или передан пустой список, проверка формата не выполняется.
/// </para>
/// </remarks>
public void AcceptFormats(params string[] formats)
{
_acceptedFormats.Clear();
if (formats != null && formats.Length > 0)
{
foreach (var format in formats)
{
_acceptedFormats.Add(format);
}
}
}
_element.AllowDrop = false;
_element = null;
_currentBounds = Rect.Empty;
/// <summary>
/// Открепляет поведение от текущего элемента.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Отписывается от всех событий элемента</item>
/// <item>Отменяет регистрацию цели в системе перетаскивания</item>
/// <item>Сбрасывает свойство <see cref="UIElement.AllowDrop"/> = false</item>
/// <item>Освобождает ссылки на связанные объекты</item>
/// </list>
/// </para>
/// <para>
/// После вызова этого метода поведение может быть повторно прикреплено к другому элементу.
/// </para>
/// </remarks>
public new void Detach()
{
if (AssociatedElement != null)
{
AssociatedElement.AllowDrop = false;
}
base.Detach();
}
#endregion
#region Обработчики событий
#region Реализация абстрактных методов DropTargetBehaviorBase<FrameworkElement>
private void SubscribeToEvents()
/// <inheritdoc/>
protected override void SubscribeToEvents(FrameworkElement element)
{
if (_element == null) return;
if (element == null) return;
_element.DragEnter += OnDragEnter;
_element.DragOver += OnDragOver;
_element.DragLeave += OnDragLeave;
_element.Drop += OnDrop;
_element.SizeChanged += OnSizeChanged;
element.DragEnter += OnDragEnter;
element.DragOver += OnDragOver;
element.DragLeave += OnDragLeave;
element.Drop += OnDrop;
element.SizeChanged += OnSizeChanged;
element.LayoutUpdated += OnLayoutUpdated;
}
private void UnsubscribeFromEvents()
/// <inheritdoc/>
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (_element == null) return;
if (element == null) return;
_element.DragEnter -= OnDragEnter;
_element.DragOver -= OnDragOver;
_element.DragLeave -= OnDragLeave;
_element.Drop -= OnDrop;
_element.SizeChanged -= OnSizeChanged;
element.DragEnter -= OnDragEnter;
element.DragOver -= OnDragOver;
element.DragLeave -= OnDragLeave;
element.Drop -= OnDrop;
element.SizeChanged -= OnSizeChanged;
element.LayoutUpdated -= OnLayoutUpdated;
}
private async void OnDragEnter(object sender, DragEventArgs e)
/// <inheritdoc/>
protected override Rect GetScreenBounds(FrameworkElement element)
{
if (_element == null) return;
if (element == null || !element.IsLoaded)
return Rect.Empty;
try
{
var position = e.GetPosition(_element);
var window = Window.Current;
if (window?.Content == null)
return Rect.Empty;
// Преобразуем локальные координаты элемента в координаты окна
var transform = element.TransformToVisual(window.Content);
var position = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
return new Rect(
position.X,
position.Y,
element.ActualWidth,
element.ActualHeight
);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(
$"Ошибка получения границ элемента: {ex.Message}");
return Rect.Empty;
}
}
#endregion
#region Реализация интерфейса IDropTarget
/// <inheritdoc/>
public override async Task<bool> CanAcceptDropAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
// Проверяем, есть ли данные для сброса
if (dropInfo.Data == null)
return false;
// Проверяем фильтр по типам
if (_acceptedTypes.Count > 0)
{
var dataType = dropInfo.Data.GetType();
if (!_acceptedTypes.Any(t => t.IsAssignableFrom(dataType)))
{
return false;
}
}
// Проверяем фильтр по форматам (если данные предоставляют информацию о формате)
if (_acceptedFormats.Count > 0 && dropInfo.Data is Windows.ApplicationModel.DataTransfer.DataPackageView dataView)
{
var availableFormats = dataView.AvailableFormats;
if (!_acceptedFormats.Any(f => availableFormats.Contains(f)))
{
return false;
}
}
// Дополнительная проверка может быть добавлена в производных классах
return await Task.FromResult(true);
}
/// <inheritdoc/>
public override async Task OnDragOverAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
await base.OnDragOverAsync(dropInfo, cancellationToken);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, обновление визуальной обратной связи через хост
}
/// <inheritdoc/>
public override async Task OnDropAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
// Базовая реализация вызывает CanAcceptDropAsync и помечает как обработанное
if (await CanAcceptDropAsync(dropInfo, cancellationToken))
{
dropInfo.MarkAsHandled();
// Здесь может быть добавлена логика обработки сброшенных данных
// Например, вызов события или обновление модели данных
}
}
/// <inheritdoc/>
public override async Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
await base.OnDragLeaveAsync(cancellationToken);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, скрытие визуальной обратной связи
}
#endregion
#region Обработчики событий WinUI
private async void OnDragEnter(object sender, DragEventArgs e)
{
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
e.Handled = true;
}
}
catch { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDragEnter: {ex.Message}");
}
}
private async void OnDragOver(object sender, DragEventArgs e)
{
if (_element == null) return;
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(_element);
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
await OnDragOverAsync(dropInfo);
e.Handled = true;
}
e.Handled = true;
}
catch { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDragOver: {ex.Message}");
}
}
private async void OnDragLeave(object sender, DragEventArgs e)
@@ -165,131 +401,114 @@ public sealed class WinUIDropTargetBehavior : IDropTarget
private async void OnDrop(object sender, DragEventArgs e)
{
if (_element == null) return;
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(_element);
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
await OnDropAsync(dropInfo);
dropInfo.MarkAsHandled();
e.Handled = true;
}
}
catch { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDrop: {ex.Message}");
}
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateBounds();
OnElementLayoutChanged();
}
private void OnLayoutUpdated(object sender, object e)
{
OnElementLayoutChanged();
}
#endregion
#region Вспомогательные методы
/// <summary>
/// Создает объект <see cref="DropInfo"/> на основе события перетаскивания WinUI.
/// </summary>
/// <param name="e">Аргументы события перетаскивания WinUI.</param>
/// <param name="position">Локальная позиция курсора относительно элемента.</param>
/// <returns>
/// Экземпляр <see cref="DropInfo"/>, содержащий информацию о потенциальном сбросе.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод извлекает данные из события перетаскивания и преобразует их
/// в формат, понятный системе <see cref="Core.DragDrop"/>.
/// </para>
/// <para>
/// Поддерживаются как пользовательские данные (через свойство "DragData"),
/// так и стандартные форматы данных WinUI.
/// </para>
/// </remarks>
private DropInfo CreateDropInfo(DragEventArgs e, Point position)
{
object? data = null;
// Пытаемся получить пользовательские данные
if (e.DataView.Properties.TryGetValue("DragData", out var dragData))
{
data = dragData;
}
// Или получаем данные из DataPackage
else if (e.DataView.Contains(Windows.ApplicationModel.DataTransfer.StandardDataFormats.Text))
{
// Для текстовых данных можем установить асинхронную загрузку
data = new AsyncDataProvider(async () =>
{
return await e.DataView.GetTextAsync();
});
}
// Определяем разрешенные эффекты на основе модификаторов клавиатуры
var allowedEffects = Core.DragDrop.Enums.DragDropEffects.None;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Copy;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Move;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Link))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Link;
return new DropInfo(
data: data,
position: position,
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy |
Core.DragDrop.Enums.DragDropEffects.Move,
target: _element
allowedEffects: allowedEffects,
target: this
);
}
/// <summary>
/// Получает границы элемента в экранных координатах.
/// </summary>
/// <returns>Прямоугольник с границами элемента или <see cref="Rect.Empty"/>, если
/// элемент не загружен или не может быть преобразован.</returns>
private Rect GetScreenBounds()
{
if (_element == null || !_element.IsLoaded)
return Rect.Empty;
try
{
var transform = _element.TransformToVisual(Window.Current.Content);
var position = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
return new Rect(
position.X,
position.Y,
_element.ActualWidth,
_element.ActualHeight
);
}
catch
{
return Rect.Empty;
}
}
private void UpdateBounds()
{
if (_element == null) return;
_currentBounds = GetScreenBounds();
if (_registrationId != null && _currentBounds != Rect.Empty)
{
_dragDropService.UpdateDropTargetBounds(_registrationId, _currentBounds);
}
}
private void RegisterToService()
{
if (_element == null) return;
_currentBounds = GetScreenBounds();
if (_currentBounds != Rect.Empty && _registrationId == null)
{
_registrationId = _dragDropService.RegisterDropTarget(this, _currentBounds);
}
}
private void UnregisterFromService()
{
if (_registrationId != null)
{
_dragDropService.UnregisterDropTarget(_registrationId);
_registrationId = null;
}
}
#endregion
}
#region IDropTarget Implementation
/// <summary>
/// Предоставляет асинхронный доступ к данным перетаскивания.
/// </summary>
/// <remarks>
/// Этот класс используется для отложенной загрузки данных перетаскивания,
/// что особенно важно для больших данных или данных, требующих обработки.
/// </remarks>
internal class AsyncDataProvider
{
private readonly Func<Task<object>> _dataLoader;
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken ct = default)
public AsyncDataProvider(Func<Task<object>> dataLoader)
{
return Task.FromResult(true); // Принимаем все по умолчанию
_dataLoader = dataLoader;
}
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken ct = default)
public async Task<object> GetDataAsync()
{
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Move;
return Task.CompletedTask;
return await _dataLoader();
}
public Task OnDropAsync(DropInfo dropInfo, CancellationToken ct = default)
{
return Task.CompletedTask;
}
public Task OnDragLeaveAsync(CancellationToken ct = default)
{
return Task.CompletedTask;
}
#endregion
}