Mike Taulty's Blog
Bits and Bytes from Microsoft UK
Expression Blend 3: Trying a Drag-and-Drop Behavior (Part 2)

Blogs

Mike Taulty's Blog

Elsewhere

Archives

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 :-)


Posted Wed, May 13 2009 2:46 AM by mtaulty
Filed under: ,

Comments

Dew Drop - May 13, 2009 | Alvin Ashcraft's Morning Dew wrote Dew Drop - May 13, 2009 | Alvin Ashcraft's Morning Dew
on Wed, May 13 2009 6:26 AM
Expression Blend and Design wrote Link Round-Up: Behaviors-Related Posts
on Tue, May 19 2009 10:50 AM

In the past couple of weeks, there have been a great number of great blog posts on Behaviors, and since

DotNetShoutout wrote Expression Blend 3: Trying a Drag-and-Drop Behavior (Part 2) - Mike Taulty
on Mon, May 25 2009 6:17 PM

Thank you for submitting this cool story - Trackback from DotNetShoutout