349 lines
16 KiB
C#
349 lines
16 KiB
C#
using Lattice.Core.DragDrop.Abstractions;
|
||
using Lattice.Core.DragDrop.Models;
|
||
using Lattice.Core.DragDrop.Services;
|
||
using Lattice.Core.Geometry;
|
||
using Microsoft.Extensions.DependencyInjection;
|
||
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"/>, используемый для регистрации цели сброса.
|
||
/// При первом обращении выполняется получение сервиса из <see cref="ServiceProvider"/>.
|
||
/// </value>
|
||
protected IDragDropService DragDropService
|
||
{
|
||
get
|
||
{
|
||
if (_dragDropService == null)
|
||
{
|
||
_dragDropService = ServiceProvider.GetRequiredService<IDragDropService>();
|
||
}
|
||
return _dragDropService;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// Получает провайдер сервисов для разрешения зависимостей.
|
||
/// </summary>
|
||
protected IServiceProvider ServiceProvider { 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="serviceProvider">Провайдер сервисов для разрешения зависимостей.</param>
|
||
/// <exception cref="ArgumentNullException">
|
||
/// Выбрасывается, когда <paramref name="serviceProvider"/> равен null.
|
||
/// </exception>
|
||
protected DropTargetBehaviorBase(IServiceProvider serviceProvider)
|
||
{
|
||
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
|
||
}
|
||
|
||
/// <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;
|
||
}
|
||
} |