This is Part 2 of my tutorial on Drag and Drop on Microsoft Surface.
As I stated in Part 1, I recently spent some time learning how to perform a drag and drop operation on Microsoft Surface, and thought I would share what I learned with everyone.
In Part 1, we created a Surface window with a Surface ListBox and a ScatterView in it. We hooked up our ListBox to a data source of products coming from Amazon.com. Now, we will get to the good stuff and make drag and drop a reality in our application!
First of all, we should go over the general flow of a drag and drop operation. Every method that is called in order is listed below. If methods loop back to a previous method that has already been listed, then it is not included. This list just shows every method used in the order it is first called.
I should note that 90% of this code is reusable, and those classes we will add to a folder called "ReusableClasses". From then on, anytime you want to implement drag and drop functionality, simply add the reusable classes folder. If you want to just copy over the reusable classes, the only steps you would need to implement after that would be 1, 2, 11, 24, 26, 32, 33, 36 (which is the order the methods are called in).
I originally got the idea for this tutorial when I was trying to implement drag and drop functionality on Surface for the first time. I basically had to reverse engineer the ShoppingCart sample that comes with the SDK, but that sample also contains a lot of code that is unnecessary for the most basic drag and drop operations. For example, the "checkout pod" and all the code required to determine if you were dragging out of or into a pod. So, I tried to distill this sample down to its simplest possible form; unfortunately, it still isn't that simple. The goal of this tutorial is to give someone a start-to-finish guide of all the steps involved, so you don't have to go through the whole reverse engineering process like I did. If anything isn't clear, accurate, or complete, please add a comment so I can improve this guide as much as possible. I plan to add more detail, explanation, and tips here as time goes on. I just wanted to get something out there to help out folks on this common, but far from trivial Surface task.
The first thing that happens is the following Routed Events are registered (in the SurfaceDragDrop class):
SurfaceDragEnter SurfaceDragLeave SurfaceDragOver SurfaceDragGiveFeedback SurfaceDragQueryTarget SurfaceDrop SurfaceDragCompleted Then, we proceed with calling the methods (the class the method is in is listed after the method, xaml.cs is the code in your Window):
OnPreviewContactChanged - SurfaceWindow1.xaml.cs TryStartDragDrop - SurfaceWindow1.xaml.cs public static Vector DraggedDelta(Contact contact, UIElement relativeTo) - in the class ContactsHelper private static Vector DraggedDelta(Contact contact, Window window, UIElement relativeTo) - in the class ContactsHelper private static void OnPreviewContactDown(object sender, ContactEventArgs args) - in the class ContactsHelper public static DependencyObject GetDragSource(Contact contact) - in the class ContactsHelper private static T GetParent<T>(DependencyObject child) where T: DependencyObject internal static DependencyObject FindParent(DependencyObject child, Predicate<DependencyObject> predicate) - SurfaceDragDrop internal static DependencyObject FindParent(DependencyObject child, Predicate<DependencyObject> predicate) - SurfaceDragDropHost public static SurfaceDragCursor BeginDragDrop(FrameworkElement source, object data, FrameworkElement cursorContent, double orientation, IEnumerable<Contact> contacts) - SurfaceDragDrop private static IEnumerable<Contact> Merge(ReadOnlyContactCollection contacts, Contact extraContact) - xaml.cs public static SurfaceDragDropHost GetOrCreateInstance(System.Windows.Window window, SurfaceDragDropHitTest hitTest) - SurfaceDragDropHost public static SurfaceDragDropHost GetInstance(System.Windows.Window window) - SurfaceDragDropHost protected override Visual GetVisualChild(int index) - ElementAdorner public SurfaceDragCursor BeginDragDrop(FrameworkElement source, object data, FrameworkElement cursorContent, double orientation, IEnumerable<Contact> contacts) - SurfaceDragDropHost private static Point GetCenterPosition(IEnumerable<Contact> contacts, UIElement relativeTo) - SurfaceDragDropHost private FrameworkElement HitTest(SurfaceDragCursor cursor) - SurfaceDragDropHost private static Rect GetBoundaryRect(ScatterViewItem item) - SurfaceDragDropHost public override FrameworkElement HitTest(Window window, Rect rect, double orientation, Point relativeOrigin) - SurfaceDragDropHitTest private static Geometry GetGeometry(Rect rect, double orientation, Point relativeOrigin) - SurfaceDragDropHitTest private void CursorMoved(object sender, RoutedEventArgs args) - SurfaceDragDropHost private static DragDropEffects RaiseEvent(RoutedEvent routedEvent, FrameworkElement routedEventSource, SurfaceDragCursor cursor) - SurfaceDragDropHost protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget) - SurfaceDragDropEventArgs private void OnQueryTargetOverScatterView(object sender, SurfaceDragDropQueryTargetEventArgs e) - xaml.cs public Point GetPosition(UIElement relativeTo) - SurfaceDragCursor private void OnDragEnterOverScatterView(object sender, SurfaceDragDropEventArgs e) - xaml.cs public static ReadOnlyCollection<SurfaceDragCursor> public static ReadOnlyCollection<SurfaceDragCursor> GetCursorsOver(FrameworkElement element) - SurfaceDragDrop internal static SurfaceDragDropHost FindHostForElement(FrameworkElement element) - SurfaceDragDrop private static bool IsCursorOver(SurfaceDragCursor cursor, FrameworkElement element) - SurfaceDragDropHost private void CursorLostCapture(object sender, ContactEventArgs args) - SurfaceDragDropHost public void EndDragDrop(SurfaceDragCursor cursor) - SurfaceDragDropHost private void OnDropOverScatterView(object sender, SurfaceDragDropEventArgs e) - xaml.cs private List<ScatterViewItem> CreateScatterViewItemsFromCursor(SurfaceDragCursor cursor) - xaml.cs public static bool GetIsAnyCursorOver(FrameworkElement element) - SurfaceDragDrop public bool GetIsAnyCursorOver(FrameworkElement element) - SurfaceDragDropHost private void OnDragCompleted(object sender, SurfaceDragDropEventArgs args) - xaml.cs Now that we have a general understanding of the code flow of a single drag and drop operation, let's get started making it happen.
OnPreviewContactChanged
To our surfaceListBox1, we are going to want to attach this event:
PreviewContactChanged="OnPreviewContactChanged"
If you use intellisense to create the event handler, then your code behind should now contain this event handler:
private void OnPreviewContactChanged(object sender, ContactEventArgs e)
{
}
The completed method looks like this:
private void OnPreviewContactChanged(object sender, ContactEventArgs e)
{
ListBox listBox = sender as ListBox;
if(listBox != null)
{
TryStartDragDrop(listBox, e);
return;
}
}
Basically, we want to catch the ContactChanged event as it is tunneling down to the source of the contact. Then, if the sender of the event turns out to be a listbox, we call the TryStartDragDrop method. Which happens to be our next step.
TryStartDragDrop method:
private void TryStartDragDrop(ListBox sourceListBox, ContactEventArgs e)
{
// check whether contact is in the ignore list
if(e.Contact.GetUserData(dragIgnoreKey) != null)
{
return;
}
Vector draggedDelta = ContactsHelper.DraggedDelta(e.Contact,
(UIElement)sourceListBox);
// if this contact has moved more than Threshold pixels vertically,
// put it to the ignore list and never try to start drag-and-drop with it
if (Math.Abs(draggedDelta.Y) > DragThreshold)
{
e.Contact.SetUserData(dragIgnoreKey, true);
return;
}
// if this contact has moved less than Threshold pixels horizontally
// then this is not a drag-and-drop yet
if (Math.Abs(draggedDelta.X) < DragThreshold)
{
return;
}
// try to start drag-and-drop,
// verify that the cursor the contact was placed at is a ListBoxItem
DependencyObject downSource = ContactsHelper.GetDragSource(e.Contact);
if(downSource == null)
{
return;
}
SurfaceListBoxItem sourceListBoxItem = GetParent<SurfaceListBoxItem>(downSource);
if(sourceListBoxItem == null || !sourceListBoxItem.IsHitTestVisible)
{
return;
}
Product data = sourceListBoxItem.Content as Product;
if(data == null)
{
return;
}
// get size
Size sourceSize = new Size(sourceListBoxItem.ActualWidth -
sourceListBoxItem.Padding.Left - sourceListBoxItem.Padding.Right,
sourceListBoxItem.ActualHeight - sourceListBoxItem.Padding.Top -
sourceListBoxItem.Padding.Bottom);
// get list of contacts that will be dragging the cursor
IEnumerable<Contact> draggingContacts =
Merge(sourceListBoxItem.ContactsCapturedWithin, e.Contact);
// get orientation
RotateTransform rotateTransform =
((FrameworkElement)sourceListBox).RenderTransform as RotateTransform;
double orientation = rotateTransform != null ? rotateTransform.Angle : 0;
BeginDragDrop(sourceListBoxItem, sourceSize, orientation, draggingContacts);
e.Handled = true;
}
We are going to need to add a couple of class level fields at the top to accommodate this method. Please add the following right after the opening curly brace of the SurfaceWindow1.xaml.cs class:
private static readonly object dragIgnoreKey = new object();
private const int DragThreshold = 15;
The first part of this method determines if the user is actually trying to do a drag and drop operation: by comparing the distance the user tried to drag an item against the set dragThreshold of 15. The down source is where the user actually made contact, which is then used by the GetParent method to determine the SurfaceListBoxItem that is the parent of this original contact. Then, we need to make sure that our sourceListBoxItem actually has content; otherwise, why drag it anywhere. The next important thing is the call to BeginDragDrop, but first, we make a call to DraggedDelta, which is our next step.
DraggedDelta in ContactsHelper.cs that we call looks like this:
public static Vector DraggedDelta(Contact contact, UIElement relativeTo)
{
if (contact == null)
{
throw new ArgumentNullException("contact");
}
if (relativeTo == null)
{
throw new ArgumentNullException("relativeTo");
}
Window window = contact.ActiveSource.RootVisual as Window;
if (window != null)
{
return DraggedDelta(contact, window, relativeTo);
}
return ZeroVector;
}
There's nothing really that interesting here, except that it calls a different overload of DraggedDelta.
DraggedDelta called looks like this:
private static Vector DraggedDelta(Contact contact,
Window window, UIElement relativeTo)
{
// get the current position
Point currentPosition = contact.GetPosition(window);
// get the down state
object downState = contact.GetUserData(key);
if (downState == null)
{
downState = new DragState(currentPosition,
(DependencyObject)contact.DirectlyOver);
contact.SetUserData(key, downState);
return ZeroVector;
}
// translate to the relativeTo elementToDrag
Point downPosition = ((DragState)downState).Position;
if (relativeTo != window)
{
currentPosition = window.TranslatePoint(currentPosition, relativeTo);
downPosition = window.TranslatePoint(downPosition, relativeTo);
}
Vector delta = currentPosition - downPosition;
return delta;
}
This version of the overload is what really does our calculation to determine if we met the dragThreshold for our drag and drop yet.
OnPreviewContactDown, which looks like this:
private static void OnPreviewContactDown(object sender, ContactEventArgs args)
{
Window window = (Window)sender;
DragState downState = new DragState(args.GetPosition(window),
(DependencyObject)args.Contact.DirectlyOver);
args.Contact.SetUserData(key, downState);
}
This event handler was registered in the ContactsHelper constructor, which was already called.
GetDragSource, also in the ContactsHelper class, and it looks like this:
public static DependencyObject GetDragSource(Contact contact)
{
if (contact == null)
{
throw new ArgumentNullException("contact");
}
object dragState = contact.GetUserData(key);
if (dragState == null)
{
Window window = contact.ActiveSource.RootVisual as Window;
if (window != null)
{
DependencyObject directlyOver = (DependencyObject)contact.DirectlyOver;
Point currentPosition = contact.GetPosition(window);
dragState = new DragState(currentPosition, directlyOver);
contact.SetUserData(key, dragState);
return directlyOver;
}
return null;
}
return ((DragState)dragState).Source;
}
The purpose of this method is probably pretty obvious. We need to know on what visual object the drag drop operation began on and then return it as a DependencyObject.
GetParent method in our SurfaceWindow1.xaml.cs file:
private static T GetParent<T>(DependencyObject child) where T: DependencyObject
{
return (T)SurfaceDragDrop.FindParent(child, delegate(DependencyObject d)
{
return d is T;
});
}
This generic method is responsible for calling the SurfaceDragDrop FindParent method and giving it the type of parent we are looking for. In this case, it is SurfaceListBoxItem.
SurfaceDragDrop, which we will add the same way we did for the ContactsHelper class. In it, we will add the FindParent method like so:
internal static DependencyObject FindParent(DependencyObject child,
Predicate<DependencyObject> predicate)
{
return SurfaceDragDropHost.FindParent(child, predicate);
}
Obviously, all we are doing is calling another version of FindParent which is in the SurfaceDragDropHost class.
FindParent method looks like this:
internal static DependencyObject FindParent(DependencyObject child,
Predicate<DependencyObject> predicate)
{
// go through visual tree first
Visual visualChild = child as Visual;
DependencyObject visualParent = null;
if (child != null)
{
// get visual parent
visualParent = VisualTreeHelper.GetParent(visualChild);
if (visualParent != null)
{
// check the the visualParent,
if (predicate(visualParent) ||
(visualParent =
FindParent(visualParent, predicate)) != null)
{
return visualParent;
}
}
}
// walk the logical tree
DependencyObject logicalParent = LogicalTreeHelper.GetParent(child);
if (logicalParent != null)
{
if (visualParent == logicalParent)
{
// we have already checked the visual branch and found nothing
return null;
}
// check the logicalParent,
// if it's not the parent then call IsDescendant recursively
// with logicalParent as a new child candidate
if (predicate(logicalParent) || (logicalParent =
FindParent(logicalParent, predicate)) != null)
{
return logicalParent;
}
}
return null;
}
Obviously, this is where the real logic to the FindParent operation lies.
BeginDragDrop method in the SurfaceDragDrop class, which looks like this:
public static SurfaceDragCursor BeginDragDrop(FrameworkElement source, object data,
FrameworkElement cursorContent, double orientation,
IEnumerable<Contact> contacts)
{
if (cursorContent == null)
{
throw new ArgumentNullException("element");
}
if (contacts == null)
{
throw new ArgumentNullException("contacts");
}
Contact firstContact = null;
foreach (Contact contact in contacts)
{
if (contact == null)
{
throw new ArgumentException("Null contact.", "contacts");
}
if (firstContact == null)
{
firstContact = contact;
}
}
if (firstContact == null)
{
throw new ArgumentException("Invalid number of contacts.",
"contacts");
}
if (hitTest == null)
{
throw new ArgumentNullException("hitTest");
}
// find the window that the given contacts are over
// assumption: all contacts given are over the same window
System.Windows.Window window =
System.Windows.Window.GetWindow(firstContact.ActiveSource.RootVisual);
// get the SurfaceDragDropHost instance for that window
// and start the drag
SurfaceDragDropHost host =
SurfaceDragDropHost.GetOrCreateInstance(window, SurfaceDragDrop.hitTest);
if (host == null)
{
return null;
}
return host.BeginDragDrop(source, data, cursorContent, orientation, contacts);
}
Merge method that you put into SurfaceWindow1.xaml.cs, which looks like this:
private static IEnumerable<Contact> Merge(ReadOnlyContactCollection contacts,
Contact extraContact)
{
bool duplicate = false;
foreach(Contact contact in contacts)
{
if(contact == extraContact)
{
duplicate = true;
}
yield return contact;
}
if(!duplicate)
{
yield return extraContact;
}
}
The purpose of the Merge method is it merges the given collection and the item without duplications. Then, it returns a new enumerator.
SurfaceDragDropHost called GetOrCreateInstance, which looks like this:
public static SurfaceDragDropHost GetOrCreateInstance(System.Windows.Window window,
SurfaceDragDropHitTest hitTest)
{
SurfaceDragDropHost host = GetInstance(window);
if (host != null)
{
return host;
}
// get AdornerLayer
UIElement windowContent = window.Content as UIElement;
if (windowContent == null)
{
return null;
}
AdornerLayer adornerLayer = AdornerLayer.GetAdornerLayer(windowContent);
if (adornerLayer == null)
{
return null;
}
// Create SurfaceDragDropHost and add it to the Adorner layer
host = new SurfaceDragDropHost(window, hitTest);
adornerLayer.Add(new ElementAdorner(windowContent, host));
window.UpdateLayout();
// register SurfaceDragDropHost for quick lookup
window.RegisterName(host.Name, host);
return host;
}
This method tries to get an instance of SurfaceDragDropHost for the given window, or creates a new one if it doesn't exist.
SurfaceDragDropHost called GetInstance, shown here:
public static SurfaceDragDropHost GetInstance(System.Windows.Window window)
{
// check whether the given window has a SurfaceDragDropHost element
SurfaceDragDropHost host = window.FindName(HostName) as SurfaceDragDropHost;
return host;
}
This method returns the existing instance of SurfaceDragDropHost if it exists.
GetVisualChild:
protected override Visual GetVisualChild(int index)
{
return adorningElement;
}
This method gets the visual by index.
BeginDragDrop. This time in the SurfaceDragDropHost class:
public SurfaceDragCursor BeginDragDrop(FrameworkElement source, object data,
FrameworkElement cursorContent, double orientation,
IEnumerable<Contact> contacts)
{
// construct a ScatterViewItem
ScatterViewItem visual = new ScatterViewItem();
visual.Template = this.template;
visual.Content = cursorContent;
visual.Height = cursorContent.Height;
visual.Width = cursorContent.Width;
visual.MinWidth = 1;
visual.MinHeight = 1;
visual.IsHitTestVisible = true;
// disable inertia
visual.DecelerationRate = double.NaN;
// center the cursor under the contacts
Point contactCenter = GetCenterPosition(contacts, this);
visual.Center = new Point(contactCenter.X, contactCenter.Y);
visual.Orientation = orientation;
visual.CanScale = false;
visual.IsActive = true;
// store information needed to track this cursor
SurfaceDragCursor cursor = new SurfaceDragCursor(source, data, visual);
// add the cursor to the dictionary
this.cursors.Add(visual, cursor);
// add the cursor to the scatterview and capture contacts to it
Items.Add(visual);
foreach (Contact contact in contacts)
{
// Try to capture the contact. Once contacts
// are captured, CursorMoved or LostContactCapture
// callbacks could be called, so make sure everything
// used in these callbacks has been initalized.
if (!contact.Capture(visual))
{
// capture failed, remove visual and return null
Items.Remove(visual);
return null;
}
}
if (!visual.IsAnyContactOver)
{
// drag and drop already complete
return null;
}
// set an initial drop target for the cursor
cursor.Target = HitTest(cursor);
return cursor;
}
This method begins a drag-and-drop operation:
ScatterViewItem to host the element being dragged ScatterView ScatterViewItem's Move and Complete events to manage the drag and drop process GetCenterPosition in the SurfaceDragDropHost class:
private static Point GetCenterPosition(IEnumerable<Contact> contacts,
UIElement relativeTo)
{
Point center = new Point();
int count = 0;
foreach (Contact c in contacts)
{
Point position = c.GetPosition(relativeTo);
center += (Vector)position;
count++;
}
if (count > 0)
{
center.X /= count;
center.Y /= count;
}
return center;
}
This method finds the center point of a set of contacts relative to a specific element.
HitTest, also in the SurfaceDragDropHost class:
private FrameworkElement HitTest(SurfaceDragCursor cursor)
{
// find the target control
FrameworkElement target = this.hitTest.HitTest(this.ownerWindow,
GetBoundaryRect(cursor.Visual),
cursor.Visual.Orientation,
cursor.Visual.RenderTransformOrigin);
if (target == null)
{
return null;
}
// find a place to drop inside the target
SurfaceDragDropQueryTargetEventArgs args =
new SurfaceDragDropQueryTargetEventArgs(target, cursor);
target.RaiseEvent(args);
// returns the new target
return args.ProposedTarget as FrameworkElement;
}
This method performs hit testing, and calls the QueryTarget routed event on the target to refine the hit test result.
GetBoundaryRect in the SurfaceDragDropHost class:
private static Rect GetBoundaryRect(ScatterViewItem item)
{
double itemLeft = item.Center.X - item.ActualWidth / 2;
double itemTop = item.Center.Y - item.ActualHeight / 2;
return new Rect(itemLeft, itemTop, item.ActualWidth, item.ActualHeight);
}
This method returns the boundary rectangle for a given ScatterViewItem.
HitTest in the SurfaceDragDropHitTest class (which is new, so we need to create it like we did the others):
public override FrameworkElement HitTest(Window window,
Rect rect, double orientation, Point relativeOrigin)
{
// get the geometry for the rect
Geometry geometry = GetGeometry(rect, orientation, relativeOrigin);
// perform hit testing and return the very top element with AllowDrop=true
FrameworkElement hitTestResult = null;
VisualTreeHelper.HitTest(window,
// filter callback
delegate(DependencyObject potentialHitTestTarget)
{
FrameworkElement element = potentialHitTestTarget as FrameworkElement;
if (element != null)
{
// skip SurfaceDragDropHost and all its children
if (element is SurfaceDragDropHost)
{
return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
}
// Skip elements that are not visible
if (!element.IsVisible || element.Opacity <= 0.0)
{
return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
}
// If the element allows drop, then that is the target
if (element.AllowDrop)
{
// found the element, stop
hitTestResult = element;
return HitTestFilterBehavior.Stop;
}
}
return HitTestFilterBehavior.Continue;
},
// result callback
// Stop after the first result, otherwise hit testing will continue to
// elements that are obscured by other elements in the visual tree
delegate(HitTestResult result) { return HitTestResultBehavior.Stop; },
// parameters
new GeometryHitTestParameters(geometry));
return hitTestResult;
}
This method returns the first element the given rect is over.
GetGeometry, still in the SurfaceDragDropHitTest class:
private static Geometry GetGeometry(Rect rect, double orientation,
Point relativeOrigin)
{
// create RectangleGeometry
Geometry geometry = new RectangleGeometry(rect);
// apply orientation
Matrix matrix = new Matrix();
Point origin = rect.TopLeft + new Vector(relativeOrigin.X * rect.Width,
relativeOrigin.Y * rect.Height);
matrix.Translate(-origin.X, -origin.Y);
matrix.Rotate(orientation);
matrix.Translate(origin.X, origin.Y);
geometry.Transform = new MatrixTransform(matrix);
return geometry;
}
This method constructs a RectangleGeometry instance for a given rect and orientation.
CursorMoved in the SurfaceDragDropHost class:
private void CursorMoved(object sender, RoutedEventArgs args)
{
// mark event as handled
args.Handled = true;
ScatterViewItem visual = (ScatterViewItem)args.Source;
SurfaceDragCursor cursor = this.cursors[visual];
FrameworkElement newTarget = HitTest(cursor);
// check if it's now a different target
FrameworkElement oldTarget = cursor.Target;
if (oldTarget != newTarget)
{
// update the target before raising Leave event
// (to be consistent with ContactLeave event)
cursor.Target = newTarget;
// Leave the old target and Enter the new target (if there is one)
// Raise Leave event for the old target first
if (oldTarget != null)
{
// ignore the result dragDrop effect from the Leave event
RaiseEvent(SurfaceDragDrop.SurfaceDragLeaveEvent,
oldTarget, cursor);
}
// set drop effect to None
cursor.DragDropEffects = DragDropEffects.None;
// raise the GiveFeedback event for the source
if (cursor.Source != null)
{
// update the dragDrop effect
cursor.DragDropEffects = RaiseEvent(
SurfaceDragDrop.SurfaceDragGiveFeedbackEvent,
cursor.Source, cursor);
}
// raise Enter event for the target
if (newTarget != null)
{
// update the dragDrop effect
cursor.DragDropEffects =
RaiseEvent(SurfaceDragDrop.SurfaceDragEnterEvent,
newTarget, cursor);
}
}
else
{
// raise the GiveFeedback event for the source
if (cursor.Source != null)
{
// update the dragDrop effect
cursor.DragDropEffects =
RaiseEvent(SurfaceDragDrop.SurfaceDragGiveFeedbackEvent,
cursor.Source, cursor);
}
}
// raise the over event for the target
if (cursor.Target != null)
{
// update the dragDrop effect
cursor.DragDropEffects =
RaiseEvent(SurfaceDragDrop.SurfaceDragOverEvent,
cursor.Target, cursor);
}
}
This method is a handler which is called when the position or orientation of the ScatterViewItem is changed.
RaiseEvent in SurfaceDragDropHost:
private static DragDropEffects RaiseEvent(RoutedEvent routedEvent,
FrameworkElement routedEventSource, SurfaceDragCursor cursor)
{
SurfaceDragDropEventArgs args = new SurfaceDragDropEventArgs(routedEvent,
routedEventSource, cursor);
routedEventSource.RaiseEvent(args);
return args.ProposedEffects;
}
This method raises a routed event.
InvokeEventHandler in the SurfaceDragDropEventArgs class (which we still need to add to our reusable classes folder):
[EditorBrowsable(EditorBrowsableState.Never)]
protected override void InvokeEventHandler(Delegate genericHandler,
object genericTarget)
{
((EventHandler<SurfaceDragDropEventArgs>)genericHandler)(genericTarget, this);
}
This method provides correct type casting to the SurfaceDragDropEventHandler.
OnQueryTargetOverScatterView in SurfaceWindow1.xaml.cs:
private void OnQueryTargetOverScatterView(object sender,
SurfaceDragDropQueryTargetEventArgs e)
{
ScatterView targetScatterView = (ScatterView)sender;
Point relativePosition = e.Cursor.GetPosition(targetScatterView);
e.ProposedTarget = targetScatterView;
}
This method handles the QueryTargetOver event for the ScatterView1. The parent ScatterView is the target for all drag-drops that occur within its bounds, even ones that occur over both the ScatterView and a ScatterViewItem on that ScatterView. QueryTargetOver gives the parent ScatterView the chance to make one of its children the drop target instead of itself.
GetPosition in the SurfaceDragCursor class (which we now need to add to our reusable classes folder):
public Point GetPosition(UIElement relativeTo)
{
Point center = new Point(visual.Width / 2, visual.Height / 2);
return this.visual.TranslatePoint(center, relativeTo);
}
This method returns the center of the cursor relative to a given element.
OnDragEnterOverScatterView found in SurfaceWindow1.xaml.cs:
private void OnDragEnterOverScatterView(object sender, SurfaceDragDropEventArgs e)
{
ScatterView targetScatterView = (ScatterView)sender;
Highlight(targetScatterView, true);
e.ProposedEffects = DragDropEffects.Move;
e.Handled = true;
}
This method handles the Enter event for a ScatterView as a target.
GetCursorsOver in the SurfaceDragDrop class:
public static ReadOnlyCollection<SurfaceDragCursor>
GetCursorsOver(FrameworkElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
// get the SurfaceDragDropHost instance for the given element
SurfaceDragDropHost host = FindHostForElement(element);
if (host == null)
{
return SurfaceDragDropHost.EmptySurfaceDraggedElementCollection;
}
return host.GetCursorsOver(element);
}
This method returns a collection of cursors over the given element.
FindHostForElement found in the SurfaceDragDrop class:
internal static SurfaceDragDropHost FindHostForElement(FrameworkElement element)
{
// find the window that the given element belongs to
System.Windows.Window window = System.Windows.Window.GetWindow(element);
// get the SurfaceDragDropHost instance for that window
return SurfaceDragDropHost.GetInstance(window);
}
This method finds the SurfaceDragDropHost for the window that owns the element.
IsCursorOver found in the SurfaceDragDropHost class:
private static bool IsCursorOver(SurfaceDragCursor cursor,
FrameworkElement element)
{
return cursor.Target != null && (cursor.Target == element ||
FindParent(cursor.Target, delegate(DependencyObject d)
{return d == element;}) != null);
}
This method determines if a specific cursor is over a specific FrameworkElement.
CursorLostCapture also found in SurfaceDragDropHost:
private void CursorLostCapture(object sender, ContactEventArgs args)
{
// mark as handled
args.Handled = true;
// check if the source of the LostCapture event is ScatterViewItem
ScatterViewItem visual = args.OriginalSource as ScatterViewItem;
if (visual != null)
{
// check if this is one of the cursors
// and whether it has any more contacts
SurfaceDragCursor cursor;
if (this.cursors.TryGetValue(visual, out cursor) &&
!visual.IsAnyContactCaptured)
{
// end the drag and drop
EndDragDrop(cursor);
}
}
}
EndDragDrop found in SurfaceDragDrop:
public static void EndDragDrop(SurfaceDragCursor cursor)
{
if (cursor == null)
{
throw new ArgumentNullException("cursor");
}
// Find the cursor's SurfaceDragDropHost
SurfaceDragDropHost host = FindHostForElement (cursor.Visual);
if (host != null)
{
host.EndDragDrop(cursor);
}
}
OnDropOverScatterView found in SurfaceWindow1.xaml.cs:
private void OnDropOverScatterView(object sender, SurfaceDragDropEventArgs e)
{
// Get the ScatterView to add the item to
ScatterView targetScatterView = (ScatterView)sender;
// Get a ScatterViewItem to add to the ScatterView
ScatterViewItem itemToAdd = CreateScatterViewItemFromCursor(e.Cursor);
// Make sure itemToAdd's center is inside
// the bounds of targetScatterView. If it isn't,
// dont add it to the ScatterView.
Rect boundary = new Rect(0, 0, targetScatterView.ActualWidth,
targetScatterView.ActualHeight);
if(!boundary.Contains(itemToAdd.Center))
return;
// Modify IsActive to give it a bouncing effect when it lands
itemToAdd.IsActive = true;
targetScatterView.Items.Add(itemToAdd);
itemToAdd.IsActive = false;
// drop highlighting
Highlight(targetScatterView, false);
e.ProposedEffects = DragDropEffects.Move;
e.Handled = true;
}
This method handles the Drop event for a ScatterView as a target.
CreateScatterViewItemFromCursor found in SurfaceWindow1.xaml.cs:
private ScatterViewItem CreateScatterViewItemFromCursor(SurfaceDragCursor cursor)
{
ScatterViewItem svi = new ScatterViewItem();
ScatterViewItem sourceScatterViewItem = cursor.Source as ScatterViewItem;
if(sourceScatterViewItem != null)
{
object sourceContent = sourceScatterViewItem.Content;
sourceScatterViewItem.Content = null;
svi.DataContext = sourceScatterViewItem.DataContext;
svi.Content = sourceContent;
svi.Height = sourceScatterViewItem.Height;
svi.Width = sourceScatterViewItem.Width;
}
else
{
// Make an Image that will be the ScatterViewItem's content
Image contentImage = new Image();
// Get the image path out of the source's contents
ContentControl source = (ContentControl)cursor.Source;
Product sourceData = (Product)source.Content;
string imagePath = sourceData.MediumImageUrl;
Uri imageUri2 = new Uri(imagePath);
// Load the image
BitmapImage bitmap = new BitmapImage(imageUri2);
contentImage.Source = bitmap;
contentImage.Stretch = Stretch.Uniform;
svi.DataContext = cursor.Source.DataContext;
svi.Content = contentImage;
svi.Height = 140;
svi.Width = 100;
}
svi.MaxHeight = 300;
svi.Center = cursor.Visual.Center;
svi.Orientation = cursor.Orientation;
svi.ScatterManipulationCompleted += new
ScatterManipulationCompletedEventHandler(ScatterManipulationCompleted);
svi.ScatterManipulationDelta += new
ScatterManipulationDeltaEventHandler(ScatterManipulationDeltaChanged);
return svi;
}
This method creates a ScatterViewItem from a SurfaceDragCursor.
GetIsAnyCursorOver is found in SurfaceDragDrop:
public static bool GetIsAnyCursorOver(FrameworkElement element)
{
if (element == null)
{
throw new ArgumentNullException("element");
}
// get the SurfaceDragDropHost instance for the given element
SurfaceDragDropHost host = FindHostForElement(element);
if (host == null)
{
return false;
}
return host.GetIsAnyCursorOver(element);
}
This method returns true if there is at least one cursor over the given element.
GetIsAnyCursorOver found in SurfaceDragDropHost:
public bool GetIsAnyCursorOver(FrameworkElement element)
{
foreach (SurfaceDragCursor cursor in this.cursors.Values)
{
if (IsCursorOver(cursor, element))
{
return true;
}
}
return false;
}
OnDragCompleted, found in SurfaceWindow1.xaml.cs:
private void OnDragCompleted(object sender, SurfaceDragDropEventArgs args)
{
SurfaceListBox senderListBox = sender as SurfaceListBox;
if(senderListBox != null)
{
args.Cursor.Source.Opacity = 1;
args.Cursor.Source.IsHitTestVisible = true;
}
activeItemsUnderManipulation.Remove(args.Cursor);
}
This method handles the DragCompleted event for a source item.
Wow! That is a lot of code just to drag something from a ListBox onto the ScatterView! Like I mentioned earlier, we only need to implement a fraction of this code once we have the reusable classes created.
Now, let's finish up the XAML and see things actually drag and drop. First, your surfaceListView1 should look like this after you have entered the two event handlers that are highlighted:
<s:SurfaceListBox
Name="surfaceListBox1"
ItemsSource="{Binding}"
Style="{StaticResource ProductListStyle}"
PreviewContactChanged="OnPreviewContactChanged"
dragdrop:SurfaceDragDrop.SurfaceDragCompleted="OnDragCompleted"
Height="Auto"
HorizontalAlignment="Left"
VerticalAlignment="Stretch"
Width="135"
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
ScrollViewer.VerticalScrollBarVisibility="Hidden"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
/>
Your ScatterView1 should look like this after you have added the highlighted items:
<s:ScatterView
Name="ScatterView1"
dragdrop:SurfaceDragDrop.SurfaceDragQueryTarget="OnQueryTargetOverScatterView"
dragdrop:SurfaceDragDrop.SurfaceDrop="OnDropOverScatterView"
dragdrop:SurfaceDragDrop.SurfaceDragEnter="OnDragEnterOverScatterView"
dragdrop:SurfaceDragDrop.SurfaceDragLeave="OnDragLeaveOverScatterView"
dragdrop:SurfaceDragDrop.SurfaceDragCompleted="OnDragCompleted"
AllowDrop="True"
/>
Now, you should be able to drag and drop away. If yours doesn't work for some reason, I am attaching the final product for you to download and compare. If I have forgotten a step in this tutorial, please let me know so I can fix it.
| You must Sign In to use this message board. | ||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||