345 lines
17 KiB
C#
345 lines
17 KiB
C#
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
|
||
}
|
||
} |