Expression Blend 3: Trying a Drag-and-Drop Behavior (Part 2)

Following on from this post, I wanted to try and connect my DraggableBehavior to my DropTrigger but I wasn’t quite sure how to make that work and so I just went for a pretty cheap-and-cheerful mechanism that’s a long way from perfect as it stands at the moment.

I modified my DraggableBehavior a little – from the previous post it’s changed in that there’s now a simple PropertyPath and when the mouse button comes up at the end of the drag, I just use reflection to try and get the property indicated by the PropertyPath and pass that through to any DropActions that I find ( if there is no PropertyPath then I just pass the AssociatedObject from the Behavior itself );

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;
using System.Collections.Generic;
using System.Reflection;

namespace MikesDragDropBits
{
  public class DraggableBehavior : Expression.Behavior<FrameworkElement>
  {
    public DraggableBehavior()
    {
    }
    public string PropertyPath { get; set; }

    protected override void OnAttached()
    {
      base.OnAttached();
      this.AssociatedObject.MouseLeftButtonDown += OnMouseDown;
      this.AssociatedObject.MouseLeftButtonUp += OnMouseUp;
      this.AssociatedObject.MouseMove += OnMouseMove;    
    }
    protected override void OnDetaching()
    {
      base.OnDetaching();
      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;

      if (injectedUI != null)
      {
        injectedUI.RemoveUI();
        injectedUI = null;
        HitTestAndInvokeTriggers(e.GetPosition(Application.Current.RootVisual));
      }
    }
    void HitTestAndInvokeTriggers(Point p)
    {
      object dataObject = GetDataObject();

      foreach (DropTrigger trigger in GetDropTriggersOfFirstElement(p))
      {
        trigger.InvokeActions(dataObject);
      }
    }
    object GetDataObject()
    {
      object dataObject = this.AssociatedObject;

      if (!string.IsNullOrEmpty(this.PropertyPath))
      {
        dataObject = null;

        Type t = this.AssociatedObject.GetType();

        PropertyInfo propertyInfo = t.GetProperty(this.PropertyPath,
          BindingFlags.Public | BindingFlags.Instance);

        if (propertyInfo != null)
        {
          dataObject = propertyInfo.GetValue(this.AssociatedObject, null);
        }
      }
      return (dataObject);
    }
    static IEnumerable<DropTrigger> GetDropTriggersOfFirstElement(Point p)
    {
      var elements = VisualTreeHelper.FindElementsInHostCoordinates(p,
        Application.Current.RootVisual); 

      if (elements != null)
      {
        foreach (var item in elements)
        {
          var triggers = Expression.Interaction.GetTriggers(item);

          if ((triggers != null) && (triggers.Count > 0))
          {
            foreach (Expression.TriggerBase trigger in triggers)
            {
              if (trigger is DropTrigger)
              {
                yield return (DropTrigger)trigger;               
              }
            }
            break;
          }
        }
      }
    }
    void OnMouseDown(object sender, MouseButtonEventArgs e)
    {
      if (!isDragging)
      {
        isDragging = true;
        this.AssociatedObject.CaptureMouse();
      }
    }
    bool isDragging;
    DragDropUIInjector injectedUI;
    Point capturePoint;
  }
}

This uses a modified ( I think ) DragDropInjectorUI as below – you might spot that I’ve hacked in a little test code as I’ve been having some trouble with WriteableBitmap ( more on that below );

//#define dragImageTest
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)
    {
#if dragImageTest
      dragImage = new Rectangle()
      {
        Width = draggedUI.ActualWidth,
        Height = draggedUI.ActualHeight,
        Fill = new SolidColorBrush(Color.FromArgb(255, 255, 0, 0))
      };
#else
      WriteableBitmap picture = new WriteableBitmap(
        (int)draggedUI.ActualWidth, (int)draggedUI.ActualHeight,
        PixelFormats.Pbgra32);

      picture.Render(draggedUI, new TranslateTransform());

      dragImage = new Image();
      dragImage.Source = picture;
      dragImage.Width = draggedUI.ActualWidth;
      dragImage.Height = draggedUI.ActualHeight;     
      dragImage.Stretch = Stretch.Fill;
      dragImage.IsHitTestVisible = false;
#endif // dragImageTest

      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(0x10, 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;   
    Canvas dragCanvas;
    UIElement originalContent;

#if dragImageTest
    Rectangle dragImage;
#else
    Image dragImage;
#endif // dragImageTest
  }
}

With that in place, I produced some very simple actions;

  public class PanelAddChild : TargetedTriggerAction<Panel>
  {
    protected override void Invoke(object parameter)
    {
      this.Target.Children.Add((UIElement)parameter);
    }
  }
  public class PanelRemoveChild : TargetedTriggerAction<Panel>
  {
    protected override void Invoke(object parameter)
    {
      this.Target.Children.Remove((UIElement)parameter);
    }
  }

and that then allows me to use these pieces in a UI like this one below where I have a Grid called sourceGrid which contains an Ellipse with a DraggableBehavior on it. I have another Grid which is unnamed but has a DropTrigger on it with 2 actions – a PanelRemoveChild action which targets the sourceGrid and a PanelAddChild which has no TargetName and so targets the Grid itself;

<Grid

    Grid.Row="1"

    Margin="10">

    <Grid.ColumnDefinitions>

        <ColumnDefinition

            Width="*" />

        <ColumnDefinition

            Width="*" />

    </Grid.ColumnDefinitions>

    <Grid

        x:Name="sourceGrid">

        <Ellipse

            Width="96"

            HorizontalAlignment="Left"

            VerticalAlignment="Top"

            Height="96"

            Fill="Orange">

            <e:Interaction.Behaviors>

                <m:DraggableBehavior />

            </e:Interaction.Behaviors>

        </Ellipse>

    </Grid>

    <Grid

        Grid.Column="1"

        Background="DarkGray">

        <e:Interaction.Triggers>

            <m:DropTrigger>

                <m:PanelRemoveChild

                    TargetName="sourceGrid" />

                <m:PanelAddChild />

            </m:DropTrigger>

        </e:Interaction.Triggers>

    </Grid>

</Grid>

The net effect of this is that I can drag the Ellipse across to the second Grid and then drop it there where it becomes parented by that Grid;

image image

A quick note: There’s a reason why I use HorizontalAlignment=Left, VerticalAlignment=Top on my Ellipse in that sample. I find that with WriteableBitmap in the current build I hit an offsetting problem where the resultant bitmap has its content offset if the content was offset in its original container. I strongly suspect that this will be different in later builds so I’ve not spent any time on it here.

With some other very simple actions;

  public class ListBoxAddOne : TargetedTriggerAction<ListBox>
  {
    protected override void Invoke(object parameter)
    {
      this.Target.Items.Add(parameter);
    }
  }
  public class ListBoxRemoveOne : TargetedTriggerAction<ListBox>
  {
    protected override void Invoke(object parameter)
    {
      IList list = this.Target.Items as IList;

      if (list != null)
      {
        list.Remove(parameter);
      }
    }
  }

I can then build another little UI with a ListBox in it that’s data-bound to some Person objects ( FirstName, LastName, Age ) and a second ListBox like this;

<Grid Margin="10">

    <Grid.ColumnDefinitions>

        <ColumnDefinition

            Width="*" />

        <ColumnDefinition

            Width="*" />

    </Grid.ColumnDefinitions>

    <ListBox

        x:Name="sourceBox"

        Margin="24,24,24,24"

        ItemTemplate="{StaticResource DragTemplate}" />

    <ListBox

        x:Name="listBox"

        Margin="24,24,24,24"

        Grid.Column="1"

        ItemTemplate="{StaticResource NonDragTemplate}">

        <e:Interaction.Triggers>

            <m:DropTrigger>

                <m:ListBoxRemoveOne

                    TargetName="sourceBox" />

                <m:ListBoxAddOne />

            </m:DropTrigger>

        </e:Interaction.Triggers>

    </ListBox>

</Grid>

where the DragTemplate looks like this;

<DataTemplate

            x:Key="DragTemplate">

            <StackPanel

                Background="Red">

                <e:Interaction.Behaviors>

                    <m:DraggableBehavior

                        PropertyPath="DataContext" />

                </e:Interaction.Behaviors>

                <TextBlock

                    Text="{Binding Path=FirstName}"

                    Margin="1"

                    HorizontalAlignment="Left"

                    VerticalAlignment="Top"

                    Width="150" />

                <TextBlock

                    Text="{Binding Path=LastName}"

                    Margin="1"

                    HorizontalAlignment="Left"

                    VerticalAlignment="Top"

                    Width="150" />

                <TextBlock

                    Text="{Binding Path=Age}"

                    Margin="1"

                    HorizontalAlignment="Left"

                    VerticalAlignment="Top"

                    Width="150" />

            </StackPanel>

        </DataTemplate>

where the significant thing is that the PropertyPath on my DraggableBehavior is set to “DataContext” so that a drag operation will cause the bound Person instance to be dragged-dropped. The NonDragTemplate looks identical except that it does not have the Interaction.Behaviors property set on it for the StackPanel because I only want dragging from the one ListBox.

With that UI (where my ListBox named sourceBox has some data added to its Items collection ) I can then drag drop as in;

image image image

This is a long, long way from being perfect but it’s part-way to what I want to get to and a lot closer than the lost post.

The project with source-code is here for download if you want to take it further, I’m leaving it for the minute 🙂