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

514 lines
20 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.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;
/// <summary>
/// Реализация поведения цели сброса для элементов WinUI.
/// Наследуется от <see cref="DropTargetBehaviorBase{FrameworkElement}"/> для использования
/// общей логики регистрации целей и обработки операций сброса.
/// </summary>
/// <remarks>
/// <para>
/// Этот класс предоставляет конкретную реализацию для платформы WinUI, обрабатывая события
/// перетаскивания WinUI и преобразуя их в вызовы методов интерфейса <see cref="Core.DragDrop.Abstractions.IDropTarget"/>.
/// </para>
/// <para>
/// Основные функции:
/// <list type="bullet">
/// <item>Автоматическая регистрация в <see cref="IDragDropService"/> при прикреплении к элементу</item>
/// <item>Обработка событий DragEnter, DragOver, DragLeave, Drop</item>
/// <item>Автоматическое обновление границ элемента при изменении размера или позиции</item>
/// <item>Поддержка фильтрации принимаемых типов данных</item>
/// <item>Интеграция с визуальной обратной связью</item>
/// </list>
/// </para>
/// <para>
/// Для использования необходимо:
/// <list type="number">
/// <item>Создать экземпляр поведения через фабрику <see cref="Factories.WinUIDragDropFactory.CreateDropTargetBehavior"/></item>
/// <item>Прикрепить к элементу с помощью метода <see cref="Attach"/></item>
/// <item>Настроить фильтры принимаемых данных (опционально)</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание поведения с фильтрацией типов
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior(dragDropService, host);
/// behavior.AcceptTypes(typeof(MyDataModel), typeof(string));
/// behavior.Attach(myDropArea);
///
/// // Или через attached properties
/// &lt;Border x:Name="DropArea"
/// local:DragDropProperties.IsDropTarget="True" /&gt;
/// </code>
/// </example>
/// </remarks>
public sealed class WinUIDropTargetBehavior : DropTargetBehaviorBase<FrameworkElement>
{
#region Поля
private readonly IDragDropHost _host;
private readonly List<Type> _acceptedTypes = new();
private readonly HashSet<string> _acceptedFormats = new();
#endregion
#region Конструктор
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <param name="dragDropService">
/// Сервис управления операциями перетаскивания. Используется для регистрации
/// цели и координации операций сброса.
/// </param>
/// <param name="host">
/// Хост для управления визуальной обратной связью. Обеспечивает отображение
/// индикаторов возможности сброса.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="dragDropService"/> или <paramref name="host"/>
/// равны null.
/// </exception>
/// <remarks>
/// Конструктор инициализирует базовый класс и сохраняет ссылки на сервисы.
/// По умолчанию цель принимает все типы данных. Для настройки фильтрации
/// используйте методы <see cref="AcceptTypes"/> и <see cref="AcceptFormats"/>.
/// </remarks>
public WinUIDropTargetBehavior(
Core.DragDrop.Services.IDragDropService dragDropService,
IDragDropHost host)
: base(dragDropService)
{
_host = host ?? throw new ArgumentNullException(nameof(host));
}
#endregion
#region Публичные методы
/// <summary>
/// Прикрепляет поведение к указанному элементу WinUI.
/// </summary>
/// <param name="element">
/// Элемент <see cref="FrameworkElement"/>, который должен стать целью сброса.
/// Не может быть null.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// После вызова этого метода:
/// <list type="bullet">
/// <item>Элементу устанавливается свойство <see cref="UIElement.AllowDrop"/> = true</item>
/// <item>Поведение подписывается на события перетаскивания WinUI</item>
/// <item>Элемент регистрируется в системе перетаскивания с текущими границами</item>
/// <item>Начинается отслеживание изменений размера и позиции элемента</item>
/// </list>
/// </para>
/// <para>
/// Для открепления поведения используйте метод <see cref="Detach"/>.
/// </para>
/// </remarks>
public void Attach(FrameworkElement element)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
element.AllowDrop = true;
AssociatedElement = element;
}
/// <summary>
/// Настраивает поведение для приема только указанных типов данных.
/// </summary>
/// <param name="types">
/// Типы данных, которые может принимать цель. Если пусто, принимаются все типы.
/// </param>
/// <remarks>
/// <para>
/// Этот метод позволяет ограничить типы данных, которые могут быть сброшены на цель.
/// Проверка выполняется в методе <see cref="CanAcceptDropAsync"/> путем сравнения
/// типа сбрасываемых данных с указанными типами.
/// </para>
/// <para>
/// Если метод не вызывался или передан пустой список, цель будет принимать данные любого типа.
/// </para>
/// <example>
/// <code>
/// // Принимать только строки и объекты MyModel
/// behavior.AcceptTypes(typeof(string), typeof(MyModel));
/// </code>
/// </example>
/// </remarks>
public void AcceptTypes(params Type[] types)
{
_acceptedTypes.Clear();
if (types != null && types.Length > 0)
{
_acceptedTypes.AddRange(types);
}
}
/// <summary>
/// Настраивает поведение для приема только указанных форматов данных.
/// </summary>
/// <param name="formats">
/// Форматы данных (например, "Text", "Bitmap", "FileDrop"), которые может принимать цель.
/// Если пусто, формат не проверяется.
/// </param>
/// <remarks>
/// <para>
/// Этот метод позволяет ограничить форматы данных, которые могут быть сброшены на цель.
/// Актуально для межпроцессного перетаскивания или работы с системными форматами.
/// </para>
/// <para>
/// Если метод не вызывался или передан пустой список, проверка формата не выполняется.
/// </para>
/// </remarks>
public void AcceptFormats(params string[] formats)
{
_acceptedFormats.Clear();
if (formats != null && formats.Length > 0)
{
foreach (var format in formats)
{
_acceptedFormats.Add(format);
}
}
}
/// <summary>
/// Открепляет поведение от текущего элемента.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Отписывается от всех событий элемента</item>
/// <item>Отменяет регистрацию цели в системе перетаскивания</item>
/// <item>Сбрасывает свойство <see cref="UIElement.AllowDrop"/> = false</item>
/// <item>Освобождает ссылки на связанные объекты</item>
/// </list>
/// </para>
/// <para>
/// После вызова этого метода поведение может быть повторно прикреплено к другому элементу.
/// </para>
/// </remarks>
public new void Detach()
{
if (AssociatedElement != null)
{
AssociatedElement.AllowDrop = false;
}
base.Detach();
}
#endregion
#region Реализация абстрактных методов DropTargetBehaviorBase<FrameworkElement>
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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;
}
/// <inheritdoc/>
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
/// <inheritdoc/>
public override async Task<bool> 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);
}
/// <inheritdoc/>
public override async Task OnDragOverAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
await base.OnDragOverAsync(dropInfo, cancellationToken);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, обновление визуальной обратной связи через хост
}
/// <inheritdoc/>
public override async Task OnDropAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
// Базовая реализация вызывает CanAcceptDropAsync и помечает как обработанное
if (await CanAcceptDropAsync(dropInfo, cancellationToken))
{
dropInfo.MarkAsHandled();
// Здесь может быть добавлена логика обработки сброшенных данных
// Например, вызов события или обновление модели данных
}
}
/// <inheritdoc/>
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 Вспомогательные методы
/// <summary>
/// Создает объект <see cref="DropInfo"/> на основе события перетаскивания WinUI.
/// </summary>
/// <param name="e">Аргументы события перетаскивания WinUI.</param>
/// <param name="position">Локальная позиция курсора относительно элемента.</param>
/// <returns>
/// Экземпляр <see cref="DropInfo"/>, содержащий информацию о потенциальном сбросе.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод извлекает данные из события перетаскивания и преобразует их
/// в формат, понятный системе <see cref="Core.DragDrop"/>.
/// </para>
/// <para>
/// Поддерживаются как пользовательские данные (через свойство "DragData"),
/// так и стандартные форматы данных WinUI.
/// </para>
/// </remarks>
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
}
/// <summary>
/// Предоставляет асинхронный доступ к данным перетаскивания.
/// </summary>
/// <remarks>
/// Этот класс используется для отложенной загрузки данных перетаскивания,
/// что особенно важно для больших данных или данных, требующих обработки.
/// </remarks>
internal class AsyncDataProvider
{
private readonly Func<Task<object>> _dataLoader;
public AsyncDataProvider(Func<Task<object>> dataLoader)
{
_dataLoader = dataLoader;
}
public async Task<object> GetDataAsync()
{
return await _dataLoader();
}
}