Files
Lattice/Lattice.Core.DragDrop/README.md

829 lines
26 KiB
Markdown
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.
# 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/ # Аргументы событий
```
## 🚀 Быстрый старт
### 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. **Инструменты разработчика** (дебаггер, профилировщик)