Drag & Drop v Silverlight

Jan Holan       22.09.2011       Silverlight, Komponenty       11900 zobrazení

Nedávno jsme do Silverlight aplikace potřebovali doplnit podporu pro operaci Drag & Drop položek z DataGrid controlu do TreeView. Zjistil jsem, že to není v Silverlight vůbec jednoduchá záležitost. Proto jsem se rozhodl udělat na to demo aplikaci, jejíž zdrojový kód popíši.

Ještě než se vrhneme k vlastnímu příkladu, podíváme se napřed, jaké máme vlastně v Silverlight možnosti Drag & Drop operací. Dají se shrnout takto (více také zde):

  • Již od Silverlight 2 je možné “přemísťovat” UI elementy pomoci drag & drop myší. Provádí se to tak, že při události MouseLeftButtonDown je volaná metoda CaptureMouse a pak je monitorována událost MouseMove, na kterou je obsloužen vlastní přesun elementu. Po dokončení drag operace (událostí MouseLeftButtonUp) je volána metoda ReleaseMouseCapture. Nejčastěji toto bývá používáno např. k přesouvání objektů po Canvasu nebo k měnění polohy nějakého Child okna nebo Popupu. (Nejedná se tedy o klasickou podporu drag & drop operace v controlech např. pro přetahování prvků z jedno listu do druhého). Některé příklady jsou zde zde zde nebo zde.
  • Silverlight 3 přináší možnost registrovat události na elementu pomoci metody AddHandler, kde lze parametrem handledEventsToo nově zaregistrovat i routed události, které již byly odchyceny (Handled). To je často při drag & drop operacích využíváno.
  • Silverlight 4 přináší novou podporu pro drag & drop – události System.Windows.DragEnter, DragLeave, DragOver, Drop a vlastnost AllowDrop na objektu UIElement (API je podobné jako ve WPF). Nejedná se ale o podporu pro drag & drop využitelnou pro prvky controlů v Silverlight aplikaci, ale o podporu pro přetažení externích souborů do Silverlight pluginu. Toto slouží tedy jako alternativa k OpenFileDialogu, nejčastěji uváděný příklad bývá přetažení souborů obrázků do Silverlight galerie, implementace je k dispozici např. zde.
  • Silverlight Toolkit nezávisle na Silverlight 4 přináší podporu pro drag & drop. Základní Drag & drop je zde realizován třídou Microsoft.Windows.DragDrop (v System.Windows.Controls.Toolkit.dll). Ta do jisté míry také kopíruje API zavedené ve WPF, pozor ale, že na rozdíl od podpory drag & drop v Silverlight 4 a WPF má jiný namespace (Microsoft.Windows místo System.Windows). Jako ve WPF obsahuje metodu DoDragDrop, jsou zde ale rozdíly, protože WPF metoda DoDragDrop blokuje dokud není drag operace ukončena, namísto toho je v této třídě v Silverlight díky asynchronnímu volání událost DragDropCompleted.
  • Silverlight Toolkit dále obsahuje pomocné DragDropTarget (DDT) controly, ty za pomoci třídy DragDrop rozšiřují standardní Silverlight controly o podporu drag & drop. Konkrétně se jedná o controly ListBoxDragDropTarget, TreeViewDragDropTarget, DataGridDragDropTargetDataPointSeriesDragDropTarget. Controly umožňují jednak start operace drag drop - uchopení prvků (ItemDragStarting) a jednak zpracovávají cíle drag drop operace (Drop target). Příklad použití zde.

Naše aplikace bude obsahovat v levé části strom TreeView a v pravé seznam položek v DataGridu. Stanovíme si následující požadavky:

  • Prvky z gridu v levé části bude možné operací drag & drop uchopit a přetáhnou na prvek v TreeView, tím bude možné prvky z gridu přidat jako podřazené (child) prvky prvku ve stromě.
  • V gridu bude možné označit více prvků (multiselect) a najednou je uchopit a přetahovat.
  • Při tažení nad stromem bude možné přidržením nad prvkem stromu uzel rozevřít.
  • Při tažení u horního nebo dolního okraje TreeView bude možné strom vertikálně posouvat (scrollovat).

Graficky bude aplikace vypadat takto:

DragDrop

Při tomto zadání brzy zjistíme, že “hotové” controly ze Silverlight Toolkitu DataGridDragDropTargetTreeViewDragDropTarget, které by měli být k tomuto určené, mají některé nedostatky a úplně nám nevyhovují. Největší problémy, které budeme postupně v aplikaci řešit jsou:

  1. Pokud máme v gridu označené multiselectem více řádků, při startování drag operace se tyto prvky sami odoznačí – to budeme řešit rozšířením samotného DataGrid controlu.
  2. Při použití controlu DataGridDragDropTarget pro DataGrid se někdy zahájí operace drag drop nechtěně např. při pouhém označování řádků – to budeme řešit zděděním controlu DataGridDragDropTarget a jeho úpravou.
  3. Pro naše zpracování Drag target controlu TreeView nám nevyhovuje control TreeViewDragDropTarget, protože ten v sobě obsahuje spousty funkčnosti, kterou nechceme (jako je vyznačování elementů prvků stromu při Drag Over nebo možnost vložení prvků i mezi prvky stromu, nejen na jeho listy) – drag target budeme tedy řešit vlastním controlem a control TreeViewDragDropTarget nepoužijeme.

Ad 1) Vytvoříme control ExtendedDataGrid odvozený od  DataGridu. Použitý DataGrid bude mít zapnutý multiselect nastavením SelectionMode="Extended". Pro možnosti drag drop při označení více řádku provedeme úpravu, která na událost OnMouseLeftButtonDown zpět označí dříve zapamatované označené řádky, protože ty se jinak automaticky při startování drag & drop sami odoznačí. Kód této části třídy vypadá takto:

private IList LastSelectionRemovedItems;
private bool KeyboardStateOnMouseLeftButtonDown;

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);
}

protected new virtual void OnMouseLeftButtonDown(MouseButtonEventArgs e)
{
    if (this.LastSelectionRemovedItems != null)
    {
        //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;
    }
}

Kompletní třídu ExtendedDataGrid si můžete prohlédnout zde.

Pozn.: Třída ExtendedDataGrid obsahuje opravu události OnMouseLeftButtonDown, kterou jsme si již ukázali zde.

Ad 2) Nyní se vrhneme na zajímavější část, vytvoříme třídu pro control DataGridDragTarget odvozenou ze třídy DataGridDragDropTarget z toolkitu (System.Windows.Controls.Data.Toolkit.dll). Z DataGridDragDropTarget nebudeme využívat část pro drag target (pouze source), a proto v konstruktoru nastavíme AllowDrop na false.

Nyní v controlu změníme spouštění Drag drop operace. V metodě OnItemDragStarting nebudeme volat base implementaci (čímž k odstartování drag operace nedojde), místo toho zapneme Timer (s intervalem 200 milisekund). Na jeho událost Tick zkontrolujeme, zda je stále ještě stisknuté levé tlačítko myši a pokud ano Drag Drop odstartujeme voláním base.OnItemDragStarting. Tato část třídy vypadá takto:

private DispatcherTimer DragStartTimer;
private ItemDragEventArgs DragStartEventArgs;

public DataGridDragTarget()
{
    this.DragStartTimer = new DispatcherTimer();
    this.DragStartTimer.Interval = TimeSpan.FromMilliseconds(200);
    this.DragStartTimer.Tick += new EventHandler(DragStartTimer_Tick);
}

protected override void OnItemDragStarting(ItemDragEventArgs eventArgs)
{
    this.DragStartEventArgs = eventArgs;
    this.DragStartTimer.Start();
}

private void DragStartTimer_Tick(object sender, EventArgs e)
{
    DragStartTimer.Stop();

    //Cancel drag drop if mouse is not pressed
    if (!this.IsMouseDown)
    {
        return;
    }

    //Start drag drop
    base.OnItemDragStarting(this.DragStartEventArgs);
}

Vlastnost IsMouseDown zjišťuje, zda je stisknuté levé tlačítko myši, to v Silverlight nelze “normálně” zjistit, ale využijeme k tomu následující trik: Metoda CaptureMouse vrací, zda bylo možné capture provést a jedna z nutných podmínek je právě to, že je stisknuté levé tlačítko myši. Ve vlastnosti pouze použijeme tuto návratovou hodnotu a vlastní mouse capture tedy hned zrušíme voláním ReleaseMouseCapture, kód je následující:

private bool IsMouseDown
{
    get
    {
        if (CaptureMouse())
        {
            ReleaseMouseCapture();
            return true;
        }
        return false;
    }
}

Tím máme ošetřen start drag operace, ještě ve třídě změníme vzhled položek při dragování (DragDecorator). Standartní chování zobrazuje celý řádek DataGridu, my budeme zobrazovat pouze první sloupec. Změnu provedeme také při ItemDragStarting, tentokrát ale ne jako override, ale na událost:

public DataGridDragTarget()
{
    this.ItemDragStarting += new EventHandler<ItemDragEventArgs>(DataGridDragTarget_ItemDragStarting);
}

private void DataGridDragTarget_ItemDragStarting(object sender, ItemDragEventArgs e)
{
    //Set diferent Drag Decorator
    var cardPanel = new CardPanel() { VerticalMargin = 3 };
    var itemContainers = (from item in ((SelectionCollection)e.Data).Select(selection => selection.Item)
                            let row =
                                this.DataGrid
                                    .GetVisualDescendants()
                                    .OfType<DataGridRow>()
                                    .Where(dataGridRow => dataGridRow.DataContext == item)
                                    .FirstOrDefault()
                            where row != null
                            select row).Take(10);

    foreach (DataGridRow row in itemContainers)
    {
        var cell = this.DataGrid.Columns[0].GetCellContent(row);
        cardPanel.Children.Add(new Image { Source = new WriteableBitmap(row, new TranslateTransform()), Width = cell.ActualWidth, Stretch = Stretch.None });
    }

    e.DragDecoratorContent = cardPanel;
    e.DragDecoratorContentMouseOffset = new System.Windows.Point(27, 17);
    e.Handled = true;
}

Ještě do projektu musíme přidat pomocné třídy CardPanel a EnumerableExtensions, které si půjčíme ze zdrojových souborů samotného Toolkitu.

Ad 3) Poslední částí je vytvoření vlastního controlu TreeViewDropTarget, ten bude obsahovat obsluhu pro Drag Over a Drop operace. Třida bude vytvořena jako ContentControl pro TreeView control. V jejím Load budeme registrovat události DragEnterEvent, DragLeaveEvent, DragOverEvent, DropEvent, nikoliv však ty ze Silverlight 4 (které slouží pro externí drag operaci), ale ty z toolkitu tj. z namespace Microsoft.Windows. Jejich registrace se provádí metodou AddHandler následujícím kódem:

/// <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.AddHandler(Microsoft.Windows.DragDrop.DragEnterEvent, new Microsoft.Windows.DragEventHandler(TreeView_DragEnterHandler), false);
    this.TreeView.AddHandler(Microsoft.Windows.DragDrop.DragLeaveEvent, new Microsoft.Windows.DragEventHandler(TreeView_DragLeaveHandler), false);
    this.TreeView.AddHandler(Microsoft.Windows.DragDrop.DragOverEvent, new Microsoft.Windows.DragEventHandler(TreeView_DragOverHandler), false);
    this.TreeView.AddHandler(Microsoft.Windows.DragDrop.DropEvent, new Microsoft.Windows.DragEventHandler(TreeView_DropHandler), false);
}

V metodách obsluhy těchto událostí, nyní budeme volat kontrolu zda je prvek stromu chtěný jako target drag operace (voláním události ValidateDropTarget našeho controlu), automatické otevření uzlu stromu na timer a posouvání stromu vertikálně nahoru nebo dolu. Hlavní část tohoto kódu vypadá takto:

private DispatcherTimer DragDropNodeExpandTimer;
private TreeViewItem DragDropPrevItem;

private void TreeView_DragEnterHandler(object sender, Microsoft.Windows.DragEventArgs e)
{
    this.DragDropNodeExpandTimer.Start();
}

private void TreeView_DragLeaveHandler(object sender, Microsoft.Windows.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_DragOverHandler(object sender, Microsoft.Windows.DragEventArgs e)
{
    e.Effects = Microsoft.Windows.DragDropEffects.None;
    var target = GetMouseTreeViewItem(e);
    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 = Microsoft.Windows.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;
}

A na událost Drop vyvoláme z kontrolu událost DropOnTarget:

private void TreeView_DropHandler(object sender, Microsoft.Windows.DragEventArgs e)
{
    this.ScrollTimer.Stop();

    var target = GetMouseTreeViewItem(e);
    if (target != null && e.Effects == Microsoft.Windows.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);
            }
        }
    }
}

Za povšimnutí ještě stojí pomocná metoda GetSourceItems, která z předaného Microsoft.Windows.DragEventArgs vrací seznam prvků, které se aktuálně ze zdrojového controlu přetahují (v našem případě z řádků DataGridu):

private IList<object> GetSourceItems(Microsoft.Windows.DragEventArgs e)
{
    var itemDragEventArgs = e.Data.GetData(e.Data.GetFormats()[0]) as ItemDragEventArgs;
    if (itemDragEventArgs == null)
    {
        return null;
    }

    var selection = itemDragEventArgs.Data as SelectionCollection;
    if (selection == null || selection.Count == 0)
    {
        return null;
    }

    return (from i in selection
            select i.Item).ToList().AsReadOnly();
}

MainPage

Zbývá nám ukázat použití připravených controlů v naší aplikaci. XAML kód obrazovky MainPage s TreeView a DataGridem vypadá následovně:

<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">
            <sdk:TreeView x:Name="TreeView">
                <sdk:TreeView.ItemTemplate>
                    <sdk: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>
                    </sdk:HierarchicalDataTemplate>
                </sdk:TreeView.ItemTemplate>
            </sdk:TreeView>
        </impcontrols:TreeViewDropTarget>

        <impcontrols:DataGridDragTarget Grid.Column="1">
            <impcontrols:ExtendedDataGrid x:Name="DataGrid" Grid.Column="1" SelectionMode="Extended" AutoGenerateColumns="False" IsReadOnly="True" RowDetailsVisibilityMode="Collapsed" GridLinesVisibility="None"
                                CanUserReorderColumns="False" CanUserSortColumns="False" FontSize="10.667" RowHeight="20">
                    <sdk:DataGrid.Columns>
                        <sdk:DataGridTemplateColumn Header="Name" Width="110">
                            <sdk: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>
                            </sdk:DataGridTemplateColumn.CellTemplate>
                        </sdk:DataGridTemplateColumn>
                        <sdk:DataGridTextColumn Binding="{Binding Number}" Header="Number" Width="60" />
                    </sdk:DataGrid.Columns>
            </impcontrols:ExtendedDataGrid>
        </impcontrols:DataGridDragTarget>
    </Grid>
</Grid>

V konstruktoru jsou controly naplněny kolekcemi typu ObservableCollection s testovacími daty typu Item. V příkladu si také vyzkoušíme použití události ValidateDropTarget controlu TreeViewDropTarget (povolíme drop pouze na prvkách začínající názvem Tree), a dále obsloužíme událost DropOnTarget, která se volá po ukončení drag operace. Jejich implementace vypadá následovně:

private void TreeViewDropTarget_ValidateDropTarget(object sender, IMP.Windows.Controls.TreeViewValidateDropTargetEventArgs e)
{
    e.IsValid = ((Item)e.TargetItem).Name.StartsWith("Tree", StringComparison.Ordinal);
}

private void TreeViewDropTarget_DropOnTarget(object sender, IMP.Windows.Controls.TreeViewDropEventArgs e)
{
    foreach (Item item in e.SourceItems)
    {
        ((Item)e.TargetItem).Items.Add(item);
    }
}

Tím jsem ukázal ty nejdůležitější části kódu, kompletní zdrojové soubory celého funkčního příkladu je možné stáhnout zde.

 

hodnocení článku

0 bodů / 1 hlasů       Hodnotit mohou jen registrované uživatelé.

 

Nový příspěvek

 

                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.

Nyní zakládáte pod článkem nové diskusní vlákno.
Pokud chcete reagovat na jiný příspěvek, klikněte na tlačítko "Odpovědět" u některého diskusního příspěvku.

Nyní odpovídáte na příspěvek pod článkem. Nebo chcete raději založit nové vlákno?

 

  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:
Heslo:

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback