Files
Lattice/Lattice.UI.DragDrop.WinUI/Services/WinUIDragDropManager.cs

625 lines
28 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.Services;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.WinUI.Behaviors;
using Lattice.UI.DragDrop.WinUI.Controls;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
namespace Lattice.UI.DragDrop.WinUI.Services;
/// <summary>
/// Центральный менеджер для управления операциями drag-and-drop в WinUI приложении.
/// Координирует работу источников и целей перетаскивания, управляет визуальной обратной связью
/// и обеспечивает согласованное взаимодействие всех компонентов системы.
/// </summary>
/// <remarks>
/// <para>
/// <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>
/// Для использования менеджера необходимо:
/// <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 Implementation
private static WinUIDragDropManager? _instance;
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 (_lockObject)
{
_instance ??= new WinUIDragDropManager();
}
}
return _instance;
}
}
#endregion
#region Fields
private readonly IDragDropService _dragDropService;
private readonly WinUIDragDropHost _host;
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 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).
/// </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 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();
_host = new WinUIDragDropHost();
}
#endregion
#region Public Methods
/// <summary>
/// Инициализирует систему перетаскивания для указанного окна WinUI.
/// Этот метод должен быть вызван один раз при запуске приложения.
/// </summary>
/// <param name="window">
/// Главное окно приложения, для которого настраивается система перетаскивания.
/// Не может быть null.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="window"/> равен null.
/// </exception>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер уже инициализирован или был удален.
/// </exception>
/// <remarks>
/// <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 (_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">
/// Элемент <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)
{
ValidateManagerState();
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">
/// Элемент <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)
{
ValidateManagerState();
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();
}
}
/// <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();
}
}
/// <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();
}
_dropTargets.Clear();
}
#endregion
#region Event Handlers
/// <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);
}
/// <summary>
/// Обрабатывает событие обновления позиции перетаскивания.
/// Обновляет позицию визуального элемента для следования за курсором.
/// </summary>
private void OnDragUpdated(object? sender, Core.DragDrop.Services.DragUpdatedEventArgs e)
{
if (_currentDragVisual != null)
{
var position = new Point(
e.Position.X + DragVisualOffset.X,
e.Position.Y + DragVisualOffset.Y
);
_currentDragVisual.UpdatePosition(position);
}
}
/// <summary>
/// Обрабатывает событие завершения перетаскивания.
/// Очищает визуальные элементы и восстанавливает состояние.
/// </summary>
private void OnDragCompleted(object? sender, Core.DragDrop.Services.DragCompletedEventArgs e)
{
CleanupDragVisual();
}
/// <summary>
/// Обрабатывает событие отмены перетаскивания.
/// Очищает визуальные элементы и восстанавливает состояние.
/// </summary>
private void OnDragCancelled(object? sender, Core.DragDrop.Services.DragCancelledEventArgs e)
{
CleanupDragVisual();
}
/// <summary>
/// Освобождает ресурсы визуального элемента перетаскивания.
/// </summary>
private void CleanupDragVisual()
{
if (_currentDragVisual != null)
{
_currentDragVisual.Hide();
_currentDragVisual = null;
}
}
#endregion
#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;
// Отписываемся от событий
_dragDropService.DragStarted -= OnDragStarted;
_dragDropService.DragUpdated -= OnDragUpdated;
_dragDropService.DragCompleted -= OnDragCompleted;
_dragDropService.DragCancelled -= OnDragCancelled;
// Очищаем все регистрации
Clear();
// Освобождаем ресурсы
_dragDropService.Dispose();
_host.Dispose();
_disposed = true;
_initialized = false;
GC.SuppressFinalize(this);
}
#endregion
}