804 lines
23 KiB
C#
804 lines
23 KiB
C#
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(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<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.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<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.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<DropTargetInfo?> 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<Abstractions.IDropTarget, Task> asyncOperation,
|
||
Action<Abstractions.IDropTarget> 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<Abstractions.IDragSource, Task> asyncOperation,
|
||
Action<Abstractions.IDragSource> 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<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(-Constants.DragDropConstants.TargetLifetimeMinutes);
|
||
|
||
_dropTargetsLock.EnterWriteLock();
|
||
try
|
||
{
|
||
var idsToRemove = new List<string>();
|
||
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
|
||
} |