using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Abstractions;
using Lattice.UI.DragDrop.Behaviors;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
///
/// Реализация поведения цели сброса для элементов WinUI.
/// Наследуется от для использования
/// общей логики регистрации целей и обработки операций сброса.
///
///
///
/// Этот класс предоставляет конкретную реализацию для платформы WinUI, обрабатывая события
/// перетаскивания WinUI и преобразуя их в вызовы методов интерфейса .
///
///
/// Основные функции:
///
/// Автоматическая регистрация в при прикреплении к элементу
/// Обработка событий DragEnter, DragOver, DragLeave, Drop
/// Автоматическое обновление границ элемента при изменении размера или позиции
/// Поддержка фильтрации принимаемых типов данных
/// Интеграция с визуальной обратной связью
///
///
///
/// Для использования необходимо:
///
/// Создать экземпляр поведения через фабрику
/// Прикрепить к элементу с помощью метода
/// Настроить фильтры принимаемых данных (опционально)
///
///
///
///
/// // Создание поведения с фильтрацией типов
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior(dragDropService, host);
/// behavior.AcceptTypes(typeof(MyDataModel), typeof(string));
/// behavior.Attach(myDropArea);
///
/// // Или через attached properties
/// <Border x:Name="DropArea"
/// local:DragDropProperties.IsDropTarget="True" />
///
///
///
public sealed class WinUIDropTargetBehavior : DropTargetBehaviorBase
{
#region Поля
private readonly IDragDropHost _host;
private readonly List _acceptedTypes = new();
private readonly HashSet _acceptedFormats = new();
#endregion
#region Конструктор
///
/// Инициализирует новый экземпляр класса .
///
///
/// Сервис управления операциями перетаскивания. Используется для регистрации
/// цели и координации операций сброса.
///
///
/// Хост для управления визуальной обратной связью. Обеспечивает отображение
/// индикаторов возможности сброса.
///
///
/// Выбрасывается, если или
/// равны null.
///
///
/// Конструктор инициализирует базовый класс и сохраняет ссылки на сервисы.
/// По умолчанию цель принимает все типы данных. Для настройки фильтрации
/// используйте методы и .
///
public WinUIDropTargetBehavior(
Core.DragDrop.Services.IDragDropService dragDropService,
IDragDropHost host)
: base(dragDropService)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
}
#endregion
#region Публичные методы
///
/// Прикрепляет поведение к указанному элементу WinUI.
///
///
/// Элемент , который должен стать целью сброса.
/// Не может быть null.
///
///
/// Выбрасывается, если равен null.
///
///
///
/// После вызова этого метода:
///
/// Элементу устанавливается свойство = true
/// Поведение подписывается на события перетаскивания WinUI
/// Элемент регистрируется в системе перетаскивания с текущими границами
/// Начинается отслеживание изменений размера и позиции элемента
///
///
///
/// Для открепления поведения используйте метод .
///
///
public void Attach(FrameworkElement element)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
element.AllowDrop = true;
AssociatedElement = element;
}
///
/// Настраивает поведение для приема только указанных типов данных.
///
///
/// Типы данных, которые может принимать цель. Если пусто, принимаются все типы.
///
///
///
/// Этот метод позволяет ограничить типы данных, которые могут быть сброшены на цель.
/// Проверка выполняется в методе путем сравнения
/// типа сбрасываемых данных с указанными типами.
///
///
/// Если метод не вызывался или передан пустой список, цель будет принимать данные любого типа.
///
///
///
/// // Принимать только строки и объекты MyModel
/// behavior.AcceptTypes(typeof(string), typeof(MyModel));
///
///
///
public void AcceptTypes(params Type[] types)
{
_acceptedTypes.Clear();
if (types != null && types.Length > 0)
{
_acceptedTypes.AddRange(types);
}
}
///
/// Настраивает поведение для приема только указанных форматов данных.
///
///
/// Форматы данных (например, "Text", "Bitmap", "FileDrop"), которые может принимать цель.
/// Если пусто, формат не проверяется.
///
///
///
/// Этот метод позволяет ограничить форматы данных, которые могут быть сброшены на цель.
/// Актуально для межпроцессного перетаскивания или работы с системными форматами.
///
///
/// Если метод не вызывался или передан пустой список, проверка формата не выполняется.
///
///
public void AcceptFormats(params string[] formats)
{
_acceptedFormats.Clear();
if (formats != null && formats.Length > 0)
{
foreach (var format in formats)
{
_acceptedFormats.Add(format);
}
}
}
///
/// Открепляет поведение от текущего элемента.
///
///
///
/// Этот метод выполняет следующие действия:
///
/// Отписывается от всех событий элемента
/// Отменяет регистрацию цели в системе перетаскивания
/// Сбрасывает свойство = false
/// Освобождает ссылки на связанные объекты
///
///
///
/// После вызова этого метода поведение может быть повторно прикреплено к другому элементу.
///
///
public new void Detach()
{
if (AssociatedElement != null)
{
AssociatedElement.AllowDrop = false;
}
base.Detach();
}
#endregion
#region Реализация абстрактных методов DropTargetBehaviorBase
///
protected override void SubscribeToEvents(FrameworkElement element)
{
if (element == null) return;
element.DragEnter += OnDragEnter;
element.DragOver += OnDragOver;
element.DragLeave += OnDragLeave;
element.Drop += OnDrop;
element.SizeChanged += OnSizeChanged;
element.LayoutUpdated += OnLayoutUpdated;
}
///
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (element == null) return;
element.DragEnter -= OnDragEnter;
element.DragOver -= OnDragOver;
element.DragLeave -= OnDragLeave;
element.Drop -= OnDrop;
element.SizeChanged -= OnSizeChanged;
element.LayoutUpdated -= OnLayoutUpdated;
}
///
protected override Rect GetScreenBounds(FrameworkElement element)
{
if (element == null || !element.IsLoaded)
return Rect.Empty;
try
{
var window = Window.Current;
if (window?.Content == null)
return Rect.Empty;
// Преобразуем локальные координаты элемента в координаты окна
var transform = element.TransformToVisual(window.Content);
var position = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
return new Rect(
position.X,
position.Y,
element.ActualWidth,
element.ActualHeight
);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(
$"Ошибка получения границ элемента: {ex.Message}");
return Rect.Empty;
}
}
#endregion
#region Реализация интерфейса IDropTarget
///
public override async Task CanAcceptDropAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
// Проверяем, есть ли данные для сброса
if (dropInfo.Data == null)
return false;
// Проверяем фильтр по типам
if (_acceptedTypes.Count > 0)
{
var dataType = dropInfo.Data.GetType();
if (!_acceptedTypes.Any(t => t.IsAssignableFrom(dataType)))
{
return false;
}
}
// Проверяем фильтр по форматам (если данные предоставляют информацию о формате)
if (_acceptedFormats.Count > 0 && dropInfo.Data is Windows.ApplicationModel.DataTransfer.DataPackageView dataView)
{
var availableFormats = dataView.AvailableFormats;
if (!_acceptedFormats.Any(f => availableFormats.Contains(f)))
{
return false;
}
}
// Дополнительная проверка может быть добавлена в производных классах
return await Task.FromResult(true);
}
///
public override async Task OnDragOverAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
await base.OnDragOverAsync(dropInfo, cancellationToken);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, обновление визуальной обратной связи через хост
}
///
public override async Task OnDropAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
// Базовая реализация вызывает CanAcceptDropAsync и помечает как обработанное
if (await CanAcceptDropAsync(dropInfo, cancellationToken))
{
dropInfo.MarkAsHandled();
// Здесь может быть добавлена логика обработки сброшенных данных
// Например, вызов события или обновление модели данных
}
}
///
public override async Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
await base.OnDragLeaveAsync(cancellationToken);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, скрытие визуальной обратной связи
}
#endregion
#region Обработчики событий WinUI
private async void OnDragEnter(object sender, DragEventArgs e)
{
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
e.Handled = true;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDragEnter: {ex.Message}");
}
}
private async void OnDragOver(object sender, DragEventArgs e)
{
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
await OnDragOverAsync(dropInfo);
e.Handled = true;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDragOver: {ex.Message}");
}
}
private async void OnDragLeave(object sender, DragEventArgs e)
{
await OnDragLeaveAsync();
}
private async void OnDrop(object sender, DragEventArgs e)
{
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
await OnDropAsync(dropInfo);
e.Handled = true;
}
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDrop: {ex.Message}");
}
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
OnElementLayoutChanged();
}
private void OnLayoutUpdated(object sender, object e)
{
OnElementLayoutChanged();
}
#endregion
#region Вспомогательные методы
///
/// Создает объект на основе события перетаскивания WinUI.
///
/// Аргументы события перетаскивания WinUI.
/// Локальная позиция курсора относительно элемента.
///
/// Экземпляр , содержащий информацию о потенциальном сбросе.
///
///
///
/// Этот метод извлекает данные из события перетаскивания и преобразует их
/// в формат, понятный системе .
///
///
/// Поддерживаются как пользовательские данные (через свойство "DragData"),
/// так и стандартные форматы данных WinUI.
///
///
private DropInfo CreateDropInfo(DragEventArgs e, Point position)
{
object? data = null;
// Пытаемся получить пользовательские данные
if (e.DataView.Properties.TryGetValue("DragData", out var dragData))
{
data = dragData;
}
// Или получаем данные из DataPackage
else if (e.DataView.Contains(Windows.ApplicationModel.DataTransfer.StandardDataFormats.Text))
{
// Для текстовых данных можем установить асинхронную загрузку
data = new AsyncDataProvider(async () =>
{
return await e.DataView.GetTextAsync();
});
}
// Определяем разрешенные эффекты на основе модификаторов клавиатуры
var allowedEffects = Core.DragDrop.Enums.DragDropEffects.None;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Copy;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Move;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Link))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Link;
return new DropInfo(
data: data,
position: position,
allowedEffects: allowedEffects,
target: this
);
}
#endregion
}
///
/// Предоставляет асинхронный доступ к данным перетаскивания.
///
///
/// Этот класс используется для отложенной загрузки данных перетаскивания,
/// что особенно важно для больших данных или данных, требующих обработки.
///
internal class AsyncDataProvider
{
private readonly Func> _dataLoader;
public AsyncDataProvider(Func> dataLoader)
{
_dataLoader = dataLoader;
}
public async Task