DragAndDrop core
This commit is contained in:
28
Lattice.Core.DragDrop/Abstractions/IAsyncDragSource.cs
Normal file
28
Lattice.Core.DragDrop/Abstractions/IAsyncDragSource.cs
Normal 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);
|
||||
}
|
||||
28
Lattice.Core.DragDrop/Abstractions/IAsyncDropTarget.cs
Normal file
28
Lattice.Core.DragDrop/Abstractions/IAsyncDropTarget.cs
Normal 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();
|
||||
}
|
||||
64
Lattice.Core.DragDrop/Abstractions/IDragSource.cs
Normal file
64
Lattice.Core.DragDrop/Abstractions/IDragSource.cs
Normal 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);
|
||||
}
|
||||
55
Lattice.Core.DragDrop/Abstractions/IDropTarget.cs
Normal file
55
Lattice.Core.DragDrop/Abstractions/IDropTarget.cs
Normal 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();
|
||||
}
|
||||
102
Lattice.Core.DragDrop/Enums/DragDropEffects.cs
Normal file
102
Lattice.Core.DragDrop/Enums/DragDropEffects.cs
Normal 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; // По умолчанию
|
||||
}
|
||||
}
|
||||
14
Lattice.Core.DragDrop/Enums/DropPosition.cs
Normal file
14
Lattice.Core.DragDrop/Enums/DropPosition.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
namespace Lattice.Core.DragDrop.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// Позиция сброса относительно цели.
|
||||
/// </summary>
|
||||
public enum DropPosition
|
||||
{
|
||||
Inside,
|
||||
Top,
|
||||
Bottom,
|
||||
Left,
|
||||
Right,
|
||||
Center
|
||||
}
|
||||
85
Lattice.Core.DragDrop/Exceptions/DragDropException.cs
Normal file
85
Lattice.Core.DragDrop/Exceptions/DragDropException.cs
Normal 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";
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
20
Lattice.Core.DragDrop/Lattice.Core.DragDrop.csproj
Normal file
20
Lattice.Core.DragDrop/Lattice.Core.DragDrop.csproj
Normal 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>
|
||||
227
Lattice.Core.DragDrop/Models/DragInfo.cs
Normal file
227
Lattice.Core.DragDrop/Models/DragInfo.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
269
Lattice.Core.DragDrop/Models/DropInfo.cs
Normal file
269
Lattice.Core.DragDrop/Models/DropInfo.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
832
Lattice.Core.DragDrop/README.md
Normal file
832
Lattice.Core.DragDrop/README.md
Normal 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. **Инструменты разработчика** (дебаггер, профилировщик)
|
||||
829
Lattice.Core.DragDrop/Services/DragDropService.cs
Normal file
829
Lattice.Core.DragDrop/Services/DragDropService.cs
Normal 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
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
174
Lattice.Core.DragDrop/Services/IDragDropService.cs
Normal file
174
Lattice.Core.DragDrop/Services/IDragDropService.cs
Normal 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; }
|
||||
}
|
||||
713
Lattice.Core.DragDrop/Utilities/AsyncDragDropUtilities.cs
Normal file
713
Lattice.Core.DragDrop/Utilities/AsyncDragDropUtilities.cs
Normal 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
|
||||
}
|
||||
275
Lattice.Core.DragDrop/Utilities/DragDropUtilities.cs
Normal file
275
Lattice.Core.DragDrop/Utilities/DragDropUtilities.cs
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user