Доработара WinUI реализация.

This commit is contained in:
2026-01-25 05:36:28 +03:00
parent bbb20edb03
commit 6ad7b5dcdb
20 changed files with 1089 additions and 1801 deletions

View File

@@ -1,49 +0,0 @@
using Microsoft.UI.Xaml;
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Attached properties для DragSource.
/// </summary>
public static class DragSource
{
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached(
"IsEnabled",
typeof(bool),
typeof(DragSource),
new PropertyMetadata(false, OnIsEnabledChanged));
public static readonly DependencyProperty DragDataProperty =
DependencyProperty.RegisterAttached(
"DragData",
typeof(object),
typeof(DragSource),
new PropertyMetadata(null));
public static bool GetIsEnabled(UIElement element) =>
(bool)element.GetValue(IsEnabledProperty);
public static void SetIsEnabled(UIElement element, bool value) =>
element.SetValue(IsEnabledProperty, value);
public static object GetDragData(UIElement element) =>
element.GetValue(DragDataProperty);
public static void SetDragData(UIElement element, object value) =>
element.SetValue(DragDataProperty, value);
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is not UIElement element) return;
// TODO: Здесь нужно создать экземпляр WinUIDragSourceBehavior
// и прикрепить его к элементу через DI
// Пока что устанавливаем данные в Tag
if ((bool)e.NewValue)
{
var data = GetDragData(element);
element.Tag = data;
}
}
}

View File

@@ -1,290 +1,267 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Services;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Behaviors;
using Lattice.UI.DragDrop.WinUI.Services;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using System;
using System.Threading;
using System.Threading.Tasks;
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Поведение источника перетаскивания для WinUI FrameworkElement.
/// Реализация поведения источника перетаскивания для элементов WinUI.
/// </summary>
public class WinUIDragSourceBehavior : DragSourceBehaviorBase<FrameworkElement>
/// <remarks>
/// <para>
/// Этот класс обрабатывает события мыши/тач и преобразует их в операции перетаскивания.
/// Он реализует интерфейс <see cref="IDragSource"/> для интеграции с системой перетаскивания.
/// </para>
/// <para>
/// Поведение автоматически отслеживает начало перемещения мыши, проверяет порог
/// перетаскивания и инициирует операцию через <see cref="IDragDropService"/>.
/// </para>
/// </remarks>
public sealed class WinUIDragSourceBehavior : IDragSource
{
/// <summary>
/// Прикрепленное свойство для данных перетаскивания.
/// </summary>
public static readonly DependencyProperty DragDataProperty =
DependencyProperty.RegisterAttached(
"DragData",
typeof(object),
typeof(WinUIDragSourceBehavior),
new PropertyMetadata(null));
#region Поля
private readonly IDragDropService _dragDropService;
private readonly WinUIDragDropHost _host;
private FrameworkElement? _element;
private object? _dragData;
private Point _dragStartPosition;
private bool _isDragging;
private CancellationTokenSource? _cancellationTokenSource;
#endregion
#region Конструктор
/// <summary>
/// Прикрепленное свойство для включения перетаскивания.
/// Инициализирует новый экземпляр класса <see cref="WinUIDragSourceBehavior"/>.
/// </summary>
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached(
"IsEnabled",
typeof(bool),
typeof(WinUIDragSourceBehavior),
new PropertyMetadata(false, OnIsEnabledChanged));
/// <summary>
/// Получает значение DragData.
/// </summary>
public static object GetDragData(FrameworkElement element)
/// <param name="dragDropService">Сервис для управления операциями перетаскивания.</param>
/// <param name="host">Хост для отображения визуальных элементов.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если любой из параметров равен null.
/// </exception>
public WinUIDragSourceBehavior(IDragDropService dragDropService, WinUIDragDropHost host)
{
return element.GetValue(DragDataProperty);
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
_host = host ?? throw new ArgumentNullException(nameof(host));
}
#endregion
#region Публичные методы
/// <summary>
/// Прикрепляет поведение к указанному элементу.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <param name="dragData">Данные для перетаскивания. Если не указано, используются
/// DataContext или Tag элемента.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// После вызова этого метода элемент начинает обрабатывать события перетаскивания.
/// </remarks>
public void Attach(FrameworkElement element, object? dragData = null)
{
Detach();
_element = element ?? throw new ArgumentNullException(nameof(element));
_dragData = dragData ?? element.DataContext ?? element.Tag;
SubscribeToEvents();
}
/// <summary>
/// Устанавливает значение DragData.
/// Открепляет поведение от элемента.
/// </summary>
public static void SetDragData(FrameworkElement element, object value)
public void Detach()
{
element.SetValue(DragDataProperty, value);
}
if (_element == null) return;
/// <summary>
/// Получает значение IsEnabled.
/// </summary>
public static bool GetIsEnabled(FrameworkElement element)
{
return (bool)element.GetValue(IsEnabledProperty);
}
UnsubscribeFromEvents();
/// <summary>
/// Устанавливает значение IsEnabled.
/// </summary>
public static void SetIsEnabled(FrameworkElement element, bool value)
{
element.SetValue(IsEnabledProperty, value);
}
private static void OnIsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (d is FrameworkElement element)
if (_isDragging)
{
// Получаем или создаем экземпляр поведения через Attached Property
var behavior = GetBehavior(element);
if ((bool)e.NewValue)
{
if (behavior == null)
{
behavior = new WinUIDragSourceBehavior();
SetBehavior(element, behavior);
}
behavior.AssociatedElement = element;
}
else
{
if (behavior != null)
{
behavior.Detach();
SetBehavior(element, null);
}
}
_dragDropService.CancelDragAsync().ConfigureAwait(false);
}
_element = null;
_dragData = null;
_isDragging = false;
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
public WinUIDragSourceBehavior()
: base(ServiceProviderHelper.GetServiceProvider())
#endregion
#region Обработчики событий
private void SubscribeToEvents()
{
if (_element == null) return;
_element.PointerPressed += OnPointerPressed;
_element.PointerMoved += OnPointerMoved;
_element.PointerReleased += OnPointerReleased;
_element.PointerCanceled += OnPointerCanceled;
_element.PointerCaptureLost += OnPointerCaptureLost;
}
protected override void SubscribeToEvents(FrameworkElement element)
private void UnsubscribeFromEvents()
{
element.PointerPressed += OnPointerPressed;
element.PointerMoved += OnPointerMoved;
element.PointerReleased += OnPointerReleased;
element.PointerCanceled += OnPointerCanceled;
element.PointerCaptureLost += OnPointerCaptureLost;
element.LostFocus += OnLostFocus;
}
if (_element == null) return;
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
element.PointerPressed -= OnPointerPressed;
element.PointerMoved -= OnPointerMoved;
element.PointerReleased -= OnPointerReleased;
element.PointerCanceled -= OnPointerCanceled;
element.PointerCaptureLost -= OnPointerCaptureLost;
element.LostFocus -= OnLostFocus;
_element.PointerPressed -= OnPointerPressed;
_element.PointerMoved -= OnPointerMoved;
_element.PointerReleased -= OnPointerReleased;
_element.PointerCanceled -= OnPointerCanceled;
_element.PointerCaptureLost -= OnPointerCaptureLost;
}
private void OnPointerPressed(object sender, PointerRoutedEventArgs e)
{
if (AssociatedElement == null) return;
if (_element == null || _isDragging) return;
var point = e.GetCurrentPoint(AssociatedElement);
OnInteractionStarted(new Point(point.Position.X, point.Position.Y));
var point = e.GetCurrentPoint(_element);
_dragStartPosition = new Point(point.Position.X, point.Position.Y);
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = new CancellationTokenSource();
}
private void OnPointerMoved(object sender, PointerRoutedEventArgs e)
private async void OnPointerMoved(object sender, PointerRoutedEventArgs e)
{
if (AssociatedElement == null) return;
if (_element == null || _isDragging || _cancellationTokenSource?.IsCancellationRequested == true)
return;
var point = e.GetCurrentPoint(AssociatedElement);
OnInteractionMoved(new Point(point.Position.X, point.Position.Y));
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)
{
OnInteractionEnded();
ResetState();
}
private void OnPointerCanceled(object sender, PointerRoutedEventArgs e)
{
OnInteractionCancelled();
ResetState();
}
private void OnPointerCaptureLost(object sender, PointerRoutedEventArgs e)
{
OnInteractionCancelled();
ResetState();
}
private void OnLostFocus(object sender, RoutedEventArgs e)
private double CalculateDistance(Point p1, Point p2)
{
OnInteractionCancelled();
var dx = p2.X - p1.X;
var dy = p2.Y - p1.Y;
return Math.Sqrt(dx * dx + dy * dy);
}
protected override Point ConvertToScreenCoordinates(Point point)
/// <summary>
/// Преобразует координаты элемента в экранные координаты.
/// </summary>
/// <param name="point">Точка в координатах элемента.</param>
/// <returns>Точка в экранных координатах.</returns>
private Point ConvertToScreenCoordinates(Point point)
{
if (AssociatedElement == null)
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
{
return point;
var transform = AssociatedElement.TransformToVisual(null);
var screenPoint = transform.TransformPoint(new Windows.Foundation.Point(point.X, point.Y));
return new Point(screenPoint.X, screenPoint.Y);
}
/// <inheritdoc/>
public override async Task<(bool CanStart, DragInfo? DragInfo)> CanStartDragAsync()
{
if (AssociatedElement == null)
{
return (false, null);
}
var data = GetDragData(AssociatedElement);
if (data == null)
{
// Пробуем получить данные из Tag или других источников
data = AssociatedElement.Tag ?? AssociatedElement.DataContext;
}
if (data == null)
{
return (false, null);
}
// Получаем начальную позицию в экранных координатах
var startPosition = ConvertToScreenCoordinates(_dragStartPosition);
// Создаем DragInfo с учетом вашего конструктора
var dragInfo = new DragInfo(
data: data,
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Move |
Core.DragDrop.Enums.DragDropEffects.Copy,
startPosition: startPosition,
source: this
);
return (true, dragInfo);
}
protected override void OnDragCompleted(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects)
{
base.OnDragCompleted(dragInfo, effects);
// Визуальная обратная связь при завершении
SetVisualState(AssociatedElement, "Normal");
}
protected override void OnDragCancelled(DragInfo dragInfo)
{
base.OnDragCancelled(dragInfo);
// Визуальная обратная связь при отмене
SetVisualState(AssociatedElement, "Normal");
}
private void SetVisualState(FrameworkElement? element, string stateName)
{
if (element is Control control)
{
try
{
VisualStateManager.GoToState(control, stateName, true);
}
catch
{
// Fallback
control.Opacity = 1.0;
}
}
else if (element != null)
{
// Альтернативная визуальная обратная связь для не-Control элементов
element.Opacity = 1.0;
}
}
// Attached property для хранения экземпляра поведения
private static readonly DependencyProperty BehaviorProperty =
DependencyProperty.RegisterAttached(
"Behavior",
typeof(WinUIDragSourceBehavior),
typeof(WinUIDragSourceBehavior),
new PropertyMetadata(null));
private static WinUIDragSourceBehavior GetBehavior(FrameworkElement element)
private async Task StartDragAsync(Point position)
{
return (WinUIDragSourceBehavior)element.GetValue(BehaviorProperty);
}
if (_element == null || _dragData == null || _isDragging) return;
private static void SetBehavior(FrameworkElement element, WinUIDragSourceBehavior? value)
{
element.SetValue(BehaviorProperty, value);
}
}
/// <summary>
/// Вспомогательный класс для получения IServiceProvider.
/// </summary>
internal static class ServiceProviderHelper
{
private static IServiceProvider? _serviceProvider;
public static IServiceProvider GetServiceProvider()
{
if (_serviceProvider == null)
try
{
// Ищем IServiceProvider в Application.Current.Resources
if (Application.Current.Resources.TryGetValue("ServiceProvider", out var provider) &&
provider is IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
else
{
throw new InvalidOperationException(
"IServiceProvider не найден. Убедитесь, что ServiceProvider зарегистрирован в ресурсах приложения.");
}
var screenPosition = ConvertToScreenCoordinates(position);
var started = await _dragDropService.StartDragAsync(this, screenPosition);
_isDragging = started;
}
catch
{
ResetState();
}
return _serviceProvider;
}
public static void SetServiceProvider(IServiceProvider serviceProvider)
private void ResetState()
{
_serviceProvider = serviceProvider;
_isDragging = false;
_dragStartPosition = default;
_cancellationTokenSource?.Cancel();
_cancellationTokenSource?.Dispose();
_cancellationTokenSource = null;
}
#endregion
#region IDragSource Implementation
public async Task<DragInfo?> TryStartDragAsync(Point startPosition, CancellationToken cancellationToken = default)
{
if (_element == null || _dragData == null)
return null;
try
{
var dragInfo = new DragInfo(
data: _dragData,
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy |
Core.DragDrop.Enums.DragDropEffects.Move,
startPosition: startPosition,
source: this
);
return dragInfo;
}
catch
{
return null;
}
}
public Task OnDragCompletedAsync(DragInfo dragInfo, Core.DragDrop.Enums.DragDropEffects effects, CancellationToken cancellationToken = default)
{
_isDragging = false;
return Task.CompletedTask;
}
public Task OnDragCancelledAsync(DragInfo dragInfo, CancellationToken cancellationToken = default)
{
_isDragging = false;
return Task.CompletedTask;
}
#endregion
}

View File

@@ -1,345 +1,295 @@
using Lattice.Core.DragDrop.Models;
using Lattice.Core.DragDrop.Abstractions;
using Lattice.Core.DragDrop.Models;
using Lattice.Core.Geometry;
using Lattice.UI.DragDrop.Behaviors;
using Lattice.UI.DragDrop.WinUI.Services;
using Microsoft.UI.Xaml;
using System;
using System.Collections.Concurrent;
using System.Threading;
using System.Threading.Tasks;
namespace Lattice.UI.DragDrop.WinUI.Behaviors
namespace Lattice.UI.DragDrop.WinUI.Behaviors;
/// <summary>
/// Реализация поведения цели сброса для элементов WinUI.
/// </summary>
/// <remarks>
/// <para>
/// Этот класс обрабатывает события перетаскивания WinUI и преобразует их в вызовы
/// методов интерфейса <see cref="IDropTarget"/>.
/// </para>
/// <para>
/// Поведение автоматически регистрирует элемент в системе перетаскивания,
/// обновляет его границы при изменении размера и обрабатывает все этапы операции сброса.
/// </para>
/// </remarks>
public sealed class WinUIDropTargetBehavior : IDropTarget
{
#region Поля
private readonly Lattice.Core.DragDrop.Services.IDragDropService _dragDropService;
private readonly WinUIDragDropHost _host;
private FrameworkElement? _element;
private string? _registrationId;
private Rect _currentBounds;
#endregion
#region Конструктор
/// <summary>
/// Поведение цели сброса для элементов WinUI.
/// Позволяет элементам принимать данные при операции перетаскивания.
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <remarks>
/// <para>
/// Это поведение должно быть прикреплено к <see cref="FrameworkElement"/>, который должен выступать в качестве цели сброса.
/// Поведение автоматически регистрирует элемент в системе перетаскивания и обрабатывает все аспекты операции сброса.
/// </para>
/// <para>
/// Для использования необходимо:
/// 1. Создать экземпляр поведения с помощью <see cref="Attach"/> или через DI.
/// 2. Переопределить методы <see cref="CanAcceptDrop"/> и <see cref="Drop"/> для реализации логики принятия данных.
/// 3. При необходимости переопределить <see cref="DragOver"/> для настройки визуальной обратной связи.
/// </para>
/// </remarks>
public class WinUIDropTargetBehavior : DropTargetBehaviorBase<FrameworkElement>
/// <param name="dragDropService">Сервис для управления операциями перетаскивания.</param>
/// <param name="host">Хост для отображения визуальных элементов.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если любой из параметров равен null.
/// </exception>
public WinUIDropTargetBehavior(Lattice.Core.DragDrop.Services.IDragDropService dragDropService, WinUIDragDropHost host)
{
private static readonly ConcurrentDictionary<FrameworkElement, WinUIDropTargetBehavior> _attachedBehaviors = new();
private readonly WeakReference<FrameworkElement>? _weakElement;
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <remarks>
/// Конструктор создает экземпляр поведения, но не прикрепляет его к элементу.
/// Для прикрепления используйте метод <see cref="Attach(FrameworkElement, IServiceProvider)"/>.
/// </remarks>
public WinUIDropTargetBehavior(IServiceProvider serviceProvider)
: base(serviceProvider)
{
}
/// <summary>
/// Инициализирует новый экземпляр класса <see cref="WinUIDropTargetBehavior"/>.
/// </summary>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <remarks>
/// Конструктор создает экземпляр поведения и сразу прикрепляет его к указанному элементу.
/// </remarks>
public WinUIDropTargetBehavior(IServiceProvider serviceProvider, FrameworkElement element)
: base(serviceProvider)
{
AssociatedElement = element ?? throw new ArgumentNullException(nameof(element));
}
/// <summary>
/// Прикрепляет поведение к указанному элементу.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <param name="serviceProvider">Провайдер сервисов.</param>
/// <returns>
/// Экземпляр поведения, прикрепленного к элементу. Если к элементу уже прикреплено поведение,
/// возвращает существующий экземпляр.
/// </returns>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, когда <paramref name="element"/> или <paramref name="serviceProvider"/> равны null.
/// </exception>
/// <remarks>
/// <para>
/// Этот метод обеспечивает, что к каждому элементу прикреплен только один экземпляр поведения.
/// Если метод вызывается повторно для того же элемента, возвращается существующий экземпляр.
/// </para>
/// <para>
/// Прикрепленное поведение автоматически отслеживает изменения макета элемента и обновляет
/// его границы в системе перетаскивания.
/// </para>
/// </remarks>
public static WinUIDropTargetBehavior Attach(FrameworkElement element, IServiceProvider serviceProvider)
{
if (element == null) throw new ArgumentNullException(nameof(element));
if (serviceProvider == null) throw new ArgumentNullException(nameof(serviceProvider));
return _attachedBehaviors.GetOrAdd(element, key =>
{
var behavior = new WinUIDropTargetBehavior(serviceProvider, key);
// Подписка на события жизненного цикла элемента
key.Unloaded += OnElementUnloaded;
return behavior;
});
}
/// <summary>
/// Открепляет поведение от указанного элемента.
/// </summary>
/// <param name="element">Элемент, от которого открепляется поведение.</param>
/// <returns>
/// true, если поведение было успешно откреплено; false, если поведение не было прикреплено к элементу.
/// </returns>
/// <remarks>
/// Этот метод освобождает все ресурсы, связанные с поведением, и отписывается от событий элемента.
/// После вызова этого метода элемент перестает быть целью сброса.
/// </remarks>
public static bool Detach(FrameworkElement element)
{
if (element == null) return false;
if (_attachedBehaviors.TryRemove(element, out var behavior))
{
element.Unloaded -= OnElementUnloaded;
behavior.Detach();
return true;
}
return false;
}
/// <summary>
/// Получает поведение, прикрепленное к указанному элементу.
/// </summary>
/// <param name="element">Элемент, для которого требуется получить поведение.</param>
/// <returns>
/// Экземпляр поведения, прикрепленного к элементу, или null, если поведение не прикреплено.
/// </returns>
public static WinUIDropTargetBehavior? GetAttachedBehavior(FrameworkElement element)
{
_attachedBehaviors.TryGetValue(element, out var behavior);
return behavior;
}
/// <summary>
/// Подписывается на события элемента.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <remarks>
/// <para>
/// Этот метод подписывается на следующие события:
/// </para>
/// <list type="bullet">
/// <item><see cref="FrameworkElement.LayoutUpdated"/> - для отслеживания изменений макета</item>
/// <item><see cref="FrameworkElement.SizeChanged"/> - для отслеживания изменений размера</item>
/// <item><see cref="FrameworkElement.Loaded"/> - для инициализации при загрузке элемента</item>
/// </list>
/// <para>
/// Переопределите этот метод, чтобы добавить подписку на дополнительные события.
/// </para>
/// </remarks>
protected override void SubscribeToEvents(FrameworkElement element)
{
if (element == null) return;
element.LayoutUpdated += OnLayoutUpdated;
element.SizeChanged += OnSizeChanged;
element.Loaded += OnLoaded;
// Если элемент уже загружен, сразу обновляем границы
if (element.IsLoaded)
{
UpdateBounds();
}
}
/// <summary>
/// Отписывается от событий элемента.
/// </summary>
/// <param name="element">Элемент, от которого отписывается поведение.</param>
/// <remarks>
/// Этот метод отписывается от всех событий, на которые подписался <see cref="SubscribeToEvents"/>.
/// </remarks>
protected override void UnsubscribeFromEvents(FrameworkElement element)
{
if (element == null) return;
element.LayoutUpdated -= OnLayoutUpdated;
element.SizeChanged -= OnSizeChanged;
element.Loaded -= OnLoaded;
}
/// <summary>
/// Получает границы элемента в экранных координатах.
/// </summary>
/// <param name="element">Элемент, границы которого нужно получить.</param>
/// <returns>
/// Прямоугольник, описывающий границы элемента в экранных координатах.
/// </returns>
/// <remarks>
/// <para>
/// Метод использует преобразование координат через <see cref="UIElement.TransformToVisual"/>
/// для получения глобальных координат элемента.
/// </para>
/// <para>
/// Если элемент не прикреплен к визуальному дереву или его границы не могут быть вычислены,
/// возвращается пустой прямоугольник.
/// </para>
/// </remarks>
protected override Rect GetScreenBounds(FrameworkElement element)
{
if (element == null || !element.IsLoaded)
return Rect.Empty;
try
{
// Получаем корневой элемент окна
var rootVisual = element.XamlRoot?.Content as UIElement;
if (rootVisual == null)
return Rect.Empty;
// Преобразуем границы элемента в координаты корневого элемента
var transform = element.TransformToVisual(rootVisual);
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;
}
}
/// <summary>
/// Определяет, может ли элемент принять сбрасываемые данные.
/// </summary>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <returns>
/// true, если элемент может принять данные; в противном случае — false.
/// </returns>
/// <remarks>
/// <para>
/// Этот метод является абстрактным и должен быть переопределен в производных классах
/// для реализации логики принятия данных.
/// </para>
/// <para>
/// Базовая реализация всегда возвращает false. Переопределите этот метод, чтобы определить,
/// какие типы данных может принимать ваш элемент и при каких условиях.
/// </para>
/// <example>
/// Пример реализации:
/// <code>
/// public override bool CanAcceptDrop(DropInfo dropInfo)
/// {
/// // Принимаем только строковые данные
/// return dropInfo.Data is string;
/// }
/// </code>
/// </example>
/// </remarks>
public override async Task<bool> CanAcceptDropAsync(DropInfo dropInfo)
{
// Базовая реализация - не принимает никакие данные.
// Переопределите этот метод в производных классах.
return false;
}
/// <summary>
/// Обрабатывает сброс данных на элемент.
/// </summary>
/// <param name="dropInfo">Информация о сбросе.</param>
/// <remarks>
/// <para>
/// Этот метод вызывается, когда пользователь отпускает кнопку мыши над элементом,
/// и данные должны быть приняты.
/// </para>
/// <para>
/// Базовая реализация ничего не делает. Переопределите этот метод, чтобы реализовать
/// логику обработки принятых данных.
/// </para>
/// <example>
/// Пример реализации:
/// <code>
/// public override void Drop(DropInfo dropInfo)
/// {
/// if (dropInfo.Data is string text)
/// {
/// // Обработка текстовых данных
/// AssociatedElement.SetValue(TextBlock.TextProperty, text);
/// dropInfo.MarkAsHandled();
/// }
/// }
/// </code>
/// </example>
/// </remarks>
public override async Task DropAsync(DropInfo dropInfo)
{
// Базовая реализация ничего не делает.
// Переопределите этот метод в производных классах.
}
/// <summary>
/// Освобождает ресурсы, связанные с поведением.
/// </summary>
/// <remarks>
/// <para>
/// Этот метод отписывается от всех событий, отменяет регистрацию в сервисе перетаскивания
/// и очищает все ресурсы.
/// </para>
/// <para>
/// После вызова этого метода поведение больше не может быть использовано.
/// </para>
/// </remarks>
public override void Detach()
{
if (AssociatedElement != null && _attachedBehaviors.TryGetValue(AssociatedElement, out _))
{
_attachedBehaviors.TryRemove(AssociatedElement, out _);
}
base.Detach();
}
#region Event Handlers
private void OnLayoutUpdated(object? sender, object e)
{
OnElementLayoutChanged();
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
OnElementLayoutChanged();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
UpdateBounds();
}
private static void OnElementUnloaded(object sender, RoutedEventArgs e)
{
if (sender is FrameworkElement element)
{
Detach(element);
}
}
#endregion
_dragDropService = dragDropService ?? throw new ArgumentNullException(nameof(dragDropService));
_host = host ?? throw new ArgumentNullException(nameof(host));
}
#endregion
#region Публичные методы
/// <summary>
/// Прикрепляет поведение к указанному элементу.
/// </summary>
/// <param name="element">Элемент, к которому прикрепляется поведение.</param>
/// <exception cref="ArgumentNullException">
/// Выбрасывается, если <paramref name="element"/> равен null.
/// </exception>
/// <remarks>
/// После вызова этого метода:
/// 1. Элементу устанавливается <see cref="UIElement.AllowDrop"/> = true
/// 2. Поведение подписывается на события перетаскивания
/// 3. Элемент регистрируется в системе перетаскивания
/// </remarks>
public void Attach(FrameworkElement element)
{
Detach();
_element = element ?? throw new ArgumentNullException(nameof(element));
element.AllowDrop = true;
SubscribeToEvents();
UpdateBounds();
RegisterToService();
}
/// <summary>
/// Открепляет поведение от элемента.
/// </summary>
public void Detach()
{
if (_element == null) return;
UnsubscribeFromEvents();
UnregisterFromService();
_element.AllowDrop = false;
_element = null;
_currentBounds = Rect.Empty;
}
#endregion
#region Обработчики событий
private void SubscribeToEvents()
{
if (_element == null) return;
_element.DragEnter += OnDragEnter;
_element.DragOver += OnDragOver;
_element.DragLeave += OnDragLeave;
_element.Drop += OnDrop;
_element.SizeChanged += OnSizeChanged;
}
private void UnsubscribeFromEvents()
{
if (_element == null) return;
_element.DragEnter -= OnDragEnter;
_element.DragOver -= OnDragOver;
_element.DragLeave -= OnDragLeave;
_element.Drop -= OnDrop;
_element.SizeChanged -= OnSizeChanged;
}
private async void OnDragEnter(object sender, DragEventArgs e)
{
if (_element == null) return;
try
{
var position = e.GetPosition(_element);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
e.AcceptedOperation = Windows.ApplicationModel.DataTransfer.DataPackageOperation.Copy;
}
}
catch { }
}
private async void OnDragOver(object sender, DragEventArgs e)
{
if (_element == null) return;
try
{
var position = e.GetPosition(_element);
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;
}
catch { }
}
private async void OnDragLeave(object sender, DragEventArgs e)
{
await OnDragLeaveAsync();
}
private async void OnDrop(object sender, DragEventArgs e)
{
if (_element == null) return;
try
{
var position = e.GetPosition(_element);
var dropInfo = CreateDropInfo(e, new Point(position.X, position.Y));
if (await CanAcceptDropAsync(dropInfo))
{
await OnDropAsync(dropInfo);
dropInfo.MarkAsHandled();
e.Handled = true;
}
}
catch { }
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
UpdateBounds();
}
private DropInfo CreateDropInfo(DragEventArgs e, Point position)
{
object? data = null;
if (e.DataView.Properties.TryGetValue("DragData", out var dragData))
{
data = dragData;
}
return new DropInfo(
data: data,
position: position,
allowedEffects: Core.DragDrop.Enums.DragDropEffects.Copy |
Core.DragDrop.Enums.DragDropEffects.Move,
target: _element
);
}
/// <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
public Task<bool> CanAcceptDropAsync(DropInfo dropInfo, CancellationToken ct = default)
{
return Task.FromResult(true); // Принимаем все по умолчанию
}
public Task OnDragOverAsync(DropInfo dropInfo, CancellationToken ct = default)
{
dropInfo.SuggestedEffects = Core.DragDrop.Enums.DragDropEffects.Move;
return Task.CompletedTask;
}
public Task OnDropAsync(DropInfo dropInfo, CancellationToken ct = default)
{
return Task.CompletedTask;
}
public Task OnDragLeaveAsync(CancellationToken ct = default)
{
return Task.CompletedTask;
}
#endregion
}