Monday, October 19, 2009

New with the Silverlight Toolkit: Drag and Drop Support for all your Favorite Controls! (Part 1)

One of the highest voted requests on the Silverlight Toolkit CodePlex page is drag and drop support for the TreeView control.  The good news is the Toolkit team listened and has not only delivered Drag and Drop for the TreeView, but they’ve added support for all the major Silverlight controls!  In the October Toolkit refresh you’ll find controls which add Drag and Drop support to all of your favorite controls – no code required!

dd1 dd2 dd4  dd4

Introducing The Drag/Drop Target Controls

A DragDropTarget is a Content Control that adds default drag and drop actions to the control nested inside of it.  DragDropTarget’s provide the following functionality:

1.  Initiates a drag operation when an item container is dragged.

2.  Displays a snapshot of the the item container by the mouse while it is being dragged.

3.  Handles drag target events and specifies which drag operations are possible by examining the item source bound to the nested control.

4.  If an item is dropped onto the drag drop target, it is added to the nested control if the nested control is bound to an ObservableCollection (or any collection that implements INotifyCollectionChanged and contains the same type of items as the item that was dropped).

5.  Where possible, scrolls vertically and horizontally when an item is dragged near the edge of the control.

This release of the toolkit introduces the following implementations:

  • ListBoxDragDropTarget
  • TreeViewDragDropTarget
  • DataGridDragDropTarget
  • DataPointSeriesDragDropTarget

ListBoxDragDropTarget

In this example we have a ListBox created in XAML and bound to an observable collection.

<ListBox x:Name=”myListBox />

To add drag and drop functionality to our ListBox we nest it inside of the ListBoxDragDropTarget control…

<toolkit:ListBoxDragDropTarget
  xmlns:toolkit="System.Windows.Controls.Toolkit"
  xmlns:mswindows="Microsoft.Windows"
  mswindows:DragDrop.AllowDrop=”True”>
     <ListBox x:Name="myListBox">
         <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <StackPanel />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
     </ListBox>
</toolkit:ListBoxDragDropTarget>

…and that’s it!  Now we can drag items to and from our ListBox.

listbox1

If the SelectionMode of the ListBox is set to Multiple or Extended it is also possible to drag and drop several items at once.

listbox2

“Why did you need to specify the ItemPanelTemplate?”

The ListBoxDragDropTarget cannot reorder items in a ListBox when it is virtualized because it cannot conclusively determine the index of each item.  Therefore we replace the default Panel (VirtualizedStackPanel) with a normal StackPanel.  If you don’t need to enable the reordering items within the ListBox then it isn’t necessary to specify the ItemPanelTemplate.

TreeViewDragDropTarget

The TreeViewDragDropTarget is used the same way as the ListBoxDragDropTarget.

<controlsToolkit:TreeViewDragDropTarget msWindows:DragDrop.AllowDrop="true"  HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
    <controlsToolkit:TreeViewDragDropTarget.Resources>
        <win:HierarchicalDataTemplate x:Key="hierarchicalTemplate" ItemsSource="{Binding Reports}">
            <StackPanel Orientation="Horizontal">
                <Image Source="{Binding Image}" Width="16" Height="16" />
                <TextBlock Text="{Binding Name}" VerticalAlignment="Center" Margin="4,0,0,0" />
            </StackPanel>
        </win:HierarchicalDataTemplate>
    </controlsToolkit:TreeViewDragDropTarget.Resources>
    <controls:TreeView ItemTemplate="{StaticResource hierarchicalTemplate}" Height="300" >
    </controls:TreeView>
</controlsToolkit:TreeViewDragDropTarget>

Note that there is no need to specify the ItemPanelTemplate because in Silverlight the TreeView is not virtualized.

treeview1 

The TreeViewDragDropTarget provides all the functionality you’ve come to expect from Windows Explorer (and more). 

  • During a drag operation parent’s expand when hovered over after a short delay
  • Dragging a parent into itself is forbidden
  • The ability to drag above, below, and into a node is supported

All of these actions can be overridden or cancelled using the events.

DataGridDragDropTarget

Using the DataGridDragDropTarget is straightforward:

<dataToolkit:DataGridDragDropTarget msWindows:DragDrop.AllowDrop="true"  HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
    <data:DataGrid x:Name="dataGrid" AutoGenerateColumns="False"  Height="300" >
        <data:DataGrid.Columns>
            <data:DataGridTextColumn Binding="{Binding Name}" Header="Name" />
            <data:DataGridTextColumn Binding="{Binding Reputation}" Header="Reputation"  />
            <data:DataGridTextColumn Binding="{Binding GamerScore}" Header="GamerScore" />
        </data:DataGrid.Columns>
    </data:DataGrid>
</dataToolkit:DataGridDragDropTarget>

datagrid1

Like the ListBoxDragDropTarget, multiple selection is supported.  However the DataGridDragDropTarget has some limitations due to the fact that it is not an ItemsControl and it is virtualized, specifically that reordering items within a DataGrid is not supported.

DataPointSeriesDragDropTarget

The DataPointSeriesDragDropTarget supports all of the series that ship with Silverlight Charts.  However it is used slightly differently than other DragDropTargets.  Instead of wrapping the individual series it is necessary to put the entire Chart in the DragDropTarget.

<chartingToolkit:DataPointSeriesDragDropTarget msWindows:DragDrop.AllowDrop="true" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch">
    <chartingToolkit:Chart x:Name="chart" Background="White">
        <chartingToolkit:ColumnSeries IndependentValueBinding="{Binding Name}" DependentValueBinding="{Binding Reputation}" Title="Reputation" />
        <chartingToolkit:PieSeries IndependentValueBinding="{Binding Name}" DependentValueBinding="{Binding GamerScore}" Title="Gamer Score" />
    </chartingToolkit:Chart>
</chartingToolkit:DataPointSeriesDragDropTarget>
chart1

“What if a Chart contains multiple series?”

The DataPointSeriesDragDropTargets allows diambiguation between the series by detecting drop operations on the legend.  In other words, items can be dropped both onto the surface of a series or its legend items.  In the example below the Chart contains two series: a Pie series and a column series.  Items can be dragged from a list box into the column series by dropping the items on the column series’  legend item.

chart2

chart3

It’s also easy to drag items between series, either by dropping data directly onto a series data point or onto their legend item entries.

chart4

A Labour of Love

Silverlight Drag and Drop support is a personal, off-hours project that I’ve been working on sporadically for the last few months.  I thought it would be a productive way of learning Rx, the new reactive programming framework for .NET.  When I approached the Toolkit team and asked if they would ship it they agreed.  Of course, having formerly worked for the Toolkit team creating the Silverlight Chart control, ISM, and the Rating control I had some pretty good contacts ;-).

Next time: Overriding the Default Actions of DragDropTargets

31 comments:

Anonymous said...

Why don't you use behaviors? Nice to have drag & drop support. But I think it's not the right way of doing it.
Benny

Mark Tucker said...

Is there a way to have a Rectangle be a drop target?

Jafar Husain said...

Anonymous: I took a look at behaviors but I don't think they are appropriate here. The difficulty is that it's necessary to inject elements into the visual tree somewhere (the insertion line indicator, the dragged icon). Any modifications I made to another control's visual tree might potentially break that control or be unexpected to the developer. DDTs are ContentControl's which means they can own a portion of the visual tree and do their work safely.

Mark: See the next post in the series. It's up now.

Tonya said...

Hi, great post! I just have one quick question, this may be basic so I apologize. I'm trying to drag and drop between two ListBoxes, I can drag but cannot drop into the other ListBox...am I missing something?

Thanks in advanced...

michael_p_lockwood said...

Jafar,
The screen shots from your post look great. I am trying to replicate something similar. I want to have a chart that show how many servers are in each group. From there I want to be able to drag part of the chart to a datagrid which would show all of the details for the servers in that group. I am close to figuring it out but it is not working as I expected. Is there a way to get the code for the screen shots that you posted? Any help would be great.

Fallon Massey said...

I agree that Behaviors don't work as well for this task. That's not to say you can't make it work, but from my experience, working it into the visual tree was difficult and messy.

Jafar Husain said...

Tonya: Did you set the attached AllowDrop property on the drop target? Take a look at the code sample above and make sure the XAML matches.

micheal_p_lockwood: There's really almost no code. Other than data-binding a collection to the ListBoxes and some XAML that is. Have a look at the next post in the series (which is up now) and see if that's got enough detail for you.

Daniel McGloin said...

Very nice. Keep up the good work!

Skye said...

Jafar,
I am pulling in a collection from RIA services and binding to my Listbox. I have included the < stackPanel > in the template. I can drag from the bound template to the drop target but I can't reorder in the source listbox nor can I drop anything back into the source listbox. I am not quite sure how to work around this. Either way this is some terrific work.

Jafar Husain said...

Skye: Make sure the DragDrop.AllowDrop attached property is set on the source. After all when reordering items the source must also accept drop events.

Koji Ishii said...

I agree that visual tree injection isn't easy in WPF/Silverlight, but still wrapping control doesn't look smart.

I haven't tried by myself but can't we use Adorner to inject visual trees?

Jafar Husain said...

Koji: Silverlight does not have the Adorner. Believe me I'm open to suggestions. This is an experimental toolkit control and its design is not written in stone. However up until this point I haven't found a better way than deriving from content control.

LiquidBoy said...

hi,

thanks and im really liking the implementation. I don't mind the fact that its a container.

How do i file bugs? thru codeplex forums?

Rick said...

I really like the concept, but I seem to have found a complication. It appears that the DDT controls don't play well with the other SLTK controls. For example, let's say you have a ListBox and you want to employ the DockPanel to fill your space as follows:
<tk:DockPanel LastChildFill="True">
<ListBox>
...

This works great until you decide you also want to make the ListBox DDT enabled:

<tk:DockPanel LastChildFill="True">
<tk:ListBoxDragDropTarget Windows:DragDrop.AllowDrop="True">
<ListBox>
...

Since ListBox is no longer LastChild, the "Fill" will no longer apply to the ListBox. Is there another way to achieve this functionality?

Jafar Husain said...

Liquid Boy: Yes. Use CodePlex for bugs please and thanks for the feedback.

Anonymous said...

I am trying to use DataGridDragDropTarget, finding that the snapshot can't display correctly.For example, I drag a record from a datagrid to a listbox, the snapshot isn't the one being dragged or selected?How could I fix it?Please help me, thanks!

Andrew said...

Is there a way to work out which TreeViewItem is the target of the drop please?

Andrew said...

Please ignore my previous post, I've managed to achieve what I need using the VisualTreeHelper.

Anonymous said...

I have checked the Datagrid drag and drop in control toolkit but its not there?

Jafar Husain said...

Anonymous: It's in the System.Windows.Controls.Data.Toolkit.dll.

Maciej Gren said...

Great job!

I am curiouse, is there any interface that can be derrived from to implement in similar way drap @ drop features on other controls as well, keeping the same approach in mind?

5gPitYx3zoJnysf._RoSVzWeqO2qf40ZVmSB said...

"or any collection that implements INotifyCollectionChanged and contains the same type of items as the item that was dropped"

I'm using .NET RIA Services, and the list box is bound to an EntityCollection.

Unfortunately I can not reorder items within the list (yes, I'm using the stack panel for the items panel template).

Is there any way I can extend the built-in functionality to work with this .NET RIA Services collection type?

Thanks!

Robert

5gPitYx3zoJnysf._RoSVzWeqO2qf40ZVmSB said...

Robert again - just saw Skye's comment. I also/already have

msWindows:DragDrop.AllowDrop="true"

on the ListBoxDragDropTarget.

Luay said...

Jafar,

Thanks for this post. It has been very helpful to get my hands dirty with SL3 DD support.

Question,
If I want to reorder multiple items in a list box (multi-select).
Can I get the item index of where the user is dropping the dragged items.

The ListBoxDragDropTarget updates the binable collection directly if it is ObservableCollection (good). What do I need to do to be notified when all updates are done to the observableCollection. (i.e. something like a DropCompleted event)

I have tried to listen to CollectionChanged on my Observable Collection but it fires once for each item that was moved in the list (Remove/Add notifications for each item).

Listening to the Drop event didn't work as I couldn't find where (item index) the user is dropping the items.

Appreciate your response.

Thanks,
Luay

Jafar Husain said...

Maceij: Yes there is an interface you can implement on your own control: IAcceptDrop. By implementing this interface your control will be delivered all the drag messages before the attached events are raised. This will give your custom controls an opportunity to cancel these events. The larger question is whether it is really necessary to implement this interface. Keep in mind you can also listen for the attached events on yourself the same way an external consumer of the control can:

this.AddListener(SW.DragDrop.DropEvent, new DragEventHandler((o,a) => Console.Write("drop")), true);

Personally I think you should implement IAcceptDrop and add the various events because it makes it easier for consumers. Take a look at the source code for the DDT's which implement IAcceptDrop.

Robert: Is EntityCollection an INotifyCollectionChanged? If not the easiest thing to do is implement an adapter, that is, create a class which implements INCC, intercept the collection change messages, and then modify the EntityCollection.

Keep in mind sometimes ORMs like Entity Framework don't support reordering collections because there is no order in the underlying database. Not sure if that's the case with EF (haven't used it yet) but it's something to look into.

Anonymous said...

I have two simple Drag and Drop ListBoxes, I add two item in one,
like: ListBox1.Items.Add("1"), ListBox1.Items.Add("2"). I drag item from one listbox to the other and back, and it works file.
However, when I add two item in one,like:
ListBox1.Items.Add(new TextBlock(){Text="1"}), ListBox1.Items.Addnew TextBlock(){Text="2"}), it fails with an exception each time I tried to drag and drop to the second list box.

{System.ArgumentException: Value does not fall within the expected range.

Has anybody else encountered this behavior?

Abubakar said...

Hi,
I want to drag a cell and drop to the same datagrid. is it possible using this version of tool kit?

Bogdan Đukić said...

@Luay:

Hi Luay,

I've just blogged about how to get extra information in Drop event in order to get the list of items which are dragged and drop target object (item in TreeView or ListBox, etc). Check out http://bdjukic.com/?p=90

Irfan said...

Jafar,

I want to restrict drop on some of my Treeview nodes.

I am trying to achieve this by utilizing ItemDroppedOnTarget event, however even after setting e.Cancel = true, the drop is not getting cancelled.

Can you tell me the correct way to achieve this functionality.

Regards,
Irfan

Anonymous said...

Thank you for this great work.

Did you write this Part 3 about custom controls you mention ?

I am trying to allow Drafg from TreeNode / Drop onto a Canvas, but can't figure it out ...

Haemogee said...

Hey there Jafar,

I am having trouble with drag and drop to a StackedColumnSeries - is this supported?

I have a full stripped back sample I can show you of what I have tried so far and what isn't working (just a single XAML and code behind). Could I get this to you somehow?

Thanks,
Hamish

About Me

My Photo
I'm a software developer who started programming at age 16 and never saw any reason to stop. I'm working on the Presentation Platform Controls team at Microsoft. My primary interests are functional programming, and Rich Internet Applications.