using Lattice.Core.DragDrop.Models; using Lattice.Core.Geometry; using Lattice.UI.DragDrop.Behaviors; using Microsoft.UI.Xaml; using System; using System.Collections.Concurrent; using System.Threading.Tasks; namespace Lattice.UI.DragDrop.WinUI.Behaviors { /// /// Поведение цели сброса для элементов WinUI. /// Позволяет элементам принимать данные при операции перетаскивания. /// /// /// /// Это поведение должно быть прикреплено к , который должен выступать в качестве цели сброса. /// Поведение автоматически регистрирует элемент в системе перетаскивания и обрабатывает все аспекты операции сброса. /// /// /// Для использования необходимо: /// 1. Создать экземпляр поведения с помощью или через DI. /// 2. Переопределить методы и для реализации логики принятия данных. /// 3. При необходимости переопределить для настройки визуальной обратной связи. /// /// public class WinUIDropTargetBehavior : DropTargetBehaviorBase { private static readonly ConcurrentDictionary _attachedBehaviors = new(); private readonly WeakReference? _weakElement; /// /// Инициализирует новый экземпляр класса . /// /// Провайдер сервисов. /// /// Конструктор создает экземпляр поведения, но не прикрепляет его к элементу. /// Для прикрепления используйте метод . /// public WinUIDropTargetBehavior(IServiceProvider serviceProvider) : base(serviceProvider) { } /// /// Инициализирует новый экземпляр класса . /// /// Провайдер сервисов. /// Элемент, к которому прикрепляется поведение. /// /// Конструктор создает экземпляр поведения и сразу прикрепляет его к указанному элементу. /// public WinUIDropTargetBehavior(IServiceProvider serviceProvider, FrameworkElement element) : base(serviceProvider) { AssociatedElement = element ?? throw new ArgumentNullException(nameof(element)); } /// /// Прикрепляет поведение к указанному элементу. /// /// Элемент, к которому прикрепляется поведение. /// Провайдер сервисов. /// /// Экземпляр поведения, прикрепленного к элементу. Если к элементу уже прикреплено поведение, /// возвращает существующий экземпляр. /// /// /// Выбрасывается, когда или равны null. /// /// /// /// Этот метод обеспечивает, что к каждому элементу прикреплен только один экземпляр поведения. /// Если метод вызывается повторно для того же элемента, возвращается существующий экземпляр. /// /// /// Прикрепленное поведение автоматически отслеживает изменения макета элемента и обновляет /// его границы в системе перетаскивания. /// /// 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; }); } /// /// Открепляет поведение от указанного элемента. /// /// Элемент, от которого открепляется поведение. /// /// true, если поведение было успешно откреплено; false, если поведение не было прикреплено к элементу. /// /// /// Этот метод освобождает все ресурсы, связанные с поведением, и отписывается от событий элемента. /// После вызова этого метода элемент перестает быть целью сброса. /// 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; } /// /// Получает поведение, прикрепленное к указанному элементу. /// /// Элемент, для которого требуется получить поведение. /// /// Экземпляр поведения, прикрепленного к элементу, или null, если поведение не прикреплено. /// public static WinUIDropTargetBehavior? GetAttachedBehavior(FrameworkElement element) { _attachedBehaviors.TryGetValue(element, out var behavior); return behavior; } /// /// Подписывается на события элемента. /// /// Элемент, к которому прикрепляется поведение. /// /// /// Этот метод подписывается на следующие события: /// /// /// - для отслеживания изменений макета /// - для отслеживания изменений размера /// - для инициализации при загрузке элемента /// /// /// Переопределите этот метод, чтобы добавить подписку на дополнительные события. /// /// protected override void SubscribeToEvents(FrameworkElement element) { if (element == null) return; element.LayoutUpdated += OnLayoutUpdated; element.SizeChanged += OnSizeChanged; element.Loaded += OnLoaded; // Если элемент уже загружен, сразу обновляем границы if (element.IsLoaded) { UpdateBounds(); } } /// /// Отписывается от событий элемента. /// /// Элемент, от которого отписывается поведение. /// /// Этот метод отписывается от всех событий, на которые подписался . /// protected override void UnsubscribeFromEvents(FrameworkElement element) { if (element == null) return; element.LayoutUpdated -= OnLayoutUpdated; element.SizeChanged -= OnSizeChanged; element.Loaded -= OnLoaded; } /// /// Получает границы элемента в экранных координатах. /// /// Элемент, границы которого нужно получить. /// /// Прямоугольник, описывающий границы элемента в экранных координатах. /// /// /// /// Метод использует преобразование координат через /// для получения глобальных координат элемента. /// /// /// Если элемент не прикреплен к визуальному дереву или его границы не могут быть вычислены, /// возвращается пустой прямоугольник. /// /// 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; } } /// /// Определяет, может ли элемент принять сбрасываемые данные. /// /// Информация о сбросе. /// /// true, если элемент может принять данные; в противном случае — false. /// /// /// /// Этот метод является абстрактным и должен быть переопределен в производных классах /// для реализации логики принятия данных. /// /// /// Базовая реализация всегда возвращает false. Переопределите этот метод, чтобы определить, /// какие типы данных может принимать ваш элемент и при каких условиях. /// /// /// Пример реализации: /// /// public override bool CanAcceptDrop(DropInfo dropInfo) /// { /// // Принимаем только строковые данные /// return dropInfo.Data is string; /// } /// /// /// public override async Task CanAcceptDropAsync(DropInfo dropInfo) { // Базовая реализация - не принимает никакие данные. // Переопределите этот метод в производных классах. return false; } /// /// Обрабатывает сброс данных на элемент. /// /// Информация о сбросе. /// /// /// Этот метод вызывается, когда пользователь отпускает кнопку мыши над элементом, /// и данные должны быть приняты. /// /// /// Базовая реализация ничего не делает. Переопределите этот метод, чтобы реализовать /// логику обработки принятых данных. /// /// /// Пример реализации: /// /// public override void Drop(DropInfo dropInfo) /// { /// if (dropInfo.Data is string text) /// { /// // Обработка текстовых данных /// AssociatedElement.SetValue(TextBlock.TextProperty, text); /// dropInfo.MarkAsHandled(); /// } /// } /// /// /// public override async Task DropAsync(DropInfo dropInfo) { // Базовая реализация ничего не делает. // Переопределите этот метод в производных классах. } /// /// Освобождает ресурсы, связанные с поведением. /// /// /// /// Этот метод отписывается от всех событий, отменяет регистрацию в сервисе перетаскивания /// и очищает все ресурсы. /// /// /// После вызова этого метода поведение больше не может быть использовано. /// /// 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 } }