Click here to Skip to main content
Email Password   helpLost your password?

Introduction

This is Part 2 of my tutorial on Drag and Drop on Microsoft Surface.

Background

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.

Using the code

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):

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):

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.

  1. 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.

  2. Here is the complete code for the 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.

  3. We are now moving to another class, so please create a new class by right-clicking on the DragDropTutorial project node, then choosing "Add", then "New Class"; name it ContactsHelper.cs. The first overload of 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.

  4. The next overload of 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.

  5. The next method called is 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.

  6. The next step in our drag and drop journey is the 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.

  7. Now, we need to look at the 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.

  8. We need to add a new class to hold our next method called 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.

  9. Now, we need to add this class as well. The other 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.

  10. Now, it's time to start with the 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);
    }
  11. Next is the 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.

  12. The next method is found in 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.

  13. Our next method again comes from 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.

  14. The next method is GetVisualChild:
    protected override Visual GetVisualChild(int index)
    {
        return adorningElement;
    }

    This method gets the visual by index.

  15. Next is 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:

    1. constructs a ScatterViewItem to host the element being dragged
    2. adds it to the ScatterView
    3. listens for ScatterViewItem's Move and Complete events to manage the drag and drop process
  16. The next method is 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.

  17. The next method is 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.

  18. The next method is 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.

  19. The next method is another method named 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.

  20. The next method is 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.

  21. The next method is 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.

  22. The next one is 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.

  23. The next method is 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.

  24. The next method is 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.

  25. The next method is 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.

  26. The next method is 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.

  27. The next method is 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.

  28. The next method is 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.

  29. The next method is 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.

  30. The next method is 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);
            }
        }
    }
  31. The next method is 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);
        }
    }
  32. The next method is 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.

  33. The next method is 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.

  34. The next method 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.

  35. The next method is the second GetIsAnyCursorOver found in SurfaceDragDropHost:
    public bool GetIsAnyCursorOver(FrameworkElement element)
    {
    
        foreach (SurfaceDragCursor cursor in this.cursors.Values)
        {
            if (IsCursorOver(cursor, element))
            {
                return true;
            }
        }
        return false;
    }
  36. This method returns true if there is at least one cursor over the given element.
  37. The last method is 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.
 
 
Per page   
 FirstPrevNext
GeneralHitTestParameters not supported on Viewport3DVisual
dreambeast
0:04 28 Jul '09  
Hi,

thanks for this informative tutorial. However, I have ran into a problem when I tried to use the reusable code for my own program. As I tried to drag an item from the surface listbox, I get a debug error -'GeometryHitTestParameters'HitTestParameters are not supported on 'Viewport3DVisual'. The code breaks right at the VisualTreeHelper.HitTest method in the SurfaceDragDropHitTest class. Do you have any idea what might have caused it? I'm using a StackPanel with an ImageSource in my surface listbox item. Both my scatterview and surface list box are in a Dock Panel. Thanks for any help.
GeneralRe: HitTestParameters not supported on Viewport3DVisual
radupoe
4:50 5 Aug '09  
Hi, I have the same problem. Did you find any solving?
Best,
Radu
http://www.radupoenaru.com/[^]
GeneralDrag and Drop under SP1
Jianping Yan
15:10 15 Jun '09  
Hi,

The drag and drop feature in the sample application runs well under SDK on Surface unit. However, the same feature crashes after install MS SP1.

Do you have any idea?

thanks,
jianping Yan
Generalproblems with building
dabsabre
7:28 1 May '09  
I grabbed your final source code and are getting "The attachable property 'SurfaceDragXXX' was not found in type 'SurfaceDragDrop'.

for each of the dragdrop: items in the ScatterView1 in my .xaml

I double checked to make sure I had the namespace set up, and it is as follows:

xmlns:dragdrop="clr-namespace:DragAndDropTutorial"

but I'm still getting errors in the ScatterView1 section for all the dragdrop namespace references in my .xaml

any ideas?
GeneralRe: problems with building
rbunn83815
9:35 1 May '09  
Does it still run even though the xaml errors are present?
GeneralHow start
Jianping Yan
11:53 28 Apr '09  
Hi,

there is DragAndDropTutorial.vshost.exe in the bin folder from your zip file. I click it and got a error msg said that the vshost.exe didn't work.

any idea how to start your application, such as system setting requirment, OS, and so on.

thanks,
jp
GeneralRe: How start
rbunn83815
15:27 28 Apr '09  
In order to run this tutorial you would need to have the Microsoft Surface SDK or an actual Microsoft Surface unit. Unfortunately, the SDK costs money to get right now, so unless you plan to seriously pursue Surface development it might not be worth it. Also, to run the app you wouldn't start the vshost.exe file, you would need to build the solution then run the .exe file (without the vshost part in it). Thanks for taking the time to check out my tutorial!

Bob


Last Updated 19 Apr 2009 | Advertise | Privacy | Terms of Use | Copyright © CodeProject, 1999-2010