namespace Lattice.Core.DragDrop.Services; /// /// Реализация сервиса управления операциями перетаскивания. /// Полностью потокобезопасная реализация с поддержкой async/await. /// 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 _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; } = 3.0; public bool EnableAsyncOperations { get; set; } = true; public int AsyncOperationTimeout { get; set; } = 5000; 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 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(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.IDragSource asyncSource) { var result = await ExecuteWithTimeoutAsync( asyncSource.CanStartDragAsync(), "CanStartDragAsync", source); if (!result.CanStart || result.DragInfo == null) return false; dragInfo = result.DragInfo; } else { var startDragResult = await source.CanStartDragAsync(); if (!startDragResult.CanStart || startDragResult.DragInfo == null) return false; dragInfo = startDragResult.DragInfo; } var updatedDragInfo = dragInfo.CloneWithPosition(startPosition); // Начало перетаскивания bool started; if (EnableAsyncOperations && source is Abstractions.IDragSource asyncSource2) { started = await ExecuteWithTimeoutAsync( asyncSource2.StartDragAsync(updatedDragInfo), "StartDragAsync", source); } else { started = await source.StartDragAsync(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 = updatedDragInfo; context.DragInfo.Dispose(); // Поиск новой цели сброса var newDropTarget = await FindDropTargetAsync(position, updatedDragInfo); // Обработка смены цели if (context.CurrentDropTarget != newDropTarget?.Target) { if (context.CurrentDropTarget != null) { await ExecuteTargetOperationAsync( context.CurrentDropTarget, t => t.DragLeaveAsync(), t => t.DragLeaveAsync(), "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.DragOverAsync(dropInfo), "DragOver"); } _dragUpdated?.Invoke(this, new DragUpdatedEventArgs(updatedDragInfo, position)); } catch (Exception ex) { Interlocked.Increment(ref _errorCount); HandleError(ex, "UpdateDragAsync", context); } } public async Task 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.DropAsync(dropInfo), "Drop"); if (dropInfo.Handled) { effects = dropInfo.SuggestedEffects; Interlocked.Increment(ref _successfulDrops); } } // Уведомление источника await ExecuteSourceOperationAsync( context.Source, s => s.DragCompletedAsync(context.DragInfo, effects), s => s.DragCompletedAsync(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.DragCancelledAsync(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 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( Abstractions.IDropTarget target, Func asyncOperation, Action syncOperation, string operationName) { try { if (EnableAsyncOperations && target is Abstractions.IDropTarget 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 asyncOperation, Action syncOperation, string operationName, Enums.DragDropEffects effects = Enums.DragDropEffects.None) { try { if (EnableAsyncOperations && source is Abstractions.IDragSource 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 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 }