Notes
A more detailed explanation of attached properties can be found here.
Introduction
After finishing with the GlassEffect article, I started dreaming of more ways to use Attached Properties. What better way than to use it to enable drag & drop on Listbox
controls?
The DragAndDrop
class will be provided by me with two attached properties: DragEnabled
to enable dragging on a ListBox
and DropEnabled
to enable dropping on a ListBox
. This can be applied to the same ListBox
or on multiple ListBox
controls. To enable dragging, just add the following one line of code:
<ListBox x:Name="SourceListBox" src:DragAndDrop.DragEnabled="true"/>
and the following to allow dropping:
<ListBox x:Name="DestinationListBox" src:DragAndDrop.DropEnabled="true"/>
Drag & Drop
Drag & Drop in WPF is relatively easy to implement. Here are the definitions of the two attached properties:
#region DragEnabled
public static readonly DependencyProperty DragEnabledProperty =
DependencyProperty.RegisterAttached("DragEnabled", typeof(Boolean),
typeof(DragAndDrop), new FrameworkPropertyMetadata(OnDragEnabledChanged));
public static void SetDragEnabled(DependencyObject element, Boolean value)
{
element.SetValue(DragEnabledProperty, value);
}
public static Boolean GetDragEnabled(DependencyObject element)
{
return (Boolean)element.GetValue(DragEnabledProperty);
}
public static void OnDragEnabledChanged
(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if ((bool)args.NewValue == true)
{
ListBox listbox = (ListBox)obj;
listbox.PreviewMouseLeftButtonDown +=
new MouseButtonEventHandler(listbox_PreviewMouseLeftButtonDown);
}
}
#endregion
#region DropEnabled
public static readonly DependencyProperty DropEnabledProperty =
DependencyProperty.RegisterAttached("DropEnabled", typeof(Boolean),
typeof(DragAndDrop), new FrameworkPropertyMetadata(OnDropEnabledChanged));
public static void SetDropEnabled(DependencyObject element, Boolean value)
{
element.SetValue(DropEnabledProperty, value);
}
public static Boolean GetDropEnabled(DependencyObject element)
{
return (Boolean)element.GetValue(DropEnabledProperty);
}
public static void OnDropEnabledChanged
(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if ((bool)args.NewValue == true)
{
ListBox listbox = (ListBox)obj;
listbox.AllowDrop = true;
listbox.Drop += new DragEventHandler(listbox_Drop);
}
}
#endregion
It is important to notice that to enable dragging, we add a handler to the PreviewMouseLeftButtonDown
and to enable dropping of items, we set AllowDrop
to true
and add a handler for the Drop
event.
Before we implement the event handlers, let's also look at two other important parts of this class. First let's look at the helper class we use to determine the item being dragged based on the point where the mouse is clicked. We use the provided hit testing here. This code is taken from here:
#region Helper
private static object GetObjectDataFromPoint(ListBox source, Point point)
{
UIElement element = source.InputHitTest(point) as UIElement;
if (element != null)
{
object data = DependencyProperty.UnsetValue;
while (data == DependencyProperty.UnsetValue)
{
data = source.ItemContainerGenerator.ItemFromContainer(element);
if (data == DependencyProperty.UnsetValue)
element = VisualTreeHelper.GetParent(element) as UIElement;
if (element == source)
return null;
}
if (data != DependencyProperty.UnsetValue)
return data;
}
return null;
}
#endregion
We also have two static
objects you need to know about. Once the mouse is clicked over an item, I keep a reference of its ListBox
and I also remember the type of its ListBoxItem
's content. Why do I do this? ListBoxItem
s can contain two types of items: the ListBox
can contain UIElement
s (like TextBlock
, Image
, etc.) or it can be bound to a data structure (CLR objects, XML data, etc.). These need to be handled differently, thus we need to know its type! I will explain later how each type is handled...
Next, let's look at the handling to enable the dragging:
static void listbox_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
DragSource = (ListBox)sender;
object data = (object)GetObjectDataFromPoint(DragSource, e.GetPosition(DragSource));
if (data != null)
{
DragType = data.GetType();
DragDrop.DoDragDrop(DragSource, data, DragDropEffects.Copy);
}
We set the DragSource
to the sending ListBox
. Next, use the GetObjectDataFromPoint
helper function to determine if we are dragging a valid item. Also store the type of the dragged item. Lastly we call DragDrop.DoDragDrop
. This handles the rest of the work for us!
static void listbox_Drop(object sender, DragEventArgs e)
{
object data = e.Data.GetData(DragType);
if (DragType.IsVisible == true)
DragSource.Items.Remove(data);
((ListBox)sender).Items.Add(data);
}
This is where the real action happens. When an item is dropped, this event is invoked. It returns the item that was dropped. This is where we need to know what the type of the item is that gets dropped. If it was a UIElement
, then we will receive a type equal to typeof(ListBoxItem)
. We need to remove it from the source or else we get an exception! All that is left now is to add the dropped data into our destination ListBox
!
Attached properties are great! It gives us a clean and concise way of enabling functionality on existing controls and as always... it can be turned on using only one line of code!!!
History
- 11 January 2008: Initial release
Links