Files
Lattice/Lattice.UI.DragDrop/Behaviors/DragSourceBehaviorBase.cs

366 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Lattice.UI.DragDrop.Behaviors;
/// <summary>
/// Базовый класс поведения источника перетаскивания для UI элементов.
/// </summary>
/// <typeparam name="TElement">Тип UI элемента, к которому прикрепляется поведение.</typeparam>
/// <remarks>
/// <para>
/// Этот класс предоставляет базовую реализацию поведения перетаскивания для UI элементов.
/// Он обрабатывает события мыши/тач, управляет порогом начала перетаскивания и
/// интегрируется с сервисом <see cref="IDragDropService"/> из ядра.
/// </para>
/// <para>
/// Производные классы должны реализовать абстрактные методы для конкретной
/// UI-платформы и предоставить логику создания информации о перетаскивании.
/// </para>
/// </remarks>
public abstract class DragSourceBehaviorBase<TElement> : IDragSource
where TElement : class
{
private IDragDropService? _dragDropService;
private Point _dragStartPosition;
private bool _isDragging;
private TElement? _associatedElement;
private CancellationTokenSource? _dragCancellationTokenSource;
/// <summary>
/// Получает или задает связанный UI элемент.
/// </summary>
/// <value>
/// Элемент UI, к которому прикреплено поведение перетаскивания.
/// При изменении значения автоматически выполняется переподключение событий.
/// </value>
protected TElement? AssociatedElement
{
get => _associatedElement;
set
{
if (_associatedElement != value)
{
DetachFromElement();
_associatedElement = value;
AttachToElement();
}
}
}
/// <summary>
/// Получает сервис перетаскивания из контейнера зависимостей.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDragDropService"/>, используемый для управления операциями перетаскивания.
/// </value>
protected IDragDropService DragDropService { get; }
/// <summary>
/// Получает значение, указывающее, выполняется ли в данный момент операция перетаскивания.
/// </summary>
protected bool IsDragging => _isDragging;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragSourceBehaviorBase{TElement}"/>.
/// </summary>
/// <param name="dragDropService">Сервис перетаскивания.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="dragDropService"/> равен null.
/// </exception>
protected DragSourceBehaviorBase(IDragDropService dragDropService)
{
DragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
}
/// <summary>
/// Вызывается при прикреплении поведения к элементу.
/// </summary>
/// <remarks>
/// Реализация по умолчанию подписывается на события элемента через <see cref="SubscribeToEvents"/>.
/// Производные классы могут переопределить этот метод для дополнительной инициализации.
/// </remarks>
protected virtual void AttachToElement()
{
if (_associatedElement != null)
{
SubscribeToEvents(_associatedElement);
}
}
/// <summary>
/// Вызывается при откреплении поведения от элемента.
/// </summary>
/// <remarks>
/// Реализация по умолчанию отписывается от событий элемента через <see cref="UnsubscribeFromEvents"/>.
/// Производные классы могут переопределить этот метод для дополнительной очистки.
/// </remarks>
protected virtual void DetachFromElement()
{
if (_associatedElement != null)
{
UnsubscribeFromEvents(_associatedElement);
}
}
/// <summary>
/// Подписывается на события элемента, необходимые для отслеживания начала перетаскивания.
/// </summary>
/// <param name="element">Элемент, к событиям которого нужно подписаться.</param>
/// <remarks>
/// Производные классы должны реализовать этот метод для подписки на события конкретной
/// UI-платформы (например, MouseDown для WPF, PointerPressed для Avalonia).
/// </remarks>
protected abstract void SubscribeToEvents(TElement element);
/// <summary>
/// Отписывается от событий элемента.
/// </summary>
/// <param name="element">Элемент, от событий которого нужно отписаться.</param>
/// <remarks>
/// Производные классы должны реализовать этот метод для корректной отписки
/// от событий, на которые была выполнена подписка в <see cref="SubscribeToEvents"/>.
/// </remarks>
protected abstract void UnsubscribeFromEvents(TElement element);
/// <summary>
/// Обрабатывает начало взаимодействия с элементом (например, нажатие кнопки мыши).
/// </summary>
/// <param name="position">Позиция взаимодействия в координатах элемента.</param>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <remarks>
/// <para>
/// Этот метод вызывается из обработчиков событий UI-платформы при начале
/// взаимодействия, которое может привести к перетаскиванию.
/// </para>
/// <para>
/// Реализация по умолчанию сохраняет начальную позицию для последующей
/// проверки порога перетаскивания.
/// </para>
/// </remarks>
protected virtual Task OnInteractionStarted(Point position)
{
if (_isDragging)
return Task.CompletedTask;
_dragStartPosition = position;
_dragCancellationTokenSource = new CancellationTokenSource();
return Task.CompletedTask;
}
/// <summary>
/// Обрабатывает перемещение во время взаимодействия с элементом.
/// </summary>
/// <param name="position">Текущая позиция взаимодействия в координатах элемента.</param>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <remarks>
/// <para>
/// Этот метод вызывается при перемещении курсора/тач-точки во время удержания
/// взаимодействия (например, перемещение мыши с нажатой кнопкой).
/// </para>
/// <para>
/// Реализация по умолчанию проверяет, превышено ли расстояние от начальной
/// точки порога перетаскивания, и если да - начинает операцию перетаскивания.
/// </para>
/// </remarks>
protected virtual async Task OnInteractionMoved(Point position)
{
if (_isDragging || AssociatedElement == null)
return;
var distance = CalculateDistance(_dragStartPosition, position);
if (distance > DragDropService.DragStartThreshold)
{
await StartDragOperation();
}
}
/// <summary>
/// Обрабатывает завершение взаимодействия с элементом.
/// </summary>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <remarks>
/// <para>
/// Этот метод вызывается при завершении взаимодействия (например, отпускании кнопки мыши).
/// </para>
/// <para>
/// Реализация по умолчанию сбрасывает состояние поведения, если перетаскивание не было начато.
/// </para>
/// </remarks>
protected virtual Task OnInteractionEnded()
{
// Сброс состояния, если перетаскивание не началось
if (!_isDragging)
{
Reset();
}
return Task.CompletedTask;
}
/// <summary>
/// Обрабатывает отмену взаимодействия с элементом.
/// </summary>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <remarks>
/// <para>
/// Этот метод вызывается при отмене взаимодействия (например, нажатии клавиши Escape
/// или выходе за пределы допустимой области).
/// </para>
/// <para>
/// Реализация по умолчанию отменяет текущую операцию перетаскивания, если она активна,
/// и сбрасывает состояние поведения.
/// </para>
/// </remarks>
protected virtual async Task OnInteractionCancelled()
{
if (_isDragging)
{
await DragDropService.CancelDragAsync();
}
Reset();
}
/// <summary>
/// Начинает операцию перетаскивания.
/// </summary>
/// <returns>Задача, представляющая асинхронную операцию.</returns>
/// <remarks>
/// <para>
/// Этот метод преобразует начальную позицию в экранные координаты и вызывает
/// сервис перетаскивания для начала операции.
/// </para>
/// <para>
/// Операция начинается только если поведение прикреплено к элементу и
/// не выполняется другая операция перетаскивания.
/// </para>
/// </remarks>
protected virtual async Task StartDragOperation()
{
if (_isDragging || AssociatedElement == null || _dragCancellationTokenSource == null)
return;
// Получаем начальную позицию в экранных координатах
var screenPosition = ConvertToScreenCoordinates(_dragStartPosition);
// Начинаем перетаскивание
try
{
_isDragging = await DragDropService.StartDragAsync(this, screenPosition);
}
catch (OperationCanceledException)
{
// Операция была отменена
Reset();
}
}
/// <summary>
/// Преобразует координаты элемента в экранные координаты.
/// </summary>
/// <param name="point">Точка в координатах элемента.</param>
/// <returns>Точка в экранных координатах.</returns>
/// <remarks>
/// Производные классы должны реализовать этот метод для преобразования
/// координат в соответствии с конкретной UI-платформой.
/// </remarks>
protected abstract Point ConvertToScreenCoordinates(Point point);
/// <summary>
/// Вычисляет расстояние между двумя точками.
/// </summary>
/// <param name="p1">Первая точка.</param>
/// <param name="p2">Вторая точка.</param>
/// <returns>Расстояние между точками.</returns>
protected virtual 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>
/// <remarks>
/// Этот метод очищает все временные данные и отменяет токены отмены,
/// связанные с текущей операцией перетаскивания.
/// </remarks>
protected virtual void Reset()
{
_isDragging = false;
_dragStartPosition = default;
_dragCancellationTokenSource?.Dispose();
_dragCancellationTokenSource = null;
}
#region IDragSource Implementation
/// <inheritdoc/>
public abstract Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default);
/// <inheritdoc/>
public async Task OnDragCompletedAsync(DragInfo dragInfo, Lattice.Core.DragDrop.Enums.DragDropEffects effects, CancellationToken cancellationToken = default)
{
_isDragging = false;
OnDragCompleted(dragInfo, effects);
}
/// <inheritdoc/>
public async Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
{
_isDragging = false;
OnDragCancelled(dragInfo);
}
#endregion
#region Virtual Methods for Derived Classes
/// <summary>
/// Вызывается при успешном завершении операции перетаскивания.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании, использованная в операции.</param>
/// <param name="effects">Эффекты, примененные при завершении операции.</param>
/// <remarks>
/// Производные классы могут переопределить этот метод для выполнения
/// дополнительных действий после успешного завершения перетаскивания,
/// например, удаления исходного элемента при перемещении.
/// </remarks>
protected virtual void OnDragCompleted(DragInfo dragInfo, Lattice.Core.DragDrop.Enums.DragDropEffects effects)
{
}
/// <summary>
/// Вызывается при отмене операции перетаскивания.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании, использованная в операции.</param>
/// <remarks>
/// Производные классы могут переопределить этот метод для выполнения
/// действий по восстановлению состояния после отмены перетаскивания.
/// </remarks>
protected virtual void OnDragCancelled(DragInfo dragInfo)
{
}
#endregion
/// <summary>
/// Открепляет поведение от элемента и освобождает ресурсы.
/// </summary>
/// <remarks>
/// После вызова этого метода поведение больше не будет обрабатывать события
/// элемента и может быть безопасно удалено.
/// </remarks>
public virtual void Detach()
{
DetachFromElement();
_associatedElement = null;
Reset();
}
}