using System; using System.Collections.Generic; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Shapes; using System.Windows.Interactivity; using System.Windows.Threading; using System.Linq; namespace mdcScreens { /// /// --------------------------------------------------------------------------------------- /// SilverlightDragDropCopyItem.cs /// Original by Microsoft. /// This Silverlight (0.)3 port by Michael DiBernardo (mikedebo@gmail.com). /// --------------------------------------------------------------------------------------- /// /// This behaviour implements the listbox drag-and-drop behaviour that was demoed /// for WPF apps in that original freaking video that made me want to try SketchFlow /// in the first place, you bastards. It is a hacky, hacky port to SilverLight 3.0 (beta, /// shipped August something something 2009). /// /// The original implementation binds a bunch of mouse down events to the list items /// in the associated object when it loads, and then figures out whether you've dropped it /// onto a target list box using VisualTreeHelpers.FindHostCoor... blah blah, in the frame /// of reference of the panel that houses the grids. This implementation had to hack around /// a bunch of stuff. See the list of hacks, below. /// /// Requirements: /// - Your root layout panel MUST BE CALLED "LayoutRoot". I know, I'm sorry. I really /// lost my patience near the end. /// - Any target listbox that you'd like to drag items ONTO must have a data template /// that lays out the item in question. This was also a requirement of the original /// implementation. /// /// List of WTFs: /// #WTF1: /// There are no preview events on ListBoxItems in Silverlight. Thanks guys. The way /// I hacked around this is by finding ALL UI elements in each ListBoxItem, and binding /// the MouseDown events to all of them. /// /// #WTF2: /// In the WPF implementation of this behaviour, all data items in the associated list box /// are iterated through when the AssociatedObject loads. The corresponding ListBoxItems /// are then fetched from the ItemContainerGenerator on the source box, and the mouse events /// are bound to them. Cute. /// /// In Silverlight, these containers are not necessarily available when the box loads. You /// should theoretically be able to bind to ItemsChanged on the generator and use that as /// a signal that all containers are ready to go, but experimentation shows that they are /// actually materialized randomly at runtime as you mess around with the listbox itself. So, /// the only way I could find to bind the mouse events to all the listbox items was to set /// a timer that checks every 100ms and remembers which items have already been bound. Seriously /// WTF. Fix this shit please. /// /// #WTF3: /// When the old VisualTreeHelper coordinate translater method was changed from 2.0 /// to 3.0, someone fed it a WHOLE LOTTA paint chips. I've never found it to be reliable /// in any frame of reference except the visual root. The WPF implementation of this behaviour /// uses the associated listbox's parent panel as the frame of reference when trying to find /// a target box under the MouseUp. Here, I had to replace that with the control's visual root, /// and fetch the base layout panel whenever I needed to render the draggy plus grid. I couldn't /// be assed to find a way to find this panel by searching the visual tree at runtime, so I /// decided to force the user to name this panel "LayoutRoot", and I fetch it by name. /// /// PLEASE PLEASE PLEASE improve this if you find ways to remove some of the hacks. A lot of what /// I did here may have been completely unnecessary, and that would actually make me feel a lot /// better about the whole thing. /// public class SilverlightDragDropCopyItem : Behavior { // The "plus" thing that appears when you drag an item. private Grid plusGrid_; // The deepest, darkest root of this page. We use this instead of the // AssociatedObject's parent panel because of #WTF1. private UserControl visualRoot_; // The data item being dragged. private object draggedItem_; // Are we draggin'? private bool isDragging_; // Used to hack the binding of mousedown events to list items. See #WTF2 for // more details. DispatcherTimer eventBindingTimer_; // Used to keep track of what list items we've already bound mouse events to. // See #WTF2 for more details. Dictionary boundItemsSet_; #region Initialization /// /// Initialize a new DragDropCopyItem behaviour. /// public SilverlightDragDropCopyItem() { plusGrid_ = createPlusGrid(); eventBindingTimer_ = new DispatcherTimer(); eventBindingTimer_.Tick += new EventHandler(checkForAndBindNewListItems); eventBindingTimer_.Interval = TimeSpan.FromMilliseconds(100); boundItemsSet_ = new Dictionary(); } /// /// Create the Plus grid thing that appears when you drag-drop. /// private static Grid createPlusGrid() { Rectangle backdrop = new Rectangle() { Width = 20, Height = 20, Fill = new SolidColorBrush(Colors.Gray), RadiusX = 4, RadiusY = 4, IsHitTestVisible = false, }; Line verticalBar = new Line() { X1 = 10, X2 = 10, Y1 = 5, Y2 = 15, Stroke = new SolidColorBrush(Colors.White), StrokeThickness = 3, }; Line horizontalBar = new Line() { X1 = 5, X2 = 15, Y1 = 10, Y2 = 10, Stroke = new SolidColorBrush(Colors.White), StrokeThickness = 3, }; return new Grid() { VerticalAlignment = VerticalAlignment.Top, HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(-21), Children = { backdrop, verticalBar, horizontalBar }, }; } protected override void OnAttached() { base.OnAttached(); AssociatedObject.Loaded += OnAssociatedObjectLoaded; } protected override void OnDetaching() { AssociatedObject.Loaded -= OnAssociatedObjectLoaded; base.OnDetaching(); } /// /// Binds the visualRoot_ and triggers the timer that will bind mousedown /// events to list items as they appear in the associated list box. /// private void OnAssociatedObjectLoaded(object sender, RoutedEventArgs e) { AssociatedObject.SelectedIndex = -1; visualRoot_ = findParentUserControl(); eventBindingTimer_.Start(); if (visualRoot_ == null) return; visualRoot_.MouseLeftButtonUp += Parent_MouseLeftButtonUp; } /// /// Finds the visual root of the control by traversing upwards /// from the associated object. /// /// The visual root of the base control. private UserControl findParentUserControl() { DependencyObject current = AssociatedObject; while (true) { var nextParent = VisualTreeHelper.GetParent(current); if (nextParent == null) break; current = nextParent; } return current as UserControl; } #endregion #region Binding mousedown events to list items /// /// Timer event that checks for new list items that haven't /// had MouseDown events bound to them yet. See #WTF2 for details. /// private void checkForAndBindNewListItems(object sender, EventArgs e) { if (AssociatedObject.Items.Count == 0) { eventBindingTimer_.Stop(); return; } BindMouseEventsToItems(); } /// /// Iterates through all items in the associated listbox and gets any containers /// that Silverlight may have freaking decided to materialize out of nowhere at its /// bloody convenience, and binds mousedown items to them if they haven't been thus /// bound already. /// private void BindMouseEventsToItems() { foreach (object item in AssociatedObject.Items) { ListBoxItem listBoxItem = AssociatedObject.ItemContainerGenerator.ContainerFromItem(item) as ListBoxItem; if (listBoxItem == null || boundItemsSet_.ContainsKey(listBoxItem)) continue; // This is hackspeak for 'add listBoxItem to the set'. boundItemsSet_[listBoxItem] = NoneType.None; // Get everything in the listbox item and wire it with the mousedown event, since we can't use the preview events // from WPF. int listBoxChildCount = VisualTreeHelper.GetChildrenCount(listBoxItem); // God forbid they give us an enumerable. for (int childIndex = 0; childIndex < listBoxChildCount; ++childIndex) { UIElement listBoxChild = VisualTreeHelper.GetChild(listBoxItem, childIndex) as UIElement; if (listBoxChild == null) continue; listBoxChild.MouseLeftButtonDown += Item_SourceItemMouseLeftButtonDown; } } } #endregion #region Dragging and dropping private void Item_SourceItemMouseLeftButtonDown(object sender, MouseButtonEventArgs e) { visualRoot_.MouseMove += Parent_MouseDrag; } private void Parent_MouseDrag(object sender, MouseEventArgs e) { if (!isDragging_) { isDragging_ = true; draggedItem_ = AssociatedObject.SelectedItem as Object; visualRoot_.CaptureMouse(); // Grab the panel on the visual root so that we can stick the draggy // grid there. Panel rootPanel = getRootPanel(); if (!rootPanel.Children.Contains(plusGrid_)) { rootPanel.Children.Add(plusGrid_); } } Point pos = e.GetPosition(visualRoot_); plusGrid_.RenderTransform = new TranslateTransform() { X = pos.X, Y = pos.Y}; } private void Parent_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (isDragging_) { ProcessDrop(e.GetPosition(visualRoot_)); } Panel rootPanel = getRootPanel(); rootPanel.Children.Remove(plusGrid_); visualRoot_.ReleaseMouseCapture(); visualRoot_.MouseMove -= Parent_MouseDrag; isDragging_ = false; } private void ProcessDrop(Point point) { // Compute using visual root coordinates. See WTF3 for details. var result = VisualTreeHelper.FindElementsInHostCoordinates(point, visualRoot_); if (result == null) return; ListBox target = null; // Walk up the visual tree, looking for a ListBox foreach (var element in result) { if (!(element is ListBox)) continue; target = element as ListBox; break; } if (target == null || target == AssociatedObject || target.ItemsSource != null) return; target.Items.Add(draggedItem_); target.ScrollIntoView(draggedItem_); } #endregion #region Helpers /// /// Gets the root panel that we use to stick the draggy plus grid /// onto. This is a hack right now, it requires the root panel to /// be named LayoutRoot. /// /// The root panel. private Panel getRootPanel() { return visualRoot_.FindName("LayoutRoot") as Panel; } /// /// Represents the nil type. Used to fake a Set from a dictionary. /// enum NoneType { None } #endregion } }