In part 1 you were introduced to the new Silverlight DragDropTarget controls. In addition to being powerful these controls are very flexible, allowing developers to customize almost every single aspect of their behavior. In order to understand how to customize these controls you must first understand how they communicate. The good news is that DDT’s communicate via a WPF-compatible implementation of the drag/drop events!
A WPF-Compatible (mostly) Implementation of Drag and Drop
The Silverlight Toolkit Drag and Drop Framework is a compatible subset of WPF’s drag and drop methods and events – with a few small exceptions. The diagram below demonstrates how DDTs are layered on top of this WPF-compat framework. DDTs use these events and methods to communicate with each other.
The fact that the Silverlight Toolkit contains a full implementation of WPF’s drag and drop stack gives the developer tremendous flexibility. They can…
- Use DDTs.
- Communicate with and customize DDTs by handling the drag and drop events.
- Choose not to use DDTs and use the WPF-compat API’s to initiate and manage their own drag operations.
- Add native drag and drop support to their custom controls by implementing the IAcceptDrop interface.
- Create new DDTs for existing controls by creating a class that inherits from DragDropTarget.
Differences between Silverlight Toolkit and WPF Drag and Drop API’s
Different Namespaces
The most obvious difference between the Silverlight implementation of drag and drop and that of WPF is that the Silverlight drag and drop objects are located in the Microsoft.Windows namespace rather than in the System.Windows namespace. When writing drag and drop code it’s good practice to insert the following code in your source file:
#if SILVERLIGHT using Microsoft.Windows; using SW = Microsoft.Windows; #else using SW = System.Windows; #endif
Using an alias for the namespace with the drag/drop objects ensures that you can write code that can be easily ported to WPF or a future version of Silverlight with core-level drag/drop support. All of the code samples in this series of blog posts assume that you’ve defined the alias “SW” as Microsoft.Windows in Silverlight and System.Windows in WPF.
Starting a Drag Operation
Keep in mind that if you intend to use a DDT you don’t need to know how to do this because DDT’s automatically initiate drag operations when an item is dragged. However if you want to initiate a drag operation yourself you’ll need to know about some subtle (but necessary) differences between the Silverlight’s and WPF’s DoDragDrop methods.
In WPF the DoDragDrop method blocks until the drag operation completes. In Silverlight there are no blocking UI methods so it is necessary to listen for a DragDropCompleted event if you want to perform an operation when a drag completes.
It is still possible to write cross-platform code if you use the following pattern:
// Define an action to be executed when the drag completes. Action<SW.DragDropEffects> dragDropCompleted = effects => { Console.WriteLine(string.Format(“Drag finished: {0}”, effects))); }; #if SILVERLIGHT // Create a handler for the DragDropCompleted event which invokes the action. EventHandler<DragDropCompletedEventArgs> dragDropCompletedHandler = null; dragDropCompletedHandler = (sender, args) => { // Detach the handler so that this action is only executed once. SW.DragDrop.DragDropCompleted -= dragDropCompletedHandler; dragDropCompleted(args.Effects); }; // Attach the handler to the DragDropCompleted event. SW.DragDrop.DragDropCompleted += dragDropCompletedHandler; // Begin the drag operation. SW.DragDrop.DoDragDrop( sourceControl, data, SW.DragDropEffects.All, SW.DragDropKeyStates.LeftMouseButton); #else // If WPF then just begin the drag operation. SW.DragDropEffects effects = SW.DragDrop.DoDragDrop( sourceControl, data, SW.DragDropEffects.All); // Code blocks until drag is finished... dragDropCompleted(effects); #endif
Note that in addition to being asynchronous the Silverlight version of DoDragDrop accepts an additional parameter: the initial DragDropKeyStates. In the example below this is SW.DragDropKeyStates.LeftMouseButton.
SW.DragDrop.DoDragDrop(
sourceControl,
data,
SW.DragDropEffects.All,
SW.DragDropKeyStates.LeftMouseButton);
In Silverlight (unlike WPF) it is not possible to sample the mouse state. Therefore the developer must track the state of the mouse keys and pass it to DoDragDrop. This is straightforward if, as is commonly the case, the drag operation begins with a mouse click.
button.MouseLeftButtonDown += (sender, args) => { SW.DragDrop.DoDragDrop( button, "Some text to transfer", SW.DragDropEffects.All, SW.DragDropKeyStates.LeftMouseButton); };
Although the DragDropKeyStates enumeration is a flags enum which includes members that track keyboard states there is no need to include these. The DoDragDrop method will sample the keyboard and update the value when invoked.
Listening to Drag Source and Drag Target Events
In WPF there are two ways to attach a handler to an event. The first is to attach a handler to the event on the UIElement and the second is to attach a handler to the attached event object:
// Works in WPF. myButton.DragOver += new SW.DragEventHandler((o, a) => Console.Write(“button dragged over.”)); // Works in WPF and Silverlight Toolkit Drag Drop Framework. myButton.AttachEvent( SW.DragDrop.DragOverEvent, new SW.DragEventHandler((o, a) => Console.Write(“button dragged over.”), false);
Silverlight Toolkit’s Drag and Drop Framework supports the latter approach for all drag target and source events. By using AttachEvent and RemoveEvent you can write code that will work on WPF and Silverlight. For more information on attached events see the Attached Events Overview on MSDN.
Note: DDTs implement the familiar WPF events so you don’t need to use the more verbose attached event approach when you’re working with them.
// DragDropTarget's have the WPF events. myListBoxDragDropTarget.DragOver += new SW.DragEventHandler( (o, a) => Console.Write(“DDTs make everything easier!”));
Indicating that an Element Can Accept a Drop Operation
In WPF all UIElements have an AllowDrop property which determines whether they will receive drag/drop events. As it isn’t possible to add properties to existing types the Silverlight Tookit implementation of drag and drop uses an AllowDrop attached property on the static DragDrop object:
WPF
<Rectangle AllowDrop="True" />
Silverlight
<Rectangle xmlns:msWindows="clr-namespace:Microsoft.Windows;assembly=System.Windows.Controls.Toolkit" mswindows:DragDrop.AllowDrop="True" />
Customizing DragDropTarget Behavior
DDT’s do their best to determine the allowed effects of a drag operation by examining the bound collection and the items inside of them. For example if a DDT’s control is bound to a ReadOnlyCollection or an array then the allowed effects of a drag will not include DragDropEffects.Move and the allowed effects of a drop will be DragDropEffects.None. By default a DDT never sets the allowed effects of a drag to DragDropEffects.Copy because there is no generic way of cloning an item in Silverlight (ICloneable does not exist). But what if we wanted the effect of a drop to be a Copy operation?
Let’s say we have two DDT’s, one containing a ListBox and the other containing a DataGrid.
<StackPanel Orientation="Horizontal"> <controlsToolkit:ListBoxDragDropTarget x:Name="listBoxDragDropTarget" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch"> <ListBox x:Name="listBox" VerticalAlignment="Stretch" SelectionMode="Extended" Height="300" > <ListBox.ItemTemplate> <DataTemplate> <StackPanel Orientation="Horizontal"> <Image Source="{Binding Image}" /> <TextBlock Text="{Binding Name}" VerticalAlignment="Center" Margin="4,0,0,0" /> </StackPanel> </DataTemplate> </ListBox.ItemTemplate> <ListBox.ItemsPanel> <ItemsPanelTemplate> <StackPanel Orientation="Vertical" /> </ItemsPanelTemplate> </ListBox.ItemsPanel> </ListBox> </controlsToolkit:ListBoxDragDropTarget> <dataToolkit:DataGridDragDropTarget x:Name="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> </StackPanel>
Each control is bound to an collection of Gamer instances.
class Gamer { public string Name {get; set} public double Reputation {get; set} public int GamerScore {get;set;} // Our custom copy method. public Gamer Clone() { return new Gamer { Name = this.Name, Reputation = this.Reputation, this.GamerScore = this.GamerScore }; } } // snip... // No need to use an ObservableCollection. We don't // want items removed from the ListBox. listBox.ItemsSource = new [] { new Gamer { Name = "Mark Rideout", Reputation = 82.234, GamerScore = 23432 }, // etc. }; // Using an observable collection ensures that changes // to the items source collection will be reflected // in the grid immediately. dataGrid.ItemsSource = new ObservableCollection<Person>();
When the user drags Gamers from the ListBox to the DataGrid we’d like to add a copy of the Gamer object to the DataGrid rather than add a reference to the object in the ListBox to the DataGrid’s bound collection (the default action). In order to ensure that when an item is dragged from a ListBox the allowed effect is DragDropEffects.Copy rather than DragDropEffects.Link we set the ListBoxDragDropTarget’s AllowedSourceEffects property.
listBoxDragDropTarget.AllowedSourceEffects = SW.DragDropEffects.Copy;
Since the DataGridDragDropTarget doesn’t know how to handle a Copy operation we need to handle its Drop event.
using System.Collections.ObjectModel; // snip... dataGridDragDropTarget.Drop += (sender, args) => { // Only handle this event if it's a copy. If the event is not handled // the DDTs base implementation of Drop will execute. if ((args.AllowedEffects & SW.DragDropEffects.Copy) == SW.DragDropEffects.Copy) { IList<Person> dataSource = dataGrid.ItemsSource as IList<Person>; // Retrieve the dropped data in the first available format. object data = args.Data.GetData(args.Data.GetFormats()[0]); // The data is the ItemDragEventArgs that was created by the DDT when // the drag started. It contains a SelectionCollection. // SelectionCollection's are used by DDTs because they can transfer // multiple objects. The fact that they store the indexes of the // objects within the source collection also makes reordering items // within a source possible. ItemDragEventArgs dragEventArgs = data as ItemDragEventArgs; SelectionCollection selectionCollection = dragEventArgs.Data as SelectionCollection; if (selectionCollection != null) { IEnumerable<Gamer> gamers = selectionCollection.Select(selection => selection.Item).OfType<Gamer>(); if (gamers.Any()) { foreach(Gamer gamer in gamers) { // Invoke our custom Clone method to make a copy. dataSource.Add(gamer.Clone()); } args.Effects = SW.DragDropEffects.Copy; args.Handled = true; } } } };
Now when we drag items from our ListBox to our DataGrid we will get copies of the Gamer objects and the ListBox’s contents will be unchanged.
There are many more extensibility points in DDT’s. I encourage you to take a close look at the events and properties. I think you’ll be impressed by how flexible these controls are.
Powerful and Flexible
By now it should be clear that the Silverlight Toolkit Drag and Drop Framework is more than a few DDTs that add drag and drop behavior to a certain Silverlight controls. It is also a rich set of WPF-compatible events and methods that can be used with any Silverlight control - including your own!
Next time: Adding Drag and Drop support to your custom controls!