Jeff Simon's Blog

What A Drag...

Not to sound too much like a fanboy, but Avalon just plain rocks.  I’ve spent the last fifteen years simultaneously cursing (“I hate you!”) and coercing (“…but I’ll love you if you work!”) GDI and User into doing my evil bidding, and I’ve finally found a replacement that I actually want to use.  Lord knows there’s been no dearth of presentation layers in the interim, but none of them caught my eye; either they didn’t have the full functionality I wanted, or they just plain felt heavy.  Avalon, though, really fits the bill (ok, yes, it’s heavy, but for whatever reason it doesn’t feel that way).  Especially now while I’m still learning about it, there are lots of ‘ah, suh-weet’ moments as I find that the pain of programming has been reduced.

 

Aaaaand then there’s Drag and Drop.

 

For whatever reason - perhaps there are other more important things to focus on shipping, or perhaps the old way was “good enough” – Avalon’s Drag & Drop implementation is simply a very thin wrapper around the same OLE drag & drop that I’ve been scratching my head over for years.  You can see this for yourself by using the excellent Reflector .Net class browser and looking at the System.Windows.DragDrop implementation.

 

On the plus side this means that, unlike with the rest of Avalon (aside from the indispensible O’Reilly book), there is an absolute glut of information available discussing how to do Drag & Drop.

 

On the down side though, this means that it’s still challenging to do anything other than the most basic Drag&Drop implementation.  Inter-app support, custom cursors, multi-drop-target scenarios, and custom drag windows require a little more effort than the rest of Avalon implies should be necessary.

 

Given some of the visual cues I’ll be adding to the DragDropTilePanel, the inter-app support (e.g., between two different instances of your app) provides the biggest challenge, as it means that we have to fully decouple our DragSource and DropTarget implementations. For many developers, such as those working on prototype, beta, or constrained-scenario apps, disallowing inter-app drag & drop is an acceptable limitation; that said, we really should write it correctly so that the drag source and drop target don’t share implementation.

 

That said; I’m going to wuss out for the next couple of posts and describe a simple Drag&Drop implementation that ignores inter-app support and clean factoring of drag source and drop target.  Hopefully Dan and Ben won’t read this and give me grief about taking the easy way out…

 

Here’s the minimum code necessary to add Drag & Drop support to the TilePanel base class. On the plus side, this is very little code!  Also, notice that even though we’re not doing anything related to animation here, all of the child objects smoothly animate to their new positions after a drop – all thanks to the animation code that Dan originally implemented.

 

    public class DragDropTilePanel : TilePanel

    {

        protected override void OnInitialized(EventArgs e)

        {

            // Add mouse event handlers to each of our child objects

            foreach (FrameworkElement child in Children)

            {

                child.MouseLeftButtonDown += new MouseButtonEventHandler(child_MouseLeftButtonDown);

                child.MouseMove += new MouseEventHandler(child_MouseMove);

            }

            base.OnInitialized(e);

        }

 

The code above sets up the mouse event handlers that inform us when the user interacts with one of the TilePanel’s child objects.

 

        void child_MouseLeftButtonDown(object sender, MouseButtonEventArgs eventArgs)

        {

            // User pressed the mouse button on one of our draggable children.  Don't start dragging

            // just yet, but do track where the mouse was when the button was pressed.

            FrameworkElement childElement = (FrameworkElement)sender;

            _mouseDownLocationRelativeToChild = eventArgs.GetPosition(childElement);

        }

 

We don’t start dragging until the user’s moved a sufficient distance from where they pressed the mouse button, so don’t start the drag in the MouseDown handler…

 

        void child_MouseMove(object sender, MouseEventArgs eventArgs)

        {

            if (eventArgs.LeftButton == MouseButtonState.Pressed)

            {

                FrameworkElement draggedChildElement = (FrameworkElement)sender;

 

                // The Mousebutton is pressed, but we haven't started dragging yet; check

                // to see if we should.  Only do so if we've moved far enough away from

                // the initial mouse-down location. 

                Point currentMouseLocationRelativeToChild = eventArgs.GetPosition(draggedChildElement);

                if (!PointIsOutsideDragThreshold(currentMouseLocationRelativeToChild))

                {

                    // We're not ready to start dragging quite yet 'cause the user hasn't

                    // moved the mouse far enough.

                    return;

                }

                // Fade out the dragged item to give visual feedback that it's being moved.

                draggedChildElement.Opacity = 0.5f;

 

                // Create the DataObject that will contain the dragged child. This won’t work

                // When dropped on another app (since it won’t be able to deref the DraggedObject)

                DataObject dataObject = new DataObject();

                dataObject.SetData("DraggedObject", draggedChildElement);

 

                // Do the actual drop.

                DragDrop.DoDragDrop(this, dataObject, DragDropEffects.Copy | DragDropEffects.Move);

 

                // Restore the object's opacity.

                draggedChildElement.Opacity = 1.0f;

            }

        }

 

The code above belongs to the ‘drag source’ – the object which instantiates the drag event.  It first checks to ensure that the user has moved the mouse far enough to trigger the Drag; once that’s happened, it sets up the Drag by wrapping the dragged child element and adding it to the dataObject (which you can think of as a varargs passed to the Drag event handlers).  Note that, to support inter-app drag&drop, we’d need to ensure that all data we store in dataObject is serializable (or, I suppose, we could use Remoting, but that’s more of a black box to me right now).  If we try to pass an object like a FrameworkElement, then the OnDrop handler, which could well be called in another app, would fail as it tried to get that data from the DataObject.

 

Finally, it calls DoDragDrop.  This function call is synchronous – it doesn’t return until the drag event is completed.

 

        protected override void OnDrop(DragEventArgs dragArgs)

        {

            FrameworkElement childElement = (FrameworkElement)dragArgs.Data.GetData("DraggedObject");

 

            // Get the current mouse position, and add half-an-object-width to it so that

            // the drop position is more natural when an object is dropped onto another object.

            // (drop onto the left half of an object, it'll insert before that one; drop onto

            // the right half of an object, it'll insert after it).

            Point currentMousePosition = dragArgs.GetPosition(this);

            Point halfOffsetMousePosition = new Point(currentMousePosition.X + ChildSize / 2, currentMousePosition.Y);

 

            // Move the object to the drop point.  Set the childElement's DataContext

            // to the object's new index in the list (caculated from the mouse position)

            Children.Remove(childElement);

            int dropIndex = GetChildIndexFromPosition(halfOffsetMousePosition);

            Children.Insert(dropIndex, childElement);

            childElement.DataContext = dropIndex;

        }

 

The OnDrop code above belongs to the “drop target” – the object on which the dragged object was dropped.  As described above, for our purposes here we’re assuming that OnDrop is in the same object (and same object instance) as the DoDragDrop call.

 

        internal bool PointIsOutsideDragThreshold(Point currentMouseLocationRelativeToChild)

        {

            // Use the SystemParameter's drag distances and create a rectangle centered

            // around the mousedown position - check to see if the current mouse position

            // is outside that rectangle; if so, start a'draggin.

            double horizontalDragThreshold = SystemParameters.MinimumHorizontalDragDistance;

            double verticalDragThreshold   = SystemParameters.MinimumVerticalDragDistance;

            Rect dragThresholdRect = new Rect(_mouseDownLocationRelativeToChild.X - horizontalDragThreshold,

                                              _mouseDownLocationRelativeToChild.Y - verticalDragThreshold,

                                              horizontalDragThreshold * 2,

                                              verticalDragThreshold * 2);

            return !dragThresholdRect.Contains(currentMouseLocationRelativeToChild);

        }

       

        Point _mouseDownLocationRelativeToChild;

    }

 

The function (and Point variable) above are used to determine if the mouse has moved outside the drag threshold.

 

There are a few other minor tweaks to round out our Drag support.  First, we add the GetChildIndexFromPosition support function to TilePanel.  This could have gone in DragDropTilePanel, but it could find other uses in TilePanel as well.  Here’s that code.

 

    protected int GetChildIndexFromPosition(Point position)

    {

        int row = (int)(position.Y / _oldChildSize);

        int column = Math.Min(_oldChildrenPerRow, (int)(position.X / _oldChildSize));

        int index = row * _oldChildrenPerRow + column;

        return Math.Min(index, Children.Count);

    }

 

The ‘Math.Min’s are there to deal with cases where the user drops after the end of a row.

 

And then finally, we need to tweak the xaml slightly to support our new DragDropTilePanel:


 

<?Mapping XmlNamespace="MyApp" ClrNamespace="BasicDragDrop" ?>

<Window x:Class="BasicDragDrop.Window1"

    xmlns="http://schemas.microsoft.com/winfx/avalon/2005"

    xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"

    xmlns:myapp="MyApp"

    Title="BasicDragDrop"

    Height="300"

    Width="300">

  <Grid>

    <RowDefinition Height="24"/>

    <RowDefinition Height="*"/>

    <Slider MinWidth="200" Minimum="50" Maximum="300"

            SmallChange="40" Name="_slider" Grid.Row="0"/>

    <!-- Note: it's important to have a background in the TilePanel,

           otherwise the OnDragOver call won't get to it! Not sure why... -->

    <myapp:DragDropTilePanel Background="White" Grid.Row="1"

                             ChildSize="{Binding ElementName=_slider, Path=Value}">

      <Border Background="Red" Margin="4" />

      <Border Background="Green" Margin="4" />

      <Border Background="Blue" Margin="4" />

      <Border Background="Red" Margin="4" />

      <Border Background="Green" Margin="4" />

      <Border Background="Blue" Margin="4" />

      <Border Background="Red" Margin="4" />

      <Border Background="Green" Margin="4" />

      <Border Background="Blue" Margin="4" />

      <Border Background="Red" Margin="4" />

      <Border Background="Green" Margin="4" />

      <Border Background="Blue" Margin="4" />

    </myapp:DragDropTilePanel>

  </Grid>

</Window>

 

Most of the changes are in converting from a StackPanel to a Grid so that the TilePanel completely fills the remaining window space.  We also specify “AllowDrop” here to tell Avalon that we’re a valid drop target (otherwise we wouldn’t get drop events).  Also, note that for whatever reason, we need to specify a background to our tilePanel, otherwise our OnDragOver doesn’t get called (we don’t have OnDragOver in the DragDropTilePanel implementation above, but we will soon enough, and this will prob’ly catch us, so we’ll add it now). 

[Edit: Yeah, I realized I should be using a DockPanel instead of a Grid.  I'll rewrite it...]


And that’s it.  All in all, not a lot of code to add basic drag and drop support that works with any child object type!  Of course, I’m really punting on some key issues (especially that inter-app bit).  We’ll clean that up soon enough.

 

Updated project file attached (built against the Dec CTP.  Compile against other versions at your own risk).

 

Next up: Custom drag windows.

 

Published Wednesday, February 22, 2006 11:44 PM by JeffSim

Attachment(s): BasicDragDrop.zip

Comments

 

MueMeister said:

Hi Jeff,

thanks for the good example but I have a question on using this panel in an items control.
If you want to provide selection and item templating a.s.o. it is nice to use the ListView e.g. Now that
you can attach a panel (which takes over the layout process) on ListView.ItemsPanel this
really powerful.
My question is now, where to provide the drag and drop functionality? Is it really the panel which should
implement all the mouse and drag event handler? I would imagine the ListView to do that and
the panel to implement the givefeedback and dragover eventhandler. The problem is the following:
If you bind the itemsource of the listview to a collection you cannot remove and add children
in the panel's drop handler (as you do)! What do you think is the best approach for a scenario with a
ListView (managing selection and logical child stuff) + a panel (doing layout of the ListView)? I really
get confused with this stuff! Morover when the ListView does the Drag and Drop (and if you want to
use DataBinding it have to!!!) how can a retrieve the calculated drop index since I cannot refernce the
panel from a ListView??

Thanks a lot!
Chris :-)
March 20, 2006 10:01 AM
Anonymous comments are disabled

© 2009 Microsoft Corporation. All rights reserved. Terms of Use  |  Trademarks  |  Privacy Statement
Microsoft
Page view tracker