Files
Lattice/Lattice.UI.DragDrop.WinUI/Behaviors/WinUIDropTargetBehavior.cs

345 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.Models;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Behaviors;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Concurrent;
using System.Threading.Tasks;
namespace Lattice.UI.DragDrop.WinUI.Behaviors
{
/// <summary>
/// Поведение цели сброса для элементов WinUI.
/// Позволяет элементам принимать данные при операции перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// Это поведение должно быть прикреплено к <see cref="FrameworkElement"/>, который должен выступать в качестве цели сброса.
/// Поведение автоматически регистрирует элемент в системе перетаскивания и обрабатывает все аспекты операции сброса.
/// </para>
/// <para>
/// Для использования необходимо:
/// 1. Создать экземпляр поведения с помощью <see cref="Attach"/> или через DI.
/// 2. Переопределить методы <see cref="CanAcceptDrop"/> и <see cref="Drop"/> для реализации логики принятия данных.
/// 3. При необходимости переопределить <see cref="DragOver"/> для настройки визуальной обратной связи.
/// </para>
/// </remarks>
public class WinUIDropTargetBehavior : DropTargetBehaviorBase<FrameworkElement>
{
private static readonly ConcurrentDictionary<FrameworkElement, WinUIDropTargetBehavior> _attachedBehaviors = new();
private readonly WeakReference<FrameworkElement>? _weakElement;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <remarks>
/// Конструктор создает экземпляр поведения, но не прикрепляет его к элементу.
/// Для прикрепления используйте метод <see cref="Attach(FrameworkElement, IServiceProvider)"/>.
/// </remarks>
public WinUIDropTargetBehavior(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <remarks>
/// Конструктор создает экземпляр поведения и сразу прикрепляет его к указанному элементу.
/// </remarks>
public WinUIDropTargetBehavior(IServiceProvider serviceProvider, FrameworkElement element)
: base(serviceProvider)
{
AssociatedElement = element ?? throw new ArgumentNullException(nameof(element));
}
/// <summary>
/// Прикрепляет поведение к указанному элементу.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <returns>
/// Экземпляр поведения, прикрепленного к элементу. Если к элементу уже прикреплено поведение,
/// возвращает существующий экземпляр.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="element"/> или <paramref name="serviceProvider"/> равны null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод обеспечивает, что к каждому элементу прикреплен только один экземпляр поведения.
/// Если метод вызывается повторно для того же элемента, возвращается существующий экземпляр.
/// </para>
/// <para>
/// Прикрепленное поведение автоматически отслеживает изменения макета элемента и обновляет
/// его границы в системе перетаскивания.
/// </para>
/// </remarks>
public static WinUIDropTargetBehavior Attach(FrameworkElement element, IServiceProvider serviceProvider)
{
if (element == null) throw new ArgumentNullException(nameof(element));
if (serviceProvider == null) throw new ArgumentNullException(nameof(serviceProvider));
return _attachedBehaviors.GetOrAdd(element, key =>
{
var behavior = new WinUIDropTargetBehavior(serviceProvider, key);
// Подписка на события жизненного цикла элемента
key.Unloaded += OnElementUnloaded;
return behavior;
});
}
/// <summary>
/// Открепляет поведение от указанного элемента.
/// </summary>
/// <param name="element">Элемент, от которого открепляется поведение.</param>
/// <returns>
/// true, если поведение было успешно откреплено; false, если поведение не было прикреплено к элементу.
/// </returns>
/// <remarks>
/// Этот метод освобождает все ресурсы, связанные с поведением, и отписывается от событий элемента.
/// После вызова этого метода элемент перестает быть целью сброса.
/// </remarks>
public static bool Detach(FrameworkElement element)
{
if (element == null) return false;
if (_attachedBehaviors.TryRemove(element, out var behavior))
{
element.Unloaded -= OnElementUnloaded;
behavior.Detach();
return true;
}
return false;
}
/// <summary>
/// Получает поведение, прикрепленное к указанному элементу.
/// </summary>
/// <param name="element">Элемент, для которого требуется получить поведение.</param>
/// <returns>
/// Экземпляр поведения, прикрепленного к элементу, или null, если поведение не прикреплено.
/// </returns>
public static WinUIDropTargetBehavior? GetAttachedBehavior(FrameworkElement element)
{
_attachedBehaviors.TryGetValue(element, out var behavior);
return behavior;
}
/// <summary>
/// Подписывается на события элемента.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <remarks>
/// <para>
/// Этот метод подписывается на следующие события:
/// </para>
/// <list type="bullet">
/// <item><see cref="FrameworkElement.LayoutUpdated"/> - для отслеживания изменений макета</item>
/// <item><see cref="FrameworkElement.SizeChanged"/> - для отслеживания изменений размера</item>
/// <item><see cref="FrameworkElement.Loaded"/> - для инициализации при загрузке элемента</item>
/// </list>
/// <para>
/// Переопределите этот метод, чтобы добавить подписку на дополнительные события.
/// </para>
/// </remarks>
protected override void SubscribeToEvents(FrameworkElement element)
{
if (element == null) return;
element.LayoutUpdated += OnLayoutUpdated;
element.SizeChanged += OnSizeChanged;
element.Loaded += OnLoaded;
// Если элемент уже загружен, сразу обновляем границы
if (element.IsLoaded)
{
UpdateBounds();
}
}
/// <summary>
/// Отписывается от событий элемента.
/// </summary>
/// <param name="element">Элемент, от которого отписывается поведение.</param>
/// <remarks>
/// Этот метод отписывается от всех событий, на которые подписался <see cref="SubscribeToEvents"/>.
/// </remarks>
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (element == null) return;
element.LayoutUpdated -= OnLayoutUpdated;
element.SizeChanged -= OnSizeChanged;
element.Loaded -= OnLoaded;
}
/// <summary>
/// Получает границы элемента в экранных координатах.
/// </summary>
/// <param name="element">Элемент, границы которого нужно получить.</param>
/// <returns>
/// Прямоугольник, описывающий границы элемента в экранных координатах.
/// </returns>
/// <remarks>
/// <para>
/// Метод использует преобразование координат через <see cref="UIElement.TransformToVisual"/>
/// для получения глобальных координат элемента.
/// </para>
/// <para>
/// Если элемент не прикреплен к визуальному дереву или его границы не могут быть вычислены,
/// возвращается пустой прямоугольник.
/// </para>
/// </remarks>
protected override Rect GetScreenBounds(FrameworkElement element)
{
if (element == null || !element.IsLoaded)
return Rect.Empty;
try
{
// Получаем корневой элемент окна
var rootVisual = element.XamlRoot?.Content as UIElement;
if (rootVisual == null)
return Rect.Empty;
// Преобразуем границы элемента в координаты корневого элемента
var transform = element.TransformToVisual(rootVisual);
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;
}
}
/// <summary>
/// Определяет, может ли элемент принять сбрасываемые данные.
/// </summary>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <returns>
/// true, если элемент может принять данные; в противном случае — false.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод является абстрактным и должен быть переопределен в производных классах
/// для реализации логики принятия данных.
/// </para>
/// <para>
/// Базовая реализация всегда возвращает false. Переопределите этот метод, чтобы определить,
/// какие типы данных может принимать ваш элемент и при каких условиях.
/// </para>
/// <example>
/// Пример реализации:
/// <code>
/// public override bool CanAcceptDrop(DropInfo dropInfo)
/// {
/// // Принимаем только строковые данные
/// return dropInfo.Data is string;
/// }
/// </code>
/// </example>
/// </remarks>
public override async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
{
// Базовая реализация - не принимает никакие данные.
// Переопределите этот метод в производных классах.
return false;
}
/// <summary>
/// Обрабатывает сброс данных на элемент.
/// </summary>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <remarks>
/// <para>
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над элементом,
/// и данные должны быть приняты.
/// </para>
/// <para>
/// Базовая реализация ничего не делает. Переопределите этот метод, чтобы реализовать
/// логику обработки принятых данных.
/// </para>
/// <example>
/// Пример реализации:
/// <code>
/// public override void Drop(DropInfo dropInfo)
/// {
/// if (dropInfo.Data is string text)
/// {
/// // Обработка текстовых данных
/// AssociatedElement.SetValue(TextBlock.TextProperty, text);
/// dropInfo.MarkAsHandled();
/// }
/// }
/// </code>
/// </example>
/// </remarks>
public override async Task DropAsync(DropInfo dropInfo)
{
// Базовая реализация ничего не делает.
// Переопределите этот метод в производных классах.
}
/// <summary>
/// Освобождает ресурсы, связанные с поведением.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод отписывается от всех событий, отменяет регистрацию в сервисе перетаскивания
/// и очищает все ресурсы.
/// </para>
/// <para>
/// После вызова этого метода поведение больше не может быть использовано.
/// </para>
/// </remarks>
public override void Detach()
{
if (AssociatedElement != null && _attachedBehaviors.TryGetValue(AssociatedElement, out _))
{
_attachedBehaviors.TryRemove(AssociatedElement, out _);
}
base.Detach();
}
#region Event Handlers
private void OnLayoutUpdated(object? sender, object e)
{
OnElementLayoutChanged();
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
OnElementLayoutChanged();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
UpdateBounds();
}
private static void OnElementUnloaded(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement element)
{
Detach(element);
}
}
#endregion
}
}