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