доработана winUI реализация

This commit is contained in:
2026-01-25 06:29:37 +03:00
parent 0e050b452a
commit a902474345
8 changed files with 1594 additions and 452 deletions

View File

@@ -1,5 +1,6 @@
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.WinUI.Behaviors;
using Lattice.UI.DragDrop.WinUI.Controls;
using Microsoft.UI.Xaml;
using System;
@@ -9,41 +10,87 @@ namespace Lattice.UI.DragDrop.WinUI.Services;
/// <summary>
/// Центральный менеджер для управления операциями drag-and-drop в WinUI приложении.
/// Координирует работу источников и целей перетаскивания, управляет визуальной обратной связью
/// и обеспечивает согласованное взаимодействие всех компонентов системы.
/// </summary>
/// <remarks>
/// <para>
/// Этот класс реализует шаблон Singleton и предоставляет единую точку для
/// настройки и управления всеми операциями перетаскивания в приложении.
/// <see cref="WinUIDragDropManager"/> реализует шаблон Singleton и служит единой точкой
/// входа для настройки и управления операциями перетаскивания в WinUI-приложении.
/// </para>
/// <para>
/// Менеджер отвечает за:
/// - Инициализацию системы перетаскивания
/// - Регистрацию и отслеживание источников и целей перетаскивания
/// - Создание и управление визуальной обратной связью
/// - Координацию между поведением элементов и базовым сервисом перетаскивания
/// Основные функции менеджера:
/// <list type="bullet">
/// <item>Инициализация системы перетаскивания для конкретного окна</item>
/// <item>Регистрация и отслеживание источников и целей перетаскивания</item>
/// <item>Управление жизненным циклом операций перетаскивания</item>
/// <item>Обеспечение визуальной обратной связи через <see cref="WinUIDragDropHost"/></item>
/// <item>Координация взаимодействия между <see cref="WinUIDragSourceBehavior"/> и <see cref="WinUIDropTargetBehavior"/></item>
/// </list>
/// </para>
/// <para>
/// Для использования необходимо вызвать <see cref="Initialize"/> при запуске приложения
/// и использовать attached properties или методы расширения для настройки элементов.
/// Для использования менеджера необходимо:
/// <list type="number">
/// <item>Вызвать <see cref="Initialize"/> при создании главного окна приложения</item>
/// <item>Настроить элементы как источники или цели через методы <see cref="MakeDragSource"/> и <see cref="MakeDropTarget"/></item>
/// <item>Использовать attached properties для декларативной настройки в XAML</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Инициализация в коде
/// public partial class MainWindow : Window
/// {
/// private WinUIDragDropManager _manager;
///
/// public MainWindow()
/// {
/// InitializeComponent();
/// _manager = WinUIDragDropManager.Instance;
/// _manager.Initialize(this);
///
/// // Настройка элементов
/// _manager.MakeDragSource(myElement, myData);
/// _manager.MakeDropTarget(myDropArea);
/// }
/// }
///
/// // Или через attached properties в XAML
/// &lt;Border x:Name="DragElement"
/// local:DragDropProperties.IsDragSource="True"
/// local:DragDropProperties.DragData="{Binding MyData}" /&gt;
/// &lt;Border x:Name="DropArea"
/// local:DragDropProperties.IsDropTarget="True" /&gt;
/// </code>
/// </example>
/// </remarks>
public sealed class WinUIDragDropManager : IDisposable
{
#region Singleton
#region Singleton Implementation
private static WinUIDragDropManager? _instance;
private static readonly object _lock = new();
private static readonly object _lockObject = new();
/// <summary>
/// Получает единственный экземпляр менеджера.
/// Получает единственный экземпляр <see cref="WinUIDragDropManager"/>.
/// Реализует шаблон Singleton с ленивой инициализацией и потокобезопасностью.
/// </summary>
/// <value>
/// Единственный экземпляр менеджера перетаскивания для всего приложения.
/// Если экземпляр еще не создан, он будет инициализирован при первом обращении.
/// </value>
/// <remarks>
/// Использование Singleton гарантирует, что во всем приложении существует только один
/// экземпляр менеджера, что обеспечивает согласованное управление всеми операциями
/// перетаскивания и предотвращает конфликты между разными компонентами системы.
/// </remarks>
public static WinUIDragDropManager Instance
{
get
{
if (_instance == null)
{
lock (_lock)
lock (_lockObject)
{
_instance ??= new WinUIDragDropManager();
}
@@ -54,37 +101,107 @@ public sealed class WinUIDragDropManager : IDisposable
#endregion
#region Поля
#region Fields
private readonly DragDropService _dragDropService;
private readonly IDragDropService _dragDropService;
private readonly WinUIDragDropHost _host;
private readonly Dictionary<FrameworkElement, Behaviors.WinUIDragSourceBehavior> _dragSources = new();
private readonly Dictionary<FrameworkElement, Behaviors.WinUIDropTargetBehavior> _dropTargets = new();
private readonly Dictionary<FrameworkElement, WinUIDragSourceBehavior> _dragSources = new();
private readonly Dictionary<FrameworkElement, WinUIDropTargetBehavior> _dropTargets = new();
private DragAdorner? _currentDragVisual;
private bool _disposed;
private bool _initialized;
#endregion
#region Свойства
#region Properties
/// <summary>
/// Получает основной сервис перетаскивания.
/// Получает сервис перетаскивания, используемый менеджером для координации операций.
/// </summary>
/// <value>
/// Экземпляр <see cref="IDragDropService"/>, через который менеджер взаимодействует
/// с ядром системы перетаскивания.
/// </value>
/// <remarks>
/// Этот сервис предоставляет низкоуровневый API для управления операциями перетаскивания
/// и может использоваться для расширенной настройки системы.
/// </remarks>
public IDragDropService DragDropService => _dragDropService;
/// <summary>
/// Получает хост для управления визуальными элементами перетаскивания.
/// </summary>
/// <value>
/// Экземпляр <see cref="WinUIDragDropHost"/>, отвечающий за отображение и позиционирование
/// визуальной обратной связи во время операций перетаскивания.
/// </value>
public WinUIDragDropHost Host => _host;
/// <summary>
/// Получает или задает смещение визуального элемента перетаскивания относительно курсора.
/// </summary>
/// <value>
/// Точка, определяющая смещение по осям X и Y. Значение по умолчанию: (-20, -20).
/// Отрицательные значения поднимают визуальный элемент вверх и влево относительно курсора.
/// Точка, определяющая смещение по осям X и Y в пикселях.
/// Значение по умолчанию: (-20, -20).
/// </value>
/// <remarks>
/// <para>
/// Отрицательные значения смещают визуальный элемент вверх и влево относительно курсора,
/// что является стандартным поведением в большинстве систем drag-and-drop.
/// </para>
/// <para>
/// Настройка смещения позволяет:
/// <list type="bullet">
/// <item>Предотвратить перекрытие курсора визуальным элементом</item>
/// <item>Обеспечить лучшую видимость области под курсором</item>
/// <item>Создать более естественное визуальное восприятие</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Настройка смещения через фабрику
/// var manager = WinUIDragDropFactory.CreateManager(window, m =>
/// {
/// m.DragVisualOffset = new Point(-15, -15); // Более близко к курсору
/// });
/// </code>
/// </example>
/// </remarks>
public Point DragVisualOffset { get; set; } = new Point(-20, -20);
/// <summary>
/// Получает значение, указывающее, инициализирован ли менеджер.
/// </summary>
/// <value>
/// true, если метод <see cref="Initialize"/> был успешно вызван;
/// в противном случае — false.
/// </value>
/// <remarks>
/// Проверка этого свойства позволяет избежать повторной инициализации менеджера
/// и гарантирует, что система перетаскивания готова к использованию.
/// </remarks>
public bool IsInitialized => _initialized;
#endregion
#region Конструктор
#region Constructor
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDragDropManager"/>.
/// Конструктор является приватным в соответствии с шаблоном Singleton.
/// </summary>
/// <remarks>
/// <para>
/// Внутренний конструктор создает:
/// <list type="bullet">
/// <item>Экземпляр <see cref="DragDropService"/> для управления операциями перетаскивания</item>
/// <item>Экземпляр <see cref="WinUIDragDropHost"/> для визуальной обратной связи</item>
/// </list>
/// </para>
/// <para>
/// Для получения экземпляра менеджера используйте свойство <see cref="Instance"/>.
/// </para>
/// </remarks>
private WinUIDragDropManager()
{
_dragDropService = new DragDropService();
@@ -93,71 +210,196 @@ public sealed class WinUIDragDropManager : IDisposable
#endregion
#region Публичные методы
#region Public Methods
/// <summary>
/// Инициализирует систему перетаскивания для указанного окна.
/// Инициализирует систему перетаскивания для указанного окна WinUI.
/// Этот метод должен быть вызван один раз при запуске приложения.
/// </summary>
/// <param name="window">Основное окно приложения, в котором будет работать перетаскивание.</param>
/// <exception cref="ObjectDisposedException">
/// Выбрасывается, если менеджер был удален.
/// <param name="window">
/// Главное окно приложения, для которого настраивается система перетаскивания.
/// Не может быть null.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="window"/> равен null.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер уже инициализирован или был удален.
/// </exception>
/// <remarks>
/// Этот метод должен быть вызван один раз при запуске приложения, обычно в методе
/// <see cref="Application.OnLaunched"/>.
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Настраивает хост визуальных элементов для работы с указанным окном</item>
/// <item>Подписывается на события сервиса перетаскивания для управления визуальной обратной связью</item>
/// <item>Помечает менеджер как инициализированный</item>
/// </list>
/// </para>
/// <para>
/// Метод должен быть вызван до использования любых других методов менеджера.
/// Рекомендуется вызывать его в конструкторе главного окна приложения.
/// </para>
/// <example>
/// <code>
/// public MainWindow()
/// {
/// InitializeComponent();
/// WinUIDragDropManager.Instance.Initialize(this);
/// }
/// </code>
/// </example>
/// </remarks>
public void Initialize(Window window)
{
if (_disposed) throw new ObjectDisposedException(nameof(WinUIDragDropManager));
if (_disposed)
throw new ObjectDisposedException(nameof(WinUIDragDropManager));
if (_initialized)
throw new InvalidOperationException("Менеджер уже инициализирован.");
if (window == null)
throw new ArgumentNullException(nameof(window));
// Инициализируем хост для работы с окном
_host.Initialize(window);
// Подписываемся на события
// Подписываемся на события сервиса перетаскивания
_dragDropService.DragStarted += OnDragStarted;
_dragDropService.DragUpdated += OnDragUpdated;
_dragDropService.DragCompleted += OnDragCompleted;
_dragDropService.DragCancelled += OnDragCancelled;
_initialized = true;
}
/// <summary>
/// Делает указанный элемент источником перетаскивания.
/// Настраивает указанный элемент как источник перетаскивания.
/// </summary>
/// <param name="element">Элемент, который станет источником перетаскивания.</param>
/// <param name="dragData">Данные, которые будут перетаскиваться. Если не указано, используются
/// DataContext или Tag элемента.</param>
/// <param name="element">
/// Элемент <see cref="FrameworkElement"/ который должен стать источником перетаскивания.
/// Не может быть null.
/// </param>
/// <param name="dragData">
/// Данные, которые будут перетаскиваться. Может быть null.
/// Если не указано, используются <see cref="FrameworkElement.DataContext"/> или
/// <see cref="FrameworkElement.Tag"/> элемента.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер не инициализирован или был удален.
/// </exception>
/// <remarks>
/// <para>
/// После вызова этого метода элемент приобретает следующие возможности:
/// <list type="bullet">
/// <item>Реагирует на жесты перетаскивания (удержание и перемещение указателя)</item>
/// <item>Предоставляет указанные данные для перетаскивания</item>
/// <item>Интегрируется с системой визуальной обратной связи</item>
/// </list>
/// </para>
/// <para>
/// Если элемент уже зарегистрирован как источник перетаскивания, метод не выполняет действий.
/// </para>
/// <para>
/// Для отмены регистрации используйте метод <see cref="RemoveDragSource"/>.
/// </para>
/// </remarks>
public void MakeDragSource(FrameworkElement element, object? dragData = null)
{
if (_disposed || _dragSources.ContainsKey(element)) return;
ValidateManagerState();
var behavior = new Behaviors.WinUIDragSourceBehavior(_dragDropService, _host);
if (element == null)
throw new ArgumentNullException(nameof(element));
// Если элемент уже зарегистрирован, ничего не делаем
if (_dragSources.ContainsKey(element))
return;
// Создаем и настраиваем поведение
var behavior = new WinUIDragSourceBehavior(_dragDropService, _host);
behavior.Attach(element, dragData);
_dragSources[element] = behavior;
}
/// <summary>
/// Делает указанный элемент целью сброса.
/// Настраивает указанный элемент как цель сброса.
/// </summary>
/// <param name="element">Элемент, который станет целью сброса.</param>
/// <param name="element">
/// Элемент <see cref="FrameworkElement"/>, который должен стать целью сброса.
/// Не может быть null.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер не инициализирован или был удален.
/// </exception>
/// <remarks>
/// <para>
/// После вызова этого метода элемент приобретает следующие возможности:
/// <list type="bullet">
/// <item>Принимает данные, сбрасываемые пользователем</item>
/// <item>Предоставляет визуальную обратную связь при наведении</item>
/// <item>Автоматически обновляет свои границы при изменении размера или позиции</item>
/// </list>
/// </para>
/// <para>
/// По умолчанию цель принимает данные любого типа. Для настройки фильтрации типов
/// используйте методы <see cref="WinUIDropTargetBehavior.AcceptTypes"/> и
/// <see cref="WinUIDropTargetBehavior.AcceptFormats"/>.
/// </para>
/// <para>
/// Если элемент уже зарегистрирован как цель сброса, метод не выполняет действий.
/// </para>
/// <para>
/// Для отмены регистрации используйте метод <see cref="RemoveDropTarget"/>.
/// </para>
/// </remarks>
public void MakeDropTarget(FrameworkElement element)
{
if (_disposed || _dropTargets.ContainsKey(element)) return;
ValidateManagerState();
var behavior = new Behaviors.WinUIDropTargetBehavior(_dragDropService, _host);
if (element == null)
throw new ArgumentNullException(nameof(element));
// Если элемент уже зарегистрирован, ничего не делаем
if (_dropTargets.ContainsKey(element))
return;
// Создаем и настраиваем поведение
var behavior = new WinUIDropTargetBehavior(_dragDropService, _host);
behavior.Attach(element);
_dropTargets[element] = behavior;
}
/// <summary>
/// Удаляет возможность перетаскивания.
/// Удаляет возможность перетаскивания у указанного элемента.
/// </summary>
/// <param name="element">
/// Элемент, у которого нужно отключить возможность перетаскивания.
/// Если элемент не зарегистрирован как источник перетаскивания, метод не выполняет действий.
/// </param>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Открепляет поведение перетаскивания от элемента</item>
/// <item>Отписывается от всех событий элемента</item>
/// <item>Удаляет элемент из внутреннего словаря источников</item>
/// <item>Освобождает ресурсы, связанные с поведением</item>
/// </list>
/// </para>
/// <para>
/// Метод безопасен для вызова даже если элемент не был зарегистрирован как источник.
/// </para>
/// </remarks>
public void RemoveDragSource(FrameworkElement element)
{
if (element == null || _disposed || !_dragSources.ContainsKey(element))
return;
if (_dragSources.Remove(element, out var behavior))
{
behavior.Detach();
@@ -165,10 +407,31 @@ public sealed class WinUIDragDropManager : IDisposable
}
/// <summary>
/// Удаляет возможность сброса.
/// Удаляет возможность сброса у указанного элемента.
/// </summary>
/// <param name="element">
/// Элемент, у которого нужно отключить возможность сброса.
/// Если элемент не зарегистрирован как цель сброса, метод не выполняет действий.
/// </param>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Открепляет поведение цели сброса от элемента</item>
/// <item>Восстанавливает свойство <see cref="UIElement.AllowDrop"/> = false</item>
/// <item>Удаляет элемент из внутреннего словаря целей</item>
/// <item>Освобождает ресурсы, связанные с поведением</item>
/// </list>
/// </para>
/// <para>
/// Метод безопасен для вызова даже если элемент не был зарегистрирован как цель.
/// </para>
/// </remarks>
public void RemoveDropTarget(FrameworkElement element)
{
if (element == null || _disposed || !_dropTargets.ContainsKey(element))
return;
if (_dropTargets.Remove(element, out var behavior))
{
behavior.Detach();
@@ -176,16 +439,35 @@ public sealed class WinUIDragDropManager : IDisposable
}
/// <summary>
/// Очищает все регистрации.
/// Очищает все регистрации источников и целей перетаскивания.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод полезен в следующих сценариях:
/// <list type="bullet">
/// <item>При перезагрузке содержимого интерфейса</item>
/// <item>При смене контекста данных</item>
/// <item>При освобождении ресурсов перед удалением менеджера</item>
/// </list>
/// </para>
/// <para>
/// После вызова этого метода все элементы теряют возможность участвовать в операциях
/// перетаскивания. Для восстановления функциональности необходимо повторно
/// зарегистрировать элементы через <see cref="MakeDragSource"/> и <see cref="MakeDropTarget"/>.
/// </para>
/// </remarks>
public void Clear()
{
if (_disposed) return;
// Открепляем все источники
foreach (var behavior in _dragSources.Values)
{
behavior.Detach();
}
_dragSources.Clear();
// Открепляем все цели
foreach (var behavior in _dropTargets.Values)
{
behavior.Detach();
@@ -195,27 +477,37 @@ public sealed class WinUIDragDropManager : IDisposable
#endregion
#region Обработчики событий
#region Event Handlers
private void OnDragStarted(object? sender, DragStartedEventArgs e)
/// <summary>
/// Обрабатывает событие начала перетаскивания.
/// Создает и отображает визуальный элемент для обратной связи.
/// </summary>
private void OnDragStarted(object? sender, Core.DragDrop.Services.DragStartedEventArgs e)
{
// Создаем визуальное представление
// Создаем визуальное представление перетаскивания
_currentDragVisual = new DragAdorner
{
DragData = e.DragInfo.Data,
Opacity = 0.8
};
// Рассчитываем позицию с учетом смещения
var position = new Point(
e.Position.X + DragVisualOffset.X,
e.Position.Y + DragVisualOffset.Y
);
// Обновляем позицию и показываем элемент
_currentDragVisual.UpdatePosition(position);
_host.ShowDragVisual(_currentDragVisual, position);
}
private void OnDragUpdated(object? sender, DragUpdatedEventArgs e)
/// <summary>
/// Обрабатывает событие обновления позиции перетаскивания.
/// Обновляет позицию визуального элемента для следования за курсором.
/// </summary>
private void OnDragUpdated(object? sender, Core.DragDrop.Services.DragUpdatedEventArgs e)
{
if (_currentDragVisual != null)
{
@@ -228,16 +520,27 @@ public sealed class WinUIDragDropManager : IDisposable
}
}
private void OnDragCompleted(object? sender, DragCompletedEventArgs e)
/// <summary>
/// Обрабатывает событие завершения перетаскивания.
/// Очищает визуальные элементы и восстанавливает состояние.
/// </summary>
private void OnDragCompleted(object? sender, Core.DragDrop.Services.DragCompletedEventArgs e)
{
CleanupDragVisual();
}
private void OnDragCancelled(object? sender, DragCancelledEventArgs e)
/// <summary>
/// Обрабатывает событие отмены перетаскивания.
/// Очищает визуальные элементы и восстанавливает состояние.
/// </summary>
private void OnDragCancelled(object? sender, Core.DragDrop.Services.DragCancelledEventArgs e)
{
CleanupDragVisual();
}
/// <summary>
/// Освобождает ресурсы визуального элемента перетаскивания.
/// </summary>
private void CleanupDragVisual()
{
if (_currentDragVisual != null)
@@ -249,24 +552,72 @@ public sealed class WinUIDragDropManager : IDisposable
#endregion
#region IDisposable
#region Helper Methods
/// <summary>
/// Проверяет состояние менеджера перед выполнением операций.
/// </summary>
/// <exception cref="ObjectDisposedException">
/// Выбрасывается, если менеджер был удален.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер не инициализирован.
/// </exception>
private void ValidateManagerState()
{
if (_disposed)
throw new ObjectDisposedException(nameof(WinUIDragDropManager));
if (!_initialized)
throw new InvalidOperationException(
"Менеджер не инициализирован. Вызовите метод Initialize перед использованием.");
}
#endregion
#region IDisposable Implementation
/// <summary>
/// Освобождает все ресурсы, используемые <see cref="WinUIDragDropManager"/>.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Отписывается от всех событий сервиса перетаскивания</item>
/// <item>Очищает все зарегистрированные источники и цели</item>
/// <item>Освобождает ресурсы хоста визуальных элементов</item>
/// <item>Освобождает ресурсы сервиса перетаскивания</item>
/// </list>
/// </para>
/// <para>
/// После вызова этого метода менеджер перестает быть пригодным для использования.
/// Попытка использовать методы менеджера после удаления приведет к исключению
/// <see cref="ObjectDisposedException"/>.
/// </para>
/// <para>
/// Метод безопасен для многократного вызова.
/// </para>
/// </remarks>
public void Dispose()
{
if (_disposed) return;
Clear();
// Отписываемся от событий
_dragDropService.DragStarted -= OnDragStarted;
_dragDropService.DragUpdated -= OnDragUpdated;
_dragDropService.DragCompleted -= OnDragCompleted;
_dragDropService.DragCancelled -= OnDragCancelled;
// Очищаем все регистрации
Clear();
// Освобождаем ресурсы
_dragDropService.Dispose();
_host.Dispose();
_disposed = true;
_initialized = false;
GC.SuppressFinalize(this);
}