DragAndDrop core

This commit is contained in:
FrigaT
2026-01-18 16:33:35 +03:00
parent 9ea82af329
commit 79bdd8bc62
229 changed files with 21214 additions and 2494 deletions

View File

@@ -0,0 +1,28 @@
namespace Lattice.Core.DragDrop.Abstractions;
/// <summary>
/// Определяет контракт для объектов, которые могут быть источником данных
/// в операции перетаскивания с поддержкой асинхронных операций.
/// </summary>
public interface IAsyncDragSource : IDragSource
{
/// <summary>
/// Определяет, может ли объект начать операцию перетаскивания (асинхронно).
/// </summary>
Task<(bool CanStart, Models.DragInfo? DragInfo)> CanStartDragAsync();
/// <summary>
/// Начинает операцию перетаскивания (асинхронно).
/// </summary>
Task<bool> StartDragAsync(Models.DragInfo dragInfo);
/// <summary>
/// Вызывается при завершении операции перетаскивания (асинхронно).
/// </summary>
Task DragCompletedAsync(Models.DragInfo dragInfo, Enums.DragDropEffects effects);
/// <summary>
/// Вызывается при отмене операции перетаскивания (асинхронно).
/// </summary>
Task DragCancelledAsync(Models.DragInfo dragInfo);
}

View File

@@ -0,0 +1,28 @@
namespace Lattice.Core.DragDrop.Abstractions;
/// <summary>
/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные
/// в операции перетаскивания с поддержкой асинхронных операций.
/// </summary>
public interface IAsyncDropTarget : IDropTarget
{
/// <summary>
/// Определяет, может ли объект принять сбрасываемые данные (асинхронно).
/// </summary>
Task<bool> CanAcceptDropAsync(Models.DropInfo dropInfo);
/// <summary>
/// Вызывается, когда перетаскиваемый объект находится над целью (асинхронно).
/// </summary>
Task DragOverAsync(Models.DropInfo dropInfo);
/// <summary>
/// Вызывается, когда пользователь сбрасывает данные на цель (асинхронно).
/// </summary>
Task DropAsync(Models.DropInfo dropInfo);
/// <summary>
/// Вызывается, когда перетаскиваемый объект покидает область цели (асинхронно).
/// </summary>
Task DragLeaveAsync();
}

View File

@@ -0,0 +1,64 @@
namespace Lattice.Core.DragDrop.Abstractions;
/// <summary>
/// Определяет контракт для объектов, которые могут быть источником данных
/// в операции перетаскивания.
/// </summary>
/// <remarks>
/// Объекты, реализующие этот интерфейс, могут инициировать операции перетаскивания
/// и предоставлять данные для передачи другим элементам через механизм drag-and-drop.
/// </remarks>
public interface IDragSource
{
/// <summary>
/// Определяет, может ли объект начать операцию перетаскивания.
/// </summary>
/// <param name="dragInfo">
/// Информация о перетаскивании, которая будет заполнена данными, если операция разрешена.
/// </param>
/// <returns>
/// true, если объект может начать перетаскивание; в противном случае — false.
/// </returns>
/// <remarks>
/// Этот метод вызывается системой перетаскивания для проверки возможности
/// начала операции. Если метод возвращает true, он должен заполнить
/// <paramref name="dragInfo"/> необходимыми данными.
/// </remarks>
bool CanStartDrag(out Models.DragInfo? dragInfo);
/// <summary>
/// Начинает операцию перетаскивания.
/// </summary>
/// <param name="dragInfo">Информация о перетаскивании.</param>
/// <returns>
/// true, если операция перетаскивания успешно начата; в противном случае — false.
/// </returns>
/// <remarks>
/// Этот метод вызывается, когда пользователь начинает перетаскивание элемента.
/// Реализация должна подготовить данные для перетаскивания и, возможно,
/// создать визуальное представление перетаскиваемого объекта.
/// </remarks>
bool StartDrag(Models.DragInfo dragInfo);
/// <summary>
/// Вызывается при завершении операции перетаскивания.
/// </summary>
/// <param name="dragInfo">Исходная информация о перетаскивании.</param>
/// <param name="effects">Эффекты, которые были применены при сбросе.</param>
/// <remarks>
/// Этот метод вызывается после завершения операции перетаскивания
/// (успешного или неуспешного). Реализация может выполнить очистку
/// или обновить состояние на основе результата операции.
/// </remarks>
void DragCompleted(Models.DragInfo dragInfo, Enums.DragDropEffects effects);
/// <summary>
/// Вызывается при отмене операции перетаскивания.
/// </summary>
/// <param name="dragInfo">Исходная информация о перетаскивании.</param>
/// <remarks>
/// Этот метод вызывается, когда операция перетаскивания была отменена
/// пользователем (например, нажатием клавиши Escape).
/// </remarks>
void DragCancelled(Models.DragInfo dragInfo);
}

View File

@@ -0,0 +1,55 @@
namespace Lattice.Core.DragDrop.Abstractions;
/// <summary>
/// Определяет контракт для объектов, которые могут принимать сбрасываемые данные
/// в операции перетаскивания.
/// </summary>
/// <remarks>
/// Объекты, реализующие этот интерфейс, могут обрабатывать данные, сброшенные
/// пользователем, и предоставлять визуальную обратную связь во время перетаскивания.
/// </remarks>
public interface IDropTarget
{
/// <summary>
/// Определяет, может ли объект принять сбрасываемые данные.
/// </summary>
/// <param name="dropInfo">Информация о потенциальном сбросе.</param>
/// <returns>
/// true, если объект может принять данные; в противном случае — false.
/// </returns>
/// <remarks>
/// Этот метод вызывается, когда перетаскиваемый объект находится над целью.
/// Реализация должна проверить, совместимы ли данные с целью, и установить
/// предлагаемые эффекты в <paramref name="dropInfo"/>.
/// </remarks>
bool CanAcceptDrop(Models.DropInfo dropInfo);
/// <summary>
/// Вызывается, когда перетаскиваемый объект находится над целью.
/// </summary>
/// <param name="dropInfo">Информация о текущем положении перетаскивания.</param>
/// <remarks>
/// Этот метод вызывается постоянно, пока пользователь перемещает объект над целью.
/// Реализация может обновить визуальную обратную связь или изменить предлагаемые эффекты.
/// </remarks>
void DragOver(Models.DropInfo dropInfo);
/// <summary>
/// Вызывается, когда пользователь сбрасывает данные на цель.
/// </summary>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <remarks>
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над целью.
/// Реализация должна обработать принятие данных и выполнить соответствующее действие.
/// </remarks>
void Drop(Models.DropInfo dropInfo);
/// <summary>
/// Вызывается, когда перетаскиваемый объект покидает область цели.
/// </summary>
/// <remarks>
/// Этот метод вызывается, когда пользователь перемещает объект за пределы цели.
/// Реализация должна очистить любую визуальную обратную связь, установленную ранее.
/// </remarks>
void DragLeave();
}

View File

@@ -0,0 +1,102 @@
namespace Lattice.Core.DragDrop.Enums;
/// <summary>
/// Определяет эффекты, которые могут быть применены при операции перетаскивания.
/// </summary>
/// <remarks>
/// Этот перечисление используется для указания допустимых операций перетаскивания
/// и передачи информации о результате операции между источником и целью.
/// </remarks>
[Flags]
public enum DragDropEffects
{
/// <summary>
/// Операция перетаскивания не разрешена.
/// </summary>
None = 0,
/// <summary>
/// Данные копируются из источника в цель.
/// </summary>
Copy = 1 << 0,
/// <summary>
/// Данные перемещаются из источника в цель.
/// </summary>
Move = 1 << 1,
/// <summary>
/// Создается ссылка на исходные данные.
/// </summary>
Link = 1 << 2,
/// <summary>
/// Целевой элемент может прокручиваться во время перетаскивания.
/// </summary>
Scroll = 1 << 3,
/// <summary>
/// Комбинированный эффект копирования и перемещения.
/// </summary>
CopyOrMove = Copy | Move,
/// <summary>
/// Все эффекты разрешены.
/// </summary>
All = Copy | Move | Link | Scroll
}
/// <summary>
/// Расширения для работы с DragDropEffects.
/// </summary>
public static class DragDropEffectsExtensions
{
/// <summary>
/// Проверяет, содержит ли эффекты указанный эффект.
/// </summary>
public static bool HasEffect(this DragDropEffects effects, DragDropEffects effect)
{
return (effects & effect) == effect;
}
/// <summary>
/// Проверяет, содержат ли эффекты копирование.
/// </summary>
public static bool CanCopy(this DragDropEffects effects)
{
return effects.HasEffect(DragDropEffects.Copy);
}
/// <summary>
/// Проверяет, содержат ли эффекты перемещение.
/// </summary>
public static bool CanMove(this DragDropEffects effects)
{
return effects.HasEffect(DragDropEffects.Move);
}
/// <summary>
/// Проверяет, содержат ли эффекты ссылку.
/// </summary>
public static bool CanLink(this DragDropEffects effects)
{
return effects.HasEffect(DragDropEffects.Link);
}
/// <summary>
/// Получает наиболее подходящий эффект на основе модификаторов клавиатуры.
/// </summary>
public static DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey)
{
if (controlKey && altKey)
return DragDropEffects.Link;
if (controlKey)
return DragDropEffects.Copy;
if (shiftKey)
return DragDropEffects.Move;
if (altKey)
return DragDropEffects.Link;
return DragDropEffects.Move; // По умолчанию
}
}

View File

@@ -0,0 +1,14 @@
namespace Lattice.Core.DragDrop.Enums;
/// <summary>
/// Позиция сброса относительно цели.
/// </summary>
public enum DropPosition
{
Inside,
Top,
Bottom,
Left,
Right,
Center
}

View File

@@ -0,0 +1,85 @@
namespace Lattice.Core.DragDrop.Exceptions;
/// <summary>
/// Исключение, возникающее при ошибках в системе перетаскивания.
/// </summary>
public class DragDropException : Exception
{
/// <summary>
/// Код ошибки.
/// </summary>
public string ErrorCode { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/>.
/// </summary>
public DragDropException()
: base("Drag & Drop operation failed.")
{
ErrorCode = "DRAGDROP_0001";
}
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/> с указанным сообщением.
/// </summary>
public DragDropException(string message)
: base(message)
{
ErrorCode = "DRAGDROP_0002";
}
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/> с кодом ошибки.
/// </summary>
public DragDropException(string errorCode, string message)
: base(message)
{
ErrorCode = errorCode;
}
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/>
/// с указанным сообщением и внутренним исключением.
/// </summary>
public DragDropException(string message, Exception innerException)
: base(message, innerException)
{
ErrorCode = "DRAGDROP_0003";
}
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragDropException"/>
/// с кодом ошибки, сообщением и внутренним исключением.
/// </summary>
public DragDropException(string errorCode, string message, Exception innerException)
: base(message, innerException)
{
ErrorCode = errorCode;
}
}
/// <summary>
/// Коды ошибок Drag & Drop системы.
/// </summary>
public static class DragDropErrorCodes
{
// Общие ошибки
public const string OperationAlreadyActive = "DRAGDROP_1001";
public const string OperationNotActive = "DRAGDROP_1002";
public const string InvalidData = "DRAGDROP_1003";
public const string Timeout = "DRAGDROP_1004";
// Ошибки источников
public const string SourceCannotDrag = "DRAGDROP_2001";
public const string SourceStartFailed = "DRAGDROP_2002";
// Ошибки целей
public const string TargetNotFound = "DRAGDROP_3001";
public const string TargetCannotAccept = "DRAGDROP_3002";
public const string TargetDropFailed = "DRAGDROP_3003";
// Ошибки системы
public const string SystemNotInitialized = "DRAGDROP_4001";
public const string SystemDisposed = "DRAGDROP_4002";
public const string MemoryAllocationFailed = "DRAGDROP_4003";
}

View File

@@ -0,0 +1,85 @@
namespace Lattice.Core.DragDrop.Extensions;
/// <summary>
/// Методы расширения для регистрации сервисов перетаскивания.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Добавляет сервис перетаскивания.
/// </summary>
/// <param name="serviceCollection">Коллекция сервисов.</param>
/// <returns>Коллекция сервисов.</returns>
/// <remarks>
/// Реализация DI должна быть предоставлена конкретным приложением.
/// </remarks>
public static object AddDragDropService(this object serviceCollection)
{
// Реализация регистрации сервиса должна быть в конкретном приложении
// Это абстрактный метод для поддержки DI без зависимостей
return serviceCollection;
}
/// <summary>
/// Добавляет сервис перетаскивания с конфигурацией.
/// </summary>
/// <param name="serviceCollection">Коллекция сервисов.</param>
/// <param name="configure">Действие конфигурации.</param>
/// <returns>Коллекция сервисов.</returns>
public static object AddDragDropService(
this object serviceCollection,
Action<DragDropServiceOptions> configure)
{
var options = new DragDropServiceOptions();
configure(options);
// Реализация регистрации с опциями должна быть в конкретном приложении
return serviceCollection;
}
}
/// <summary>
/// Опции конфигурации сервиса перетаскивания.
/// </summary>
public class DragDropServiceOptions
{
/// <summary>
/// Порог начала перетаскивания в пикселях.
/// </summary>
public double DragStartThreshold { get; set; } = 3.0;
/// <summary>
/// Включить ведение журнала операций.
/// </summary>
public bool EnableLogging { get; set; } = false;
/// <summary>
/// Включить автоматическую очистку неиспользуемых целей.
/// </summary>
public bool EnableAutoCleanup { get; set; } = true;
/// <summary>
/// Интервал автоматической очистки в миллисекундах.
/// </summary>
public int AutoCleanupInterval { get; set; } = 60000;
/// <summary>
/// Включить асинхронную обработку операций.
/// </summary>
public bool EnableAsyncOperations { get; set; } = true;
/// <summary>
/// Время ожидания асинхронных операций в миллисекундах.
/// </summary>
public int AsyncOperationTimeout { get; set; } = 5000;
/// <summary>
/// Включить сбор статистики.
/// </summary>
public bool EnableStatistics { get; set; } = true;
/// <summary>
/// Включить проверку типов данных.
/// </summary>
public bool EnableTypeChecking { get; set; } = true;
}

View File

@@ -0,0 +1,20 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net8.0;net9.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<PackageId>Lattice.Core.DragDrop</PackageId>
<Version>1.0.0</Version>
<Authors>FrigaT</Authors>
<Description>Professional drag-and-drop system for Lattice UI Framework</Description>
<PackageTags>ui;framework;drag;drop;docking;toolbox</PackageTags>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,227 @@
using Lattice.Core.Geometry;
using System.Collections.Concurrent;
namespace Lattice.Core.DragDrop.Models;
/// <summary>
/// Содержит информацию о начале операции перетаскивания.
/// Этот класс передается от источника перетаскивания к системе перетаскивания
/// для инициализации и управления операцией.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="DragInfo"/> является ключевым компонентом системы перетаскивания,
/// инкапсулирующим все необходимые данные для начала операции. Он содержит:
/// </para>
/// <list type="bullet">
/// <item>Данные для передачи</item>
/// <item>Разрешенные эффекты перетаскивания</item>
/// <item>Начальную позицию операции</item>
/// <item>Ссылку на источник перетаскивания</item>
/// <item>Дополнительные параметры операции</item>
/// </list>
/// <para>
/// Этот класс используется как внутренний механизм передачи данных между
/// <see cref="Abstractions.IDragSource"/> и системой управления перетаскиванием.
/// </para>
/// </remarks>
public class DragInfo : IDisposable, ICloneable
{
private readonly ConcurrentDictionary<string, object> _parameters = new();
private bool _disposed;
/// <summary>
/// Получает данные, которые передаются в операции перетаскивания.
/// </summary>
/// <value>
/// Объект, содержащий данные для передачи. Может быть любого типа,
/// поддерживаемого системой перетаскивания.
/// </value>
/// <remarks>
/// Эти данные будут доступны цели сброса через <see cref="DropInfo.Data"/>.
/// Важно, чтобы данные были сериализуемыми, если операция перетаскивания
/// может выходить за пределы процесса приложения.
/// </remarks>
public object Data { get; }
/// <summary>
/// Получает разрешенные эффекты для этой операции перетаскивания.
/// </summary>
/// <value>
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, определяющая,
/// какие операции разрешены для этого перетаскивания.
/// </value>
/// <remarks>
/// Этот параметр используется системой для фильтрации допустимых операций
/// и предоставления соответствующей визуальной обратной связи пользователю.
/// </remarks>
public Enums.DragDropEffects AllowedEffects { get; }
/// <summary>
/// Получает начальную позицию операции перетаскивания в координатах экрана.
/// </summary>
/// <value>
/// Точка в экранных координатах, где была начата операция перетаскивания.
/// </value>
/// <remarks>
/// Эта позиция используется для вычисления смещения при создании визуального
/// представления перетаскивания и для определения порога начала операции.
/// </remarks>
public Point StartPosition { get; }
/// <summary>
/// Получает источник перетаскивания, который инициировал операцию.
/// </summary>
/// <value>
/// Объект, реализующий <see cref="Abstractions.IDragSource"/>, или null,
/// если источник не доступен или не требуется.
/// </value>
/// <remarks>
/// Эта ссылка может использоваться для уведомления источника о результате
/// операции перетаскивания (завершении или отмене).
/// </remarks>
public object? Source { get; }
/// <summary>
/// Получает или задает дополнительные параметры, специфичные для конкретной
/// реализации перетаскивания.
/// </summary>
/// <value>
/// Словарь, содержащий пары ключ-значение с дополнительными параметрами.
/// </value>
/// <remarks>
/// Используется для передачи контекстной информации, которая не входит
/// в стандартный набор свойств, но может быть полезной для обработки
/// операции перетаскивания.
/// </remarks>
public IReadOnlyDictionary<string, object> Parameters => _parameters;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragInfo"/>.
/// </summary>
/// <param name="data">
/// Данные, которые передаются в операции перетаскивания.
/// Не может быть null.
/// </param>
/// <param name="allowedEffects">
/// Разрешенные эффекты для этой операции перетаскивания.
/// </param>
/// <param name="startPosition">
/// Начальная позиция операции перетаскивания в координатах экрана.
/// </param>
/// <param name="source">
/// Источник перетаскивания, который инициировал операцию. Может быть null.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="data"/> равен null.
/// </exception>
/// <remarks>
/// Конструктор создает экземпляр <see cref="DragInfo"/> с указанными
/// параметрами и инициализирует коллекцию параметров пустым словарем.
/// </remarks>
public DragInfo(object data, Enums.DragDropEffects allowedEffects, Point startPosition, object? source = null)
{
Data = data ?? throw new ArgumentNullException(nameof(data));
AllowedEffects = allowedEffects;
StartPosition = startPosition;
Source = source;
}
/// <summary>
/// Создает новый экземпляр <see cref="DragInfo"/> с теми же данными,
/// но новой позицией.
/// </summary>
/// <param name="newPosition">
/// Новая позиция для информации о перетаскивании.
/// </param>
/// <returns>
/// Новый экземпляр <see cref="DragInfo"/> с обновленной позицией.
/// </returns>
/// <remarks>
/// Этот метод используется для обновления информации о перетаскивании
/// при перемещении курсора, сохраняя исходные данные и параметры.
/// </remarks>
public DragInfo CloneWithPosition(Point newPosition)
{
ThrowIfDisposed();
var clone = new DragInfo(Data, AllowedEffects, newPosition, Source);
foreach (var kvp in _parameters)
{
clone._parameters[kvp.Key] = kvp.Value;
}
return clone;
}
/// <summary>
/// Создает новый экземпляр <see cref="DragInfo"/> с теми же данными.
/// </summary>
public DragInfo Clone() => new DragInfo(Data, AllowedEffects, StartPosition, Source);
/// <inheritdoc/>
object ICloneable.Clone() => this.Clone();
/// <summary>
/// Получает или дополнительные параметры, специфичные для конкретной
/// реализации перетаскивания.
/// </summary>
public T? GetParameter<T>(string key, T? defaultValue = default)
{
if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
{
return typedValue;
}
return defaultValue;
}
/// <summary>
/// Получает или дополнительные параметры, специфичные для конкретной
/// реализации перетаскивания.
/// </summary>
public bool TryGetParameter<T>(string key, out T? value)
{
value = default;
if (_parameters.TryGetValue(key, out var objValue) && objValue is T typedValue)
{
value = typedValue;
return true;
}
return false;
}
/// <summary>
/// Задает дополнительные параметры, специфичные для конкретной
/// реализации перетаскивания.
/// </summary>
public void SetParameter<T>(string key, T value)
{
_parameters[key] = value!;
}
/// <summary>
/// Освобождает ресурсы.
/// </summary>
public void Dispose()
{
if (_disposed) return;
_parameters.Clear();
_disposed = true;
GC.SuppressFinalize(this);
}
private void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(DragInfo));
}
~DragInfo()
{
Dispose();
}
}

View File

@@ -0,0 +1,269 @@
using Lattice.Core.DragDrop.Enums;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Models;
/// <summary>
/// Содержит информацию о потенциальном или фактическом сбросе в операции перетаскивания.
/// Этот класс используется для передачи данных между системой перетаскивания
/// и целью сброса (<see cref="Abstractions.IDropTarget"/>).
/// </summary>
/// <remarks>
/// <para>
/// <see cref="DropInfo"/> предоставляет цель сброса всей необходимой информацией
/// для принятия решения о возможности сброса и выполнения соответствующей операции.
/// Ключевые аспекты включают:
/// </para>
/// <list type="bullet">
/// <item>Предлагаемые для сброса данные</item>
/// <item>Текущую позицию курсора</item>
/// <item>Разрешенные эффекты от источника</item>
/// <item>Предлагаемые эффекты для сброса</item>
/// <item>Ссылку на цель сброса</item>
/// <item>Флаг обработки операции</item>
/// </list>
/// <para>
/// Этот класс является изменяемым, позволяя цели сброса обновлять предлагаемые
/// эффекты и помечать операцию как обработанную.
/// </para>
/// </remarks>
public class DropInfo
{
private DragDropEffects _effects = DragDropEffects.None;
public DropPosition DropPosition { get; set; } = DropPosition.Inside;
public bool ShowVisualFeedback { get; set; } = true;
public object? VisualFeedbackData { get; set; }
/// <summary>
/// Получает данные, которые предлагаются для сброса.
/// </summary>
/// <value>
/// Данные, переданные от источника перетаскивания, или null, если данные
/// не доступны или операция была отменена.
/// </value>
/// <remarks>
/// Эти данные соответствуют свойству <see cref="DragInfo.Data"/> из
/// исходной информации о перетаскивании.
/// </remarks>
public object? Data { get; }
/// <summary>
/// Получает текущую позицию курсора в координатах экрана.
/// </summary>
/// <value>
/// Точка в экранных координатах, представляющая текущее положение курсора
/// мыши во время операции перетаскивания.
/// </value>
/// <remarks>
/// Эта позиция используется для определения точного места сброса и может
/// влиять на предлагаемые эффекты (например, различные операции для
/// разных областей цели сброса).
/// </remarks>
public Point Position { get; }
/// <summary>
/// Получает разрешенные эффекты от источника перетаскивания.
/// </summary>
/// <value>
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, определяющая,
/// какие операции разрешил источник.
/// </value>
/// <remarks>
/// Цель сброса должна уважать эти ограничения и не предлагать эффекты,
/// которые не разрешены источником.
/// </remarks>
public Enums.DragDropEffects AllowedEffects { get; }
/// <summary>
/// Получает или задает предлагаемые эффекты для операции сброса.
/// </summary>
/// <value>
/// Комбинация флагов <see cref="Enums.DragDropEffects"/>, предлагаемая
/// целью сброса. По умолчанию равно <see cref="Enums.DragDropEffects.None"/>.
/// </value>
/// <remarks>
/// <para>
/// Цель сброса должна установить это свойство в методе <see cref="Abstractions.IDropTarget.DragOver"/>
/// на основе анализа предоставленных данных и текущего контекста.
/// </para>
/// <para>
/// Если цель не устанавливает это свойство, система перетаскивания
/// будет использовать эффекты по умолчанию.
/// </para>
/// </remarks>
public Enums.DragDropEffects SuggestedEffects
{
get => _effects;
set => _effects = value;
}
/// <summary>
/// Получает цель сброса, которая обрабатывает эту информацию.
/// </summary>
/// <value>
/// Объект, реализующий <see cref="Abstractions.IDropTarget"/>, или null,
/// если цель не определена.
/// </value>
/// <remarks>
/// Эта ссылка позволяет системе идентифицировать, какая цель обрабатывает
/// информацию о сбросе, и используется для отслеживания изменений цели
/// во время операции перетаскивания.
/// </remarks>
public object? Target { get; }
/// <summary>
/// Получает или задает дополнительные параметры, специфичные для конкретной
/// реализации перетаскивания.
/// </summary>
/// <value>
/// Словарь, содержащий пары ключ-значение с дополнительными параметрами.
/// </value>
/// <remarks>
/// Может использоваться для передачи контекстной информации между
/// различными компонентами системы перетаскивания или для хранения
/// временных данных во время обработки операции.
/// </remarks>
public Dictionary<string, object> Parameters { get; set; }
/// <summary>
/// Получает значение, указывающее, был ли сброс уже обработан.
/// </summary>
/// <value>
/// true, если операция сброса была помечена как обработанная;
/// в противном случае — false.
/// </value>
/// <remarks>
/// <para>
/// Это свойство используется для предотвращения множественной обработки
/// одной и той же операции сброса. После вызова метода <see cref="MarkAsHandled"/>,
/// свойство становится true.
/// </para>
/// <para>
/// Система перетаскивания может проверять это свойство, чтобы определить,
/// нужно ли выполнять дополнительную обработку по умолчанию.
/// </para>
/// </remarks>
public bool Handled { get; private set; }
/// <summary>
/// Получает дополнительные параметры, специфичные для конкретной
/// реализации перетаскивания.
/// </summary>
public T? GetParameter<T>(string key, T? defaultValue = default)
{
if (Parameters.TryGetValue(key, out var value) && value is T typedValue)
{
return typedValue;
}
return defaultValue;
}
/// <summary>
/// Получает дополнительные параметры, специфичные для конкретной
/// реализации перетаскивания.
/// </summary>
public bool TryGetParameter<T>(string key, out T? value)
{
value = default;
if (Parameters.TryGetValue(key, out var objValue) && objValue is T typedValue)
{
value = typedValue;
return true;
}
return false;
}
/// <summary>
/// Задает дополнительные параметры, специфичные для конкретной
/// реализации перетаскивания.
/// </summary>
public void SetParameter<T>(string key, T value)
{
Parameters[key] = value!;
}
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DropInfo"/>.
/// </summary>
/// <param name="data">
/// Данные, которые предлагаются для сброса. Может быть null.
/// </param>
/// <param name="position">
/// Текущая позиция курсора в координатах экрана.
/// </param>
/// <param name="allowedEffects">
/// Разрешенные эффекты от источника перетаскивания.
/// </param>
/// <param name="target">
/// Цель сброса, которая обрабатывает эту информацию. Может быть null.
/// </param>
/// <remarks>
/// Конструктор создает экземпляр <see cref="DropInfo"/> с указанными
/// параметрами, инициализирует коллекцию параметров пустым словарем
/// и устанавливает флаг <see cref="Handled"/> в false.
/// </remarks>
public DropInfo(object? data, Point position, Enums.DragDropEffects allowedEffects, object? target = null)
{
Data = data;
Position = position;
AllowedEffects = allowedEffects;
Target = target;
Parameters = new Dictionary<string, object>();
Handled = false;
}
/// <summary>
/// Помечает сброс как обработанный.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод должен вызываться целью сброса в методе <see cref="Abstractions.IDropTarget.Drop"/>,
/// если она успешно обработала операцию сброса.
/// </para>
/// <para>
/// После вызова этого метода свойство <see cref="Handled"/> становится true,
/// что сигнализирует системе перетаскивания о том, что дополнительная
/// обработка не требуется.
/// </para>
/// </remarks>
public void MarkAsHandled()
{
Handled = true;
}
/// <summary>
/// Создает новый экземпляр <see cref="DropInfo"/> с теми же данными,
/// но новой позицией.
/// </summary>
/// <param name="newPosition">
/// Новая позиция для информации о сбросе.
/// </param>
/// <returns>
/// Новый экземпляр <see cref="DropInfo"/> с обновленной позицией.
/// </returns>
/// <remarks>
/// Этот метод используется для обновления информации о сбросе при
/// перемещении курсора, сохраняя исходные данные и параметры.
/// </remarks>
public DropInfo WithPosition(Point newPosition)
{
return new DropInfo(Data, newPosition, AllowedEffects, Target)
{
Parameters = new Dictionary<string, object>(Parameters),
SuggestedEffects = _effects,
DropPosition = DropPosition,
ShowVisualFeedback = ShowVisualFeedback,
VisualFeedbackData = VisualFeedbackData
};
}
/// <summary>
/// Проверка установки эффекта перетаскивания в разрешенные эффекты.
/// </summary>
public bool CanAcceptEffect(Enums.DragDropEffects effect)
{
return (AllowedEffects & effect) != Enums.DragDropEffects.None;
}
}

View File

@@ -0,0 +1,832 @@
# Lattice.Core.DragDrop
Профессиональная, асинхронная система перетаскивания для .NET приложений. Полностью потокобезопасная, расширяемая архитектура с поддержкой кросс-платформенности.
## 📋 Особенности
-**Полная асинхронная поддержка** - async/await для всех операций
-**Потокобезопасность** - `ReaderWriterLockSlim` для эффективной синхронизации
-**Производительность** - Оптимизированные алгоритмы, кэширование, минимальные аллокации
-**Расширяемость** - Легкая интеграция с любыми UI фреймворками
-**Надежность** - Таймауты, обработка ошибок, корректное освобождение ресурсов
-**Статистика** - Встроенный мониторинг производительности
## 🏗️ Архитектура
### Основные компоненты
```
Lattice.Core.DragDrop/
├── Abstractions/ # Интерфейсы
│ ├── IDragSource.cs # Источник перетаскивания (синхронный)
│ ├── IAsyncDragSource.cs # Асинхронный источник
│ ├── IDropTarget.cs # Цель сброса (синхронная)
│ └── IAsyncDropTarget.cs # Асинхронная цель
├── Enums/ # Перечисления
├── Exceptions/ # Исключения с кодами ошибок
├── Extensions/ # Расширения для DI
├── Models/ # Модели данных
│ ├── DragInfo.cs # Информация о перетаскивании
│ └── DropInfo.cs # Информация о сбросе
├── Services/ # Сервисы
│ ├── IDragDropService.cs # Основной интерфейс
│ ├── DragDropService.cs # Реализация сервиса
│ └── EventArgs/ # Аргументы событий
└── Utilities/ # Утилиты и фабрики
├── DragDropUtilities.cs # Синхронные утилиты
└── AsyncDragDropUtilities.cs # Асинхронные утилиты
```
## 🚀 Быстрый старт
### 1. Установка
```csharp
// Добавьте проект Lattice.Core.DragDrop в ваше решение
// или создайте NuGet пакет
```
### 2. Базовое использование
```csharp
using Lattice.Core.DragDrop;
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
// Создаем сервис
var dragDropService = new DragDropService();
// Создаем простой источник перетаскивания
var dragSource = DragDropUtilities.CreateSimpleDragSource(
dataProvider: () => "Example Data",
canDrag: () => true,
onCompleted: (dragInfo, effects) =>
Console.WriteLine($"Drag completed with effects: {effects}"),
onCancelled: dragInfo =>
Console.WriteLine("Drag cancelled")
);
// Создаем простую цель сброса
var dropTarget = DragDropUtilities.CreateSimpleDropTarget(
canAccept: dropInfo => dropInfo.Data is string,
onDragOver: dropInfo =>
dropInfo.SuggestedEffects = DragDropEffects.Copy,
onDrop: dropInfo =>
{
Console.WriteLine($"Dropped: {dropInfo.Data}");
dropInfo.MarkAsHandled();
}
);
// Регистрируем цель
string targetId = dragDropService.RegisterDropTarget(
dropTarget,
new Rect(100, 100, 300, 200)
);
// Начинаем перетаскивание
bool started = dragDropService.StartDrag(
dragSource,
new Point(50, 50)
);
if (started)
{
// Обновляем позицию
dragDropService.UpdateDrag(new Point(150, 150));
// Завершаем
var effects = dragDropService.EndDrag(new Point(200, 200));
}
```
## 📖 Подробное руководство
### Сервис перетаскивания
Основной класс системы - `DragDropService`, реализующий `IDragDropService`.
```csharp
// Создание с кастомными настройками
var service = new DragDropService(options =>
{
options.DragStartThreshold = 5.0;
options.EnableAsyncOperations = true;
options.AsyncOperationTimeout = 3000;
options.EnableAutoCleanup = true;
});
// Свойства
bool isActive = service.IsDragActive; // Активна ли операция
DragInfo? currentDrag = service.CurrentDragInfo; // Текущая информация
double threshold = service.DragStartThreshold; // Порог начала
// События
service.DragStarted += OnDragStarted;
service.DragUpdated += OnDragUpdated;
service.DragCompleted += OnDragCompleted;
service.DragCancelled += OnDragCancelled;
service.ErrorOccurred += OnErrorOccurred;
// Регистрация целей
string id = service.RegisterDropTarget(
target, // IDropTarget
bounds, // Rect
priority: 1, // Приоритет (выше = выше приоритет)
group: "main" // Группа для группового удаления
);
// Обновление границ
service.UpdateDropTargetBounds(id, newBounds);
// Удаление
service.UnregisterDropTarget(id);
service.UnregisterDropTargetsInGroup("main");
```
### Асинхронное использование
```csharp
// Асинхронные методы
bool started = await service.StartDragAsync(source, startPosition);
await service.UpdateDragAsync(currentPosition);
DragDropEffects effects = await service.EndDragAsync(dropPosition);
await service.CancelDragAsync();
// Статистика
var stats = service.GetStats();
Console.WriteLine($"Operations: {stats.TotalDragOperations}");
Console.WriteLine($"Success rate: {stats.SuccessfulDrops}/{stats.TotalDragOperations}");
Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms");
```
### Создание кастомных источников и целей
#### Синхронная реализация
```csharp
public class FileDragSource : IDragSource
{
private readonly FileInfo _file;
public FileDragSource(FileInfo file) => _file = file;
public bool CanStartDrag(out DragInfo? dragInfo)
{
// Проверяем условия
if (!_file.Exists || _file.Length > 100 * 1024 * 1024) // 100 MB limit
{
dragInfo = null;
return false;
}
// Создаем DragInfo
dragInfo = new DragInfo(
data: _file,
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
startPosition: Point.Zero,
source: this
);
// Добавляем дополнительные параметры
dragInfo.SetParameter("FileSize", _file.Length);
dragInfo.SetParameter("MimeType", GetMimeType(_file));
return true;
}
public bool StartDrag(DragInfo dragInfo)
{
// Подготовка к перетаскиванию
// Можно создать визуальное представление и т.д.
Console.WriteLine($"Starting drag of {_file.Name}");
return true;
}
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
{
Console.WriteLine($"File drag completed with {effects}");
if (effects == DragDropEffects.Move)
{
// Файл был перемещен - возможно, удалить оригинал
// _file.Delete();
}
}
public void DragCancelled(DragInfo dragInfo)
{
Console.WriteLine("File drag cancelled");
}
}
```
#### Асинхронная реализация
```csharp
public class DatabaseItemDragSource : IAsyncDragSource
{
private readonly DatabaseService _db;
private readonly int _itemId;
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
{
try
{
// Асинхронные проверки
var canDrag = await _db.CanDragItemAsync(_itemId);
if (!canDrag) return (false, null);
// Асинхронная загрузка данных
var data = await _db.GetItemForDragAsync(_itemId);
if (data == null) return (false, null);
var dragInfo = new DragInfo(
data: data,
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
startPosition: Point.Zero,
source: this
);
return (true, dragInfo);
}
catch (Exception ex)
{
// Логирование ошибки
return (false, null);
}
}
public Task<bool> StartDragAsync(DragInfo dragInfo)
{
// Асинхронная подготовка
return Task.FromResult(true);
}
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
{
// Асинхронная обработка завершения
await _db.LogDragOperationAsync(_itemId, effects);
if (effects == DragDropEffects.Move)
{
await _db.MarkItemAsMovedAsync(_itemId);
}
}
public Task DragCancelledAsync(DragInfo dragInfo)
{
return Task.CompletedTask;
}
// Синхронные методы для совместимости
public bool CanStartDrag(out DragInfo? dragInfo)
{
var result = Task.Run(() => CanStartDragAsync()).GetAwaiter().GetResult();
dragInfo = result.DragInfo;
return result.CanStart;
}
// ... остальные синхронные методы
}
```
### Работа с моделями данных
#### DragInfo
```csharp
// Создание
var dragInfo = new DragInfo(
data: myObject,
allowedEffects: DragDropEffects.Copy | DragDropEffects.Move,
startPosition: new Point(x, y),
source: this
);
// Параметры
dragInfo.SetParameter("Timestamp", DateTime.UtcNow);
dragInfo.SetParameter("UserId", currentUser.Id);
// Получение параметров
if (dragInfo.TryGetParameter<string>("Category", out var category))
{
// Используем категорию
}
// Клонирование с новой позицией
var updatedDragInfo = dragInfo.CloneWithPosition(newPosition);
// Очистка ресурсов
dragInfo.Dispose();
```
#### DropInfo
```csharp
// Создается сервисом автоматически
// Работа с DropInfo в методах цели:
public void DragOver(DropInfo dropInfo)
{
// Проверяем данные
if (dropInfo.Data is MyDataType myData)
{
// Определяем позицию относительно цели
dropInfo.DropPosition = CalculateDropPosition(dropInfo.Position);
// Предлагаем эффекты
if (CanAcceptData(myData))
{
dropInfo.SuggestedEffects = DragDropEffects.Move;
dropInfo.ShowVisualFeedback = true;
dropInfo.VisualFeedbackData = CreatePreview(myData);
}
else
{
dropInfo.SuggestedEffects = DragDropEffects.None;
}
}
}
public void Drop(DropInfo dropInfo)
{
if (dropInfo.Data is MyDataType myData)
{
// Обработка сброса
ProcessDrop(myData, dropInfo.DropPosition);
// Помечаем как обработанное
dropInfo.MarkAsHandled();
}
}
```
### Утилиты и фабрики
#### Синхронные утилиты
```csharp
// Простые реализации
var simpleSource = DragDropUtilities.CreateSimpleDragSource(
() => data,
() => true,
(dragInfo, effects) => Console.WriteLine("Completed"),
dragInfo => Console.WriteLine("Cancelled")
);
var simpleTarget = DragDropUtilities.CreateSimpleDropTarget(
dropInfo => dropInfo.Data != null,
dropInfo => dropInfo.SuggestedEffects = DragDropEffects.Copy,
dropInfo => Console.WriteLine($"Dropped: {dropInfo.Data}"),
() => Console.WriteLine("Drag left")
);
// Геометрия
double distance = DragDropUtilities.CalculateDistance(p1, p2);
bool exceeded = DragDropUtilities.HasExceededDragThreshold(start, current, threshold);
DropPosition position = DragDropUtilities.GetDropPosition(point, bounds, edgeThreshold);
// Проверка совместимости
bool compatible = DragDropUtilities.AreEffectsCompatible(sourceEffects, targetEffects);
bool typeMatch = DragDropUtilities.IsDataCompatible(data, new[] { typeof(string), typeof(int) });
```
#### Асинхронные утилиты
```csharp
// Асинхронные реализации
var asyncSource = AsyncDragDropUtilities.CreateAsyncDragSource(
async () => await LoadDataAsync(),
async () => await CanDragAsync(),
async (dragInfo, effects) => await OnCompletedAsync(dragInfo, effects),
async dragInfo => await OnCancelledAsync(dragInfo)
);
var asyncTarget = AsyncDragDropUtilities.CreateAsyncDropTarget(
async dropInfo => await CanAcceptAsync(dropInfo.Data),
async dropInfo => await OnDragOverAsync(dropInfo),
async dropInfo => await OnDropAsync(dropInfo),
async () => await OnDragLeaveAsync()
);
// Адаптеры для синхронных интерфейсов
IAsyncDragSource asyncFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncSource);
IAsyncDropTarget asyncTargetFromSync = AsyncDragDropUtilities.CreateAsyncAdapter(syncTarget);
// Комбинированные реализации (fallback стратегия)
var combined = AsyncDragDropUtilities.Combine(
syncSource,
asyncSource,
preferAsync: true // При ошибке в async использует sync
);
// Таймауты
var result = await AsyncDragDropUtilities.ExecuteWithTimeoutAsync(
task: LongOperationAsync(),
timeout: TimeSpan.FromSeconds(5),
defaultValue: fallbackValue
);
```
### Обработка ошибок
```csharp
// Подписка на ошибки
service.ErrorOccurred += (sender, e) =>
{
Console.WriteLine($"Error in {e.Operation}: {e.Exception.Message}");
// Коды ошибок определены в DragDropErrorCodes
switch (e.ErrorCode)
{
case DragDropErrorCodes.Timeout:
Console.WriteLine("Operation timed out");
break;
case DragDropErrorCodes.SourceCannotDrag:
Console.WriteLine("Source cannot drag");
break;
case DragDropErrorCodes.TargetCannotAccept:
Console.WriteLine("Target cannot accept");
break;
}
};
// Использование в коде
try
{
await service.StartDragAsync(source, position);
}
catch (DragDropException ex)
{
// Обработка специфичных для DragDrop ошибок
Console.WriteLine($"DragDrop error {ex.ErrorCode}: {ex.Message}");
}
catch (Exception ex)
{
// Обработка других ошибок
Console.WriteLine($"General error: {ex.Message}");
}
```
## 🔧 Интеграция с UI фреймворками
### Базовый адаптер для WinUI/WPF
```csharp
public class UIElementDragSource : IAsyncDragSource
{
private readonly FrameworkElement _element;
private readonly Func<object> _dataProvider;
public UIElementDragSource(FrameworkElement element, Func<object> dataProvider)
{
_element = element;
_dataProvider = dataProvider;
// Подписка на события
_element.PointerPressed += OnPointerPressed;
_element.PointerMoved += OnPointerMoved;
_element.PointerReleased += OnPointerReleased;
}
private Point _dragStartPosition;
private bool _isDragging;
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
var point = e.GetCurrentPoint(_element);
_dragStartPosition = new Point(point.Position.X, point.Position.Y);
}
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_isDragging) return;
var point = e.GetCurrentPoint(_element);
var current = new Point(point.Position.X, point.Position.Y);
var distance = Math.Sqrt(
Math.Pow(current.X - _dragStartPosition.X, 2) +
Math.Pow(current.Y - _dragStartPosition.Y, 2));
if (distance > 3.0) // Порог
{
_isDragging = true;
// Начинаем перетаскивание через сервис
var service = GetDragDropService();
await service.StartDragAsync(this, ConvertToScreen(_dragStartPosition));
}
}
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
{
var data = _dataProvider();
if (data == null) return (false, null);
var dragInfo = new DragInfo(
data,
DragDropEffects.Copy | DragDropEffects.Move,
Point.Zero,
this
);
return (true, dragInfo);
}
// ... остальная реализация
}
```
## 🧪 Тестирование
### Примеры модульных тестов
```csharp
[TestClass]
public class DragDropServiceTests
{
private DragDropService _service;
private Mock<IAsyncDragSource> _mockSource;
private Mock<IAsyncDropTarget> _mockTarget;
[TestInitialize]
public void Setup()
{
_service = new DragDropService();
_mockSource = new Mock<IAsyncDragSource>();
_mockTarget = new Mock<IAsyncDropTarget>();
}
[TestMethod]
public async Task StartDrag_ValidSource_ReturnsTrue()
{
// Arrange
var dragInfo = new DragInfo("test", DragDropEffects.Copy, Point.Zero);
_mockSource.Setup(s => s.CanStartDragAsync())
.ReturnsAsync((true, dragInfo));
_mockSource.Setup(s => s.StartDragAsync(It.IsAny<DragInfo>()))
.ReturnsAsync(true);
// Act
var result = await _service.StartDragAsync(_mockSource.Object, Point.Zero);
// Assert
Assert.IsTrue(result);
Assert.IsTrue(_service.IsDragActive);
}
[TestMethod]
public async Task UpdateDrag_FindsTarget_CallsDragOver()
{
// Arrange
var targetId = _service.RegisterDropTarget(
_mockTarget.Object,
new Rect(0, 0, 100, 100)
);
await StartTestDrag();
_mockTarget.Setup(t => t.CanAcceptDropAsync(It.IsAny<DropInfo>()))
.ReturnsAsync(true);
// Act
await _service.UpdateDragAsync(new Point(50, 50));
// Assert
_mockTarget.Verify(t => t.DragOverAsync(It.IsAny<DropInfo>()), Times.Once);
}
private async Task StartTestDrag()
{
var dragInfo = new DragInfo("test", DragDropEffects.Copy, Point.Zero);
_mockSource.Setup(s => s.CanStartDragAsync())
.ReturnsAsync((true, dragInfo));
_mockSource.Setup(s => s.StartDragAsync(It.IsAny<DragInfo>()))
.ReturnsAsync(true);
await _service.StartDragAsync(_mockSource.Object, Point.Zero);
}
}
```
## 📊 Мониторинг и производительность
### Сбор статистики
```csharp
// Получение статистики
var stats = service.GetStats();
Console.WriteLine($"Total operations: {stats.TotalDragOperations}");
Console.WriteLine($"Successful: {stats.SuccessfulDrops}");
Console.WriteLine($"Cancelled: {stats.CancelledOperations}");
Console.WriteLine($"Errors: {stats.ErrorCount}");
Console.WriteLine($"Avg time: {stats.AverageOperationTime.TotalMilliseconds}ms");
// Мониторинг в реальном времени
private Stopwatch _operationTimer;
service.DragStarted += (s, e) =>
{
_operationTimer = Stopwatch.StartNew();
Console.WriteLine($"Drag started from {e.DragInfo.Source}");
};
service.DragCompleted += (s, e) =>
{
_operationTimer.Stop();
Console.WriteLine($"Drag completed in {_operationTimer.ElapsedMilliseconds}ms");
if (service.EnableAsyncOperations)
{
var stats = service.GetStats();
Console.WriteLine($"Success rate: {(double)stats.SuccessfulDrops / stats.TotalDragOperations:P}");
}
};
```
### Оптимизация производительности
```csharp
// 1. Настройка параметров
var service = new DragDropService(options =>
{
options.DragStartThreshold = 4.0; // Увеличить порог для предотвращения случайных перетаскиваний
options.AsyncOperationTimeout = 2000; // Уменьшить таймаут для отзывчивости
options.EnableAutoCleanup = true; // Автоочистка неиспользуемых целей
});
// 2. Группировка целей
_service.RegisterDropTarget(target1, bounds1, group: "toolbox");
_service.RegisterDropTarget(target2, bounds2, group: "toolbox");
// Быстрое удаление всех целей группы
_service.UnregisterDropTargetsInGroup("toolbox");
// 3. Приоритеты для оптимизации поиска
_service.RegisterDropTarget(importantTarget, bounds, priority: 100); // Высокий приоритет
_service.RegisterDropTarget(defaultTarget, bounds, priority: 0); // Низкий приоритет
// 4. Периодическая очистка
service.ClearAllDropTargets(); // При смене контекста
```
## 🚀 Продвинутые сценарии
### Переупорядочивание элементов
```csharp
public class ReorderableListDropTarget : IAsyncDropTarget
{
private readonly IList<object> _items;
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
{
return dropInfo.Data is object && _items.Contains(dropInfo.Data);
}
public async Task DropAsync(DropInfo dropInfo)
{
var item = dropInfo.Data;
var insertIndex = CalculateInsertIndex(dropInfo);
// Удаляем из старой позиции
_items.Remove(item);
// Вставляем в новую позицию
if (insertIndex < _items.Count)
_items.Insert(insertIndex, item);
else
_items.Add(item);
dropInfo.MarkAsHandled();
}
private int CalculateInsertIndex(DropInfo dropInfo)
{
// Логика определения позиции вставки на основе dropInfo.Position
// и визуального расположения элементов
return 0;
}
}
```
### Мультиселект и групповое перетаскивание
```csharp
public class MultiSelectionDragSource : IAsyncDragSource
{
private readonly IEnumerable<object> _selectedItems;
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
{
if (!_selectedItems.Any()) return (false, null);
// Создаем коллекцию для перетаскивания
var dragData = new DragItemCollection(_selectedItems);
var dragInfo = new DragInfo(
dragData,
DragDropEffects.Copy | DragDropEffects.Move,
Point.Zero,
this
);
dragInfo.SetParameter("ItemCount", _selectedItems.Count());
dragInfo.SetParameter("IsMultiSelect", true);
return (true, dragInfo);
}
}
```
## 📚 API Reference
### Основные интерфейсы
#### IDragDropService
```csharp
bool IsDragActive { get; }
DragInfo? CurrentDragInfo { get; }
IDropTarget? CurrentDropTarget { get; }
double DragStartThreshold { get; set; }
bool EnableAsyncOperations { get; set; }
// Регистрация целей
string RegisterDropTarget(IDropTarget target, Rect bounds, int priority = 0, string? group = null);
bool UpdateDropTargetBounds(string id, Rect bounds);
bool UnregisterDropTarget(string id);
void UnregisterDropTargetsInGroup(string group);
// Асинхронные операции
Task<bool> StartDragAsync(IDragSource source, Point startPosition);
Task UpdateDragAsync(Point position);
Task<DragDropEffects> EndDragAsync(Point position);
Task CancelDragAsync();
// Синхронные операции
bool StartDrag(IDragSource source, Point startPosition);
void UpdateDrag(Point position);
DragDropEffects EndDrag(Point position);
void CancelDrag();
// Утилиты
void ClearAllDropTargets();
DragDropStats GetStats();
```
#### IAsyncDragSource
```csharp
Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync();
Task<bool> StartDragAsync(DragInfo dragInfo);
Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects);
Task DragCancelledAsync(DragInfo dragInfo);
```
#### IAsyncDropTarget
```csharp
Task<bool> CanAcceptDropAsync(DropInfo dropInfo);
Task DragOverAsync(DropInfo dropInfo);
Task DropAsync(DropInfo dropInfo);
Task DragLeaveAsync();
```
### Перечисления
#### DragDropEffects
```csharp
[Flags]
None = 0
Copy = 1 << 0 // Копирование данных
Move = 1 << 1 // Перемещение данных
Link = 1 << 2 // Ссылка на данные
CopyOrMove = Copy | Move
All = Copy | Move | Link
// Методы расширения:
bool CanCopy(this DragDropEffects effects)
bool CanMove(this DragDropEffects effects)
bool CanLink(this DragDropEffects effects)
DragDropEffects GetEffectFromKeys(bool controlKey, bool shiftKey, bool altKey)
```
#### DropPosition
```csharp
Inside // Внутри элемента
Top // Сверху
Bottom // Снизу
Left // Слева
Right // Справа
Center // По центру
```
## 🔮 Планы развития
1. **Интеграция с популярными UI фреймворками** (WinUI, Uno Platform, Avalonia)
2. **Поддержка жестов** (тач, мультитач)
3. **Виртуализация** для работы с большими наборами данных
4. **Продвинутые визуальные эффекты** (анимации, превью)
5. **Source Generators** для автоматической генерации кода
6. **Инструменты разработчика** (дебаггер, профилировщик)

View File

@@ -0,0 +1,829 @@
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Реализация сервиса управления операциями перетаскивания.
/// Полностью потокобезопасная реализация с поддержкой async/await.
/// </summary>
public sealed class DragDropService : IDragDropService
{
#region Nested Types
private sealed class DropTargetInfo : IDisposable
{
public required Abstractions.IDropTarget Target { get; init; }
public required Geometry.Rect Bounds { get; set; }
public required int Priority { get; init; }
public required string? Group { get; init; }
public required string Id { get; init; }
public DateTime LastAccessTime { get; set; } = DateTime.UtcNow;
public int UsageCount { get; set; }
public void Dispose()
{
if (Target is IDisposable disposable)
disposable.Dispose();
}
}
private sealed class DragOperationContext : IDisposable
{
public Abstractions.IDragSource? Source { get; set; }
public Models.DragInfo? DragInfo { get; set; }
public Abstractions.IDropTarget? CurrentDropTarget { get; set; }
public CancellationTokenSource? CancellationTokenSource { get; set; }
public DateTime StartTime { get; set; } = DateTime.UtcNow;
public bool ThresholdExceeded { get; set; }
public void Dispose()
{
DragInfo?.Dispose();
CancellationTokenSource?.Dispose();
}
}
#endregion
#region Fields
private readonly Dictionary<string, DropTargetInfo> _dropTargets = new();
private readonly ReaderWriterLockSlim _dropTargetsLock = new(LockRecursionPolicy.NoRecursion);
private readonly object _dragOperationLock = new();
private DragOperationContext? _currentDragOperation;
private Timer? _cleanupTimer;
private bool _disposed;
private int _totalDragOperations;
private int _successfulDrops;
private int _cancelledOperations;
private int _errorCount;
private long _totalOperationTicks;
#endregion
#region Events
private event EventHandler<DragStartedEventArgs>? _dragStarted;
private event EventHandler<DragUpdatedEventArgs>? _dragUpdated;
private event EventHandler<DropTargetChangedEventArgs>? _dropTargetChanged;
private event EventHandler<DragCompletedEventArgs>? _dragCompleted;
private event EventHandler<DragCancelledEventArgs>? _dragCancelled;
private event EventHandler<DragDropErrorEventArgs>? _errorOccurred;
#endregion
#region Properties
public bool IsDragActive => Volatile.Read(ref _currentDragOperation) != null;
public Models.DragInfo? CurrentDragInfo => _currentDragOperation?.DragInfo;
public Abstractions.IDropTarget? CurrentDropTarget => _currentDragOperation?.CurrentDropTarget;
public double DragStartThreshold { get; set; } = 3.0;
public bool EnableAsyncOperations { get; set; } = true;
public int AsyncOperationTimeout { get; set; } = 5000;
public event EventHandler<DragStartedEventArgs> DragStarted
{
add => _dragStarted += value;
remove => _dragStarted -= value;
}
public event EventHandler<DragUpdatedEventArgs> DragUpdated
{
add => _dragUpdated += value;
remove => _dragUpdated -= value;
}
public event EventHandler<DropTargetChangedEventArgs> DropTargetChanged
{
add => _dropTargetChanged += value;
remove => _dropTargetChanged -= value;
}
public event EventHandler<DragCompletedEventArgs> DragCompleted
{
add => _dragCompleted += value;
remove => _dragCompleted -= value;
}
public event EventHandler<DragCancelledEventArgs> DragCancelled
{
add => _dragCancelled += value;
remove => _dragCancelled -= value;
}
public event EventHandler<DragDropErrorEventArgs> ErrorOccurred
{
add => _errorOccurred += value;
remove => _errorOccurred -= value;
}
#endregion
#region Constructor
public DragDropService()
{
// Инициализация таймера очистки (каждые 5 минут)
_cleanupTimer = new Timer(CleanupExpiredTargets, null,
TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
}
#endregion
#region Registration Methods
public string RegisterDropTarget(Abstractions.IDropTarget target, Geometry.Rect bounds, int priority = 0, string? group = null)
{
ThrowIfDisposed();
if (target == null) throw new ArgumentNullException(nameof(target));
var id = Guid.NewGuid().ToString("N");
var info = new DropTargetInfo
{
Target = target,
Bounds = bounds,
Priority = priority,
Group = group,
Id = id
};
_dropTargetsLock.EnterWriteLock();
try
{
_dropTargets[id] = info;
return id;
}
finally
{
_dropTargetsLock.ExitWriteLock();
}
}
public bool UpdateDropTargetBounds(string id, Geometry.Rect bounds)
{
ThrowIfDisposed();
_dropTargetsLock.EnterUpgradeableReadLock();
try
{
if (!_dropTargets.TryGetValue(id, out var info))
return false;
_dropTargetsLock.EnterWriteLock();
try
{
info.Bounds = bounds;
info.LastAccessTime = DateTime.UtcNow;
return true;
}
finally
{
_dropTargetsLock.ExitWriteLock();
}
}
finally
{
_dropTargetsLock.ExitUpgradeableReadLock();
}
}
public bool UnregisterDropTarget(string id)
{
ThrowIfDisposed();
_dropTargetsLock.EnterWriteLock();
try
{
if (_dropTargets.Remove(id, out var info))
{
info.Dispose();
return true;
}
return false;
}
finally
{
_dropTargetsLock.ExitWriteLock();
}
}
public void UnregisterDropTargetsInGroup(string group)
{
ThrowIfDisposed();
if (string.IsNullOrEmpty(group)) return;
_dropTargetsLock.EnterWriteLock();
try
{
var idsToRemove = new List<string>();
foreach (var kvp in _dropTargets)
{
if (kvp.Value.Group == group)
{
idsToRemove.Add(kvp.Key);
}
}
foreach (var id in idsToRemove)
{
if (_dropTargets.Remove(id, out var info))
{
info.Dispose();
}
}
}
finally
{
_dropTargetsLock.ExitWriteLock();
}
}
#endregion
#region Async Operations
public async Task<bool> StartDragAsync(Abstractions.IDragSource source, Geometry.Point startPosition)
{
ThrowIfDisposed();
if (source == null) throw new ArgumentNullException(nameof(source));
lock (_dragOperationLock)
{
if (_currentDragOperation != null)
return false;
}
try
{
Interlocked.Increment(ref _totalDragOperations);
Models.DragInfo? dragInfo = null;
// Проверка возможности начала перетаскивания
if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource)
{
var result = await ExecuteWithTimeoutAsync(
asyncSource.CanStartDragAsync(),
"CanStartDragAsync",
source);
if (!result.CanStart || result.DragInfo == null)
return false;
dragInfo = result.DragInfo;
}
else
{
if (!source.CanStartDrag(out dragInfo) || dragInfo == null)
return false;
}
var updatedDragInfo = dragInfo.CloneWithPosition(startPosition);
// Начало перетаскивания
bool started;
if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource2)
{
started = await ExecuteWithTimeoutAsync(
asyncSource2.StartDragAsync(updatedDragInfo),
"StartDragAsync",
source);
}
else
{
started = source.StartDrag(updatedDragInfo);
}
if (!started)
{
updatedDragInfo.Dispose();
return false;
}
lock (_dragOperationLock)
{
_currentDragOperation = new DragOperationContext
{
Source = source,
DragInfo = updatedDragInfo,
CancellationTokenSource = new CancellationTokenSource()
};
}
// Вызов события
_dragStarted?.Invoke(this, new DragStartedEventArgs(updatedDragInfo, startPosition));
return true;
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "StartDragAsync", source);
return false;
}
}
public async Task UpdateDragAsync(Geometry.Point position)
{
ThrowIfDisposed();
DragOperationContext? context;
lock (_dragOperationLock)
{
context = _currentDragOperation;
}
if (context?.DragInfo == null || context.Source == null)
return;
try
{
// Проверка порога начала
if (!context.ThresholdExceeded && DragStartThreshold > 0)
{
var distance = CalculateDistance(context.DragInfo.StartPosition, position);
if (distance < DragStartThreshold)
return;
context.ThresholdExceeded = true;
}
var updatedDragInfo = context.DragInfo.CloneWithPosition(position);
context.DragInfo.Dispose();
context.DragInfo = updatedDragInfo;
// Поиск новой цели сброса
var newDropTarget = await FindDropTargetAsync(position, updatedDragInfo);
// Обработка смены цели
if (context.CurrentDropTarget != newDropTarget?.Target)
{
if (context.CurrentDropTarget != null)
{
await ExecuteTargetOperationAsync(
context.CurrentDropTarget,
t => t.DragLeaveAsync(),
t => t.DragLeave(),
"DragLeave");
}
context.CurrentDropTarget = newDropTarget?.Target;
if (newDropTarget != null)
{
newDropTarget.UsageCount++;
_dropTargetChanged?.Invoke(this, new DropTargetChangedEventArgs(
updatedDragInfo, newDropTarget.Target, newDropTarget.Bounds));
}
}
// Уведомление текущей цели
if (context.CurrentDropTarget != null)
{
var dropInfo = new Models.DropInfo(
updatedDragInfo.Data,
position,
updatedDragInfo.AllowedEffects,
context.CurrentDropTarget);
await ExecuteTargetOperationAsync(
context.CurrentDropTarget,
t => t.DragOverAsync(dropInfo),
t => t.DragOver(dropInfo),
"DragOver");
}
_dragUpdated?.Invoke(this, new DragUpdatedEventArgs(updatedDragInfo, position));
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "UpdateDragAsync", context);
}
}
public async Task<Enums.DragDropEffects> EndDragAsync(Geometry.Point position)
{
ThrowIfDisposed();
DragOperationContext? context;
lock (_dragOperationLock)
{
context = _currentDragOperation;
_currentDragOperation = null;
}
if (context == null || context.DragInfo == null || context.Source == null)
{
Reset();
return Enums.DragDropEffects.None;
}
try
{
var effects = Enums.DragDropEffects.None;
var operationTime = DateTime.UtcNow - context.StartTime;
Interlocked.Add(ref _totalOperationTicks, operationTime.Ticks);
// Выполнение сброса
if (context.CurrentDropTarget != null)
{
var dropInfo = new Models.DropInfo(
context.DragInfo.Data,
position,
context.DragInfo.AllowedEffects,
context.CurrentDropTarget);
await ExecuteTargetOperationAsync(
context.CurrentDropTarget,
t => t.DropAsync(dropInfo),
t => t.Drop(dropInfo),
"Drop");
if (dropInfo.Handled)
{
effects = dropInfo.SuggestedEffects;
Interlocked.Increment(ref _successfulDrops);
}
}
// Уведомление источника
await ExecuteSourceOperationAsync(
context.Source,
s => s.DragCompletedAsync(context.DragInfo, effects),
s => s.DragCompleted(context.DragInfo, effects),
"DragCompleted",
effects);
// Событие завершения
_dragCompleted?.Invoke(this, new DragCompletedEventArgs(
context.DragInfo, position, effects));
return effects;
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "EndDragAsync", context);
return Enums.DragDropEffects.None;
}
finally
{
context.Dispose();
}
}
public async Task CancelDragAsync()
{
ThrowIfDisposed();
DragOperationContext? context;
lock (_dragOperationLock)
{
context = _currentDragOperation;
_currentDragOperation = null;
}
if (context == null || context.DragInfo == null || context.Source == null)
{
Reset();
return;
}
try
{
context.CancellationTokenSource?.Cancel();
Interlocked.Increment(ref _cancelledOperations);
await ExecuteSourceOperationAsync(
context.Source,
s => s.DragCancelledAsync(context.DragInfo),
s => s.DragCancelled(context.DragInfo),
"DragCancelled");
_dragCancelled?.Invoke(this, new DragCancelledEventArgs(context.DragInfo));
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "CancelDragAsync", context);
}
finally
{
context.Dispose();
}
}
#endregion
#region Synchronous Operations (for compatibility)
public bool StartDrag(Abstractions.IDragSource source, Geometry.Point startPosition)
{
return Task.Run(() => StartDragAsync(source, startPosition)).GetAwaiter().GetResult();
}
public void UpdateDrag(Geometry.Point position)
{
Task.Run(() => UpdateDragAsync(position)).GetAwaiter().GetResult();
}
public Enums.DragDropEffects EndDrag(Geometry.Point position)
{
return Task.Run(() => EndDragAsync(position)).GetAwaiter().GetResult();
}
public void CancelDrag()
{
Task.Run(CancelDragAsync).GetAwaiter().GetResult();
}
#endregion
#region Utility Methods
public void ClearAllDropTargets()
{
ThrowIfDisposed();
_dropTargetsLock.EnterWriteLock();
try
{
foreach (var info in _dropTargets.Values)
{
info.Dispose();
}
_dropTargets.Clear();
}
finally
{
_dropTargetsLock.ExitWriteLock();
}
}
public DragDropStats GetStats()
{
return new DragDropStats
{
TotalDragOperations = _totalDragOperations,
SuccessfulDrops = _successfulDrops,
CancelledOperations = _cancelledOperations,
ErrorCount = _errorCount,
RegisteredTargets = _dropTargets.Count,
AverageOperationTime = _totalDragOperations > 0
? TimeSpan.FromTicks(_totalOperationTicks / _totalDragOperations)
: TimeSpan.Zero
};
}
#endregion
#region Private Helper Methods
private async Task<DropTargetInfo?> FindDropTargetAsync(Geometry.Point position, Models.DragInfo dragInfo)
{
DropTargetInfo? bestTarget = null;
_dropTargetsLock.EnterReadLock();
try
{
foreach (var info in _dropTargets.Values)
{
if (!info.Bounds.Contains(position))
continue;
var dropInfo = new Models.DropInfo(
dragInfo.Data,
position,
dragInfo.AllowedEffects,
info.Target);
bool canAccept;
if (EnableAsyncOperations && info.Target is Abstractions.IAsyncDropTarget asyncTarget)
{
canAccept = await ExecuteWithTimeoutAsync(
asyncTarget.CanAcceptDropAsync(dropInfo),
"CanAcceptDropAsync",
info.Target);
}
else
{
canAccept = info.Target.CanAcceptDrop(dropInfo);
}
if (canAccept)
{
info.LastAccessTime = DateTime.UtcNow;
if (bestTarget == null || info.Priority > bestTarget.Priority)
{
bestTarget = info;
}
}
}
}
finally
{
_dropTargetsLock.ExitReadLock();
}
return bestTarget;
}
private async Task ExecuteTargetOperationAsync(
Abstractions.IDropTarget target,
Func<Abstractions.IAsyncDropTarget, Task> asyncOperation,
Action<Abstractions.IDropTarget> syncOperation,
string operationName)
{
try
{
if (EnableAsyncOperations && target is Abstractions.IAsyncDropTarget asyncTarget)
{
await ExecuteWithTimeoutAsync(
asyncOperation(asyncTarget),
$"{operationName}Async",
target);
}
else
{
syncOperation(target);
}
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, operationName, target);
}
}
private async Task ExecuteSourceOperationAsync(
Abstractions.IDragSource source,
Func<Abstractions.IAsyncDragSource, Task> asyncOperation,
Action<Abstractions.IDragSource> syncOperation,
string operationName,
Enums.DragDropEffects effects = Enums.DragDropEffects.None)
{
try
{
if (EnableAsyncOperations && source is Abstractions.IAsyncDragSource asyncSource)
{
await ExecuteWithTimeoutAsync(
asyncOperation(asyncSource),
$"{operationName}Async",
source);
}
else
{
syncOperation(source);
}
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, operationName, source);
}
}
private async Task<T> ExecuteWithTimeoutAsync<T>(Task<T> task, string operationName, object? context = null)
{
if (AsyncOperationTimeout <= 0)
return await task;
var timeoutTask = Task.Delay(AsyncOperationTimeout);
var completedTask = await Task.WhenAny(task, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"{operationName} timed out after {AsyncOperationTimeout}ms");
}
return await task;
}
private async Task ExecuteWithTimeoutAsync(Task task, string operationName, object? context = null)
{
if (AsyncOperationTimeout <= 0)
{
await task;
return;
}
var timeoutTask = Task.Delay(AsyncOperationTimeout);
var completedTask = await Task.WhenAny(task, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException($"{operationName} timed out after {AsyncOperationTimeout}ms");
}
await task;
}
private double CalculateDistance(Geometry.Point p1, Geometry.Point p2)
{
var dx = p2.X - p1.X;
var dy = p2.Y - p1.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
private void Reset()
{
lock (_dragOperationLock)
{
_currentDragOperation?.Dispose();
_currentDragOperation = null;
}
}
private void CleanupExpiredTargets(object? state)
{
var expirationTime = DateTime.UtcNow.AddMinutes(-10); // Цели старше 10 минут
_dropTargetsLock.EnterWriteLock();
try
{
var idsToRemove = new List<string>();
foreach (var kvp in _dropTargets)
{
if (kvp.Value.LastAccessTime < expirationTime)
{
idsToRemove.Add(kvp.Key);
}
}
foreach (var id in idsToRemove)
{
if (_dropTargets.Remove(id, out var info))
{
info.Dispose();
}
}
}
finally
{
_dropTargetsLock.ExitWriteLock();
}
}
private void HandleError(Exception exception, string operation, object? context = null)
{
_errorOccurred?.Invoke(this, new DragDropErrorEventArgs(exception, operation, context));
}
private void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(GetType().Name);
}
#endregion
#region IDisposable Implementation
public void Dispose()
{
if (_disposed) return;
lock (_dragOperationLock)
{
if (_disposed) return;
_cleanupTimer?.Dispose();
_cleanupTimer = null;
if (_currentDragOperation != null)
{
_currentDragOperation.CancellationTokenSource?.Cancel();
_currentDragOperation.Dispose();
_currentDragOperation = null;
}
ClearAllDropTargets();
_dropTargetsLock.Dispose();
// Очистка событий
_dragStarted = null;
_dragUpdated = null;
_dropTargetChanged = null;
_dragCompleted = null;
_dragCancelled = null;
_errorOccurred = null;
_disposed = true;
}
GC.SuppressFinalize(this);
}
#endregion
}

View File

@@ -0,0 +1,22 @@
using Lattice.Core.DragDrop.Models;
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Аргументы события отмены перетаскивания.
/// </summary>
public class DragCancelledEventArgs : EventArgs
{
/// <summary>
/// Информация о перетаскивании.
/// </summary>
public DragInfo DragInfo { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragCancelledEventArgs"/>.
/// </summary>
public DragCancelledEventArgs(DragInfo dragInfo)
{
DragInfo = dragInfo;
}
}

View File

@@ -0,0 +1,35 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Аргументы события завершения перетаскивания.
/// </summary>
public class DragCompletedEventArgs : EventArgs
{
/// <summary>
/// Информация о перетаскивании.
/// </summary>
public DragInfo DragInfo { get; }
/// <summary>
/// Позиция завершения перетаскивания.
/// </summary>
public Point DropPosition { get; }
/// <summary>
/// Примененные эффекты перетаскивания.
/// </summary>
public Enums.DragDropEffects Effects { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragCompletedEventArgs"/>.
/// </summary>
public DragCompletedEventArgs(DragInfo dragInfo, Point dropPosition, Enums.DragDropEffects effects)
{
DragInfo = dragInfo;
DropPosition = dropPosition;
Effects = effects;
}
}

View File

@@ -0,0 +1,32 @@
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Аргументы события ошибки в операции перетаскивания.
/// </summary>
public class DragDropErrorEventArgs : EventArgs
{
/// <summary>
/// Ошибка, которая произошла.
/// </summary>
public Exception Exception { get; }
/// <summary>
/// Операция, во время которой произошла ошибка.
/// </summary>
public string Operation { get; }
/// <summary>
/// Контекст операции.
/// </summary>
public object? Context { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragDropErrorEventArgs"/>.
/// </summary>
public DragDropErrorEventArgs(Exception exception, string operation, object? context = null)
{
Exception = exception;
Operation = operation;
Context = context;
}
}

View File

@@ -0,0 +1,29 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Аргументы события начала перетаскивания.
/// </summary>
public class DragStartedEventArgs : EventArgs
{
/// <summary>
/// Информация о перетаскивании.
/// </summary>
public DragInfo DragInfo { get; }
/// <summary>
/// Начальная позиция перетаскивания.
/// </summary>
public Point StartPosition { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragStartedEventArgs"/>.
/// </summary>
public DragStartedEventArgs(DragInfo dragInfo, Point startPosition)
{
DragInfo = dragInfo;
StartPosition = startPosition;
}
}

View File

@@ -0,0 +1,29 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Аргументы события обновления перетаскивания.
/// </summary>
public class DragUpdatedEventArgs : EventArgs
{
/// <summary>
/// Информация о перетаскивании.
/// </summary>
public DragInfo DragInfo { get; }
/// <summary>
/// Текущая позиция перетаскивания.
/// </summary>
public Point Position { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragUpdatedEventArgs"/>.
/// </summary>
public DragUpdatedEventArgs(DragInfo dragInfo, Point position)
{
DragInfo = dragInfo;
Position = position;
}
}

View File

@@ -0,0 +1,35 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Аргументы события изменения цели сброса.
/// </summary>
public class DropTargetChangedEventArgs : EventArgs
{
/// <summary>
/// Информация о перетаскивании.
/// </summary>
public DragInfo DragInfo { get; }
/// <summary>
/// Новая цель сброса.
/// </summary>
public Abstractions.IDropTarget Target { get; }
/// <summary>
/// Границы цели.
/// </summary>
public Rect TargetBounds { get; }
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DropTargetChangedEventArgs"/>.
/// </summary>
public DropTargetChangedEventArgs(DragInfo dragInfo, Abstractions.IDropTarget target, Rect targetBounds)
{
DragInfo = dragInfo;
Target = target;
TargetBounds = targetBounds;
}
}

View File

@@ -0,0 +1,174 @@
namespace Lattice.Core.DragDrop.Services;
/// <summary>
/// Предоставляет централизованный сервис для управления операциями перетаскивания.
/// </summary>
public interface IDragDropService : IDisposable
{
#region Свойства
/// <summary>
/// Активна ли операция перетаскивания.
/// </summary>
bool IsDragActive { get; }
/// <summary>
/// Информация о текущей операции.
/// </summary>
Models.DragInfo? CurrentDragInfo { get; }
/// <summary>
/// Текущая цель сброса.
/// </summary>
Abstractions.IDropTarget? CurrentDropTarget { get; }
/// <summary>
/// Порог начала перетаскивания в пикселях.
/// </summary>
double DragStartThreshold { get; set; }
/// <summary>
/// Включены ли асинхронные операции.
/// </summary>
bool EnableAsyncOperations { get; set; }
/// <summary>
/// Максимальное время ожидания асинхронной операции (мс).
/// </summary>
int AsyncOperationTimeout { get; set; }
#endregion
#region События
/// <summary>
/// Событие начала операции перетаскивания.
/// </summary>
event EventHandler<DragStartedEventArgs> DragStarted;
/// <summary>
/// Событие обновления позиции перетаскивания.
/// </summary>
event EventHandler<DragUpdatedEventArgs> DragUpdated;
/// <summary>
/// Событие изменения цели сброса.
/// </summary>
event EventHandler<DropTargetChangedEventArgs> DropTargetChanged;
/// <summary>
/// Событие завершения операции перетаскивания.
/// </summary>
event EventHandler<DragCompletedEventArgs> DragCompleted;
/// <summary>
/// Событие отмены операции перетаскивания.
/// </summary>
event EventHandler<DragCancelledEventArgs> DragCancelled;
/// <summary>
/// Событие ошибки в операции перетаскивания.
/// </summary>
event EventHandler<DragDropErrorEventArgs> ErrorOccurred;
#endregion
#region Регистрация целей сброса
/// <summary>
/// Регистрирует цель сброса.
/// </summary>
string RegisterDropTarget(Abstractions.IDropTarget target, Geometry.Rect bounds, int priority = 0, string? group = null);
/// <summary>
/// Обновляет границы цели сброса.
/// </summary>
bool UpdateDropTargetBounds(string id, Geometry.Rect bounds);
/// <summary>
/// Отменяет регистрацию цели сброса.
/// </summary>
bool UnregisterDropTarget(string id);
/// <summary>
/// Отменяет регистрацию всех целей в группе.
/// </summary>
void UnregisterDropTargetsInGroup(string group);
#endregion
#region Асинхронные операции
/// <summary>
/// Начинает операцию перетаскивания (асинхронно).
/// </summary>
Task<bool> StartDragAsync(Abstractions.IDragSource source, Geometry.Point startPosition);
/// <summary>
/// Обновляет позицию перетаскивания (асинхронно).
/// </summary>
Task UpdateDragAsync(Geometry.Point position);
/// <summary>
/// Завершает операцию перетаскивания (асинхронно).
/// </summary>
Task<Enums.DragDropEffects> EndDragAsync(Geometry.Point position);
/// <summary>
/// Отменяет операцию перетаскивания (асинхронно).
/// </summary>
Task CancelDragAsync();
#endregion
#region Синхронные операции (для обратной совместимости)
/// <summary>
/// Начинает операцию перетаскивания (синхронно).
/// </summary>
bool StartDrag(Abstractions.IDragSource source, Geometry.Point startPosition);
/// <summary>
/// Обновляет позицию перетаскивания (синхронно).
/// </summary>
void UpdateDrag(Geometry.Point position);
/// <summary>
/// Завершает операцию перетаскивания (синхронно).
/// </summary>
Enums.DragDropEffects EndDrag(Geometry.Point position);
/// <summary>
/// Отменяет операцию перетаскивания (синхронно).
/// </summary>
void CancelDrag();
#endregion
#region Утилиты
/// <summary>
/// Очищает все зарегистрированные цели.
/// </summary>
void ClearAllDropTargets();
/// <summary>
/// Получает статистику использования.
/// </summary>
DragDropStats GetStats();
#endregion
}
/// <summary>
/// Статистика использования Drag & Drop.
/// </summary>
public class DragDropStats
{
public int TotalDragOperations { get; set; }
public int SuccessfulDrops { get; set; }
public int CancelledOperations { get; set; }
public int ErrorCount { get; set; }
public int RegisteredTargets { get; set; }
public TimeSpan AverageOperationTime { get; set; }
}

View File

@@ -0,0 +1,713 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Enums;
using Lattice.Core.DragDrop.Models;
namespace Lattice.Core.DragDrop.Utilities;
/// <summary>
/// Предоставляет утилитарные методы и фабричные методы для работы с системой перетаскивания с поддержкой async.
/// </summary>
public static class AsyncDragDropUtilities
{
/// <summary>
/// Создает асинхронную реализацию источника перетаскивания.
/// </summary>
public static IAsyncDragSource CreateAsyncDragSource(
Func<Task<object>> dataProviderAsync,
Func<Task<bool>>? canDragAsync = null,
Func<DragInfo, DragDropEffects, Task>? onCompletedAsync = null,
Func<DragInfo, Task>? onCancelledAsync = null)
{
return new AsyncDragSourceWrapper(dataProviderAsync, canDragAsync, onCompletedAsync, onCancelledAsync);
}
/// <summary>
/// Создает асинхронную реализацию цели сброса.
/// </summary>
public static IAsyncDropTarget CreateAsyncDropTarget(
Func<DropInfo, Task<bool>>? canAcceptAsync = null,
Func<DropInfo, Task>? onDragOverAsync = null,
Func<DropInfo, Task>? onDropAsync = null,
Func<Task>? onDragLeaveAsync = null)
{
return new AsyncDropTargetWrapper(canAcceptAsync, onDragOverAsync, onDropAsync, onDragLeaveAsync);
}
/// <summary>
/// Создает адаптер для преобразования синхронного источника в асинхронный.
/// </summary>
public static IAsyncDragSource CreateAsyncAdapter(IDragSource syncSource)
{
return new SyncToAsyncDragSourceAdapter(syncSource);
}
/// <summary>
/// Создает адаптер для преобразования синхронной цели в асинхронную.
/// </summary>
public static IAsyncDropTarget CreateAsyncAdapter(IDropTarget syncTarget)
{
return new SyncToAsyncDropTargetAdapter(syncTarget);
}
#region Обертки-реализации
/// <summary>
/// Обертка для создания асинхронного источника перетаскивания.
/// </summary>
private sealed class AsyncDragSourceWrapper : IAsyncDragSource
{
private readonly Func<Task<object>> _dataProviderAsync;
private readonly Func<Task<bool>>? _canDragAsync;
private readonly Func<DragInfo, DragDropEffects, Task>? _onCompletedAsync;
private readonly Func<DragInfo, Task>? _onCancelledAsync;
public AsyncDragSourceWrapper(
Func<Task<object>> dataProviderAsync,
Func<Task<bool>>? canDragAsync = null,
Func<DragInfo, DragDropEffects, Task>? onCompletedAsync = null,
Func<DragInfo, Task>? onCancelledAsync = null)
{
_dataProviderAsync = dataProviderAsync ?? throw new ArgumentNullException(nameof(dataProviderAsync));
_canDragAsync = canDragAsync;
_onCompletedAsync = onCompletedAsync;
_onCancelledAsync = onCancelledAsync;
}
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
{
try
{
// Проверяем, может ли начаться перетаскивание
if (_canDragAsync != null)
{
var canDrag = await _canDragAsync().ConfigureAwait(false);
if (!canDrag)
return (false, null);
}
// Получаем данные
var data = await _dataProviderAsync().ConfigureAwait(false);
if (data == null)
return (false, null);
// Создаем информацию о перетаскивании
var dragInfo = DragDropUtilities.CreateDragInfo(
data,
Geometry.Point.Zero,
DragDropEffects.Copy | DragDropEffects.Move,
this);
return (true, dragInfo);
}
catch
{
return (false, null);
}
}
public Task<bool> StartDragAsync(DragInfo dragInfo)
{
// Базовая реализация всегда разрешает начало
return Task.FromResult(true);
}
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
{
if (_onCompletedAsync != null)
{
await _onCompletedAsync(dragInfo, effects).ConfigureAwait(false);
}
}
public async Task DragCancelledAsync(DragInfo dragInfo)
{
if (_onCancelledAsync != null)
{
await _onCancelledAsync(dragInfo).ConfigureAwait(false);
}
}
#region Синхронная реализация (для IDragSource)
public bool CanStartDrag(out DragInfo? dragInfo)
{
// Для синхронного вызова используем Task.Result
var result = Task.Run(() => CanStartDragAsync()).GetAwaiter().GetResult();
dragInfo = result.DragInfo;
return result.CanStart;
}
public bool StartDrag(DragInfo dragInfo)
{
return Task.Run(() => StartDragAsync(dragInfo)).GetAwaiter().GetResult();
}
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
{
Task.Run(() => DragCompletedAsync(dragInfo, effects)).Wait();
}
public void DragCancelled(DragInfo dragInfo)
{
Task.Run(() => DragCancelledAsync(dragInfo)).Wait();
}
#endregion
}
/// <summary>
/// Обертка для создания асинхронной цели сброса.
/// </summary>
private sealed class AsyncDropTargetWrapper : IAsyncDropTarget
{
private readonly Func<DropInfo, Task<bool>>? _canAcceptAsync;
private readonly Func<DropInfo, Task>? _onDragOverAsync;
private readonly Func<DropInfo, Task>? _onDropAsync;
private readonly Func<Task>? _onDragLeaveAsync;
public AsyncDropTargetWrapper(
Func<DropInfo, Task<bool>>? canAcceptAsync = null,
Func<DropInfo, Task>? onDragOverAsync = null,
Func<DropInfo, Task>? onDropAsync = null,
Func<Task>? onDragLeaveAsync = null)
{
_canAcceptAsync = canAcceptAsync;
_onDragOverAsync = onDragOverAsync;
_onDropAsync = onDropAsync;
_onDragLeaveAsync = onDragLeaveAsync;
}
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
{
try
{
if (_canAcceptAsync != null)
{
return await _canAcceptAsync(dropInfo).ConfigureAwait(false);
}
return true; // По умолчанию принимаем все
}
catch
{
return false; // При ошибке не принимаем
}
}
public async Task DragOverAsync(DropInfo dropInfo)
{
try
{
if (_onDragOverAsync != null)
{
await _onDragOverAsync(dropInfo).ConfigureAwait(false);
}
}
catch
{
// Игнорируем ошибки в обработчике
}
}
public async Task DropAsync(DropInfo dropInfo)
{
try
{
if (_onDropAsync != null)
{
await _onDropAsync(dropInfo).ConfigureAwait(false);
}
}
catch
{
// Игнорируем ошибки в обработчике
}
}
public async Task DragLeaveAsync()
{
try
{
if (_onDragLeaveAsync != null)
{
await _onDragLeaveAsync().ConfigureAwait(false);
}
}
catch
{
// Игнорируем ошибки в обработчике
}
}
#region Синхронная реализация (для IDropTarget)
public bool CanAcceptDrop(DropInfo dropInfo)
{
return Task.Run(() => CanAcceptDropAsync(dropInfo)).GetAwaiter().GetResult();
}
public void DragOver(DropInfo dropInfo)
{
Task.Run(() => DragOverAsync(dropInfo)).Wait();
}
public void Drop(DropInfo dropInfo)
{
Task.Run(() => DropAsync(dropInfo)).Wait();
}
public void DragLeave()
{
Task.Run(DragLeaveAsync).Wait();
}
#endregion
}
/// <summary>
/// Адаптер для преобразования синхронного источника в асинхронный.
/// </summary>
private sealed class SyncToAsyncDragSourceAdapter : IAsyncDragSource
{
private readonly IDragSource _syncSource;
public SyncToAsyncDragSourceAdapter(IDragSource syncSource)
{
_syncSource = syncSource ?? throw new ArgumentNullException(nameof(syncSource));
}
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
{
return await Task.Run(() =>
{
var canStart = _syncSource.CanStartDrag(out var dragInfo);
return (canStart, dragInfo);
}).ConfigureAwait(false);
}
public async Task<bool> StartDragAsync(DragInfo dragInfo)
{
return await Task.Run(() => _syncSource.StartDrag(dragInfo)).ConfigureAwait(false);
}
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
{
await Task.Run(() => _syncSource.DragCompleted(dragInfo, effects)).ConfigureAwait(false);
}
public async Task DragCancelledAsync(DragInfo dragInfo)
{
await Task.Run(() => _syncSource.DragCancelled(dragInfo)).ConfigureAwait(false);
}
#region Синхронная реализация (делегируем синхронному источнику)
public bool CanStartDrag(out DragInfo? dragInfo)
{
return _syncSource.CanStartDrag(out dragInfo);
}
public bool StartDrag(DragInfo dragInfo)
{
return _syncSource.StartDrag(dragInfo);
}
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
{
_syncSource.DragCompleted(dragInfo, effects);
}
public void DragCancelled(DragInfo dragInfo)
{
_syncSource.DragCancelled(dragInfo);
}
#endregion
}
/// <summary>
/// Адаптер для преобразования синхронной цели в асинхронную.
/// </summary>
private sealed class SyncToAsyncDropTargetAdapter : IAsyncDropTarget
{
private readonly IDropTarget _syncTarget;
public SyncToAsyncDropTargetAdapter(IDropTarget syncTarget)
{
_syncTarget = syncTarget ?? throw new ArgumentNullException(nameof(syncTarget));
}
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
{
return await Task.Run(() => _syncTarget.CanAcceptDrop(dropInfo)).ConfigureAwait(false);
}
public async Task DragOverAsync(DropInfo dropInfo)
{
await Task.Run(() => _syncTarget.DragOver(dropInfo)).ConfigureAwait(false);
}
public async Task DropAsync(DropInfo dropInfo)
{
await Task.Run(() => _syncTarget.Drop(dropInfo)).ConfigureAwait(false);
}
public async Task DragLeaveAsync()
{
await Task.Run(() => _syncTarget.DragLeave()).ConfigureAwait(false);
}
#region Синхронная реализация (делегируем синхронной цели)
public bool CanAcceptDrop(DropInfo dropInfo)
{
return _syncTarget.CanAcceptDrop(dropInfo);
}
public void DragOver(DropInfo dropInfo)
{
_syncTarget.DragOver(dropInfo);
}
public void Drop(DropInfo dropInfo)
{
_syncTarget.Drop(dropInfo);
}
public void DragLeave()
{
_syncTarget.DragLeave();
}
#endregion
}
#endregion
#region Утилитарные методы
/// <summary>
/// Выполняет асинхронную операцию с таймаутом.
/// </summary>
public static async Task<T> ExecuteWithTimeoutAsync<T>(
Task<T> task,
TimeSpan timeout,
T defaultValue = default!)
{
if (timeout <= TimeSpan.Zero)
return await task.ConfigureAwait(false);
var delayTask = Task.Delay(timeout);
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
if (completedTask == delayTask)
{
return defaultValue;
}
return await task.ConfigureAwait(false);
}
/// <summary>
/// Выполняет асинхронную операцию с таймаутом.
/// </summary>
public static async Task<bool> ExecuteWithTimeoutAsync(
Task task,
TimeSpan timeout)
{
if (timeout <= TimeSpan.Zero)
{
await task.ConfigureAwait(false);
return true;
}
var delayTask = Task.Delay(timeout);
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
return completedTask == task;
}
/// <summary>
/// Выполняет асинхронную операцию с таймаутом и обработкой ошибок.
/// </summary>
public static async Task<T?> ExecuteSafeWithTimeoutAsync<T>(
Task<T> task,
TimeSpan timeout,
Func<Exception, T?> errorHandler = null) where T : class
{
try
{
if (timeout <= TimeSpan.Zero)
return await task.ConfigureAwait(false);
var delayTask = Task.Delay(timeout);
var completedTask = await Task.WhenAny(task, delayTask).ConfigureAwait(false);
if (completedTask == delayTask)
{
return default;
}
return await task.ConfigureAwait(false);
}
catch (Exception ex)
{
return errorHandler?.Invoke(ex) ?? default;
}
}
/// <summary>
/// Создает комбинированный источник из синхронного и асинхронного.
/// </summary>
public static IAsyncDragSource Combine(
IDragSource syncSource,
IAsyncDragSource asyncSource,
bool preferAsync = true)
{
return new CombinedDragSource(syncSource, asyncSource, preferAsync);
}
/// <summary>
/// Создает комбинированную цель из синхронной и асинхронной.
/// </summary>
public static IAsyncDropTarget Combine(
IDropTarget syncTarget,
IAsyncDropTarget asyncTarget,
bool preferAsync = true)
{
return new CombinedDropTarget(syncTarget, asyncTarget, preferAsync);
}
#endregion
#region Комбинированные реализации
/// <summary>
/// Комбинированный источник, поддерживающий как синхронный, так и асинхронный API.
/// </summary>
private sealed class CombinedDragSource : IAsyncDragSource
{
private readonly IDragSource _syncSource;
private readonly IAsyncDragSource _asyncSource;
private readonly bool _preferAsync;
public CombinedDragSource(IDragSource syncSource, IAsyncDragSource asyncSource, bool preferAsync)
{
_syncSource = syncSource ?? throw new ArgumentNullException(nameof(syncSource));
_asyncSource = asyncSource ?? throw new ArgumentNullException(nameof(asyncSource));
_preferAsync = preferAsync;
}
public async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
{
if (_preferAsync)
{
try
{
return await _asyncSource.CanStartDragAsync().ConfigureAwait(false);
}
catch
{
// В случае ошибки пробуем синхронную версию
}
}
// Используем синхронную версию в отдельной задаче
return await Task.Run(() =>
{
var canStart = _syncSource.CanStartDrag(out var dragInfo);
return (canStart, dragInfo);
}).ConfigureAwait(false);
}
public async Task<bool> StartDragAsync(DragInfo dragInfo)
{
if (_preferAsync)
{
try
{
return await _asyncSource.StartDragAsync(dragInfo).ConfigureAwait(false);
}
catch
{
// В случае ошибки пробуем синхронную версию
}
}
return await Task.Run(() => _syncSource.StartDrag(dragInfo)).ConfigureAwait(false);
}
public async Task DragCompletedAsync(DragInfo dragInfo, DragDropEffects effects)
{
if (_preferAsync)
{
try
{
await _asyncSource.DragCompletedAsync(dragInfo, effects).ConfigureAwait(false);
return;
}
catch
{
// В случае ошибки пробуем синхронную версию
}
}
await Task.Run(() => _syncSource.DragCompleted(dragInfo, effects)).ConfigureAwait(false);
}
public async Task DragCancelledAsync(DragInfo dragInfo)
{
if (_preferAsync)
{
try
{
await _asyncSource.DragCancelledAsync(dragInfo).ConfigureAwait(false);
return;
}
catch
{
// В случае ошибки пробуем синхронную версию
}
}
await Task.Run(() => _syncSource.DragCancelled(dragInfo)).ConfigureAwait(false);
}
#region Синхронная реализация
public bool CanStartDrag(out DragInfo? dragInfo)
{
return _syncSource.CanStartDrag(out dragInfo);
}
public bool StartDrag(DragInfo dragInfo)
{
return _syncSource.StartDrag(dragInfo);
}
public void DragCompleted(DragInfo dragInfo, DragDropEffects effects)
{
_syncSource.DragCompleted(dragInfo, effects);
}
public void DragCancelled(DragInfo dragInfo)
{
_syncSource.DragCancelled(dragInfo);
}
#endregion
}
/// <summary>
/// Комбинированная цель, поддерживающая как синхронный, так и асинхронный API.
/// </summary>
private sealed class CombinedDropTarget : IAsyncDropTarget
{
private readonly IDropTarget _syncTarget;
private readonly IAsyncDropTarget _asyncTarget;
private readonly bool _preferAsync;
public CombinedDropTarget(IDropTarget syncTarget, IAsyncDropTarget asyncTarget, bool preferAsync)
{
_syncTarget = syncTarget ?? throw new ArgumentNullException(nameof(syncTarget));
_asyncTarget = asyncTarget ?? throw new ArgumentNullException(nameof(asyncTarget));
_preferAsync = preferAsync;
}
public async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
{
if (_preferAsync)
{
try
{
return await _asyncTarget.CanAcceptDropAsync(dropInfo).ConfigureAwait(false);
}
catch
{
// В случае ошибки пробуем синхронную версию
}
}
return await Task.Run(() => _syncTarget.CanAcceptDrop(dropInfo)).ConfigureAwait(false);
}
public async Task DragOverAsync(DropInfo dropInfo)
{
if (_preferAsync)
{
try
{
await _asyncTarget.DragOverAsync(dropInfo).ConfigureAwait(false);
return;
}
catch
{
// В случае ошибки пробуем синхронную версию
}
}
await Task.Run(() => _syncTarget.DragOver(dropInfo)).ConfigureAwait(false);
}
public async Task DropAsync(DropInfo dropInfo)
{
if (_preferAsync)
{
try
{
await _asyncTarget.DropAsync(dropInfo).ConfigureAwait(false);
return;
}
catch
{
// В случае ошибки пробуем синхронную версию
}
}
await Task.Run(() => _syncTarget.Drop(dropInfo)).ConfigureAwait(false);
}
public async Task DragLeaveAsync()
{
if (_preferAsync)
{
try
{
await _asyncTarget.DragLeaveAsync().ConfigureAwait(false);
return;
}
catch
{
// В случае ошибки пробуем синхронную версию
}
}
await Task.Run(() => _syncTarget.DragLeave()).ConfigureAwait(false);
}
#region Синхронная реализация
public bool CanAcceptDrop(DropInfo dropInfo)
{
return _syncTarget.CanAcceptDrop(dropInfo);
}
public void DragOver(DropInfo dropInfo)
{
_syncTarget.DragOver(dropInfo);
}
public void Drop(DropInfo dropInfo)
{
_syncTarget.Drop(dropInfo);
}
public void DragLeave()
{
_syncTarget.DragLeave();
}
#endregion
}
#endregion
}

View File

@@ -0,0 +1,275 @@
namespace Lattice.Core.DragDrop.Utilities;
/// <summary>
/// Утилиты для работы с системой перетаскивания.
/// </summary>
public static class DragDropUtilities
{
#region Effect Utilities
/// <summary>
/// Проверяет, совместимы ли эффекты источника и цели.
/// </summary>
public static bool AreEffectsCompatible(Enums.DragDropEffects sourceEffects, Enums.DragDropEffects targetEffects)
{
if (sourceEffects == Enums.DragDropEffects.None || targetEffects == Enums.DragDropEffects.None)
return false;
return (sourceEffects & targetEffects) != Enums.DragDropEffects.None;
}
/// <summary>
/// Получает наиболее подходящий эффект на основе доступных.
/// </summary>
public static Enums.DragDropEffects GetBestEffect(Enums.DragDropEffects available, Enums.DragDropEffects preferred)
{
if ((available & preferred) != Enums.DragDropEffects.None)
return available & preferred;
if ((available & Enums.DragDropEffects.Move) != Enums.DragDropEffects.None)
return Enums.DragDropEffects.Move;
if ((available & Enums.DragDropEffects.Copy) != Enums.DragDropEffects.None)
return Enums.DragDropEffects.Copy;
if ((available & Enums.DragDropEffects.Link) != Enums.DragDropEffects.None)
return Enums.DragDropEffects.Link;
return Enums.DragDropEffects.None;
}
#endregion
#region Geometry Utilities
/// <summary>
/// Вычисляет расстояние между двумя точками.
/// </summary>
public static double CalculateDistance(Geometry.Point p1, Geometry.Point p2)
{
var dx = p2.X - p1.X;
var dy = p2.Y - p1.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
/// <summary>
/// Проверяет, превысило ли перемещение пороговое значение.
/// </summary>
public static bool HasExceededDragThreshold(Geometry.Point startPoint, Geometry.Point currentPoint, double threshold)
{
var distance = CalculateDistance(startPoint, currentPoint);
return distance >= threshold;
}
/// <summary>
/// Определяет позицию сброса относительно прямоугольника.
/// </summary>
public static Enums.DropPosition GetDropPosition(Geometry.Point point, Geometry.Rect bounds, double edgeThreshold = 20.0)
{
if (!bounds.Contains(new Geometry.Point(point.X, point.Y)))
return Enums.DropPosition.Inside;
var relativeX = (point.X - bounds.X) / bounds.Width;
var relativeY = (point.Y - bounds.Y) / bounds.Height;
if (relativeX < edgeThreshold / bounds.Width)
return Enums.DropPosition.Left;
if (relativeX > 1 - edgeThreshold / bounds.Width)
return Enums.DropPosition.Right;
if (relativeY < edgeThreshold / bounds.Height)
return Enums.DropPosition.Top;
if (relativeY > 1 - edgeThreshold / bounds.Height)
return Enums.DropPosition.Bottom;
return Enums.DropPosition.Center;
}
#endregion
#region Factory Methods
/// <summary>
/// Создает информацию о перетаскивании.
/// </summary>
public static Models.DragInfo CreateDragInfo(
object data,
Geometry.Point startPosition,
Enums.DragDropEffects allowedEffects = Enums.DragDropEffects.Copy | Enums.DragDropEffects.Move,
object? source = null,
Dictionary<string, object>? parameters = null)
{
var dragInfo = new Models.DragInfo(data, allowedEffects, startPosition, source);
if (parameters != null)
{
foreach (var param in parameters)
{
dragInfo.SetParameter(param.Key, param.Value);
}
}
return dragInfo;
}
/// <summary>
/// Создает простую реализацию источника перетаскивания.
/// </summary>
public static Abstractions.IDragSource CreateSimpleDragSource(
Func<object> dataProvider,
Func<bool>? canDrag = null,
Action<Models.DragInfo, Enums.DragDropEffects>? onCompleted = null,
Action<Models.DragInfo>? onCancelled = null)
{
return new SimpleDragSource(dataProvider, canDrag, onCompleted, onCancelled);
}
/// <summary>
/// Создает простую реализацию цели сброса.
/// </summary>
public static Abstractions.IDropTarget CreateSimpleDropTarget(
Func<Models.DropInfo, bool>? canAccept = null,
Action<Models.DropInfo>? onDragOver = null,
Action<Models.DropInfo>? onDrop = null,
Action? onDragLeave = null)
{
return new SimpleDropTarget(canAccept, onDragOver, onDrop, onDragLeave);
}
#endregion
#region Data Extraction
/// <summary>
/// Извлекает данные из элемента с поддержкой различных паттернов.
/// </summary>
public static object? ExtractData(object? element)
{
if (element == null)
return null;
// Проверяем, реализует ли элемент специальный интерфейс
if (element is Abstractions.IDragSource dragSource)
{
if (dragSource.CanStartDrag(out var dragInfo) && dragInfo != null)
return dragInfo.Data;
}
// В реальной реализации здесь будет рефлексия для проверки свойств
// DataContext, Content и т.д.
// Возвращаем сам элемент как данные
return element;
}
/// <summary>
/// Проверяет, совместимы ли данные с указанными типами.
/// </summary>
public static bool IsDataCompatible(object? data, IEnumerable<Type>? acceptedTypes)
{
if (data == null || acceptedTypes == null)
return false;
var dataType = data.GetType();
foreach (var acceptedType in acceptedTypes)
{
if (acceptedType.IsAssignableFrom(dataType))
return true;
}
return false;
}
#endregion
#region Helper Classes
private sealed class SimpleDragSource : Abstractions.IDragSource
{
private readonly Func<object> _dataProvider;
private readonly Func<bool>? _canDrag;
private readonly Action<Models.DragInfo, Enums.DragDropEffects>? _onCompleted;
private readonly Action<Models.DragInfo>? _onCancelled;
public SimpleDragSource(
Func<object> dataProvider,
Func<bool>? canDrag = null,
Action<Models.DragInfo, Enums.DragDropEffects>? onCompleted = null,
Action<Models.DragInfo>? onCancelled = null)
{
_dataProvider = dataProvider ?? throw new ArgumentNullException(nameof(dataProvider));
_canDrag = canDrag;
_onCompleted = onCompleted;
_onCancelled = onCancelled;
}
public bool CanStartDrag(out Models.DragInfo? dragInfo)
{
dragInfo = null;
if (_canDrag?.Invoke() == false)
return false;
var data = _dataProvider();
if (data == null)
return false;
dragInfo = CreateDragInfo(data, Geometry.Point.Zero, Enums.DragDropEffects.Copy | Enums.DragDropEffects.Move, this);
return true;
}
public bool StartDrag(Models.DragInfo dragInfo) => true;
public void DragCompleted(Models.DragInfo dragInfo, Enums.DragDropEffects effects)
{
_onCompleted?.Invoke(dragInfo, effects);
}
public void DragCancelled(Models.DragInfo dragInfo)
{
_onCancelled?.Invoke(dragInfo);
}
}
private sealed class SimpleDropTarget : Abstractions.IDropTarget
{
private readonly Func<Models.DropInfo, bool>? _canAccept;
private readonly Action<Models.DropInfo>? _onDragOver;
private readonly Action<Models.DropInfo>? _onDrop;
private readonly Action? _onDragLeave;
public SimpleDropTarget(
Func<Models.DropInfo, bool>? canAccept = null,
Action<Models.DropInfo>? onDragOver = null,
Action<Models.DropInfo>? onDrop = null,
Action? onDragLeave = null)
{
_canAccept = canAccept;
_onDragOver = onDragOver;
_onDrop = onDrop;
_onDragLeave = onDragLeave;
}
public bool CanAcceptDrop(Models.DropInfo dropInfo)
{
return _canAccept?.Invoke(dropInfo) ?? true;
}
public void DragOver(Models.DropInfo dropInfo)
{
_onDragOver?.Invoke(dropInfo);
}
public void Drop(Models.DropInfo dropInfo)
{
_onDrop?.Invoke(dropInfo);
}
public void DragLeave()
{
_onDragLeave?.Invoke();
}
}
#endregion
}