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
{
///
/// Поведение цели сброса для элементов WinUI.
/// Позволяет элементам принимать данные при операции перетаскивания.
///
///
///
/// Это поведение должно быть прикреплено к , который должен выступать в качестве цели сброса.
/// Поведение автоматически регистрирует элемент в системе перетаскивания и обрабатывает все аспекты операции сброса.
///
///
/// Для использования необходимо:
/// 1. Создать экземпляр поведения с помощью или через DI.
/// 2. Переопределить методы и для реализации логики принятия данных.
/// 3. При необходимости переопределить для настройки визуальной обратной связи.
///
///
public class WinUIDropTargetBehavior : DropTargetBehaviorBase
{
private static readonly ConcurrentDictionary _attachedBehaviors = new();
private readonly WeakReference? _weakElement;
///
/// Инициализирует новый экземпляр класса .
///
/// Провайдер сервисов.
///
/// Конструктор создает экземпляр поведения, но не прикрепляет его к элементу.
/// Для прикрепления используйте метод .
///
public WinUIDropTargetBehavior(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
///
/// Инициализирует новый экземпляр класса .
///
/// Провайдер сервисов.
/// Элемент, к которому прикрепляется поведение.
///
/// Конструктор создает экземпляр поведения и сразу прикрепляет его к указанному элементу.
///
public WinUIDropTargetBehavior(IServiceProvider serviceProvider, FrameworkElement element)
: base(serviceProvider)
{
AssociatedElement = element ?? throw new ArgumentNullException(nameof(element));
}
///
/// Прикрепляет поведение к указанному элементу.
///
/// Элемент, к которому прикрепляется поведение.
/// Провайдер сервисов.
///
/// Экземпляр поведения, прикрепленного к элементу. Если к элементу уже прикреплено поведение,
/// возвращает существующий экземпляр.
///
///
/// Выбрасывается, когда или равны null.
///
///
///
/// Этот метод обеспечивает, что к каждому элементу прикреплен только один экземпляр поведения.
/// Если метод вызывается повторно для того же элемента, возвращается существующий экземпляр.
///
///
/// Прикрепленное поведение автоматически отслеживает изменения макета элемента и обновляет
/// его границы в системе перетаскивания.
///
///
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;
});
}
///
/// Открепляет поведение от указанного элемента.
///
/// Элемент, от которого открепляется поведение.
///
/// true, если поведение было успешно откреплено; false, если поведение не было прикреплено к элементу.
///
///
/// Этот метод освобождает все ресурсы, связанные с поведением, и отписывается от событий элемента.
/// После вызова этого метода элемент перестает быть целью сброса.
///
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;
}
///
/// Получает поведение, прикрепленное к указанному элементу.
///
/// Элемент, для которого требуется получить поведение.
///
/// Экземпляр поведения, прикрепленного к элементу, или null, если поведение не прикреплено.
///
public static WinUIDropTargetBehavior? GetAttachedBehavior(FrameworkElement element)
{
_attachedBehaviors.TryGetValue(element, out var behavior);
return behavior;
}
///
/// Подписывается на события элемента.
///
/// Элемент, к которому прикрепляется поведение.
///
///
/// Этот метод подписывается на следующие события:
///
///
/// - - для отслеживания изменений макета
/// - - для отслеживания изменений размера
/// - - для инициализации при загрузке элемента
///
///
/// Переопределите этот метод, чтобы добавить подписку на дополнительные события.
///
///
protected override void SubscribeToEvents(FrameworkElement element)
{
if (element == null) return;
element.LayoutUpdated += OnLayoutUpdated;
element.SizeChanged += OnSizeChanged;
element.Loaded += OnLoaded;
// Если элемент уже загружен, сразу обновляем границы
if (element.IsLoaded)
{
UpdateBounds();
}
}
///
/// Отписывается от событий элемента.
///
/// Элемент, от которого отписывается поведение.
///
/// Этот метод отписывается от всех событий, на которые подписался .
///
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (element == null) return;
element.LayoutUpdated -= OnLayoutUpdated;
element.SizeChanged -= OnSizeChanged;
element.Loaded -= OnLoaded;
}
///
/// Получает границы элемента в экранных координатах.
///
/// Элемент, границы которого нужно получить.
///
/// Прямоугольник, описывающий границы элемента в экранных координатах.
///
///
///
/// Метод использует преобразование координат через
/// для получения глобальных координат элемента.
///
///
/// Если элемент не прикреплен к визуальному дереву или его границы не могут быть вычислены,
/// возвращается пустой прямоугольник.
///
///
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;
}
}
///
/// Определяет, может ли элемент принять сбрасываемые данные.
///
/// Информация о сбросе.
///
/// true, если элемент может принять данные; в противном случае — false.
///
///
///
/// Этот метод является абстрактным и должен быть переопределен в производных классах
/// для реализации логики принятия данных.
///
///
/// Базовая реализация всегда возвращает false. Переопределите этот метод, чтобы определить,
/// какие типы данных может принимать ваш элемент и при каких условиях.
///
///
/// Пример реализации:
///
/// public override bool CanAcceptDrop(DropInfo dropInfo)
/// {
/// // Принимаем только строковые данные
/// return dropInfo.Data is string;
/// }
///
///
///
public override async Task CanAcceptDropAsync(DropInfo dropInfo)
{
// Базовая реализация - не принимает никакие данные.
// Переопределите этот метод в производных классах.
return false;
}
///
/// Обрабатывает сброс данных на элемент.
///
/// Информация о сбросе.
///
///
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над элементом,
/// и данные должны быть приняты.
///
///
/// Базовая реализация ничего не делает. Переопределите этот метод, чтобы реализовать
/// логику обработки принятых данных.
///
///
/// Пример реализации:
///
/// public override void Drop(DropInfo dropInfo)
/// {
/// if (dropInfo.Data is string text)
/// {
/// // Обработка текстовых данных
/// AssociatedElement.SetValue(TextBlock.TextProperty, text);
/// dropInfo.MarkAsHandled();
/// }
/// }
///
///
///
public override async Task DropAsync(DropInfo dropInfo)
{
// Базовая реализация ничего не делает.
// Переопределите этот метод в производных классах.
}
///
/// Освобождает ресурсы, связанные с поведением.
///
///
///
/// Этот метод отписывается от всех событий, отменяет регистрацию в сервисе перетаскивания
/// и очищает все ресурсы.
///
///
/// После вызова этого метода поведение больше не может быть использовано.
///
///
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
}
}