Silverlight Sortables

August 19, 2008 09:02 by wjchristenson2

If any of you have ever developed with the jQuery UI framework, you have probably seen or used their "Sortables".  A sortable is a list of items that you can drag around & adjust their sort order.  In this post, I am going to show you how we can sort Border objects within a StackPanel.  Here is a screen shot of what we are after:

When the user drags an item over another, they will swap places.  A transparent placeholder will represent the current item being dragged.  First, let's setup our XAML page before we get into actual code.


   1:  <UserControl x:Class="Sortables.Page"
   2:      xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" 
   3:      xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
   4:      Width="400" Height="300">
   5:      <UserControl.Resources>
   6:          <Style x:Key="borderStyle" TargetType="Border">
   7:              <Setter Property="Width" Value="220" />
   8:              <Setter Property="Height" Value="50" />
   9:              <Setter Property="Margin" Value="5,5,5,0" />
  10:              <Setter Property="BorderBrush" Value="Black" />
  11:              <Setter Property="Background" Value="SteelBlue" />
  12:              <Setter Property="BorderThickness" Value="1" />
  13:              <Setter Property="CornerRadius" Value="10" />
  14:              <Setter Property="HorizontalAlignment" Value="Left" />
  15:          </Style>
  16:      </UserControl.Resources>
  17:      
  18:      <Canvas x:Name="LayoutRoot" Background="White">
  19:          <StackPanel x:Name="StackPanel1" Orientation="Vertical" Width="250">
  20:              <Border x:Name="border1" Style="{StaticResource borderStyle}">
  21:                  <TextBlock Text="Item 1" Foreground="White" Margin="0,10,0,0" HorizontalAlignment="Center" />
  22:              </Border>
  23:              <Border x:Name="border2" Style="{StaticResource borderStyle}">
  24:                  <TextBlock Text="Item 2" Foreground="White" Margin="0,10,0,0" HorizontalAlignment="Center" />
  25:              </Border>
  26:              <Border x:Name="border3" Style="{StaticResource borderStyle}">
  27:                  <TextBlock Text="Item 3" Foreground="White" Margin="0,10,0,0" HorizontalAlignment="Center" />
  28:              </Border>
  29:              <Border x:Name="border4" Style="{StaticResource borderStyle}">
  30:                  <TextBlock Text="Item 4" Foreground="White" Margin="0,10,0,0" HorizontalAlignment="Center" />
  31:              </Border>
  32:          </StackPanel>
  33:      </Canvas>
  34:  </UserControl>

You can see we have a Canvas that has a StackPanel with 4 border objects.  We will be dragging (sorting) the items within the StackPanel.  This example builds off my previous post Drag and Drop in Silverlight.  I'm not going to review the code for dragging and dropping, so if you are having problems, revisit the previous tutorial.  The Border items in the StackPanel will utilize all of my basic drag & drop code from before, but it will also perform extra logic for the "sorting" abilities.  Let's first define our variables.


   1:      Private isMouseDown As Boolean = False
   2:      Private mousePosition As Point = Nothing
   3:      Private itemIndex As Integer = -1   'used to keep track of the index of the item we are dragging

The itemIndex variable is used to keep track of what index the dragged item should be tied to within the StackPanel.  The other 2 variables are for dragging the item.  When the page is loaded, we'll wire in the necessary events for each Border object within our StackPanel.


   1:      Private Sub Page_Loaded(ByVal sender As Object, ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
   2:          Me.InitializeComponent()
   3:   
   4:          For Each element As UIElement In StackPanel1.Children
   5:              If TypeOf (element) Is Border Then
   6:                  Dim item As Border = DirectCast(element, Border)
   7:                  AddHandler item.MouseLeftButtonDown, AddressOf item_MouseLeftButtonDown
   8:                  AddHandler item.MouseLeftButtonUp, AddressOf item_MouseLeftButtonUp
   9:                  AddHandler item.MouseMove, AddressOf item_MouseMove
  10:              End If
  11:          Next
  12:      End Sub

When the user clicks a Border object within our StackPanel, well do our basic drag & drop routines and then we'll also add to that.  First we'll remove the Border object from the StackPanel and add it to our LayoutRoot (parent).  What this does is allow us to drag the Border object around the screen.  We also want to add a placeholder where the Border object used to be in our StackPanel.  I'll be using a transparent Rectangle object to accomplish this.  Here's the logic for the MouseLeftButtonDown event.


   1:      Sub item_MouseLeftButtonDown(ByVal sender As Object, ByVal e As MouseEventArgs)
   2:          Dim item As Border = DirectCast(sender, Border)
   3:          mousePosition = e.GetPosition(Nothing)
   4:          Me.isMouseDown = True
   5:          item.CaptureMouse()
   6:   
   7:          'get the item's position relative to the LayoutRoot canvas object
   8:          Dim newTopLeft As Point = GetRelativePosition(item, LayoutRoot)
   9:          item.SetValue(Canvas.TopProperty, newTopLeft.Y - item.Margin.Top)
  10:          item.SetValue(Canvas.LeftProperty, newTopLeft.X - item.Margin.Left)
  11:          item.SetValue(Canvas.ZIndexProperty, 2000)
  12:   
  13:          'define a rectangle placeholder for the item we are dragging
  14:          Dim recPlaceHolder As Rectangle = New Rectangle()
  15:          With recPlaceHolder
  16:              .Fill = New SolidColorBrush(Colors.Transparent)
  17:              .Margin = New Thickness(5, 5, 5, 0)
  18:              .Width = 220
  19:              .Height = 50
  20:              .HorizontalAlignment = Windows.HorizontalAlignment.Left
  21:          End With
  22:   
  23:          'remove the item we are dragging and insert our placeholder
  24:          itemIndex = StackPanel1.Children.IndexOf(item)
  25:          StackPanel1.Children.Remove(item)
  26:          StackPanel1.Children.Insert(itemIndex, recPlaceHolder)
  27:          LayoutRoot.Children.Add(item)
  28:      End Sub

At this point, we have a placeholder inserted into our StackPanel where our dragged Border object used to be.  As we drag our Border object around the screen, we want to perform a "HitTest" to see if we are hovering over another Border object within the StackPanel.  If we are, we'll want to switch the placeholder with the hovered over Border object.  We also want to keep track of the new index for the dragged item.


   1:      Sub item_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs)
   2:          Dim item As Border = DirectCast(sender, Border)
   3:   
   4:          If (Me.isMouseDown) Then
   5:              Dim deltaV As Double = e.GetPosition(Nothing).Y - mousePosition.Y
   6:              Dim deltaH As Double = e.GetPosition(Nothing).X - mousePosition.X
   7:              Dim newTop As Double = deltaV + DirectCast(item.GetValue(Canvas.TopProperty), Double)
   8:              Dim newLeft As Double = deltaH + DirectCast(item.GetValue(Canvas.LeftProperty), Double)
   9:              item.SetValue(Canvas.TopProperty, newTop)
  10:              item.SetValue(Canvas.LeftProperty, newLeft)
  11:              mousePosition = e.GetPosition(Nothing)
  12:   
  13:              'run a hit test to see if the dragged item is hovering over another item in the StackPanel
  14:              For Each element As UIElement In HitTest(New Point(e.GetPosition(Nothing).X, e.GetPosition(Nothing).Y))
  15:                  If TypeOf (element) Is Border AndAlso TypeOf (DirectCast(element, Border).Parent) Is StackPanel AndAlso DirectCast(DirectCast(element, Border).Parent, StackPanel).Name = "StackPanel1" Then
  16:                      'item order changed - switch placeholder with hovered over item
  17:                      Dim newItemIndex = StackPanel1.Children.IndexOf(DirectCast(element, Border))
  18:                      Dim recPlaceHolder As Rectangle = DirectCast(StackPanel1.Children(itemIndex), Rectangle)
  19:                      StackPanel1.Children.RemoveAt(itemIndex)
  20:                      StackPanel1.Children.Insert(newItemIndex, recPlaceHolder)
  21:                      itemIndex = newItemIndex
  22:                      Exit For
  23:                  End If
  24:              Next
  25:          End If
  26:      End Sub

When the user stops dragging their item, we want to snap the dragged item back into place within the StackPanel.  To accomplish this, we will remove the placeholder Rectangle object and insert the dragged Border object where the placeholder was within the StackPanel.  Here is the final event we handle and we are finished.


   1:      Sub item_MouseLeftButtonUp(ByVal sender As Object, ByVal e As MouseEventArgs)
   2:          Dim item As Border = DirectCast(sender, Border)
   3:          isMouseDown = False
   4:          mousePosition = New Point(0, 0)
   5:          item.ReleaseMouseCapture()
   6:   
   7:          'replace the placeholder with the dragged item
   8:          LayoutRoot.Children.Remove(item)
   9:          StackPanel1.Children.RemoveAt(itemIndex)
  10:          StackPanel1.Children.Insert(itemIndex, item)
  11:      End Sub

Sortables.zip (551.97 kb)

** Edit 9/30/2008 **
Silverlight 2 RC0 no longer supports the HitTest function within UIElement.  You have to use the VisualTree.FindElementInHostCoordinates method instead.  Below you'll find the SL2B2 version of this example.

Sortables_SL2RC0.zip (556.49 kb)


Be the first to rate this post

  • Currently 0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Comments

Add comment


(Will show your Gravatar icon)  

  Country flag

biuquote
  • Comment
  • Preview
Loading