Compare commits

...

2 Commits

14 changed files with 2599 additions and 633 deletions

View File

@@ -0,0 +1,581 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Enums;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
namespace Lattice.Core.DragDrop.Factories;
/// <summary>
/// Фабрика для создания компонентов системы перетаскивания.
/// Предоставляет методы для создания сервисов, источников и целей перетаскивания.
/// </summary>
/// <remarks>
/// Эта фабрика позволяет создавать компоненты системы перетаскивания без использования
/// Dependency Injection, предоставляя простой и понятный API для наиболее распространенных сценариев.
/// </remarks>
public static class DragDropFactory
{
#region Сервисы перетаскивания
/// <summary>
/// Создает новый экземпляр сервиса перетаскивания с настройками по умолчанию.
/// </summary>
/// <returns>
/// Экземпляр <see cref="IDragDropService"/> с настройками по умолчанию.
/// </returns>
/// <remarks>
/// Созданный сервис имеет следующие настройки по умолчанию:
/// <list type="bullet">
/// <item>Порог начала перетаскивания: 3.0 пикселей</item>
/// <item>Таймаут асинхронных операций: 5000 миллисекунд</item>
/// <item>Асинхронные операции: включены</item>
/// </list>
/// </remarks>
public static IDragDropService CreateDragDropService()
{
return new DragDropService();
}
/// <summary>
/// Создает новый экземпляр сервиса перетаскивания с пользовательскими настройками.
/// </summary>
/// <param name="configure">
/// Делегат для настройки опций сервиса. Передает экземпляр <see cref="DragDropServiceOptions"/>
/// для настройки параметров.
/// </param>
/// <returns>
/// Настроенный экземпляр <see cref="IDragDropService"/>.
/// </returns>
/// <example>
/// <code>
/// var service = DragDropFactory.CreateDragDropService(options =>
/// {
/// options.DragStartThreshold = 5.0;
/// options.AsyncOperationTimeout = 3000;
/// options.EnableAsyncOperations = true;
/// });
/// </code>
/// </example>
public static IDragDropService CreateDragDropService(Action<DragDropServiceOptions> configure)
{
var options = new DragDropServiceOptions();
configure(options);
return new DragDropService
{
DragStartThreshold = options.DragStartThreshold,
AsyncOperationTimeout = options.AsyncOperationTimeout,
EnableAsyncOperations = options.EnableAsyncOperations
};
}
/// <summary>
/// Создает сервис перетаскивания, оптимизированный для сенсорных устройств.
/// </summary>
/// <returns>
/// Экземпляр <see cref="IDragDropService"/> с увеличенным порогом перетаскивания.
/// </returns>
/// <remarks>
/// Этот метод создает сервис с увеличенным порогом начала перетаскивания (10.0 пикселей),
/// что уменьшает вероятность случайного начала перетаскивания при использовании сенсорного экрана.
/// </remarks>
public static IDragDropService CreateTouchOptimizedService()
{
return new DragDropService
{
DragStartThreshold = 10.0,
AsyncOperationTimeout = 3000,
EnableAsyncOperations = true
};
}
/// <summary>
/// Создает сервис перетаскивания для точных операций (графические редакторы, карты).
/// </summary>
/// <returns>
/// Экземпляр <see cref="IDragDropService"/> с уменьшенным порогом перетаскивания.
/// </returns>
/// <remarks>
/// Этот метод создает сервис с минимальным порогом начала перетаскивания (1.0 пиксель),
/// что позволяет начинать перетаскивание с максимальной точностью.
/// </remarks>
public static IDragDropService CreatePrecisionDragService()
{
return new DragDropService
{
DragStartThreshold = 1.0,
AsyncOperationTimeout = 10000, // Больше времени для сложных операций
EnableAsyncOperations = true
};
}
#endregion
#region Источники перетаскивания
/// <summary>
/// Создает простой источник перетаскивания с фиксированными данными.
/// </summary>
/// <param name="data">
/// Данные, которые будут перетаскиваться. Не может быть null.
/// </param>
/// <param name="allowedEffects">
/// Разрешенные эффекты перетаскивания. По умолчанию разрешены копирование и перемещение.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDragSource"/>, который всегда предоставляет указанные данные.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="data"/> равен null.
/// </exception>
/// <remarks>
/// Этот источник подходит для случаев, когда данные для перетаскивания известны заранее
/// и не изменяются в процессе операции.
/// </remarks>
public static IDragSource CreateSimpleSource(object data, DragDropEffects allowedEffects = DragDropEffects.Copy | DragDropEffects.Move)
{
if (data == null)
throw new ArgumentNullException(nameof(data));
return new SimpleDragSource(data, allowedEffects);
}
/// <summary>
/// Создает источник перетаскивания с отложенной загрузкой данных.
/// </summary>
/// <param name="dataFactory">
/// Фабрика данных, которая будет вызвана при начале перетаскивания.
/// </param>
/// <param name="canDragChecker">
/// Функция проверки возможности начала перетаскивания.
/// </param>
/// <param name="allowedEffects">
/// Разрешенные эффекты перетаскивания.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDragSource"/> с отложенной загрузкой данных.
/// </returns>
/// <remarks>
/// Этот источник полезен, когда данные для перетаскивания дорого создавать заранее
/// или зависят от контекста в момент начала операции.
/// </remarks>
public static IDragSource CreateLazySource(
Func<object> dataFactory,
Func<bool> canDragChecker = null,
DragDropEffects allowedEffects = DragDropEffects.Copy | DragDropEffects.Move)
{
if (dataFactory == null)
throw new ArgumentNullException(nameof(dataFactory));
return new LazyDragSource(dataFactory, canDragChecker, allowedEffects);
}
/// <summary>
/// Создает источник перетаскивания для коллекции элементов.
/// </summary>
/// <typeparam name="T">
/// Тип элементов в коллекции.
/// </typeparam>
/// <param name="items">
/// Коллекция элементов для перетаскивания.
/// </param>
/// <param name="itemSelector">
/// Функция выбора конкретного элемента для перетаскивания из коллекции.
/// </param>
/// <param name="allowedEffects">
/// Разрешенные эффекты перетаскивания.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDragSource"/> для коллекции элементов.
/// </returns>
/// <remarks>
/// Этот источник позволяет перетаскивать элементы из коллекции. Функция <paramref name="itemSelector"/>
/// определяет, какой именно элемент из коллекции будет перетаскиваться в текущем контексте.
/// </remarks>
public static IDragSource CreateCollectionSource<T>(
IEnumerable<T> items,
Func<IEnumerable<T>, T> itemSelector,
DragDropEffects allowedEffects = DragDropEffects.Copy | DragDropEffects.Move)
{
if (items == null)
throw new ArgumentNullException(nameof(items));
if (itemSelector == null)
throw new ArgumentNullException(nameof(itemSelector));
return new CollectionDragSource<T>(items, itemSelector, allowedEffects);
}
#endregion
#region Цели сброса
/// <summary>
/// Создает простую цель сброса, которая принимает данные любого типа.
/// </summary>
/// <param name="onDrop">
/// Обработчик, вызываемый при сбросе данных.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDropTarget"/>, который принимает любые данные.
/// </returns>
/// <remarks>
/// Эта цель подходит для простых сценариев, когда не требуется валидация типа данных.
/// </remarks>
public static IDropTarget CreateSimpleTarget(Action<DropInfo> onDrop)
{
if (onDrop == null)
throw new ArgumentNullException(nameof(onDrop));
return new SimpleDropTarget(onDrop);
}
/// <summary>
/// Создает цель сброса с фильтрацией по типу данных.
/// </summary>
/// <typeparam name="T">
/// Тип данных, которые может принимать цель.
/// </typeparam>
/// <param name="onDrop">
/// Обработчик, вызываемый при сбросе данных.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDropTarget"/>, который принимает только данные типа <typeparamref name="T"/>.
/// </returns>
/// <remarks>
/// Эта цель автоматически проверяет тип сбрасываемых данных и вызывает обработчик только
/// если данные могут быть приведены к указанному типу.
/// </remarks>
public static IDropTarget CreateTypedTarget<T>(Action<T, DropInfo> onDrop) where T : class
{
if (onDrop == null)
throw new ArgumentNullException(nameof(onDrop));
return new TypedDropTarget<T>(onDrop);
}
/// <summary>
/// Создает цель сброса с пользовательской логикой валидации.
/// </summary>
/// <param name="canAccept">
/// Функция проверки возможности приема данных.
/// </param>
/// <param name="onDrop">
/// Обработчик, вызываемый при сбросе данных.
/// </param>
/// <returns>
/// Экземпляр <see cref="IDropTarget"/> с пользовательской логикой валидации.
/// </returns>
/// <remarks>
/// Эта цель позволяет реализовать сложную логику валидации, выходящую за рамки простой проверки типа.
/// </remarks>
public static IDropTarget CreateConditionalTarget(Func<DropInfo, bool> canAccept, Action<DropInfo> onDrop)
{
if (canAccept == null)
throw new ArgumentNullException(nameof(canAccept));
if (onDrop == null)
throw new ArgumentNullException(nameof(onDrop));
return new ConditionalDropTarget(canAccept, onDrop);
}
#endregion
#region Вспомогательные методы
/// <summary>
/// Создает стандартные эффекты перетаскивания на основе модификаторов клавиатуры.
/// </summary>
/// <param name="controlKey">
/// Нажата ли клавиша Control.
/// </param>
/// <param name="shiftKey">
/// Нажата ли клавиша Shift.
/// </param>
/// <param name="altKey">
/// Нажата ли клавиша Alt.
/// </param>
/// <returns>
/// <see cref="DragDropEffects"/>, соответствующие комбинации клавиш.
/// </returns>
/// <remarks>
/// Стандартная логика:
/// <list type="bullet">
/// <item>Control + Shift: Link</item>
/// <item>Control: Copy</item>
/// <item>Shift: Move</item>
/// <item>Alt: Link</item>
/// <item>Без модификаторов: Move</item>
/// </list>
/// </remarks>
public static DragDropEffects GetEffectsFromKeys(bool controlKey, bool shiftKey, bool altKey)
{
return DragDropEffectsExtensions.GetEffectFromKeys(controlKey, shiftKey, altKey);
}
#endregion
}
/// <summary>
/// Опции для настройки сервиса перетаскивания.
/// </summary>
public class DragDropServiceOptions
{
/// <summary>
/// Получает или задает порог начала перетаскивания в пикселях.
/// </summary>
/// <value>
/// Минимальное расстояние, которое должен пройти курсор, чтобы началась операция перетаскивания.
/// Значение по умолчанию: 3.0.
/// </value>
public double DragStartThreshold { get; set; } = Constants.DragDropConstants.DefaultDragThreshold;
/// <summary>
/// Получает или задает максимальное время ожидания асинхронных операций в миллисекундах.
/// </summary>
/// <value>
/// Время в миллисекундах, после которого асинхронная операция будет прервана.
/// Значение по умолчанию: 5000.
/// </value>
public int AsyncOperationTimeout { get; set; } = Constants.DragDropConstants.DefaultAsyncTimeout;
/// <summary>
/// Получает или задает значение, указывающее, включены ли асинхронные операции.
/// </summary>
/// <value>
/// true, если асинхронные операции включены; в противном случае — false.
/// Значение по умолчанию: true.
/// </value>
public bool EnableAsyncOperations { get; set; } = true;
/// <summary>
/// Получает или задает интервал автоматической очистки неиспользуемых целей в минутах.
/// </summary>
/// <value>
/// Интервал в минутах, через который будут удаляться цели сброса, к которым не было обращений.
/// Значение по умолчанию: 10.
/// </value>
public int CleanupInterval { get; set; } = Constants.DragDropConstants.TargetLifetimeMinutes;
}
#region Внутренние реализации
internal class SimpleDragSource : IDragSource
{
private readonly object _data;
private readonly DragDropEffects _allowedEffects;
public SimpleDragSource(object data, DragDropEffects allowedEffects)
{
_data = data ?? throw new ArgumentNullException(nameof(data));
_allowedEffects = allowedEffects;
}
public Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
{
var dragInfo = new DragInfo(_data, _allowedEffects, startPosition, this);
return Task.FromResult<DragInfo?>(dragInfo);
}
public Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
internal class LazyDragSource : IDragSource
{
private readonly Func<object> _dataFactory;
private readonly Func<bool> _canDragChecker;
private readonly DragDropEffects _allowedEffects;
public LazyDragSource(Func<object> dataFactory, Func<bool> canDragChecker, DragDropEffects allowedEffects)
{
_dataFactory = dataFactory ?? throw new ArgumentNullException(nameof(dataFactory));
_canDragChecker = canDragChecker ?? (() => true);
_allowedEffects = allowedEffects;
}
public Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
{
if (!_canDragChecker())
return Task.FromResult<DragInfo?>(null);
var data = _dataFactory();
if (data == null)
return Task.FromResult<DragInfo?>(null);
var dragInfo = new DragInfo(data, _allowedEffects, startPosition, this);
return Task.FromResult<DragInfo?>(dragInfo);
}
public Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
internal class CollectionDragSource<T> : IDragSource
{
private readonly IEnumerable<T> _items;
private readonly Func<IEnumerable<T>, T> _itemSelector;
private readonly DragDropEffects _allowedEffects;
public CollectionDragSource(IEnumerable<T> items, Func<IEnumerable<T>, T> itemSelector, DragDropEffects allowedEffects)
{
_items = items ?? throw new ArgumentNullException(nameof(items));
_itemSelector = itemSelector ?? throw new ArgumentNullException(nameof(itemSelector));
_allowedEffects = allowedEffects;
}
public Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
{
var selectedItem = _itemSelector(_items);
if (selectedItem == null)
return Task.FromResult<DragInfo?>(null);
var dragInfo = new DragInfo(selectedItem, _allowedEffects, startPosition, this);
return Task.FromResult<DragInfo?>(dragInfo);
}
public Task OnDragCompletedAsync(DragInfo dragInfo, DragDropEffects effects, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
internal class SimpleDropTarget : IDropTarget
{
private readonly Action<DropInfo> _onDrop;
public SimpleDropTarget(Action<DropInfo> onDrop)
{
_onDrop = onDrop ?? throw new ArgumentNullException(nameof(onDrop));
}
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
return Task.FromResult(dropInfo.Data != null);
}
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (dropInfo.Data != null)
{
dropInfo.SuggestedEffects = DragDropEffects.Move;
}
return Task.CompletedTask;
}
public Task OnDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (dropInfo.Data != null)
{
_onDrop(dropInfo);
dropInfo.MarkAsHandled();
}
return Task.CompletedTask;
}
public Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
internal class TypedDropTarget<T> : IDropTarget where T : class
{
private readonly Action<T, DropInfo> _onDrop;
public TypedDropTarget(Action<T, DropInfo> onDrop)
{
_onDrop = onDrop ?? throw new ArgumentNullException(nameof(onDrop));
}
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
return Task.FromResult(dropInfo.Data is T);
}
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (dropInfo.Data is T)
{
dropInfo.SuggestedEffects = DragDropEffects.Move;
}
return Task.CompletedTask;
}
public Task OnDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (dropInfo.Data is T data)
{
_onDrop(data, dropInfo);
dropInfo.MarkAsHandled();
}
return Task.CompletedTask;
}
public Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
internal class ConditionalDropTarget : IDropTarget
{
private readonly Func<DropInfo, bool> _canAccept;
private readonly Action<DropInfo> _onDrop;
public ConditionalDropTarget(Func<DropInfo, bool> canAccept, Action<DropInfo> onDrop)
{
_canAccept = canAccept ?? throw new ArgumentNullException(nameof(canAccept));
_onDrop = onDrop ?? throw new ArgumentNullException(nameof(onDrop));
}
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
return Task.FromResult(_canAccept(dropInfo));
}
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (_canAccept(dropInfo))
{
dropInfo.SuggestedEffects = DragDropEffects.Move;
}
return Task.CompletedTask;
}
public Task OnDropAsync(DropInfo dropInfo, CancellationToken cancellationToken = default)
{
if (_canAccept(dropInfo))
{
_onDrop(dropInfo);
dropInfo.MarkAsHandled();
}
return Task.CompletedTask;
}
public Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
#endregion

View File

@@ -1,17 +1,4 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

View File

@@ -1,17 +1,4 @@
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Controls.Primitives;
using Microsoft.UI.Xaml.Data;
using Microsoft.UI.Xaml.Input;
using Microsoft.UI.Xaml.Media;
using Microsoft.UI.Xaml.Navigation;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
// To learn more about WinUI, the WinUI project structure,
// and more about our project templates, see: http://aka.ms/winui-project-info.

View File

@@ -1,5 +1,4 @@
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Lattice.Core.Geometry;
using Lattice.UI.Docking.Abstractions;
using Lattice.UI.Docking.Models;
using Lattice.UI.Docking.Services;

View File

@@ -1,7 +1,8 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Abstractions;
using Lattice.UI.DragDrop.Behaviors;
using Lattice.UI.DragDrop.WinUI.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Input;
@@ -13,29 +14,54 @@ namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Реализация поведения источника перетаскивания для элементов WinUI.
/// Наследуется от <see cref="DragSourceBehaviorBase{FrameworkElement}"/> для использования
/// общей логики управления операциями перетаскивания и интеграции с системой <see cref="Core.DragDrop"/>.
/// </summary>
/// <remarks>
/// <para>
/// Этот класс обрабатывает события мыши/тач и преобразует их в операции перетаскивания.
/// Он реализует интерфейс <see cref="IDragSource"/> для интеграции с системой перетаскивания.
/// Этот класс предоставляет конкретную реализацию для платформы WinUI, обрабатывая события
/// указателя (мышь, тач, перо) и преобразуя их в операции перетаскивания через центральный
/// сервис <see cref="IDragDropService"/>.
/// </para>
/// <para>
/// Поведение автоматически отслеживает начало перемещения мыши, проверяет порог
/// перетаскивания и инициирует операцию через <see cref="IDragDropService"/>.
/// Основные функции:
/// <list type="bullet">
/// <item>Обработка событий PointerPressed, PointerMoved, PointerReleased</item>
/// <item>Автоматическое отслеживание порога начала перетаскивания</item>
/// <item>Создание информации о перетаскивании на основе данных элемента</item>
/// <item>Интеграция с визуальной обратной связью через <see cref="WinUIDragDropHost"/></item>
/// <item>Преобразование координат между локальной системой элемента и экранными координатами</item>
/// </list>
/// </para>
/// <para>
/// Для использования необходимо:
/// <list type="number">
/// <item>Создать экземпляр поведения через фабрику <see cref="Factories.WinUIDragDropFactory.CreateDragSourceBehavior"/></item>
/// <item>Прикрепить к элементу с помощью метода <see cref="Attach"/></item>
/// <item>Указать данные для перетаскивания (опционально, по умолчанию используется DataContext)</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание поведения
/// var behavior = WinUIDragDropFactory.CreateDragSourceBehavior(dragDropService, host);
///
/// // Прикрепление к элементу
/// behavior.Attach(myElement, myData);
///
/// // Или через attached properties
/// &lt;Border x:Name="DragElement"
/// local:DragDropProperties.IsDragSource="True"
/// local:DragDropProperties.DragData="{Binding MyData}" /&gt;
/// </code>
/// </example>
/// </remarks>
public sealed class WinUIDragSourceBehavior : IDragSource
public sealed class WinUIDragSourceBehavior : DragSourceBehaviorBase<FrameworkElement>
{
#region Поля
private readonly IDragDropService _dragDropService;
private readonly WinUIDragDropHost _host;
private FrameworkElement? _element;
private readonly IDragDropHost _host;
private object? _dragData;
private Point _dragStartPosition;
private bool _isDragging;
private CancellationTokenSource? _cancellationTokenSource;
#endregion
@@ -44,14 +70,27 @@ public sealed class WinUIDragSourceBehavior : IDragSource
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDragSourceBehavior"/>.
/// </summary>
/// <param name="dragDropService">Сервис для управления операциями перетаскивания.</param>
/// <param name="host">Хост для отображения визуальных элементов.</param>
/// <param name="dragDropService">
/// Сервис управления операциями перетаскивания. Используется для координации
/// между источниками и целями перетаскивания.
/// </param>
/// <param name="host">
/// Хост для управления визуальными элементами перетаскивания. Обеспечивает
/// отображение визуальной обратной связи во время операции.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если любой из параметров равен null.
/// Выбрасывается, если <paramref name="dragDropService"/> или <paramref name="host"/>
/// равны null.
/// </exception>
public WinUIDragSourceBehavior(IDragDropService dragDropService, WinUIDragDropHost host)
/// <remarks>
/// Конструктор инициализирует базовый класс <see cref="DragSourceBehaviorBase{FrameworkElement}"/>
/// и сохраняет ссылки на необходимые сервисы для последующего использования.
/// </remarks>
public WinUIDragSourceBehavior(
Core.DragDrop.Services.IDragDropService dragDropService,
WinUIDragDropHost host)
: base(dragDropService)
{
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
_host = host ?? throw new ArgumentNullException(nameof(host));
}
@@ -60,207 +99,202 @@ public sealed class WinUIDragSourceBehavior : IDragSource
#region Публичные методы
/// <summary>
/// Прикрепляет поведение к указанному элементу.
/// Прикрепляет поведение к указанному элементу WinUI.
/// </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>
/// <remarks>
/// После вызова этого метода элемент начинает обрабатывать события перетаскивания.
/// <para>
/// После вызова этого метода:
/// <list type="bullet">
/// <item>Элементу автоматически подписываются обработчики событий указателя</item>
/// <item>Поведение начинает отслеживать взаимодействия с элементом</item>
/// <item>При превышении порога перетаскивания инициируется операция через <see cref="Core.DragDrop.Services.IDragDropService"/></item>
/// </list>
/// </para>
/// <para>
/// Для открепления поведения используйте метод <see cref="Detach"/>.
/// </para>
/// </remarks>
public void Attach(FrameworkElement element, object? dragData = null)
{
Detach();
if (element == null)
throw new ArgumentNullException(nameof(element));
_element = element ?? throw new ArgumentNullException(nameof(element));
_dragData = dragData ?? element.DataContext ?? element.Tag;
SubscribeToEvents();
AssociatedElement = element;
}
/// <summary>
/// Открепляет поведение от элемента.
/// Открепляет поведение от текущего элемента.
/// </summary>
public void Detach()
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Отписывается от всех событий элемента</item>
/// <item>Сбрасывает внутреннее состояние</item>
/// <item>Освобождает ссылки на связанные объекты</item>
/// </list>
/// </para>
/// <para>
/// Если в момент вызова активна операция перетаскивания, она будет автоматически отменена.
/// </para>
/// <para>
/// После вызова этого метода поведение может быть повторно прикреплено к другому элементу.
/// </para>
/// </remarks>
public new void Detach()
{
if (_element == null) return;
UnsubscribeFromEvents();
if (_isDragging)
{
_dragDropService.CancelDragAsync().ConfigureAwait(false);
}
_element = null;
base.Detach();
_dragData = null;
_isDragging = false;
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
#endregion
#region Обработчики событий
#region Реализация абстрактных методов DragSourceBehaviorBase<FrameworkElement>
private void SubscribeToEvents()
/// <inheritdoc/>
protected override void SubscribeToEvents(FrameworkElement element)
{
if (_element == null) return;
if (element == null) return;
_element.PointerPressed += OnPointerPressed;
_element.PointerMoved += OnPointerMoved;
_element.PointerReleased += OnPointerReleased;
_element.PointerCanceled += OnPointerCanceled;
_element.PointerCaptureLost += OnPointerCaptureLost;
element.PointerPressed += OnPointerPressed;
element.PointerMoved += OnPointerMoved;
element.PointerReleased += OnPointerReleased;
element.PointerCanceled += OnPointerCanceled;
element.PointerCaptureLost += OnPointerCaptureLost;
}
private void UnsubscribeFromEvents()
/// <inheritdoc/>
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (_element == null) return;
if (element == null) return;
_element.PointerPressed -= OnPointerPressed;
_element.PointerMoved -= OnPointerMoved;
_element.PointerReleased -= OnPointerReleased;
_element.PointerCanceled -= OnPointerCanceled;
_element.PointerCaptureLost -= OnPointerCaptureLost;
element.PointerPressed -= OnPointerPressed;
element.PointerMoved -= OnPointerMoved;
element.PointerReleased -= OnPointerReleased;
element.PointerCanceled -= OnPointerCanceled;
element.PointerCaptureLost -= OnPointerCaptureLost;
}
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
/// <inheritdoc/>
protected override Point ConvertToScreenCoordinates(Point point)
{
if (_element == null || _isDragging) return;
var point = e.GetCurrentPoint(_element);
_dragStartPosition = new Point(point.Position.X, point.Position.Y);
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
}
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (_element == null || _isDragging || _cancellationTokenSource?.IsCancellationRequested == true)
return;
var point = e.GetCurrentPoint(_element);
var currentPosition = new Point(point.Position.X, point.Position.Y);
var distance = CalculateDistance(_dragStartPosition, currentPosition);
if (distance > _dragDropService.DragStartThreshold)
{
await StartDragAsync(currentPosition);
}
}
private void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
ResetState();
}
private void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
{
ResetState();
}
private void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
ResetState();
}
private double CalculateDistance(Point p1, Point p2)
{
var dx = p2.X - p1.X;
var dy = p2.Y - p1.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
/// <summary>
/// Преобразует координаты элемента в экранные координаты.
/// </summary>
/// <param name="point">Точка в координатах элемента.</param>
/// <returns>Точка в экранных координатах.</returns>
private Point ConvertToScreenCoordinates(Point point)
{
if (_element == null) return point;
try
{
var transform = _element.TransformToVisual(Window.Current.Content);
var screenPoint = transform.TransformPoint(new Windows.Foundation.Point(point.X, point.Y));
return new Point(screenPoint.X, screenPoint.Y);
}
catch
{
if (AssociatedElement == null)
return point;
}
}
private async Task StartDragAsync(Point position)
{
if (_element == null || _dragData == null || _isDragging) return;
try
{
var screenPosition = ConvertToScreenCoordinates(position);
var started = await _dragDropService.StartDragAsync(this, screenPosition);
_isDragging = started;
}
catch
{
ResetState();
}
}
private void ResetState()
{
_isDragging = false;
_dragStartPosition = default;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
return WinUIWindowHelper.ConvertToScreenCoordinates(AssociatedElement, point);
}
#endregion
#region IDragSource Implementation
#region Реализация интерфейса IDragSource
public async Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
/// <inheritdoc/>
public override async Task<DragInfo?> TryStartDragAsync(
Point startPosition,
CancellationToken cancellationToken = default)
{
if (_element == null || _dragData == null)
if (AssociatedElement == null || _dragData == null)
return null;
try
{
// Создаем информацию о перетаскивании
var dragInfo = new DragInfo(
data: _dragData,
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy |
Core.DragDrop.Enums.DragDropEffects.Move,
Core.DragDrop.Enums.DragDropEffects.Move |
Core.DragDrop.Enums.DragDropEffects.Link,
startPosition: startPosition,
source: this
);
return dragInfo;
// Добавляем дополнительные параметры
dragInfo.SetParameter("SourceElement", AssociatedElement);
dragInfo.SetParameter("SourceType", AssociatedElement.GetType().Name);
return await Task.FromResult(dragInfo);
}
catch
catch (Exception ex)
{
// Логирование ошибки создания информации о перетаскивании
System.Diagnostics.Debug.WriteLine(
$"Ошибка создания DragInfo: {ex.Message}");
return null;
}
}
public Task OnDragCompletedAsync(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects, CancellationToken cancellationToken = default)
#endregion
#region Обработчики событий WinUI
private async void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
_isDragging = false;
return Task.CompletedTask;
if (AssociatedElement == null) return;
var point = e.GetCurrentPoint(AssociatedElement);
var position = new Point(point.Position.X, point.Position.Y);
await OnInteractionStarted(position);
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
_isDragging = false;
return Task.CompletedTask;
if (AssociatedElement == null) return;
var point = e.GetCurrentPoint(AssociatedElement);
var position = new Point(point.Position.X, point.Position.Y);
await OnInteractionMoved(position);
}
private async void OnPointerReleased(object sender, PointerRoutedEventArgs e)
{
await OnInteractionEnded();
}
private async void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
{
await OnInteractionCancelled();
}
private async void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
await OnInteractionCancelled();
}
#endregion
#region Переопределение виртуальных методов
/// <inheritdoc/>
protected override void OnDragCompleted(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
{
base.OnDragCompleted(dragInfo, effects);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, обновление состояния элемента после успешного перетаскивания
}
/// <inheritdoc/>
protected override void OnDragCancelled(DragInfo dragInfo)
{
base.OnDragCancelled(dragInfo);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, восстановление визуального состояния элемента после отмены
}
#endregion

View File

@@ -1,9 +1,11 @@
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.WinUI.Services;
using Lattice.UI.DragDrop.Abstractions;
using Lattice.UI.DragDrop.Behaviors;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
@@ -11,27 +13,52 @@ namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Реализация поведения цели сброса для элементов WinUI.
/// Наследуется от <see cref="DropTargetBehaviorBase{FrameworkElement}"/> для использования
/// общей логики регистрации целей и обработки операций сброса.
/// </summary>
/// <remarks>
/// <para>
/// Этот класс обрабатывает события перетаскивания WinUI и преобразует их в вызовы
/// методов интерфейса <see cref="IDropTarget"/>.
/// Этот класс предоставляет конкретную реализацию для платформы WinUI, обрабатывая события
/// перетаскивания WinUI и преобразуя их в вызовы методов интерфейса <see cref="Core.DragDrop.Abstractions.IDropTarget"/>.
/// </para>
/// <para>
/// Поведение автоматически регистрирует элемент в системе перетаскивания,
/// обновляет его границы при изменении размера и обрабатывает все этапы операции сброса.
/// Основные функции:
/// <list type="bullet">
/// <item>Автоматическая регистрация в <see cref="IDragDropService"/> при прикреплении к элементу</item>
/// <item>Обработка событий DragEnter, DragOver, DragLeave, Drop</item>
/// <item>Автоматическое обновление границ элемента при изменении размера или позиции</item>
/// <item>Поддержка фильтрации принимаемых типов данных</item>
/// <item>Интеграция с визуальной обратной связью</item>
/// </list>
/// </para>
/// <para>
/// Для использования необходимо:
/// <list type="number">
/// <item>Создать экземпляр поведения через фабрику <see cref="Factories.WinUIDragDropFactory.CreateDropTargetBehavior"/></item>
/// <item>Прикрепить к элементу с помощью метода <see cref="Attach"/></item>
/// <item>Настроить фильтры принимаемых данных (опционально)</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание поведения с фильтрацией типов
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior(dragDropService, host);
/// behavior.AcceptTypes(typeof(MyDataModel), typeof(string));
/// behavior.Attach(myDropArea);
///
/// // Или через attached properties
/// &lt;Border x:Name="DropArea"
/// local:DragDropProperties.IsDropTarget="True" /&gt;
/// </code>
/// </example>
/// </remarks>
public sealed class WinUIDropTargetBehavior : IDropTarget
public sealed class WinUIDropTargetBehavior : DropTargetBehaviorBase<FrameworkElement>
{
#region Поля
private readonly Lattice.Core.DragDrop.Services.IDragDropService _dragDropService;
private readonly WinUIDragDropHost _host;
private FrameworkElement? _element;
private string? _registrationId;
private Rect _currentBounds;
private readonly IDragDropHost _host;
private readonly List<Type> _acceptedTypes = new();
private readonly HashSet<string> _acceptedFormats = new();
#endregion
@@ -40,14 +67,28 @@ public sealed class WinUIDropTargetBehavior : IDropTarget
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <param name="dragDropService">Сервис для управления операциями перетаскивания.</param>
/// <param name="host">Хост для отображения визуальных элементов.</param>
/// <param name="dragDropService">
/// Сервис управления операциями перетаскивания. Используется для регистрации
/// цели и координации операций сброса.
/// </param>
/// <param name="host">
/// Хост для управления визуальной обратной связью. Обеспечивает отображение
/// индикаторов возможности сброса.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если любой из параметров равен null.
/// Выбрасывается, если <paramref name="dragDropService"/> или <paramref name="host"/>
/// равны null.
/// </exception>
public WinUIDropTargetBehavior(Lattice.Core.DragDrop.Services.IDragDropService dragDropService, WinUIDragDropHost host)
/// <remarks>
/// Конструктор инициализирует базовый класс и сохраняет ссылки на сервисы.
/// По умолчанию цель принимает все типы данных. Для настройки фильтрации
/// используйте методы <see cref="AcceptTypes"/> и <see cref="AcceptFormats"/>.
/// </remarks>
public WinUIDropTargetBehavior(
Core.DragDrop.Services.IDragDropService dragDropService,
IDragDropHost host)
: base(dragDropService)
{
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
_host = host ?? throw new ArgumentNullException(nameof(host));
}
@@ -56,106 +97,301 @@ public sealed class WinUIDropTargetBehavior : IDropTarget
#region Публичные методы
/// <summary>
/// Прикрепляет поведение к указанному элементу.
/// Прикрепляет поведение к указанному элементу WinUI.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <param name="element">
/// Элемент <see cref="FrameworkElement"/>, который должен стать целью сброса.
/// Не может быть null.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// После вызова этого метода:
/// 1. Элементу устанавливается <see cref="UIElement.AllowDrop"/> = true
/// 2. Поведение подписывается на события перетаскивания
/// 3. Элемент регистрируется в системе перетаскивания
/// <list type="bullet">
/// <item>Элементу устанавливается свойство <see cref="UIElement.AllowDrop"/> = true</item>
/// <item>Поведение подписывается на события перетаскивания WinUI</item>
/// <item>Элемент регистрируется в системе перетаскивания с текущими границами</item>
/// <item>Начинается отслеживание изменений размера и позиции элемента</item>
/// </list>
/// </para>
/// <para>
/// Для открепления поведения используйте метод <see cref="Detach"/>.
/// </para>
/// </remarks>
public void Attach(FrameworkElement element)
{
Detach();
if (element == null)
throw new ArgumentNullException(nameof(element));
_element = element ?? throw new ArgumentNullException(nameof(element));
element.AllowDrop = true;
SubscribeToEvents();
UpdateBounds();
RegisterToService();
AssociatedElement = element;
}
/// <summary>
/// Открепляет поведение от элемента.
/// Настраивает поведение для приема только указанных типов данных.
/// </summary>
public void Detach()
/// <param name="types">
/// Типы данных, которые может принимать цель. Если пусто, принимаются все типы.
/// </param>
/// <remarks>
/// <para>
/// Этот метод позволяет ограничить типы данных, которые могут быть сброшены на цель.
/// Проверка выполняется в методе <see cref="CanAcceptDropAsync"/> путем сравнения
/// типа сбрасываемых данных с указанными типами.
/// </para>
/// <para>
/// Если метод не вызывался или передан пустой список, цель будет принимать данные любого типа.
/// </para>
/// <example>
/// <code>
/// // Принимать только строки и объекты MyModel
/// behavior.AcceptTypes(typeof(string), typeof(MyModel));
/// </code>
/// </example>
/// </remarks>
public void AcceptTypes(params Type[] types)
{
if (_element == null) return;
_acceptedTypes.Clear();
if (types != null && types.Length > 0)
{
_acceptedTypes.AddRange(types);
}
}
UnsubscribeFromEvents();
UnregisterFromService();
/// <summary>
/// Настраивает поведение для приема только указанных форматов данных.
/// </summary>
/// <param name="formats">
/// Форматы данных (например, "Text", "Bitmap", "FileDrop"), которые может принимать цель.
/// Если пусто, формат не проверяется.
/// </param>
/// <remarks>
/// <para>
/// Этот метод позволяет ограничить форматы данных, которые могут быть сброшены на цель.
/// Актуально для межпроцессного перетаскивания или работы с системными форматами.
/// </para>
/// <para>
/// Если метод не вызывался или передан пустой список, проверка формата не выполняется.
/// </para>
/// </remarks>
public void AcceptFormats(params string[] formats)
{
_acceptedFormats.Clear();
if (formats != null && formats.Length > 0)
{
foreach (var format in formats)
{
_acceptedFormats.Add(format);
}
}
}
_element.AllowDrop = false;
_element = null;
_currentBounds = Rect.Empty;
/// <summary>
/// Открепляет поведение от текущего элемента.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод выполняет следующие действия:
/// <list type="bullet">
/// <item>Отписывается от всех событий элемента</item>
/// <item>Отменяет регистрацию цели в системе перетаскивания</item>
/// <item>Сбрасывает свойство <see cref="UIElement.AllowDrop"/> = false</item>
/// <item>Освобождает ссылки на связанные объекты</item>
/// </list>
/// </para>
/// <para>
/// После вызова этого метода поведение может быть повторно прикреплено к другому элементу.
/// </para>
/// </remarks>
public new void Detach()
{
if (AssociatedElement != null)
{
AssociatedElement.AllowDrop = false;
}
base.Detach();
}
#endregion
#region Обработчики событий
#region Реализация абстрактных методов DropTargetBehaviorBase<FrameworkElement>
private void SubscribeToEvents()
/// <inheritdoc/>
protected override void SubscribeToEvents(FrameworkElement element)
{
if (_element == null) return;
if (element == null) return;
_element.DragEnter += OnDragEnter;
_element.DragOver += OnDragOver;
_element.DragLeave += OnDragLeave;
_element.Drop += OnDrop;
_element.SizeChanged += OnSizeChanged;
element.DragEnter += OnDragEnter;
element.DragOver += OnDragOver;
element.DragLeave += OnDragLeave;
element.Drop += OnDrop;
element.SizeChanged += OnSizeChanged;
element.LayoutUpdated += OnLayoutUpdated;
}
private void UnsubscribeFromEvents()
/// <inheritdoc/>
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (_element == null) return;
if (element == null) return;
_element.DragEnter -= OnDragEnter;
_element.DragOver -= OnDragOver;
_element.DragLeave -= OnDragLeave;
_element.Drop -= OnDrop;
_element.SizeChanged -= OnSizeChanged;
element.DragEnter -= OnDragEnter;
element.DragOver -= OnDragOver;
element.DragLeave -= OnDragLeave;
element.Drop -= OnDrop;
element.SizeChanged -= OnSizeChanged;
element.LayoutUpdated -= OnLayoutUpdated;
}
private async void OnDragEnter(object sender, DragEventArgs e)
/// <inheritdoc/>
protected override Rect GetScreenBounds(FrameworkElement element)
{
if (_element == null) return;
if (element == null || !element.IsLoaded)
return Rect.Empty;
try
{
var position = e.GetPosition(_element);
var window = Window.Current;
if (window?.Content == null)
return Rect.Empty;
// Преобразуем локальные координаты элемента в координаты окна
var transform = element.TransformToVisual(window.Content);
var position = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
return new Rect(
position.X,
position.Y,
element.ActualWidth,
element.ActualHeight
);
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine(
$"Ошибка получения границ элемента: {ex.Message}");
return Rect.Empty;
}
}
#endregion
#region Реализация интерфейса IDropTarget
/// <inheritdoc/>
public override async Task<bool> CanAcceptDropAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
// Проверяем, есть ли данные для сброса
if (dropInfo.Data == null)
return false;
// Проверяем фильтр по типам
if (_acceptedTypes.Count > 0)
{
var dataType = dropInfo.Data.GetType();
if (!_acceptedTypes.Any(t => t.IsAssignableFrom(dataType)))
{
return false;
}
}
// Проверяем фильтр по форматам (если данные предоставляют информацию о формате)
if (_acceptedFormats.Count > 0 && dropInfo.Data is Windows.ApplicationModel.DataTransfer.DataPackageView dataView)
{
var availableFormats = dataView.AvailableFormats;
if (!_acceptedFormats.Any(f => availableFormats.Contains(f)))
{
return false;
}
}
// Дополнительная проверка может быть добавлена в производных классах
return await Task.FromResult(true);
}
/// <inheritdoc/>
public override async Task OnDragOverAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
await base.OnDragOverAsync(dropInfo, cancellationToken);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, обновление визуальной обратной связи через хост
}
/// <inheritdoc/>
public override async Task OnDropAsync(
DropInfo dropInfo,
CancellationToken cancellationToken = default)
{
// Базовая реализация вызывает CanAcceptDropAsync и помечает как обработанное
if (await CanAcceptDropAsync(dropInfo, cancellationToken))
{
dropInfo.MarkAsHandled();
// Здесь может быть добавлена логика обработки сброшенных данных
// Например, вызов события или обновление модели данных
}
}
/// <inheritdoc/>
public override async Task OnDragLeaveAsync(CancellationToken cancellationToken = default)
{
await base.OnDragLeaveAsync(cancellationToken);
// Дополнительная логика для WinUI может быть добавлена здесь
// Например, скрытие визуальной обратной связи
}
#endregion
#region Обработчики событий WinUI
private async void OnDragEnter(object sender, DragEventArgs e)
{
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
e.Handled = true;
}
}
catch { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDragEnter: {ex.Message}");
}
}
private async void OnDragOver(object sender, DragEventArgs e)
{
if (_element == null) return;
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(_element);
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
await OnDragOverAsync(dropInfo);
e.Handled = true;
}
e.Handled = true;
}
catch { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDragOver: {ex.Message}");
}
}
private async void OnDragLeave(object sender, DragEventArgs e)
@@ -165,131 +401,114 @@ public sealed class WinUIDropTargetBehavior : IDropTarget
private async void OnDrop(object sender, DragEventArgs e)
{
if (_element == null) return;
if (AssociatedElement == null) return;
try
{
var position = e.GetPosition(_element);
var position = e.GetPosition(AssociatedElement);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
await OnDropAsync(dropInfo);
dropInfo.MarkAsHandled();
e.Handled = true;
}
}
catch { }
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка в OnDrop: {ex.Message}");
}
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateBounds();
OnElementLayoutChanged();
}
private void OnLayoutUpdated(object sender, object e)
{
OnElementLayoutChanged();
}
#endregion
#region Вспомогательные методы
/// <summary>
/// Создает объект <see cref="DropInfo"/> на основе события перетаскивания WinUI.
/// </summary>
/// <param name="e">Аргументы события перетаскивания WinUI.</param>
/// <param name="position">Локальная позиция курсора относительно элемента.</param>
/// <returns>
/// Экземпляр <see cref="DropInfo"/>, содержащий информацию о потенциальном сбросе.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод извлекает данные из события перетаскивания и преобразует их
/// в формат, понятный системе <see cref="Core.DragDrop"/>.
/// </para>
/// <para>
/// Поддерживаются как пользовательские данные (через свойство "DragData"),
/// так и стандартные форматы данных WinUI.
/// </para>
/// </remarks>
private DropInfo CreateDropInfo(DragEventArgs e, Point position)
{
object? data = null;
// Пытаемся получить пользовательские данные
if (e.DataView.Properties.TryGetValue("DragData", out var dragData))
{
data = dragData;
}
// Или получаем данные из DataPackage
else if (e.DataView.Contains(Windows.ApplicationModel.DataTransfer.StandardDataFormats.Text))
{
// Для текстовых данных можем установить асинхронную загрузку
data = new AsyncDataProvider(async () =>
{
return await e.DataView.GetTextAsync();
});
}
// Определяем разрешенные эффекты на основе модификаторов клавиатуры
var allowedEffects = Core.DragDrop.Enums.DragDropEffects.None;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Copy;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Move))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Move;
if (e.AllowedOperations.HasFlag(Windows.ApplicationModel.DataTransfer.DataPackageOperation.Link))
allowedEffects |= Core.DragDrop.Enums.DragDropEffects.Link;
return new DropInfo(
data: data,
position: position,
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy |
Core.DragDrop.Enums.DragDropEffects.Move,
target: _element
allowedEffects: allowedEffects,
target: this
);
}
/// <summary>
/// Получает границы элемента в экранных координатах.
/// </summary>
/// <returns>Прямоугольник с границами элемента или <see cref="Rect.Empty"/>, если
/// элемент не загружен или не может быть преобразован.</returns>
private Rect GetScreenBounds()
{
if (_element == null || !_element.IsLoaded)
return Rect.Empty;
try
{
var transform = _element.TransformToVisual(Window.Current.Content);
var position = transform.TransformPoint(new Windows.Foundation.Point(0, 0));
return new Rect(
position.X,
position.Y,
_element.ActualWidth,
_element.ActualHeight
);
}
catch
{
return Rect.Empty;
}
}
private void UpdateBounds()
{
if (_element == null) return;
_currentBounds = GetScreenBounds();
if (_registrationId != null && _currentBounds != Rect.Empty)
{
_dragDropService.UpdateDropTargetBounds(_registrationId, _currentBounds);
}
}
private void RegisterToService()
{
if (_element == null) return;
_currentBounds = GetScreenBounds();
if (_currentBounds != Rect.Empty && _registrationId == null)
{
_registrationId = _dragDropService.RegisterDropTarget(this, _currentBounds);
}
}
private void UnregisterFromService()
{
if (_registrationId != null)
{
_dragDropService.UnregisterDropTarget(_registrationId);
_registrationId = null;
}
}
#endregion
}
#region IDropTarget Implementation
/// <summary>
/// Предоставляет асинхронный доступ к данным перетаскивания.
/// </summary>
/// <remarks>
/// Этот класс используется для отложенной загрузки данных перетаскивания,
/// что особенно важно для больших данных или данных, требующих обработки.
/// </remarks>
internal class AsyncDataProvider
{
private readonly Func<Task<object>> _dataLoader;
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken ct = default)
public AsyncDataProvider(Func<Task<object>> dataLoader)
{
return Task.FromResult(true); // Принимаем все по умолчанию
_dataLoader = dataLoader;
}
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken ct = default)
public async Task<object> GetDataAsync()
{
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Move;
return Task.CompletedTask;
return await _dataLoader();
}
public Task OnDropAsync(DropInfo dropInfo, CancellationToken ct = default)
{
return Task.CompletedTask;
}
public Task OnDragLeaveAsync(CancellationToken ct = default)
{
return Task.CompletedTask;
}
#endregion
}

View File

@@ -0,0 +1,950 @@
using Lattice.Core.DragDrop.Services;
using Lattice.UI.DragDrop.WinUI.Behaviors;
using Lattice.UI.DragDrop.WinUI.Controls;
using Lattice.UI.DragDrop.WinUI.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using System;
namespace Lattice.UI.DragDrop.WinUI.Factories;
/// <summary>
/// Фабрика для создания и настройки компонентов системы перетаскивания WinUI.
/// Предоставляет удобные методы для быстрой интеграции drag-and-drop функциональности
/// в приложениях WinUI с поддержкой различных сценариев использования.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="WinUIDragDropFactory"/> служит высокоуровневым API для работы с системой
/// перетаскивания, инкапсулируя сложность инициализации и настройки компонентов.
/// </para>
/// <para>
/// Основные возможности фабрики:
/// <list type="bullet">
/// <item>Создание и инициализация менеджера перетаскивания</item>
/// <item>Генерация поведений для источников и целей перетаскивания</item>
/// <item>Создание визуальных элементов для обратной связи</item>
/// <item>Предварительные конфигурации для типовых сценариев</item>
/// <item>Вспомогательные методы для работы с XAML</item>
/// </list>
/// </para>
/// <para>
/// Фабрика поддерживает два подхода к использованию:
/// <list type="number">
/// <item><strong>Императивный подход</strong> - создание компонентов в коде C#</item>
/// <item><strong>Декларативный подход</strong> - использование attached properties в XAML</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Императивный подход
/// var manager = WinUIDragDropFactory.CreateManager(window);
/// manager.MakeDragSource(element, data);
/// manager.MakeDropTarget(dropArea);
///
/// // Декларативный подход (в XAML)
/// &lt;Border local:DragDropProperties.IsDragSource="True"
/// local:DragDropProperties.DragData="{Binding Item}" /&gt;
/// &lt;Border local:DragDropProperties.IsDropTarget="True" /&gt;
/// </code>
/// </example>
/// </remarks>
public static class WinUIDragDropFactory
{
#region Основные компоненты
/// <summary>
/// Создает и инициализирует менеджер перетаскивания для указанного окна WinUI.
/// </summary>
/// <param name="window">
/// Окно WinUI, для которого создается менеджер перетаскивания.
/// Не может быть null.
/// </param>
/// <returns>
/// Инициализированный экземпляр <see cref="WinUIDragDropManager"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="window"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод является основным способом получения менеджера перетаскивания.
/// Он создает (или возвращает существующий) экземпляр менеджера и инициализирует
/// его для работы с указанным окном.
/// </para>
/// <para>
/// Метод следует вызывать один раз при запуске приложения, обычно в конструкторе
/// главного окна или в методе <see cref="Application.OnLaunched"/>.
/// </para>
/// <example>
/// <code>
/// public partial class MainWindow : Window
/// {
/// public MainWindow()
/// {
/// InitializeComponent();
/// var manager = WinUIDragDropFactory.CreateManager(this);
/// }
/// }
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropManager CreateManager(Window window)
{
if (window == null)
throw new ArgumentNullException(nameof(window));
var manager = WinUIDragDropManager.Instance;
if (!manager.IsInitialized)
{
manager.Initialize(window);
}
return manager;
}
/// <summary>
/// Создает и инициализирует менеджер перетаскивания с пользовательскими настройками.
/// </summary>
/// <param name="window">
/// Окно WinUI, для которого создается менеджер перетаскивания.
/// </param>
/// <param name="configure">
/// Делегат для настройки параметров менеджера перед инициализацией.
/// Передает экземпляр <see cref="WinUIDragDropManager"/> для конфигурации.
/// </param>
/// <returns>
/// Инициализированный экземпляр <see cref="WinUIDragDropManager"/>.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="window"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод позволяет настроить параметры менеджера перед его инициализацией,
/// что полезно для тонкой настройки поведения системы перетаскивания.
/// </para>
/// <para>
/// Доступные для настройки параметры включают:
/// <list type="bullet">
/// <item><see cref="WinUIDragDropManager.DragVisualOffset"/> - смещение визуального элемента</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// var manager = WinUIDragDropFactory.CreateManager(window, m =>
/// {
/// m.DragVisualOffset = new Point(-15, -15); // Ближе к курсору
/// });
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropManager CreateManager(Window window, Action<WinUIDragDropManager> configure)
{
if (window == null)
throw new ArgumentNullException(nameof(window));
var manager = WinUIDragDropManager.Instance;
// Применяем настройки перед инициализацией
configure?.Invoke(manager);
if (!manager.IsInitialized)
{
manager.Initialize(window);
}
return manager;
}
/// <summary>
/// Создает хост для управления визуальными элементами перетаскивания.
/// </summary>
/// <param name="window">
/// Окно, к которому будет привязан хост.
/// </param>
/// <returns>
/// Экземпляр <see cref="WinUIDragDropHost"/>, готовый к использованию.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="window"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// Хост управляет отображением визуальных элементов во время операций перетаскивания,
/// включая drag-визуализации (элементы, следующие за курсором) и drop-превью
/// (подсветка областей сброса).
/// </para>
/// <para>
/// В большинстве случаев хост создается автоматически менеджером перетаскивания.
/// Этот метод полезен для продвинутых сценариев, когда требуется прямой контроль
/// над визуальной обратной связью.
/// </para>
/// <example>
/// <code>
/// var host = WinUIDragDropFactory.CreateHost(window);
/// // Настройка кастомной визуализации
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropHost CreateHost(Window window)
{
if (window == null)
throw new ArgumentNullException(nameof(window));
var host = new WinUIDragDropHost();
host.Initialize(window);
return host;
}
#endregion
#region Поведения (Behaviors)
/// <summary>
/// Создает поведение источника перетаскивания для элемента WinUI.
/// </summary>
/// <param name="dragDropService">
/// Сервис перетаскивания, который будет управлять операциями.
/// </param>
/// <param name="host">
/// Хост для отображения визуальных элементов.
/// </param>
/// <returns>
/// Экземпляр <see cref="WinUIDragSourceBehavior"/>, готовый к прикреплению к элементу.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если любой из параметров равен null.
/// </exception>
/// <remarks>
/// <para>
/// Созданное поведение необходимо прикрепить к элементу с помощью метода
/// <see cref="WinUIDragSourceBehavior.Attach"/>.
/// </para>
/// <para>
/// Этот метод полезен для продвинутых сценариев, когда требуется создавать поведения
/// вручную, например, при динамическом создании элементов интерфейса.
/// </para>
/// <example>
/// <code>
/// // Создание поведения вручную
/// var behavior = WinUIDragDropFactory.CreateDragSourceBehavior(service, host);
/// behavior.Attach(element, data);
/// </code>
/// </example>
/// </remarks>
public static WinUIDragSourceBehavior CreateDragSourceBehavior(
IDragDropService dragDropService,
WinUIDragDropHost host)
{
if (dragDropService == null)
throw new ArgumentNullException(nameof(dragDropService));
if (host == null)
throw new ArgumentNullException(nameof(host));
return new WinUIDragSourceBehavior(dragDropService, host);
}
/// <summary>
/// Создает поведение цели сброса для элемента WinUI.
/// </summary>
/// <param name="dragDropService">
/// Сервис перетаскивания, который будет управлять операциями.
/// </param>
/// <param name="host">
/// Хост для отображения визуальных элементов.
/// </param>
/// <returns>
/// Экземпляр <see cref="WinUIDropTargetBehavior"/>, готовый к прикреплению к элементу.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если любой из параметров равен null.
/// </exception>
/// <remarks>
/// <para>
/// Созданное поведение необходимо прикрепить к элементу с помощью метода
/// <see cref="WinUIDropTargetBehavior.Attach"/>.
/// </para>
/// <para>
/// Поведение можно дополнительно настроить с помощью методов
/// <see cref="WinUIDropTargetBehavior.AcceptTypes"/> и
/// <see cref="WinUIDropTargetBehavior.AcceptFormats"/> для фильтрации
/// принимаемых данных.
/// </para>
/// <example>
/// <code>
/// // Создание поведения вручную
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior(service, host);
/// behavior.AcceptTypes(typeof(string), typeof(MyModel));
/// behavior.Attach(dropArea);
/// </code>
/// </example>
/// </remarks>
public static WinUIDropTargetBehavior CreateDropTargetBehavior(
IDragDropService dragDropService,
WinUIDragDropHost host)
{
if (dragDropService == null)
throw new ArgumentNullException(nameof(dragDropService));
if (host == null)
throw new ArgumentNullException(nameof(host));
return new WinUIDropTargetBehavior(dragDropService, host);
}
/// <summary>
/// Создает поведение источника перетаскивания, используя сервисы из менеджера по умолчанию.
/// </summary>
/// <returns>
/// Экземпляр <see cref="WinUIDragSourceBehavior"/>, готовый к прикреплению к элементу.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер не инициализирован.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод использует <see cref="WinUIDragDropManager.Instance"/> для получения
/// сервиса перетаскивания и хоста, что упрощает создание поведений в контексте
/// уже инициализированной системы.
/// </para>
/// <para>
/// Перед использованием убедитесь, что менеджер инициализирован через метод
/// <see cref="CreateManager"/>.
/// </para>
/// <example>
/// <code>
/// // Инициализация менеджера
/// WinUIDragDropFactory.CreateManager(window);
///
/// // Создание поведения с использованием менеджера
/// var behavior = WinUIDragDropFactory.CreateDragSourceBehavior();
/// behavior.Attach(element, data);
/// </code>
/// </example>
/// </remarks>
public static WinUIDragSourceBehavior CreateDragSourceBehavior()
{
var manager = WinUIDragDropManager.Instance;
if (!manager.IsInitialized)
{
throw new InvalidOperationException(
"Менеджер не инициализирован. Вызовите CreateManager перед созданием поведений.");
}
return new WinUIDragSourceBehavior(manager.DragDropService, manager.Host);
}
/// <summary>
/// Создает поведение цели сброса, используя сервисы из менеджера по умолчанию.
/// </summary>
/// <returns>
/// Экземпляр <see cref="WinUIDropTargetBehavior"/>, готовый к прикреплению к элементу.
/// </returns>
/// <exception cref="InvalidOperationException">
/// Выбрасывается, если менеджер не инициализирован.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод использует <see cref="WinUIDragDropManager.Instance"/> для получения
/// сервиса перетаскивания и хоста, что упрощает создание поведений в контексте
/// уже инициализированной системы.
/// </para>
/// <para>
/// Поведение можно дополнительно настроить с помощью методов
/// <see cref="WinUIDropTargetBehavior.AcceptTypes"/> и
/// <see cref="WinUIDropTargetBehavior.AcceptFormats"/> для фильтрации
/// принимаемых данных.
/// </para>
/// <example>
/// <code>
/// // Инициализация менеджера
/// WinUIDragDropFactory.CreateManager(window);
///
/// // Создание поведения с использованием менеджера
/// var behavior = WinUIDragDropFactory.CreateDropTargetBehavior();
/// behavior.AcceptTypes(typeof(string));
/// behavior.Attach(dropArea);
/// </code>
/// </example>
/// </remarks>
public static WinUIDropTargetBehavior CreateDropTargetBehavior()
{
var manager = WinUIDragDropManager.Instance;
if (!manager.IsInitialized)
{
throw new InvalidOperationException(
"Менеджер не инициализирован. Вызовите CreateManager перед созданием поведений.");
}
return new WinUIDropTargetBehavior(manager.DragDropService, manager.Host);
}
#endregion
#region Визуальные элементы
/// <summary>
/// Создает визуальный элемент для отображения во время перетаскивания.
/// </summary>
/// <param name="dragData">
/// Данные, которые будут отображены в визуальном элементе.
/// Могут быть любого типа, поддерживаемого системой перетаскивания.
/// </param>
/// <param name="opacity">
/// Прозрачность элемента в диапазоне от 0.0 (полностью прозрачный) до 1.0 (полностью непрозрачный).
/// Значение по умолчанию: 0.8.
/// </param>
/// <returns>
/// Экземпляр <see cref="DragAdorner"/>, настроенный для отображения указанных данных.
/// </returns>
/// <remarks>
/// <para>
/// Созданный элемент можно использовать с методом <see cref="WinUIDragDropHost.ShowDragVisual"/>
/// для отображения во время операции перетаскивания.
/// </para>
/// <para>
/// Элемент автоматически адаптирует отображение в зависимости от типа данных:
/// <list type="bullet">
/// <item>Для строк отображается текстовое представление</item>
/// <item>Для изображений отображается миниатюра</item>
/// <item>Для пользовательских объектов используется DataTemplate или ToString()</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание визуального элемента
/// var adorner = WinUIDragDropFactory.CreateDragAdorner("Текст для перетаскивания");
///
/// // Отображение во время перетаскивания
/// host.ShowDragVisual(adorner, position);
/// </code>
/// </example>
/// </remarks>
public static DragAdorner CreateDragAdorner(object dragData, double opacity = 0.8)
{
return new DragAdorner
{
DragData = dragData ?? throw new ArgumentNullException(nameof(dragData)),
Opacity = Math.Clamp(opacity, 0.0, 1.0)
};
}
/// <summary>
/// Создает элемент предварительного просмотра для области сброса.
/// </summary>
/// <param name="color">
/// Цвет подсветки области. Если не указан, используется системный цвет акцента.
/// </param>
/// <param name="thickness">
/// Толщина границы подсветки в пикселях.
/// Значение по умолчанию: 2.0.
/// </param>
/// <returns>
/// Экземпляр <see cref="DropPreviewAdorner"/>, готовый к отображению.
/// </returns>
/// <remarks>
/// <para>
/// Этот элемент используется для визуального указания области, на которую можно
/// сбросить данные. Он отображается как подсветка границ целевого элемента с
/// поддержкой анимации появления и скрытия.
/// </para>
/// <para>
/// Элемент автоматически адаптирует свой внешний вид в зависимости от состояния:
/// <list type="bullet">
/// <item><strong>Normal</strong> - стандартное состояние</item>
/// <item><strong>Highlighted</strong> - подсветка при наведении</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание элемента подсветки
/// var preview = WinUIDragDropFactory.CreateDropPreviewAdorner(
/// Colors.Blue, // Цвет
/// 3.0 // Толщина границы
/// );
///
/// // Отображение подсветки
/// preview.Show(bounds);
/// </code>
/// </example>
/// </remarks>
public static DropPreviewAdorner CreateDropPreviewAdorner(
Windows.UI.Color? color = null,
double thickness = 2.0)
{
var adorner = new DropPreviewAdorner
{
PreviewThickness = Math.Max(thickness, 0.0)
};
if (color.HasValue)
{
adorner.PreviewColor = color.Value;
}
return adorner;
}
#endregion
#region Готовые конфигурации для типовых сценариев
/// <summary>
/// Создает полную систему перетаскивания для WinUI приложения.
/// </summary>
/// <param name="window">
/// Главное окно приложения.
/// </param>
/// <returns>
/// Кортеж, содержащий менеджер перетаскивания и сервис перетаскивания.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод создает все необходимые компоненты для работы перетаскивания в приложении
/// и возвращает их для дальнейшего использования.
/// </para>
/// <para>
/// Возвращаемый сервис можно использовать для создания дополнительных источников и целей
/// или для низкоуровневого управления операциями перетаскивания.
/// </para>
/// <example>
/// <code>
/// // Создание полной системы
/// var (manager, service) = WinUIDragDropFactory.CreateCompleteSystem(window);
///
/// // Использование менеджера для настройки элементов
/// manager.MakeDragSource(element, data);
///
/// // Использование сервиса для расширенного управления
/// var stats = service.GetStats();
/// </code>
/// </example>
/// </remarks>
public static (WinUIDragDropManager Manager, IDragDropService Service) CreateCompleteSystem(Window window)
{
var manager = CreateManager(window);
return (manager, manager.DragDropService);
}
/// <summary>
/// Создает систему перетаскивания, оптимизированную для переупорядочивания элементов в списках.
/// </summary>
/// <param name="window">
/// Главное окно приложения.
/// </param>
/// <returns>
/// Менеджер перетаскивания, настроенный для переупорядочивания элементов в списках.
/// </returns>
/// <remarks>
/// <para>
/// Эта конфигурация устанавливает оптимальные параметры для перетаскивания элементов
/// внутри списков и коллекций, таких как:
/// <list type="bullet">
/// <item>Изменение порядка элементов в ListView</item>
/// <item>Перемещение элементов между ItemsControl</item>
/// <item>Сортировка элементов в коллекциях</item>
/// </list>
/// </para>
/// <para>
/// Особенности конфигурации:
/// <list type="bullet">
/// <item>Уменьшенный порог начала перетаскивания для более быстрого отклика</item>
/// <item>Смещение визуального элемента для лучшего визуального выравнивания</item>
/// <item>Оптимизированная визуальная обратная связь</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание системы для переупорядочивания списков
/// var manager = WinUIDragDropFactory.CreateListReorderSystem(window);
///
/// // Настройка ListView для переупорядочивания
/// foreach (var item in myListView.Items)
/// {
/// if (item is FrameworkElement element)
/// {
/// manager.MakeDragSource(element, element.DataContext);
/// }
/// }
/// manager.MakeDropTarget(myListView);
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropManager CreateListReorderSystem(Window window)
{
var manager = CreateManager(window, m =>
{
// Уменьшенное смещение для лучшего визуального выравнивания в списках
m.DragVisualOffset = new Core.Geometry.Point(-15, -15);
});
return manager;
}
/// <summary>
/// Создает систему перетаскивания для работы с файлами и документами.
/// </summary>
/// <param name="window">
/// Главное окно приложения.
/// </param>
/// <returns>
/// Менеджер перетаскивания, настроенный для работы с файлами.
/// </returns>
/// <remarks>
/// <para>
/// Эта конфигурация оптимизирована для сценариев работы с файлами:
/// <list type="bullet">
/// <item>Перетаскивание файлов из проводника в приложение</item>
/// <item>Перемещение файлов между элементами интерфейса</item>
/// <item>Работа с большими объемами данных</item>
/// </list>
/// </para>
/// <para>
/// Особенности конфигурации:
/// <list type="bullet">
/// <item>Увеличенный порог перетаскивания для предотвращения случайных операций</item>
/// <item>Специальные визуальные эффекты, характерные для файловых операций</item>
/// <item>Оптимизация для работы с внешними источниками данных</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание системы для работы с файлами
/// var manager = WinUIDragDropFactory.CreateFileDragDropSystem(window);
///
/// // Настройка области для приема файлов
/// manager.MakeDropTarget(fileDropArea);
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropManager CreateFileDragDropSystem(Window window)
{
var manager = CreateManager(window, m =>
{
// Увеличенное смещение для файлов (имитация "переноса" файла)
m.DragVisualOffset = new Core.Geometry.Point(-25, -25);
});
return manager;
}
/// <summary>
/// Создает систему перетаскивания для графических редакторов и инструментов дизайна.
/// </summary>
/// <param name="window">
/// Главное окно приложения.
/// </param>
/// <returns>
/// Менеджер перетаскивания, настроенный для точного позиционирования.
/// </returns>
/// <remarks>
/// <para>
/// Эта конфигурация оптимизирована для приложений, требующих высокой точности
/// позиционирования, таких как:
/// <list type="bullet">
/// <item>Графические редакторы (Photoshop, Figma)</item>
/// <item>Инструменты проектирования интерфейсов</item>
/// <item>CAD-системы и приложения для 3D-моделирования</item>
/// </list>
/// </para>
/// <para>
/// Особенности конфигурации:
/// <list type="bullet">
/// <item>Минимальный порог начала перетаскивания для максимальной точности</item>
/// <item>Минималистичная визуализация для уменьшения визуального шума</item>
/// <item>Оптимизация для работы с высокоточными устройствами ввода (графические планшеты)</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Создание системы для графического редактора
/// var manager = WinUIDragDropFactory.CreateDesignToolSystem(window);
///
/// // Настройка инструментов для перетаскивания
/// manager.MakeDragSource(toolIcon, toolData);
/// manager.MakeDropTarget(canvas);
/// </code>
/// </example>
/// </remarks>
public static WinUIDragDropManager CreateDesignToolSystem(Window window)
{
var manager = CreateManager(window, m =>
{
// Минимальное смещение для точного позиционирования
m.DragVisualOffset = new Core.Geometry.Point(-10, -10);
});
return manager;
}
#endregion
#region Вспомогательные методы
/// <summary>
/// Настраивает элемент как источник перетаскивания с использованием указанного менеджера.
/// </summary>
/// <param name="manager">
/// Менеджер перетаскивания, который будет управлять операцией.
/// </param>
/// <param name="element">
/// Элемент WinUI, который должен стать источником перетаскивания.
/// </param>
/// <param name="dragData">
/// Данные для перетаскивания. Если не указаны, будут использованы DataContext или Tag элемента.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="element"/> равны null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод является удобной оберткой вокруг <see cref="WinUIDragDropManager.MakeDragSource"/>,
/// предоставляющей более лаконичный синтаксис.
/// </para>
/// <example>
/// <code>
/// // Использование вспомогательного метода
/// WinUIDragDropFactory.SetupAsDragSource(manager, myElement, myData);
///
/// // Эквивалентно:
/// manager.MakeDragSource(myElement, myData);
/// </code>
/// </example>
/// </remarks>
public static void SetupAsDragSource(WinUIDragDropManager manager, FrameworkElement element, object dragData = null)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (element == null)
throw new ArgumentNullException(nameof(element));
manager.MakeDragSource(element, dragData);
}
/// <summary>
/// Настраивает элемент как цель сброса с использованием указанного менеджера.
/// </summary>
/// <param name="manager">
/// Менеджер перетаскивания, который будет управлять операцией.
/// </param>
/// <param name="element">
/// Элемент WinUI, который должен стать целью сброса.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="element"/> равны null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод является удобной оберткой вокруг <see cref="WinUIDragDropManager.MakeDropTarget"/>,
/// предоставляющей более лаконичный синтаксис.
/// </para>
/// <example>
/// <code>
/// // Использование вспомогательного метода
/// WinUIDragDropFactory.SetupAsDropTarget(manager, myDropArea);
///
/// // Эквивалентно:
/// manager.MakeDropTarget(myDropArea);
/// </code>
/// </example>
/// </remarks>
public static void SetupAsDropTarget(WinUIDragDropManager manager, FrameworkElement element)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (element == null)
throw new ArgumentNullException(nameof(element));
manager.MakeDropTarget(element);
}
/// <summary>
/// Настраивает контейнер для поддержки переупорядочивания дочерних элементов.
/// </summary>
/// <param name="manager">
/// Менеджер перетаскивания.
/// </param>
/// <param name="container">
/// Контейнер (например, StackPanel, Grid или ItemsControl), дочерние элементы которого можно переупорядочивать.
/// </param>
/// <param name="childSelector">
/// Функция для получения данных перетаскивания из дочернего элемента.
/// Если не указана, используются DataContext дочерних элементов.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="manager"/> или <paramref name="container"/> равны null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод автоматически настраивает все дочерние элементы контейнера как источники перетаскивания,
/// а сам контейнер — как цель сброса, создавая функциональность переупорядочивания элементов.
/// </para>
/// <para>
/// Поддерживаемые типы контейнеров:
/// <list type="bullet">
/// <item><see cref="Panel"/> и его производные (StackPanel, Grid, Canvas)</item>
/// <item><see cref="ItemsControl"/> и его производные (ListView, ListBox)</item>
/// <item>Любые другие контейнеры с коллекцией дочерних элементов</item>
/// </list>
/// </para>
/// <example>
/// <code>
/// // Настройка StackPanel для переупорядочивания дочерних элементов
/// WinUIDragDropFactory.SetupReorderContainer(manager, myStackPanel);
///
/// // С кастомным селектором данных
/// WinUIDragDropFactory.SetupReorderContainer(manager, myListView,
/// element => ((FrameworkElement)element).DataContext);
/// </code>
/// </example>
/// </remarks>
public static void SetupReorderContainer(
WinUIDragDropManager manager,
FrameworkElement container,
Func<FrameworkElement, object> childSelector = null)
{
if (manager == null)
throw new ArgumentNullException(nameof(manager));
if (container == null)
throw new ArgumentNullException(nameof(container));
// Настраиваем контейнер как цель сброса
manager.MakeDropTarget(container);
// Настраиваем дочерние элементы как источники перетаскивания
if (container is Panel panel)
{
SetupPanelChildren(manager, panel, childSelector);
}
else if (container is ItemsControl itemsControl)
{
SetupItemsControlChildren(manager, itemsControl, childSelector);
}
}
/// <summary>
/// Настраивает дочерние элементы Panel как источники перетаскивания.
/// </summary>
private static void SetupPanelChildren(
WinUIDragDropManager manager,
Panel panel,
Func<FrameworkElement, object> childSelector)
{
foreach (var child in panel.Children)
{
if (child is FrameworkElement element)
{
var data = childSelector?.Invoke(element) ?? element.DataContext ?? element.Tag;
manager.MakeDragSource(element, data);
}
}
}
/// <summary>
/// Настраивает элементы ItemsControl как источники перетаскивания.
/// </summary>
private static void SetupItemsControlChildren(
WinUIDragDropManager manager,
ItemsControl itemsControl,
Func<FrameworkElement, object> childSelector)
{
// Для ItemsControl нам нужно работать с ItemContainerGenerator
// В реальной реализации здесь должна быть более сложная логика
// для обработки виртуализации и динамических элементов
foreach (var item in itemsControl.Items)
{
if (item is FrameworkElement element)
{
var data = childSelector?.Invoke(element) ?? element.DataContext ?? element.Tag;
manager.MakeDragSource(element, data);
}
}
}
#endregion
#region Методы для работы с XAML
/// <summary>
/// Настраивает attached properties для элемента источника перетаскивания.
/// </summary>
/// <param name="element">
/// Элемент, который должен стать источником перетаскивания.
/// </param>
/// <param name="dragData">
/// Данные для перетаскивания.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод устанавливает attached properties, которые активируют поведение перетаскивания
/// при использовании в XAML. Эквивалентно установке свойств IsDragSource и DragData в XAML.
/// </para>
/// <para>
/// Метод полезен для динамической настройки элементов в коде C# при сохранении
/// декларативного стиля программирования.
/// </para>
/// <example>
/// <code>
/// // Настройка элемента в коде C#
/// WinUIDragDropFactory.SetupDragSourceInXaml(myElement, myData);
///
/// // Эквивалентно в XAML:
/// &lt;Border local:DragDropProperties.IsDragSource="True"
/// local:DragDropProperties.DragData="{Binding MyData}" /&gt;
/// </code>
/// </example>
/// </remarks>
public static void SetupDragSourceInXaml(FrameworkElement element, object dragData)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
// Устанавливаем attached properties
element.SetValue(DragDropProperties.IsDragSourceProperty, true);
if (dragData != null)
{
element.SetValue(DragDropProperties.DragDataProperty, dragData);
}
}
/// <summary>
/// Настраивает attached properties для элемента цели сброса.
/// </summary>
/// <param name="element">
/// Элемент, который должен стать целью сброса.
/// </param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод устанавливает attached properties, которые активируют поведение цели сброса
/// при использовании в XAML. Эквивалентно установке свойства IsDropTarget в XAML.
/// </para>
/// <para>
/// Метод полезен для динамической настройки элементов в коде C# при сохранении
/// декларативного стиля программирования.
/// </para>
/// <example>
/// <code>
/// // Настройка элемента в коде C#
/// WinUIDragDropFactory.SetupDropTargetInXaml(myDropArea);
///
/// // Эквивалентно в XAML:
/// &lt;Border local:DragDropProperties.IsDropTarget="True" /&gt;
/// </code>
/// </example>
/// </remarks>
public static void SetupDropTargetInXaml(FrameworkElement element)
{
if (element == null)
throw new ArgumentNullException(nameof(element));
element.SetValue(DragDropProperties.IsDropTargetProperty, true);
}
#endregion
}

View File

@@ -0,0 +1,89 @@
using Lattice.Core.Geometry;
using Microsoft.UI.Xaml;
using System;
using System.Runtime.InteropServices;
/// <summary>
/// Вспомогательный класс для получения экранных координат в WinUI 3.
/// Использует P/Invoke для доступа к нативным API Windows.
/// </summary>
internal static class WinUIWindowHelper
{
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
public POINT(int x, int y)
{
X = x;
Y = y;
}
}
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint);
[DllImport("user32.dll")]
public static extern IntPtr WindowFromPoint(POINT point);
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
/// <summary>
/// Преобразует координаты элемента в экранные координаты.
/// </summary>
public static Point ConvertToScreenCoordinates(FrameworkElement element, Point localPoint)
{
if (element == null || !element.IsLoaded)
return localPoint;
try
{
var window = Window.Current;
if (window == null)
return localPoint;
// Получаем хэндл окна
var hwnd = GetWindowHandle(window);
if (hwnd == IntPtr.Zero)
return localPoint;
// Преобразуем координаты элемента в координаты окна
var transform = element.TransformToVisual(window.Content);
var windowPoint = transform.TransformPoint(
new Windows.Foundation.Point(localPoint.X, localPoint.Y));
// Преобразуем в POINT для P/Invoke
var point = new POINT(
(int)Math.Round(windowPoint.X),
(int)Math.Round(windowPoint.Y));
// Преобразуем клиентские координаты в экранные
if (ClientToScreen(hwnd, ref point))
{
return new Point(point.X, point.Y);
}
return localPoint;
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"Ошибка преобразования координат: {ex.Message}");
return localPoint;
}
}
/// <summary>
/// Получает хэндл окна WinUI.
/// </summary>
private static IntPtr GetWindowHandle(Window window)
{
// В WinUI 3 можно использовать WinRT API для получения хэндла
// или альтернативные методы в зависимости от контекста
var hwnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
return hwnd;
}
}

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);
}

View File

@@ -2,7 +2,6 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -59,24 +58,8 @@ public abstract class DragSourceBehaviorBase<TElement> : IDragSource
/// </summary>
/// <value>
/// Экземпляр <see cref="IDragDropService"/>, используемый для управления операциями перетаскивания.
/// При первом обращении выполняется получение сервиса из <see cref="ServiceProvider"/>.
/// </value>
protected IDragDropService DragDropService
{
get
{
if (_dragDropService == null)
{
_dragDropService = ServiceProvider.GetRequiredService<IDragDropService>();
}
return _dragDropService;
}
}
/// <summary>
/// Получает провайдер сервисов для разрешения зависимостей.
/// </summary>
protected IServiceProvider ServiceProvider { get; }
protected IDragDropService DragDropService { get; }
/// <summary>
/// Получает значение, указывающее, выполняется ли в данный момент операция перетаскивания.
@@ -86,13 +69,13 @@ public abstract class DragSourceBehaviorBase<TElement> : IDragSource
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DragSourceBehaviorBase{TElement}"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов для разрешения зависимостей.</param>
/// <param name="dragDropService">Сервис перетаскивания.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="serviceProvider"/> равен null.
/// Выбрасывается, когда <paramref name="dragDropService"/> равен null.
/// </exception>
protected DragSourceBehaviorBase(IServiceProvider serviceProvider)
protected DragSourceBehaviorBase(IDragDropService dragDropService)
{
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
DragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
}
/// <summary>

View File

@@ -2,7 +2,6 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading;
using System.Threading.Tasks;
@@ -76,24 +75,8 @@ public abstract class DropTargetBehaviorBase<TElement> : IDropTarget
/// </summary>
/// <value>
/// Экземпляр <see cref="IDragDropService"/>, используемый для регистрации цели сброса.
/// При первом обращении выполняется получение сервиса из <see cref="ServiceProvider"/>.
/// </value>
protected IDragDropService DragDropService
{
get
{
if (_dragDropService == null)
{
_dragDropService = ServiceProvider.GetRequiredService<IDragDropService>();
}
return _dragDropService;
}
}
/// <summary>
/// Получает провайдер сервисов для разрешения зависимостей.
/// </summary>
protected IServiceProvider ServiceProvider { get; }
protected IDragDropService DragDropService { get; }
/// <summary>
/// Получает текущие границы элемента в экранных координатах.
@@ -116,13 +99,13 @@ public abstract class DropTargetBehaviorBase<TElement> : IDropTarget
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="DropTargetBehaviorBase{TElement}"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов для разрешения зависимостей.</param>
/// <param name="dragDropService">Сервис перетаскивания.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="serviceProvider"/> равен null.
/// Выбрасывается, когда <paramref name="dragDropService"/> равен null.
/// </exception>
protected DropTargetBehaviorBase(IServiceProvider serviceProvider)
protected DropTargetBehaviorBase(IDragDropService dragDropService)
{
ServiceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider));
DragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
}
/// <summary>

View File

@@ -1,194 +0,0 @@
using Lattice.Core.DragDrop.Services;
using Lattice.UI.DragDrop.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
namespace Lattice.UI.DragDrop.Extensions;
/// <summary>
/// Методы расширения для регистрации сервисов перетаскивания в контейнере зависимостей.
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// Добавляет сервисы перетаскивания в контейнер зависимостей.
/// </summary>
/// <param name="services">Коллекция сервисов для регистрации.</param>
/// <returns>Коллекция сервисов с зарегистрированными сервисами перетаскивания.</returns>
/// <remarks>
/// <para>
/// Этот метод регистрирует основные сервисы перетаскивания, необходимые для работы системы:
/// </para>
/// <list type="bullet">
/// <item>Основной сервис управления перетаскиванием (<see cref="IDragDropService"/>)</item>
/// <item>Абстракции для визуального представления и обратной связи</item>
/// <item>Провайдеры по умолчанию с безопасной реализацией</item>
/// </list>
/// <para>
/// Для полноценной работы в конкретной UI-платформе (WPF, Avalonia, MAUI и т.д.)
/// необходимо переопределить регистрации платформенными реализациями.
/// </para>
/// </remarks>
public static IServiceCollection AddLatticeDragDrop(this IServiceCollection services)
{
// Регистрация основного сервиса перетаскивания
services.TryAddSingleton<IDragDropService, DragDropService>();
// Регистрация UI-специфичных абстракций с реализациями по умолчанию
// Эти реализации безопасны (ничего не делают), но могут быть переопределены платформенными реализациями
services.TryAddSingleton<IDragVisualProvider, DefaultDragVisualProvider>();
services.TryAddSingleton<IDropVisualAdorner, DefaultDropVisualAdorner>();
services.TryAddSingleton<IDragDropHost, DefaultDragDropHost>();
return services;
}
/// <summary>
/// Добавляет или заменяет реализацию поставщика визуального представления перетаскивания.
/// </summary>
/// <typeparam name="TProvider">Тип поставщика визуального представления.</typeparam>
/// <param name="services">Коллекция сервисов.</param>
/// <returns>Коллекция сервисов.</returns>
/// <remarks>
/// Используйте этот метод для регистрации платформенной реализации поставщика визуального представления.
/// </remarks>
public static IServiceCollection AddDragVisualProvider<TProvider>(this IServiceCollection services)
where TProvider : class, IDragVisualProvider
{
services.Replace(ServiceDescriptor.Singleton<IDragVisualProvider, TProvider>());
return services;
}
/// <summary>
/// Добавляет или заменяет реализацию визуальной обратной связи для цели сброса.
/// </summary>
/// <typeparam name="TAdorner">Тип элемента визуальной обратной связи.</typeparam>
/// <param name="services">Коллекция сервисов.</param>
/// <returns>Коллекция сервисов.</returns>
/// <remarks>
/// Используйте этот метод для регистрации платформенной реализации визуальной обратной связи.
/// </remarks>
public static IServiceCollection AddDropVisualAdorner<TAdorner>(this IServiceCollection services)
where TAdorner : class, IDropVisualAdorner
{
services.Replace(ServiceDescriptor.Singleton<IDropVisualAdorner, TAdorner>());
return services;
}
/// <summary>
/// Добавляет или заменяет реализацию хоста для отображения визуальных элементов.
/// </summary>
/// <typeparam name="THost">Тип хоста визуальных элементов.</typeparam>
/// <param name="services">Коллекция сервисов.</param>
/// <returns>Коллекция сервисов.</returns>
/// <remarks>
/// Используйте этот метод для регистрации платформенной реализации хоста визуальных элементов.
/// </remarks>
public static IServiceCollection AddDragDropHost<THost>(this IServiceCollection services)
where THost : class, IDragDropHost
{
services.Replace(ServiceDescriptor.Singleton<IDragDropHost, THost>());
return services;
}
#region Default Implementations
/// <summary>
/// Безопасная реализация поставщика визуального представления по умолчанию.
/// </summary>
/// <remarks>
/// Эта реализация ничего не делает и используется как заглушка до тех пор,
/// пока не будет предоставлена платформенная реализация.
/// </remarks>
private class DefaultDragVisualProvider : IDragVisualProvider
{
/// <inheritdoc/>
public object? CreateDragVisual(Core.DragDrop.Models.DragInfo dragInfo, Core.Geometry.Point initialPosition)
{
// Безопасная реализация: не создает визуальное представление
return null;
}
/// <inheritdoc/>
public void UpdateDragVisualPosition(object dragVisual, Core.Geometry.Point position)
{
// Ничего не делаем, так как визуальное представление не было создано
}
/// <inheritdoc/>
public void ReleaseDragVisual(object dragVisual)
{
// Ничего не делаем, так как визуальное представление не было создано
}
}
/// <summary>
/// Безопасная реализация визуальной обратной связи по умолчанию.
/// </summary>
/// <remarks>
/// Эта реализация ничего не делает и используется как заглушка до тех пор,
/// пока не будет предоставлена платформенная реализация.
/// </remarks>
private class DefaultDropVisualAdorner : IDropVisualAdorner
{
/// <inheritdoc/>
public void Show(Core.DragDrop.Models.DropInfo dropInfo, Core.Geometry.Rect targetBounds)
{
// Безопасная реализация: не отображает визуальную обратную связь
}
/// <inheritdoc/>
public void Update(Core.DragDrop.Models.DropInfo dropInfo)
{
// Ничего не делаем, так как обратная связь не отображается
}
/// <inheritdoc/>
public void Hide()
{
// Ничего не делаем, так как обратная связь не отображается
}
}
/// <summary>
/// Безопасная реализация хоста визуальных элементов по умолчанию.
/// </summary>
/// <remarks>
/// Эта реализация ничего не делает и используется как заглушка до тех пор,
/// пока не будет предоставлена платформенная реализация.
/// </remarks>
private class DefaultDragDropHost : IDragDropHost
{
/// <inheritdoc/>
public void ShowDragVisual(object dragVisual, Core.Geometry.Point position)
{
// Безопасная реализация: не отображает визуальный элемент
}
/// <inheritdoc/>
public void UpdateDragVisualPosition(object dragVisual, Core.Geometry.Point position)
{
// Ничего не делаем, так как визуальный элемент не отображается
}
/// <inheritdoc/>
public void HideDragVisual(object dragVisual)
{
// Ничего не делаем, так как визуальный элемент не отображается
}
/// <inheritdoc/>
public void ShowDropAdorner(IDropVisualAdorner adorner)
{
// Безопасная реализация: не отображает обратную связь
}
/// <inheritdoc/>
public void HideDropAdorner(IDropVisualAdorner adorner)
{
// Ничего не делаем, так как обратная связь не отображается
}
}
#endregion
}

View File

@@ -13,8 +13,4 @@
<ProjectReference Include="..\Lattice.Core.Geometry\Lattice.Core.Geometry.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
</ItemGroup>
</Project>

View File

@@ -1,4 +1,5 @@
<Solution>
<Folder Name="/demo/" />
<Folder Name="/src/" />
<Folder Name="/src/Core/">
<Project Path="Lattice.Core.Geometry/Lattice.Core.Geometry.csproj" Id="a84c6664-28fa-4082-9b91-6b2f3228ea53" />