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;
and I make the ellipse on the left hand side draggable then when I drag it I want to see;
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;
- Implement some interface e.g. IDropSite ? That’s tricky because how does a regular piece of UI implement that?
- 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;
and I can then go and add an action to my orange rectangle (I’m using the sample ShowMessageBoxAction);
and then configure that action to fire on my DropTrigger as in;
and with that in place, when I drag my circle to my rectangle and drop it I get;
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.