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

332 lines
15 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 элементов.
/// Он автоматически регистрирует элемент в сервисе перетаскивания, обновляет его границы
/// при изменении размера или позиции и предоставляет методы для обработки событий сброса.
/// </para>
/// <para>
/// Производные классы должны реализовать абстрактные методы для конкретной
/// UI-платформы и предоставить логику проверки и обработки сбрасываемых данных.
/// </para>
/// </remarks>
public abstract class DropTargetBehaviorBase<TElement> : IDropTarget
where TElement : class
{
private IDragDropService? _dragDropService;
private string? _registrationId;
private TElement? _associatedElement;
private Rect _currentBounds;
/// <summary>
/// Получает или задает связанный UI элемент.
/// </summary>
/// <value>
/// Элемент UI, к которому прикреплено поведение цели сброса.
/// При изменении значения автоматически выполняется перерегистрация в сервисе перетаскивания.
/// </value>
protected TElement? AssociatedElement
{
get => _associatedElement;
set
{
if (_associatedElement != value)
{
UnregisterFromService();
_associatedElement = value;
RegisterToService();
}
}
}
/// <summary>
/// Получает или задает приоритет цели сброса.
/// </summary>
/// <value>
/// Цели с более высоким приоритетом проверяются первыми при нахождении курсора
/// в области нескольких целей. Значение по умолчанию: 0.
/// </value>
public int Priority { get; set; }
/// <summary>
/// Получает или задает группу цели сброса.
/// </summary>
/// <value>
/// Имя группы для группового управления целями сброса. Может использоваться
/// для массовой отмены регистрации целей или применения общих настроек.
/// </value>
public string? Group { get; set; }
/// <summary>
/// Получает сервис перетаскивания из контейнера зависимостей.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDragDropService"/>, используемый для регистрации цели сброса.
/// </value>
protected IDragDropService DragDropService { get; }
/// <summary>
/// Получает текущие границы элемента в экранных координатах.
/// </summary>
/// <value>
/// Прямоугольник, описывающий границы элемента в экранных координатах.
/// Значение автоматически обновляется при изменении размера или позиции элемента.
/// </value>
protected Rect CurrentBounds => _currentBounds;
/// <summary>
/// Получает уникальный идентификатор регистрации цели в сервисе перетаскивания.
/// </summary>
/// <value>
/// Идентификатор, возвращенный методом <see cref="IDragDropService.RegisterDropTarget"/>,
/// или null, если цель не зарегистрирована.
/// </value>
protected string? RegistrationId => _registrationId;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DropTargetBehaviorBase{TElement}"/>.
/// </summary>
/// <param name="dragDropService">Сервис перетаскивания.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="dragDropService"/> равен null.
/// </exception>
protected DropTargetBehaviorBase(IDragDropService dragDropService)
{
DragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
}
/// <summary>
/// Вызывается при прикреплении поведения к элементу.
/// </summary>
/// <remarks>
/// Реализация по умолчанию подписывается на события элемента, обновляет границы
/// и регистрирует цель в сервисе перетаскивания.
/// </remarks>
protected virtual void AttachToElement()
{
if (_associatedElement != null)
{
SubscribeToEvents(_associatedElement);
UpdateBounds();
RegisterToService();
}
}
/// <summary>
/// Вызывается при откреплении поведения от элемента.
/// </summary>
/// <remarks>
/// Реализация по умолчанию отписывается от событий элемента и отменяет
/// регистрацию цели в сервисе перетаскивания.
/// </remarks>
protected virtual void DetachFromElement()
{
if (_associatedElement != null)
{
UnsubscribeFromEvents(_associatedElement);
UnregisterFromService();
}
}
/// <summary>
/// Подписывается на события элемента, необходимые для отслеживания изменений размера и позиции.
/// </summary>
/// <param name="element">Элемент, к событиям которого нужно подписаться.</param>
/// <remarks>
/// Производные классы должны реализовать этот метод для подписки на события конкретной
/// UI-платформы (например, SizeChanged, LayoutUpdated для WPF).
/// </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>
/// <remarks>
/// Этот метод вызывается при изменении размера или позиции элемента для
/// обновления области, в которой цель может принимать сбрасываемые данные.
/// </remarks>
protected virtual void UpdateBounds()
{
if (_associatedElement != null)
{
_currentBounds = GetScreenBounds(_associatedElement);
// Обновляем регистрацию в сервисе
if (_registrationId != null)
{
DragDropService.UpdateDropTargetBounds(_registrationId, _currentBounds);
}
}
}
/// <summary>
/// Получает границы элемента в экранных координатах.
/// </summary>
/// <param name="element">Элемент, границы которого нужно получить.</param>
/// <returns>Границы элемента в экранных координатах.</returns>
/// <remarks>
/// Производные классы должны реализовать этот метод для получения границ
/// элемента в соответствии с конкретной UI-платформой.
/// </remarks>
protected abstract Rect GetScreenBounds(TElement element);
/// <summary>
/// Регистрирует цель в сервисе перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод регистрирует текущий объект как цель сброса в сервисе перетаскивания
/// с указанными приоритетом и группой.
/// </para>
/// <para>
/// Регистрация выполняется только если поведение прикреплено к элементу и
/// цель еще не зарегистрирована.
/// </para>
/// </remarks>
protected virtual void RegisterToService()
{
if (_associatedElement != null && _registrationId == null)
{
UpdateBounds();
_registrationId = DragDropService.RegisterDropTarget(this, _currentBounds, Priority, Group);
}
}
/// <summary>
/// Отменяет регистрацию цели в сервисе перетаскивания.
/// </summary>
/// <remarks>
/// Этот метод отменяет регистрацию цели сброса, освобождая ресурсы
/// в сервисе перетаскивания и предотвращая дальнейшую обработку событий.
/// </remarks>
protected virtual void UnregisterFromService()
{
if (_registrationId != null)
{
DragDropService.UnregisterDropTarget(_registrationId);
_registrationId = null;
}
}
/// <summary>
/// Вызывается при изменении размера или позиции элемента.
/// </summary>
/// <remarks>
/// Производные классы должны вызывать этот метод из обработчиков событий
/// изменения размера или позиции элемента для обновления границ цели.
/// </remarks>
protected virtual void OnElementLayoutChanged()
{
UpdateBounds();
}
#region IDropTarget Implementation
/// <inheritdoc/>
public abstract Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken ct = default);
/// <inheritdoc/>
public virtual async Task OnDragOverAsync(DropInfo dropInfo, CancellationToken ct = default)
{
// Базовая реализация устанавливает эффект по умолчанию
if (await CanAcceptDropAsync(dropInfo))
{
// Установить эффект по умолчанию, если он разрешен
if (dropInfo.CanAcceptEffect(Core.DragDrop.Enums.DragDropEffects.Move))
{
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Move;
}
else if (dropInfo.CanAcceptEffect(Core.DragDrop.Enums.DragDropEffects.Copy))
{
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Copy;
}
else if (dropInfo.CanAcceptEffect(Core.DragDrop.Enums.DragDropEffects.Link))
{
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Link;
}
}
else
{
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.None;
}
}
/// <inheritdoc/>
public abstract Task OnDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default);
/// <inheritdoc/>
public virtual Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
// Базовая реализация не выполняет действий при выходе курсора из области цели
return Task.CompletedTask;
}
#endregion
/// <summary>
/// Открепляет поведение от элемента и освобождает все связанные ресурсы.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// </para>
/// <list type="bullet">
/// <item>Отписывается от всех событий связанного элемента</item>
/// <item>Отменяет регистрацию цели в сервисе перетаскивания</item>
/// <item>Освобождает ссылку на связанный элемент</item>
/// <item>Очищает все временные данные и состояние</item>
/// </list>
/// <para>
/// После вызова этого метода поведение больше не будет обрабатывать события
/// элемента и не будет реагировать на операции перетаскивания. Поведение
/// можно безопасно удалить после вызова этого метода.
/// </para>
/// <para>
/// <strong>Важно:</strong> Этот метод должен быть вызван перед удалением
/// элемента из визуального дерева или перед заменой поведения, чтобы
/// предотвратить утечки памяти и непредсказуемое поведение системы.
/// </para>
/// <example>
/// <code>
/// // Пример использования
/// var dropBehavior = new MyDropTargetBehavior(serviceProvider);
/// dropBehavior.AssociatedElement = myElement;
///
/// // ... использование поведения ...
///
/// // Перед удалением элемента или поведения
/// dropBehavior.Detach();
/// </code>
/// </example>
/// </remarks>
public virtual void Detach()
{
DetachFromElement();
_associatedElement = null;
_registrationId = null;
_currentBounds = default;
}
}