Expression Blend 3: Trying a drag-and-drop behavior (part 1)

I tried to take this post a little futher by trying to build a drag-and-drop behaviour. It’s still something that I’m working on.

I built a DraggableBehavior which is pretty similar to the one that’s found in the Sample Silverlight 3 Behaviors in the gallery. The difference between mine and that sample was that during a drag operation I don’t want the original UI to move. I want a copy of it to be moved. That is, if my original UI looks like this;

image_thumb1

and I make the ellipse on the left hand side draggable then when I drag it I want to see;

image_thumb

ideally I’d also want to see a little “+” sign on it or similar but that could come later.

Now, in Silverlight 3 this feels like it might be do-able in the sense that I could make a copy of whatever is being dragged by getting it to draw itself into a WriteableBitmap and then just putting that into an Image and dragging the Image around. Fine…but where do I parent that Image (I’ll call it the drag image) as it’s dragged around?

I can (maybe) assume that my top level UI is a UserControl but, from there-on in, it’s hard to know whether you can find a Panel to inject the drag image. So, I took an approach that I took with this post a while ago which is to essentially;

  • grab the Application.Current.RootVisual as a UserControl
  • grab its current content C
  • create a new Grid
  • put the content C into the Grid as a Child
  • add a new Canvas into the Grid as a Child on top of the content C
  • put my drag image onto that Canvas

and then try to reverse that process when the drag finishes. With that in place, I can have a DraggableBehaviour as in;

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using Expression = Microsoft.Expression.Interactivity;
using System.Linq;
using System.Windows.Data;

namespace MikesDragDropBits
{
  public class DraggableBehavior : Expression.Behavior<FrameworkElement>
  {
    public DraggableBehavior()
    {
    }
    protected override void OnAttached()
    {
      base.OnAttached();
      this.AssociatedObject.MouseLeftButtonDown += OnMouseDown;
      this.AssociatedObject.MouseLeftButtonUp += OnMouseUp;
      this.AssociatedObject.MouseMove += OnMouseMove;     
    }
    void OnMouseMove(object sender, MouseEventArgs e)
    {
      if (isDragging)
      {
        if (injectedUI == null)
        {
          Point localPoint = e.GetPosition(this.AssociatedObject);
          Point globalPoint = e.GetPosition(Application.Current.RootVisual);
          capturePoint = globalPoint;
          globalPoint.X -= localPoint.X;
          globalPoint.Y -= localPoint.Y;
          injectedUI = new DragDropUIInjector(this.AssociatedObject, globalPoint);
          injectedUI.InjectUI();
        }
        else
        {
          Point currentPoint = e.GetPosition(Application.Current.RootVisual);
          injectedUI.ApplyDelta(currentPoint.X - capturePoint.X,
            currentPoint.Y - capturePoint.Y);
          capturePoint = currentPoint;
        }
      }
    }
    void OnMouseUp(object sender, MouseButtonEventArgs e)
    {
      this.AssociatedObject.ReleaseMouseCapture();
      isDragging = false;
      injectedUI.RemoveUI();
      injectedUI = null;
    }
    void OnMouseDown(object sender, MouseButtonEventArgs e)
    {
      if (!isDragging)
      {
        isDragging = true;
        this.AssociatedObject.CaptureMouse();
      }
    }
    protected override void OnDetaching()
    {
      base.OnDetaching();
      this.AssociatedObject.MouseLeftButtonDown -= OnMouseDown;
      this.AssociatedObject.MouseLeftButtonUp -= OnMouseUp;
      this.AssociatedObject.MouseMove -= OnMouseMove;
    }
    bool isDragging;
    DragDropUIInjector injectedUI;
    Point capturePoint;
  }
}

and that’s using this little class to swap the UI in and out;

using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
using System.Windows.Media.Imaging;

namespace MikesDragDropBits
{
  internal class DragDropUIInjector 
  {
    public DragDropUIInjector(FrameworkElement draggedUI, Point position)
    {
      CreateDragImage(draggedUI, position);
    }
    void CreateDragImage(FrameworkElement draggedUI, Point position)
    {
      WriteableBitmap picture = new WriteableBitmap(
        (int)draggedUI.ActualWidth, (int)draggedUI.ActualHeight, 
        PixelFormats.Pbgra32);

      picture.Render(draggedUI, new TranslateTransform()
      {
        X = 0 - position.X,
        Y = 0 - position.Y 
      });

      dragImage = new Image();
      dragImage.Source = picture;
      dragImage.Width = draggedUI.ActualWidth;
      dragImage.Height = draggedUI.ActualHeight;      
      dragImage.Stretch = Stretch.Fill;
      dragImage.IsHitTestVisible = false;
      Canvas.SetLeft(dragImage, position.X);
      Canvas.SetTop(dragImage, position.Y);
    }
    public void InjectUI()
    {
      originalContent =
        UserControlContentAccessor.GetContent(
          (UserControl)Application.Current.RootVisual);

      injectedGrid = new Grid();
      injectedGrid.IsHitTestVisible = false;

      UserControlContentAccessor.SetContent(
        (UserControl)Application.Current.RootVisual, injectedGrid);

      injectedGrid.Children.Add(originalContent);

      dragCanvas = new Canvas()
      {
        Background = new SolidColorBrush(Color.FromArgb(16, 0, 0, 0))
      };
      dragCanvas.IsHitTestVisible = false;
      dragCanvas.Children.Add(dragImage);

      injectedGrid.Children.Add(dragCanvas);
    }
    public void RemoveUI()
    {
      injectedGrid.Children.Clear();

      UserControlContentAccessor.SetContent(
        (UserControl)Application.Current.RootVisual, originalContent);
    } 
    internal void ApplyDelta(double X, double Y)
    {
      Canvas.SetLeft(dragImage, Canvas.GetLeft(dragImage) + X);
      Canvas.SetTop(dragImage, Canvas.GetTop(dragImage) + Y);
    }
    Grid injectedGrid;
    Image dragImage;
    Canvas dragCanvas;
    UIElement originalContent;
  }
}

and this class which tries to perform a trick that lets me set the content of a UserControl;

  internal class UserControlContentAccessor : UserControl
  {
    public static UIElement GetContent(UserControl uc)
    {
      return ((UIElement)uc.GetValue(UserControl.ContentProperty));
    }
    public static void SetContent(UserControl uc, UIElement element)
    {
      uc.SetValue(UserControl.ContentProperty, element);
    }
  }

so that all seems ok and it gives me a basic drag experience.

The problem I get to from there is how to manage a drop? How do I look for drop-sites? Do I have them;

  1. Implement some interface e.g. IDropSite ? That’s tricky because how does a regular piece of UI implement that?
  2. Use some custom kind of Trigger e.g. DropTrigger.

can I avoid coupling my DraggableBehavior to a particular kind of Trigger like the DropTrigger?

What I’ve gone with for now is a DropTrigger;

  public class DropTrigger : TriggerBase<DependencyObject>
  {
    public void InvokeActions(object o)
    {
      base.InvokeActions(o);
    }
  }

and then I can change my DraggableBehavior so that when the drag ends I try to see what UI element we are over and then to see if it has any DropTriggers and if I find any DropTriggers then I ask them to invoke actions as in;

   void OnMouseUp(object sender, MouseButtonEventArgs e)
    {
      this.AssociatedObject.ReleaseMouseCapture();
      isDragging = false;
      injectedUI.RemoveUI();
      injectedUI = null;
      HitTestAndInvokeTriggers(e.GetPosition(Application.Current.RootVisual));
    }
    private void HitTestAndInvokeTriggers(Point p)
    {
      UIElement element = VisualTreeHelper.FindElementsInHostCoordinates(p,
        Application.Current.RootVisual).FirstOrDefault();

      if (element != null)
      {
        var triggers = Expression.Interaction.GetTriggers(element);
        
        if (triggers != null)
        {
          foreach (Expression.TriggerBase trigger in triggers)
          {
            if (trigger is DropTrigger)
            {            
              ((DropTrigger)trigger).InvokeActions(null);
            }
          }
        }
      }
    }

that I can take these into Blend and draw (e.g.) this circle and rectangle and then set the Circle so that it has DraggableBehavior as in;

image_thumb3image_thumb2

and I can then go and add an action to my orange rectangle (I’m using the sample ShowMessageBoxAction);

image_thumb4

and then configure that action to fire on my DropTrigger as in;

image_thumb5

and with that in place, when I drag my circle to my rectangle and drop it I get;

image_thumb6

What I’m now puzzling with is how do I transfer data from the drag site to the drop site and get it into the action running at the drop site? I’ve had a few attempts at this but nothing’s worked out quite right for me just yet.