Files
Lattice/Lattice.Core.DragDrop/Services/DragDropService.cs

846 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// Центральный сервис управления операциями перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="DragDropService"/> является основным компонентом системы drag-and-drop,
/// который координирует взаимодействие между источниками данных (<see cref="Abstractions.IDragSource"/>)
/// и целями сброса (<see cref="Abstractions.IDropTarget"/>).
/// </para>
/// <para>
/// Основные функции сервиса:
/// </para>
/// <list type="bullet">
/// <item>Регистрация и управление целями сброса</item>
/// <item>Оркестрация жизненного цикла операций перетаскивания</item>
/// <item>Обработка событий мыши и клавиатуры</item>
/// <item>Распространение информации между компонентами</item>
/// <item>Обеспечение потокобезопасности операций</item>
/// </list>
/// <para>
/// Сервис поддерживает полностью асинхронную модель работы, уведомления через события
/// и статистику использования. Все операции защищены от параллельного доступа
/// и обеспечивают корректную очистку ресурсов.
/// </para>
/// <para>
/// Для использования сервиса необходимо:
/// <list type="number">
/// <item>Зарегистрировать цели сброса с помощью <see cref="RegisterDropTarget"/></item>
/// <item>Вызывать методы <see cref="StartDragAsync"/>, <see cref="UpdateDragAsync"/>,
/// <see cref="EndDragAsync"/> в ответ на действия пользователя</item>
/// <item>Подписаться на события для отслеживания состояния операций</item>
/// </list>
/// </para>
/// </remarks>
public sealed class DragDropService : IDragDropService
{
#region Nested Types
/// <summary>
/// Информация о зарегистрированной цели сброса.
/// </summary>
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();
}
}
/// <summary>
/// Контекст текущей операции перетаскивания.
/// </summary>
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<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
/// <inheritdoc/>
public bool IsDragActive => Volatile.Read(ref _currentDragOperation) != null;
/// <inheritdoc/>
public Models.DragInfo? CurrentDragInfo => _currentDragOperation?.DragInfo;
/// <inheritdoc/>
public Abstractions.IDropTarget? CurrentDropTarget => _currentDragOperation?.CurrentDropTarget;
/// <inheritdoc/>
public double DragStartThreshold { get; set; } = DragDropConstants.DefaultDragThreshold;
/// <inheritdoc/>
public bool EnableAsyncOperations { get; set; } = true;
/// <inheritdoc/>
public int AsyncOperationTimeout { get; set; } = DragDropConstants.DefaultAsyncTimeout;
/// <inheritdoc/>
public event EventHandler<DragStartedEventArgs> DragStarted
{
add => _dragStarted += value;
remove => _dragStarted -= value;
}
/// <inheritdoc/>
public event EventHandler<DragUpdatedEventArgs> DragUpdated
{
add => _dragUpdated += value;
remove => _dragUpdated -= value;
}
/// <inheritdoc/>
public event EventHandler<DropTargetChangedEventArgs> DropTargetChanged
{
add => _dropTargetChanged += value;
remove => _dropTargetChanged -= value;
}
/// <inheritdoc/>
public event EventHandler<DragCompletedEventArgs> DragCompleted
{
add => _dragCompleted += value;
remove => _dragCompleted -= value;
}
/// <inheritdoc/>
public event EventHandler<DragCancelledEventArgs> DragCancelled
{
add => _dragCancelled += value;
remove => _dragCancelled -= value;
}
/// <inheritdoc/>
public event EventHandler<DragDropErrorEventArgs> ErrorOccurred
{
add => _errorOccurred += value;
remove => _errorOccurred -= value;
}
#endregion
#region Constructor
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragDropService"/>.
/// </summary>
/// <remarks>
/// Создает сервис с настройками по умолчанию:
/// <list type="bullet">
/// <item>Порог начала перетаскивания: <see cref="Constants.DragDropConstants.DefaultDragThreshold"/> пикселей</item>
/// <item>Таймаут асинхронных операций: <see cref="Constants.DragDropConstants.DefaultAsyncTimeout"/> миллисекунд</item>
/// <item>Включены асинхронные операции: true</item>
/// </list>
/// </remarks>
public DragDropService()
{
// Инициализация таймера очистки (каждые 5 минут)
_cleanupTimer = new Timer(CleanupExpiredTargets, null,
TimeSpan.FromMinutes(Constants.DragDropConstants.TargetLifetimeMinutes / 2),
TimeSpan.FromMinutes(Constants.DragDropConstants.TargetLifetimeMinutes / 2));
}
#endregion
#region Registration Methods
/// <inheritdoc/>
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();
}
}
/// <inheritdoc/>
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();
}
}
/// <inheritdoc/>
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();
}
}
/// <inheritdoc/>
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
/// <inheritdoc/>
public async Task<bool> 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;
}
}
/// <inheritdoc/>
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);
}
}
/// <inheritdoc/>
public async Task<DragDropEffects> 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();
}
}
/// <inheritdoc/>
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
/// <inheritdoc/>
public void ClearAllDropTargets()
{
ThrowIfDisposed();
_dropTargetsLock.EnterWriteLock();
try
{
foreach (var info in _dropTargets.Values)
{
info.Dispose();
}
_dropTargets.Clear();
}
finally
{
_dropTargetsLock.ExitWriteLock();
}
}
/// <inheritdoc/>
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(
IDropTarget target,
Func<IDropTarget, Task> 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<IDragSource, Task> 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<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
}