using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Xml.Linq; namespace CopaData.FileInspector.GUI.TilingPanels.Controls.DragAndDrop; /// /// Used to handle drag and drop callbacks via the abstract methods and . /// Can be used as a parent to any amount of s that refer to the their handler via . /// If no Handler is provided, the element itself will be considered its own handler, as long as it is a . /// In order to make an element draggable, set to true within the element's XAML attributes. /// To mark a FrameworkElement as a drop zone, set to true. /// If a draggable element is dropped on a drop zone, the handler's is called. /// public abstract class DragAndDropControl : Control { /// /// Called when a DragDrop operation starts. /// protected virtual void OnElementDragStart(UIElement draggedElement) { } /// /// Called during a DragDrop operation in order to update the cursor, if needed. /// protected virtual void OnElementDragGiveCursorFeedback(object sender, GiveFeedbackEventArgs args) { } /// /// Called during a DragDrop operation. /// protected virtual void OnElementDrag(UIElement draggedElement, MouseEventArgs args) { } /// /// Called when an element was dropped without being on an element marked as a DropZone. /// /// protected virtual void OnElementDragStop(UIElement draggedElement) { } /// /// Called when this element is the potential target of a DragDrop operation. /// protected virtual void OnElementDragOver(DragAndDropData data, UIElement targetElement) { } /// /// Called when this element stops being a potential target of a DragDrop operation. /// protected virtual void OnElementDragLeave(UIElement targetElement) { } /// /// Required callback to handle , /// provided with containing both the source element being dragged and it's original data context. /// protected abstract void OnElementDropped(DragAndDropData data, UIElement targetElement); #region Attached Properties /* === IsDraggable === */ public static readonly DependencyProperty IsDraggableProperty = DependencyProperty .RegisterAttached( "IsDraggable", typeof(bool), typeof(DragAndDropControl), new PropertyMetadata(false, OnIsDraggableChanged)); public static bool GetIsDraggable(DependencyObject obj) => (bool)obj.GetValue(IsDraggableProperty); public static void SetIsDraggable(DependencyObject obj, bool value) => obj.SetValue(IsDraggableProperty, value); /// /// Adds event listeners to the element's GiveFeedback, MouseLeftButtonUp and MouseMove events. /// private static void OnIsDraggableChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { if (sender is not FrameworkElement element) { throw new InvalidOperationException($"IsDraggable may only be attached to FrameworkElements, got {sender.GetType()} instead."); } if ((bool)e.NewValue) { element.GiveFeedback += HandleGiveFeedback; element.MouseLeftButtonUp += HandleLeftMouseButtonUp; element.MouseMove += HandleMouseMove; } else { element.GiveFeedback -= HandleGiveFeedback; element.MouseLeftButtonUp -= HandleLeftMouseButtonUp; element.MouseMove -= HandleMouseMove; } } /* === IsDropZone === */ public static readonly DependencyProperty IsDropZoneProperty = DependencyProperty .RegisterAttached( "IsDropZone", typeof(bool), typeof(DragAndDropControl), new PropertyMetadata(false, OnIsDropZoneChanged)); public static bool GetIsDropZone(DependencyObject obj) => (bool)obj.GetValue(IsDropZoneProperty); public static void SetIsDropZone(DependencyObject obj, bool value) => obj.SetValue(IsDropZoneProperty, value); /// /// Adds an event listener to this element's event. /// private static void OnIsDropZoneChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { if (sender is not FrameworkElement element) { throw new InvalidOperationException($"IsDraggable may only be attached to FrameworkElements, got {sender.GetType()} instead."); } if ((bool)e.NewValue) { element.AllowDrop = true; element.DragOver += HandleDragOver; element.DragLeave += HandleDragLeave; element.Drop += HandleDrop; } else { element.DragOver -= HandleDragOver; element.DragLeave -= HandleDragLeave; element.Drop -= HandleDrop; } } /* === Handler === */ public static readonly DependencyProperty HandlerProperty = DependencyProperty .RegisterAttached( "Handler", typeof(DependencyObject), typeof(DragAndDropControl), new PropertyMetadata(null, OnHandlerChanged)); public static DependencyObject GetHandler(DependencyObject obj) => (DependencyObject)obj.GetValue(HandlerProperty); public static void SetHandler(DependencyObject obj, bool value) => obj.SetValue(HandlerProperty, value); private static void OnHandlerChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e) { // Simple validation if (sender is not UIElement element || e.NewValue is not DragAndDropControl dndControl) { throw new InvalidOperationException($"Can not set {e.NewValue} as handler. Handler needs to be of type DragAndDropControl."); } } #endregion /// /// Attempts to retrieve the correct handler for the provided element.
/// This may either be via the defined Handler property, or the element itself if no handler property was attached. ///
private static DragAndDropControl RetrieveHandler(DependencyObject obj) { if (obj.GetValue(HandlerProperty) is not DragAndDropControl handler) { if (obj is DragAndDropControl) { handler = (DragAndDropControl)obj; } else // Todo: last resort if ((handler = FindVisualParent(obj)!) == null) { throw new InvalidOperationException($"Could not find a DragAndDropControl for element {obj}. You may explicitly provide one using the DragAndDropControl.Handler attached property."); } } return handler; } /// /// Dynamic handler for the event. /// Called when the mouse cursor moves while on an element. /// This also calls the method in order to handle any specific behaviour during the operation. /// private static void HandleMouseMove(object sender, MouseEventArgs args) { if (args.LeftButton == MouseButtonState.Pressed) { FrameworkElement element = (FrameworkElement)sender; DragAndDropControl handler = RetrieveHandler(element); DragAndDropData data = new(element, element.DataContext); DragDrop.DoDragDrop(element, data, DragDropEffects.All); handler.OnElementDrag(element, args); } } /// /// Dynamic handler for the event. /// private static void HandleGiveFeedback(object sender, GiveFeedbackEventArgs args) { FrameworkElement element = (FrameworkElement)sender; DragAndDropControl handler = RetrieveHandler(element); handler.OnElementDragGiveCursorFeedback(element, args); } /// /// Dynamic handler for the event. /// private static void HandleLeftMouseButtonUp(object sender, MouseButtonEventArgs args) { FrameworkElement element = (FrameworkElement)sender; DragAndDropControl handler = RetrieveHandler(element); handler.OnElementDragStop(element); } /// /// Dynamic handler for the event. /// private static void HandleDragOver(object sender, DragEventArgs args) { FrameworkElement element = (FrameworkElement)sender; DragAndDropControl handler = RetrieveHandler(element); handler.OnElementDragOver((DragAndDropData)args.Data.GetData(typeof(DragAndDropData)), element); } /// /// Dynamic handler for the event. /// private static void HandleDragLeave(object sender, DragEventArgs args) { FrameworkElement element = (FrameworkElement)sender; DragAndDropControl handler = RetrieveHandler(element); handler.OnElementDragLeave(element); } /// /// Dynamic handler for the event. /// private static void HandleDrop(object sender, DragEventArgs args) { FrameworkElement element = (FrameworkElement)sender; DragAndDropControl handler = RetrieveHandler(element); handler.OnElementDropped((DragAndDropData)args.Data.GetData(typeof(DragAndDropData)), element); } private static T? FindVisualParent(DependencyObject child) where T : DependencyObject { // Todo: find a better solution to this, if there even is one. // There are, in fact, many controls even in the WPF standard that do similar lookups, // so I assume it can't be _that_ bad, but every clean solution kind of seems to need some ugly spot in it. :( DependencyObject parent = VisualTreeHelper.GetParent(child); while (parent != null) { if (parent is T expected) return expected; parent = VisualTreeHelper.GetParent(parent); } return null; } }