using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Constants;
using Lattice.Core.DragDrop.Enums;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Services;
///
/// Центральный сервис управления операциями перетаскивания.
///
///
///
/// является основным компонентом системы drag-and-drop,
/// который координирует взаимодействие между источниками данных ()
/// и целями сброса ().
///
///
/// Основные функции сервиса:
///
///
/// - Регистрация и управление целями сброса
/// - Оркестрация жизненного цикла операций перетаскивания
/// - Обработка событий мыши и клавиатуры
/// - Распространение информации между компонентами
/// - Обеспечение потокобезопасности операций
///
///
/// Сервис поддерживает полностью асинхронную модель работы, уведомления через события
/// и статистику использования. Все операции защищены от параллельного доступа
/// и обеспечивают корректную очистку ресурсов.
///
///
/// Для использования сервиса необходимо:
///
/// - Зарегистрировать цели сброса с помощью
/// - Вызывать методы , ,
/// в ответ на действия пользователя
/// - Подписаться на события для отслеживания состояния операций
///
///
///
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 Point LastPosition { get; set; }
public void Dispose()
{
DragInfo?.Dispose();
CancellationTokenSource?.Dispose();
}
}
#endregion
#region Fields
private readonly Dictionary _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? _dragStarted;
private event EventHandler? _dragUpdated;
private event EventHandler? _dropTargetChanged;
private event EventHandler? _dragCompleted;
private event EventHandler? _dragCancelled;
private event EventHandler? _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; } = DragDropConstants.DefaultDragThreshold;
///
public bool EnableAsyncOperations { get; set; } = true;
///
public int AsyncOperationTimeout { get; set; } = DragDropConstants.DefaultAsyncTimeout;
///
public event EventHandler DragStarted
{
add => _dragStarted += value;
remove => _dragStarted -= value;
}
///
public event EventHandler DragUpdated
{
add => _dragUpdated += value;
remove => _dragUpdated -= value;
}
///
public event EventHandler DropTargetChanged
{
add => _dropTargetChanged += value;
remove => _dropTargetChanged -= value;
}
///
public event EventHandler DragCompleted
{
add => _dragCompleted += value;
remove => _dragCompleted -= value;
}
///
public event EventHandler DragCancelled
{
add => _dragCancelled += value;
remove => _dragCancelled -= value;
}
///
public event EventHandler ErrorOccurred
{
add => _errorOccurred += value;
remove => _errorOccurred -= value;
}
#endregion
#region Constructor
///
/// Инициализирует новый экземпляр класса .
///
///
/// Создает сервис с настройками по умолчанию:
///
/// - Порог начала перетаскивания: пикселей
/// - Таймаут асинхронных операций: миллисекунд
/// - Включены асинхронные операции: true
///
///
public DragDropService()
{
// Инициализация таймера очистки (каждые 5 минут)
_cleanupTimer = new Timer(CleanupExpiredTargets, null,
TimeSpan.FromMinutes(Constants.DragDropConstants.TargetLifetimeMinutes / 2),
TimeSpan.FromMinutes(Constants.DragDropConstants.TargetLifetimeMinutes / 2));
}
#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();
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 StartDragAsync(IDragSource source, Point startPosition)
{
ThrowIfDisposed();
if (source == null) throw new ArgumentNullException(nameof(source));
lock (_dragOperationLock)
{
if (_currentDragOperation != null)
return false;
}
try
{
Interlocked.Increment(ref _totalDragOperations);
DragInfo? dragInfo;
// Пытаемся начать перетаскивание
if (EnableAsyncOperations)
{
dragInfo = await ExecuteWithTimeoutAsync(
source.TryStartDragAsync(startPosition),
"TryStartDragAsync",
source);
}
else
{
dragInfo = await source.TryStartDragAsync(startPosition);
}
if (dragInfo == null)
return false;
var updatedDragInfo = dragInfo.CloneWithPosition(startPosition);
lock (_dragOperationLock)
{
_currentDragOperation = new DragOperationContext
{
Source = source,
DragInfo = updatedDragInfo,
CancellationTokenSource = new CancellationTokenSource(),
LastPosition = startPosition
};
}
// Вызов события
_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(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;
context.LastPosition = position;
// Поиск новой цели сброса
var newDropTarget = await FindDropTargetAsync(position, updatedDragInfo);
// Обработка смены цели
if (context.CurrentDropTarget != newDropTarget?.Target)
{
if (context.CurrentDropTarget != null)
{
await ExecuteTargetOperationAsync(
context.CurrentDropTarget,
t => t.OnDragLeaveAsync(),
"OnDragLeave");
}
context.CurrentDropTarget = newDropTarget?.Target;
if (newDropTarget != null)
{
newDropTarget.UsageCount++;
_dropTargetChanged?.Invoke(this, new DropTargetChangedEventArgs(
updatedDragInfo, position, newDropTarget.Target, newDropTarget.Bounds));
}
}
// Уведомление текущей цели
if (context.CurrentDropTarget != null)
{
var dropInfo = new DropInfo(
updatedDragInfo.Data,
position,
updatedDragInfo.AllowedEffects,
context.CurrentDropTarget);
await ExecuteTargetOperationAsync(
context.CurrentDropTarget,
t => t.OnDragOverAsync(dropInfo),
"OnDragOver");
}
_dragUpdated?.Invoke(this, new DragUpdatedEventArgs(updatedDragInfo, position));
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "UpdateDragAsync", context);
}
}
///
public async Task EndDragAsync(Point position)
{
ThrowIfDisposed();
DragOperationContext? context;
lock (_dragOperationLock)
{
context = _currentDragOperation;
_currentDragOperation = null;
}
if (context == null || context.DragInfo == null || context.Source == null)
{
Reset();
return DragDropEffects.None;
}
try
{
var effects = DragDropEffects.None;
var operationTime = DateTime.UtcNow - context.StartTime;
Interlocked.Add(ref _totalOperationTicks, operationTime.Ticks);
// Выполнение сброса
if (context.CurrentDropTarget != null)
{
var dropInfo = new DropInfo(
context.DragInfo.Data,
position,
context.DragInfo.AllowedEffects,
context.CurrentDropTarget);
await ExecuteTargetOperationAsync(
context.CurrentDropTarget,
t => t.OnDropAsync(dropInfo),
"OnDrop");
if (dropInfo.Handled)
{
effects = dropInfo.SuggestedEffects;
Interlocked.Increment(ref _successfulDrops);
}
}
// Уведомление источника
await ExecuteSourceOperationAsync(
context.Source,
s => s.OnDragCompletedAsync(context.DragInfo, effects),
"OnDragCompleted");
// Событие завершения
_dragCompleted?.Invoke(this, new DragCompletedEventArgs(
context.DragInfo, position, effects));
return effects;
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "EndDragAsync", context);
return 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.OnDragCancelledAsync(context.DragInfo),
"OnDragCancelled");
_dragCancelled?.Invoke(this, new DragCancelledEventArgs(context.DragInfo, context.LastPosition));
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, "CancelDragAsync", context);
}
finally
{
context.Dispose();
}
}
#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 FindDropTargetAsync(Geometry.Point position, Models.DragInfo dragInfo)
{
DropTargetInfo? bestTarget = null;
_dropTargetsLock.EnterReadLock();
try
{
// Фильтруем цели по границам и сортируем по приоритету
var candidates = _dropTargets.Values
.Where(info => info.Bounds.Contains(position))
.OrderByDescending(info => info.Priority)
.ToList();
foreach (var info in candidates)
{
var dropInfo = new Models.DropInfo(
dragInfo.Data,
position,
dragInfo.AllowedEffects,
info.Target);
bool canAccept = await ExecuteWithTimeoutAsync(
info.Target.CanAcceptDropAsync(dropInfo),
"CanAcceptDropAsync",
info.Target);
if (canAccept)
{
info.LastAccessTime = DateTime.UtcNow;
bestTarget = info;
break; // Берем первую подходящую с наивысшим приоритетом
}
}
}
finally
{
_dropTargetsLock.ExitReadLock();
}
return bestTarget;
}
private async Task ExecuteTargetOperationAsync(
IDropTarget target,
Func operation,
string operationName)
{
try
{
if (EnableAsyncOperations)
{
await ExecuteWithTimeoutAsync(
operation(target),
$"{operationName}Async",
target);
}
else
{
await operation(target);
}
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, operationName, target);
}
}
private async Task ExecuteSourceOperationAsync(
IDragSource source,
Func operation,
string operationName)
{
try
{
if (EnableAsyncOperations)
{
await ExecuteWithTimeoutAsync(
operation(source),
$"{operationName}Async",
source);
}
else
{
await operation(source);
}
}
catch (Exception ex)
{
Interlocked.Increment(ref _errorCount);
HandleError(ex, operationName, source);
}
}
private async Task ExecuteWithTimeoutAsync(Task 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(-Constants.DragDropConstants.TargetLifetimeMinutes);
_dropTargetsLock.EnterWriteLock();
try
{
var idsToRemove = new List();
var currentTarget = _currentDragOperation?.CurrentDropTarget;
foreach (var kvp in _dropTargets)
{
if (kvp.Value.LastAccessTime < expirationTime && !ReferenceEquals(kvp.Value.Target, currentTarget))
{
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;
var timer = Interlocked.Exchange(ref _cleanupTimer, null);
timer?.Dispose();
_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
}