Předminule jsem vyrobil a popsal demo aplikaci na Drag & Drop v Silverlight, konkrétně přesun položek z DataGridu do TreeView controlu (s podporou multiselectu). Zajímalo mě, jak by se tento problém řešil ve WPF a jak velká podpora na Drag & Drop zde je. Proto jsem tuto demo aplikaci do WPF přepsal a její zdrojové soubory jsou v tomto příspěvku k dispozici. Pro ty co pracují v obou technologiích může být také zajímavé, udělat si porovnání jednotlivých zdrojových souborů.
Problém drag & drop v aplikaci si opět rozdělíme do tří částí:
- Máme v DataGridu označeno více řádků. Pokud je chceme myší uchopit a začít drag & drop, dojde k označení pouze řádku, na který se přitom kliklo (a označení multiselectu se vyruší). Máme zde tedy stejný problém jako jsme měli v Silverlight. Řešit to budeme stejně tj. controlem ExtendedDataGrid, který pro WPF trochu upravíme.
- Pro obsluhu zahájení Drag & Drop v Silverlight jsme použili control DataGridDragDropTarget (ze Silverlight Toolkit), ten nám pomohl s inicializací operace DoDragDrop a vytvořil grafické znázornění stavu (DragDecorator) při dragování. Control jsme poté ale museli v odvozené třídě značně upravit. Zde ve WPF nebudeme používat žádný speciální ani vlastní control. Volání DoDragDrop je zde jednodušší a tak ho naimplementujeme přímo v hlavním okně. Také zde není nutné řešit grafiku stavu při dragování, stavy jsou zde automaticky znázorněny různým kurzorem myši.
- Poslední částí bude obsluha pro operaci Drag Drop na controlu TreeView. Stejně jako v případě Silverlight budeme obsluhu implementovat ve vlastním controlu TreeViewDropTarget (zde ani jinou podporu jako byl Silverlight Toolkit nemáme). Zde ale bude control na rozdíl od Silverlight verze odchytávat standartní Drag Drop události. Dále bude řešit další funkce, které jsme měli i v Silverlight variantě, tedy automatické otevření uzlu stromu při najetí na něj myší a posouvání stromu vertikálně nahoru nebo dolu při najetí myší na okraje TreeView.
Teď už rovnou přejdeme k těm nejdůležitějším částem kódu:
Ad 1) V controlu ExtendedDataGrid úplně stejně jako v Silverlight zaregistrujeme událost MouseLeftButtonDown pomoci metody AddHandler s parametrem handledEventsToo = true, aby se nám správně vyvolávala i při kliknutí i na již označenou buňku. Při OnSelectionChanged si zapamatujeme označené řádky a na poté vyvolanou událost MouseLeftButtonDown je budeme vracet. (Při tom je zde trochu rozdíl v ošetření oproti Silverlight, protože WPF DataGrid umožňuje označování více řádků přímo tažením myši.)
public new event MouseButtonEventHandler MouseLeftButtonDown;
private IList LastSelectionRemovedItems;
private bool KeyboardStateOnMouseLeftButtonDown;
private DataGridRow LeftButtonDowmLastRow;
public ExtendedDataGrid()
{
base.AddHandler(FrameworkElement.MouseLeftButtonDownEvent, new MouseButtonEventHandler(Base_MouseLeftButtonDown), true);
}
protected override void OnSelectionChanged(System.Windows.Controls.SelectionChangedEventArgs e)
{
this.LastSelectionRemovedItems = null;
if (this.SelectionMode == DataGridSelectionMode.Extended && e.RemovedItems.Count != 0 && e.AddedItems.Count == 0 &&
(Keyboard.Modifiers & ModifierKeys.Control) == 0 && (Keyboard.Modifiers & ModifierKeys.Shift) == 0)
{
//Označení záznamu, který již byl označen jako součást multiselektu
//Zapamatování pro obnovu označení na MouseLeftButtonDown
this.LastSelectionRemovedItems = e.RemovedItems;
}
base.OnSelectionChanged(e);
}
private void Base_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
this.OnMouseLeftButtonDown(e);
}
protected new virtual void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
this.LeftButtonDowmLastRow = null;
if (this.LastSelectionRemovedItems != null)
{
if (this.ItemsSource != null)
{
this.LeftButtonDowmLastRow = this.HitTest(e);
//Vrácení označení podle zapamatování v OnSelectionChanged
foreach (var item in this.LastSelectionRemovedItems)
{
try
{
this.SelectedItems.Add(item);
}
catch (ArgumentException)
{
//The item is not contained in the ItemsSource
}
}
}
this.LastSelectionRemovedItems = null;
}
this.KeyboardStateOnMouseLeftButtonDown = (Keyboard.Modifiers & ModifierKeys.Control) != 0 || (Keyboard.Modifiers & ModifierKeys.Shift) != 0;
if (MouseLeftButtonDown != null)
{
MouseLeftButtonDown(this, e);
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (!e.Handled && this.SelectionMode == DataGridSelectionMode.Extended && this.SelectedItems.Count > 1 &&
(Keyboard.Modifiers & ModifierKeys.Control) == 0 && (Keyboard.Modifiers & ModifierKeys.Shift) == 0 && !this.KeyboardStateOnMouseLeftButtonDown)
{
var row = this.HitTest(e);
if (row != null && row == this.LeftButtonDowmLastRow)
{
var item = row.DataContext;
this.SelectedItems.Clear();
this.SelectedItem = item;
}
this.LeftButtonDowmLastRow = null;
}
this.KeyboardStateOnMouseLeftButtonDown = false;
base.OnMouseLeftButtonUp(e);
}
Ad 2) Zahájení Drag Drop budeme implementovat v MouseMove události DataGridu.
private void DataGrid_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed || e.RightButton == MouseButtonState.Pressed)
{
DataGridRow row = FindVisualParent<DataGridRow>(e.OriginalSource as FrameworkElement);
if (row != null && row.IsSelected)
{
var eventArgs = new ItemDragEventArgs()
{
AllowedEffects = DragDropEffects.Copy,
Effects = DragDropEffects.Copy,
Data = DataGrid.SelectedItems.Cast<Item>().OrderBy(i => i.Number),
DragSource = DataGrid
};
var items = row.Item;
DragDropEffects finalEffects = DragDrop.DoDragDrop(DataGrid, eventArgs, eventArgs.AllowedEffects);
e.Handled = true;
}
}
}
Hodnoty si uložíme do objektu ItemDragEventArgs. Její třídu nemáme k dispozici v žádné knihovně, tak si její implementaci převezmeme ze zdrojových souborů Silverlight toolkitu. Ještě si všimněte, že označené prvky je nutné seřadit, protože ve WPF je první v kolekci ten prvek, který byl označen naposledy.
Ad 3) Základní kód controlu TreeViewDropTarget je v podstatě stejný jako v Silverlight verzi. Hlavní změny jsou používání standartního System.Windows namespace (na rozdíl od Microsoft.Windows) a registrace standartních událostí DragEnter, DragLeave, DragOver a Drop.
/// <summary>
/// Gets the TreeView that is the drag drop target.
/// </summary>
protected TreeView TreeView
{
get { return (base.Content as TreeView); }
}
private void TreeViewDropTarget_Loaded(object sender, RoutedEventArgs e)
{
if (this.TreeView == null)
{
throw new InvalidOperationException("Content is not valid TreeView control!");
}
this.TreeView.AllowDrop = true;
this.TreeView.DragEnter += new DragEventHandler(TreeView_DragEnter);
this.TreeView.DragLeave += new DragEventHandler(TreeView_DragLeave);
this.TreeView.DragOver += new DragEventHandler(TreeView_DragOver);
this.TreeView.Drop += new DragEventHandler(TreeView_Drop);
this.DragDropNodeExpandTimer = new DispatcherTimer();
this.DragDropNodeExpandTimer.Interval = TimeSpan.FromMilliseconds(800);
this.DragDropNodeExpandTimer.Tick += new EventHandler(DragDropNodeTimer_Tick);
this.ScrollTimer = new DispatcherTimer();
this.ScrollTimer.Interval = TimeSpan.FromMilliseconds(80);
this.ScrollTimer.Tick += new EventHandler(ScrollTimer_Tick);
}
private void TreeView_DragEnter(object sender, DragEventArgs e)
{
this.DragDropNodeExpandTimer.Start();
}
private void TreeView_DragLeave(object sender, DragEventArgs e)
{
this.DragDropNodeExpandTimer.Stop();
this.ScrollTimer.Stop();
}
private void DragDropNodeTimer_Tick(object sender, EventArgs e)
{
this.DragDropNodeExpandTimer.Stop();
if (this.DragDropPrevItem != null)
{
this.DragDropPrevItem.IsExpanded = true;
}
}
private void TreeView_DragOver(object sender, DragEventArgs e)
{
e.Effects = DragDropEffects.None;
var target = GetMouseTreeViewItem(e, this.TreeView);
if (target != null)
{
var sourceItems = GetSourceItems(e);
if (sourceItems != null)
{
//Check if item is valid for target
var args = new TreeViewValidateDropTargetEventArgs(target.DataContext, sourceItems, e);
if (this.ValidateDropTarget != null)
{
this.ValidateDropTarget(this, args);
}
if (args.IsValid)
{
e.Effects = DragDropEffects.Copy;
}
}
}
if (target == null || target != this.DragDropPrevItem)
{
this.DragDropPrevItem = target;
this.DragDropNodeExpandTimer.Stop();
this.DragDropNodeExpandTimer.Start();
}
//Scroll up or down if needed
BeginScroll(GetScrollDirection(e));
e.Handled = true;
}
private void TreeView_Drop(object sender, DragEventArgs e)
{
this.ScrollTimer.Stop();
var target = GetMouseTreeViewItem(e, this.TreeView);
if (target != null && e.Effects == DragDropEffects.Copy)
{
var sourceItems = GetSourceItems(e);
if (sourceItems != null)
{
var args = new TreeViewDropEventArgs(target.DataContext, sourceItems, e);
if (this.DropOnTarget != null)
{
this.DropOnTarget(this, args);
}
}
}
}
V metodě GetSourceItems je ještě také změna oproti Silverlight variantě, kde se musí přesouvané položky DataGridu načíst jiným zůsobem:
private IList<object> GetSourceItems(DragEventArgs e)
{
var itemDragEventArgs = e.Data.GetData(e.Data.GetFormats()[0]) as ItemDragEventArgs;
if (itemDragEventArgs == null)
{
return null;
}
var data = itemDragEventArgs.Data as System.Collections.IEnumerable;
if (data == null)
{
return null;
}
var items = data.Cast<object>().ToList().AsReadOnly();
return items.Count == 0 ? null : items;
}
Ještě zbývá ukázat XAML kód okna aplikace MainWindow:
<Window x:Class="WpfDragDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:impcontrols="clr-namespace:IMP.Windows.Controls"
Title="WpfDragDemo" Height="466" Width="688" Icon="/WpfDragDemo;component/Icon.ico">
<Grid x:Name="LayoutRoot" Background="White">
<Grid Margin="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="250" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<impcontrols:TreeViewDropTarget Grid.Column="0" ValidateDropTarget="TreeViewDropTarget_ValidateDropTarget" DropOnTarget="TreeViewDropTarget_DropOnTarget">
<TreeView x:Name="TreeView">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Items}">
<StackPanel Orientation="Horizontal" Background="Transparent">
<Image Source="Images/Folder.png" VerticalAlignment="Center" Margin="0,0,4,0" />
<TextBlock Text="{Binding Name}"/>
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</impcontrols:TreeViewDropTarget>
<impcontrols:ExtendedDataGrid x:Name="DataGrid" Grid.Column="1" Background="White" SelectionMode="Extended" AutoGenerateColumns="False" IsReadOnly="True" RowDetailsVisibilityMode="Collapsed" GridLinesVisibility="None"
CanUserReorderColumns="False" CanUserSortColumns="False" FontSize="10.667" RowHeight="20"
MouseMove="DataGrid_MouseMove">
<DataGrid.Columns>
<DataGridTemplateColumn Header="Name" Width="110">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Margin="4" Orientation="Horizontal">
<Image Source="Images/Item.png" VerticalAlignment="Center" Margin="0,0,4,0" />
<TextBlock VerticalAlignment="Center" Text="{Binding Name}"/>
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
<DataGridTextColumn Binding="{Binding Number}" Header="Number" Width="60" />
</DataGrid.Columns>
</impcontrols:ExtendedDataGrid>
</Grid>
</Grid>
</Window>
Implementace událostí ValidateDropTarget a DropOnTarget na TreeViewDropTarget controlu zůstává úplně stejná jako v případě Silverlight verze. Kompletní zdrojové soubory celého funkčního příkladu je možné stáhnout zde.