Do Da Drag Dance, Dude


Ha! Check out the blog title - it's so... so... alliterative. I'm so witty, sometimes I wonder why I'm not making millons on SNL.


Lately it seems everyone around me wants to know more about drag drop in Avalon. So, after sending little samples around multiple times, I've decided to just go ahead and post this. Hopefully it'll even help educate folks.


Note that this is based on the March CTP bits - YMMV.


The purpose of the app we're going to create will be to showcase how to produce, consume and customize a drag-drop operation.


Let's get it started
First, let's start off from a new Avalon Application project, code-named DragDropDance (ah! - so witty!).


<Window x:Class="DragDropDance.Window1"
    xmlns="http://schemas.microsoft.com/winfx/avalon/2005"
    xmlns:x="http://schemas.microsoft.com/winfx/xaml/2005"
    Text="DragDropDance"
    >
  <DockPanel>
    <Button ID="CreateRectButton" Click="CreateRectButtonClick">
      Click me baby one more time.
    </Button>
    <Canvas ID="MainCanvas" AllowDrop ="True"
      DragOver="MainCanvasDragOver" Drop="MainCanvasDrop"
      Background="VerticalGradient LightBlue SlateBlue" />
  </DockPanel>
</Window>


You'll see that I've placed a button here and a Canvas, and wired up a bunch of event handlers. The button will create rectangles that we'll move around and drag to other applications.


Now, let's get this up and running. In Window1.xaml.cs, add this code, and you can now build your app. It doesn't do much, sure, but we'll take care of that in a minute.


private void CreateRectButtonClick(object sender, RoutedEventArgs e)
{
}


private void MainCanvasDragOver(object sender, DragEventArgs e)
{
}


private void MainCanvasDrop(object sender, DragEventArgs e)
{
}


Creating rectangles (boooring)
Let's start by implementing some crude rectangle-creating code. Add this to the .xaml.cs file, and then run the application and create some rectangles. Fun! Profit!


private int nextBrushIndex;
private static Brush[] MyBrushes = new Brush[] {
    Brushes.Green, Brushes.Blue, Brushes.Red, Brushes.LemonChiffon
};


private Rectangle CreateRectangle(Brush brush,
    double width, double height,
    double canvasTop, double canvasLeft)
{
    Rectangle result = new Rectangle();
    result.Width = width;
    result.Height = height;
    Canvas.SetTop(result, canvasTop);
    Canvas.SetLeft(result, canvasLeft);
    result.Fill = brush;
    return result;
}


private void CreateRectButtonClick(object sender, RoutedEventArgs e)
{
    Rectangle rectangle = CreateRectangle(
        MyBrushes[nextBrushIndex++ % MyBrushes.Length],
        80, 80, 10, 10);
    MainCanvas.Children.Add(rectangle);
}


You just have to love a lemon chiffon-filled rectangle, don't you? The app is still not awfully impressive, but that's about to change.


Drag your stuff
Use your VS powers again and add this code to enable rectangles to hook up some event handlers.


private void WireDragSupport(Rectangle rectangle)
{
    rectangle.GiveFeedback += RectangleGiveFeedback;
    rectangle.MouseLeftButtonDown += RectangleMouseLeftButtonDown;
    rectangle.MouseMove += RectangleMouseMove;
}


private void CreateRectButtonClick(object sender, RoutedEventArgs e)
{
    Rectangle rectangle = CreateRectangle(
        MyBrushes[nextBrushIndex++ % MyBrushes.Length],
        80, 80, 10, 10);
    MainCanvas.Children.Add(rectangle);
    WireDragSupport(rectangle);
}


Now, let's add the implementation for these event handlers, which will in essence handle things from the drag-side.


#region Drag provider side of things.


private Point dragStart;
private DataFormat format = DataFormats.GetDataFormat("Marcelo's XAML Data Format");


private void RectangleGiveFeedback(object sender, GiveFeedbackEventArgs e)
{
    // This gets called to give us the chance to show some UI
    // to indicate what would happen if the user completed the
    // operation.
    Rectangle rectangle = (Rectangle)sender;
    if ((e.Effects & DragDropEffects.Move) == DragDropEffects.Move)
    {
        rectangle.Opacity = 0.5f;
    }
    else
    {
        rectangle.Opacity = 1f;
    }
}


private void RectangleMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
    // NOTE: add System.Windows.Input to the using clauses.
    // Let's get the point relative to the rectangle from
    // which we will start dragging.
    Rectangle rectangle = (Rectangle)sender;
    dragStart = e.GetPosition(rectangle);
}


private void RectangleMouseMove(object sender, MouseEventArgs e)
{
    Rectangle rectangle = (Rectangle)sender;
    if (e.LeftButton == MouseButtonState.Pressed)
    {
        // In a real-world app, you'd check that
        // (a) the mouse didn't just enter in a pressed state
        // (b) the mouse has moved far enough from the dragStart
        //     position to actually trigger a drag (to avoid
        //     accidental drag-drop operations)
        DragDropEffects effects;
        DataObject dataObject;


        dataObject = new DataObject();
        dataObject.SetData(format.Name,
            System.Windows.Serialization.Parser.SaveAsXml(rectangle));
        dataObject.SetData(DataFormats.Text, "Drop me in another Canvas, dude!");
        effects = DragDrop.DoDragDrop(rectangle,
            dataObject /* or just a string or other auto-serializable stuff */,
            DragDropEffects.Copy | DragDropEffects.Move);
        if ((effects & DragDropEffects.Move) == DragDropEffects.Move)
        {
            MainCanvas.Children.Remove(rectangle);
        }
        else
        {
            rectangle.Opacity = 1f;
        }
    }
}


OK, this deserves some explanation. What we do is start a drag operation when the user moves the button with the left mouse button pressed - I'm not checking a number of things for simplicity. DragDrop.DoDragDrop() does the trick. I stuff data into a DataObject in two different formats: a custom format I plan to use to accepts rectangles as I understand them, and another one that can be accepted by any application that accepts plain text. You can try this out by dragging a rectangle onto WordPad.


For extra niceness, try pressing Alt while hovering over WordPad and notice how the rectangle Opacity changes depending on whether you're moving or copying the rectangle (other apps use Ctrl for this).


Finally, note that I'm registering and using my own data format simply by retrieving it through DataFormats.GetFormat(string).


Drop your stuff (and do something useful with it!)
Add this bit of code, replacing the stubs we had for the MainCanvas events, and then go get yourself a cookie - the job is done.


#region Drop consumer side of things.


private void MainCanvasDragOver(object sender, DragEventArgs e)
{
    // Give feedback on what would happen if the user finished
    // the operation in the current state.
    if (e.Data.GetDataPresent(format.Name))
    {
        if ((e.KeyStates & KeyStates.ControlKey) == KeyStates.ControlKey)
        {
            e.Effects = DragDropEffects.Copy;
        }
        else
        {
            e.Effects = DragDropEffects.Move;
        }
    }
    else
    {
        e.Effects = DragDropEffects.None;
    }
}


private void MainCanvasDrop(object sender, DragEventArgs e)
{
    // The OS will keep track of these names, so it's cool to refer
    // to this from another process.
    if (e.Data.GetDataPresent(format.Name))
    {
        if ((e.KeyStates & KeyStates.ControlKey) == KeyStates.ControlKey)
        {
            e.Effects = DragDropEffects.Copy;
        }
        else
        {
            e.Effects = DragDropEffects.Move;
        }


        // Let's parse XAML from a different process. This opens
        // up a security hole big enough to drive trucks through.
        // Enjoy!

        string rawData = e.Data.GetData(format.Name).ToString();
        Rectangle hopefullyMyRectangleReallyShouldCheck = (Rectangle)
            System.Windows.Serialization.Parser.LoadXml(StringToStream(rawData));
        WireDragSupport(hopefullyMyRectangleReallyShouldCheck);


        // Add the rectangle to our canvas.
        MainCanvas.Children.Add(hopefullyMyRectangleReallyShouldCheck);
        Canvas.SetLeft(hopefullyMyRectangleReallyShouldCheck,
            e.GetPosition(MainCanvas).X);
        Canvas.SetTop(hopefullyMyRectangleReallyShouldCheck,
            e.GetPosition(MainCanvas).Y);
    }
    else
    {
        e.Effects = DragDropEffects.None;
    }
}


private static System.IO.Stream StringToStream(string s)
{
    System.IO.MemoryStream result;
    byte[] stringBytes;


    stringBytes = System.Text.Encoding.ASCII.GetBytes(s);
    result = new System.IO.MemoryStream(stringBytes.Length);
    result.Write(stringBytes, 0, stringBytes.Length);
    result.Position = 0;
    return result;
}


#endregion Drop consumer side of things.


Note that this directly parses out potentially evil XAML - bad, bad code. Don't do this in a app you plan to give out to anyone else (and don't use this for your own apps, for that matter!).


You can try firing up multiple instances of the app and dragging rectangles around, pressing the Ctrl key or releasing to change between moving and copying.


Short, final notes:



  • There are two sides of this: the drag provider, and the drop consumer. They can very well be different apps.
  • On the provider side of things, DragDropEffects refer to what you will allow to have happen to your content on the DoDragDrop call. On the GiveFeedback event, it refers to what the consumer intends to do.
  • On the consumer side of things, DragDropEffects refer to what the provider will allow you to do on entry, and on output it refers to what you actually intend to do with the content.
  • I’m not coding in some usability things, like checking how far the mouse has moved before starting a drag operation and the like. Real apps need to do this.
  • I’m inventing and using my own named data format. It’s a rectangle serializing data format, if you will (although there are zero checks, of course, like throughout the rest of the sample).

Thanks, thanks, thanks - I'll be here all week - enjoy your evening!



This posting is provided "AS IS" with no warranties, and confers no rights.
Use of included script samples are subject to the terms specified at http://www.microsoft.com/info/cpyright.htm.


Comments (7)

  1. Drew Marsh says:

    Great example, people are always asking for this! Thanks, now I know where to send them.

    Totally off topic nitpick is you coulda made your life easier and avoided allocations by just doing:

    Parser.LoadXml(new XmlTextReader(new StringReader(rawData)));

    No biggy though, just the simplicity/performance nut in me shining through. 😉

    Cheers,

    Drew

  2. Drew, thanks for the comment – you are completely right. Thanks for the tip!

  3. Chango V. says:

    The demo is illuminating, but I’m disappointed if that’s the easiest way to do d&d with Avalon. Things weren’t much different with MFC, say. It’s good to have more control, but I think there should also be some standard d&d handler (for both sides) when you just want to get the basic functionality, without doing anything fancy.

  4. Chango, TextBox and RichTextBox handle drag and drop natively, if that’s any help.

  5. Using a DataObjectI’ve blogged about data objects in the past (here and here for example). You can use…

Skip to main content