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)