Pull to refresh

Silverlight cookbook: drag & drop recipe

Reading time9 min
Views2.1K
В этой статье я разберу работу с drag & drop на примере вот такого приложения:

image

Идея в том, чтобы из списка всех пользователей мы могли собственноручно (используя drag & drop) рассортировать пользователей по двум группам. Причем, в белый список нельзя будет добавить пользователей с отрицательной кармой.

Но ближе к делу. Прошу под кат.


Итак, вступление.


«Из коробки» мы не можем сразу взять и начать использовать функциональность drag & drop (придется все реализовывать самостоятельно), но, чтобы облегчить нам жизнь, специально обученные парни из Silverlight Team в Microsoft уже все реализовали в Silverlight Toolkit.
Представлена эта функциональность в виде оберток для некоторых основных элементов управления (User Controls) с именем элемента управления + DragDropTarget (для ListBox это ListBoxDragDropTarget). Именно на примере ListBox'а, а точнее нескольких ListBox'ов, будет продемонстрирована работа с drag & drop.

Создадим новое Silverlight-приложение.


image

Я назвал его DragAndDropTestApp, что, собственно, значения-то вовсе и не имеет.
В следующем диалоговом окне все оставляем как есть и нажимаем ОК.

Сразу добавим ссылку на Toolkit:

image

Предполагается, что он уже скачан и установлен.

Добавим в MainPage.xaml следующие пространства имен:

xmlns:toolkit="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Toolkit"
xmlns:my="clr-namespace:DragAndDropTestApp"

* All source code (under and below) was highlighted with Source Code Highlighter.

На Toolkit. И на пространство имен, в котором лежит наше приложение.

Набросаем xaml.
Каждый из трех списков представлен следующей разметкой:
<TextBlock
      Text="Users:"
      Style="{StaticResource Header}"/>
    <toolkit:ListBoxDragDropTarget
      Grid.Row="1"
      HorizontalContentAlignment="Stretch"
      VerticalContentAlignment="Stretch">
      <ListBox>
        <ListBox.ItemTemplate>
          <DataTemplate>
            <my:UCUser />
          </DataTemplate>
        </ListBox.ItemTemplate>
      </ListBox>
    </toolkit:ListBoxDragDropTarget>

TextBox с заголовком для списка элементов (со всеми стилями я баловался в App.xaml). И, собственно, сам ListBox, обернутый в ListBoxDragDropTarget. Для отображения пользователей переопределен ItemTemplate. UCUser — мой собственный элемент управления. Внутри нет ничего сверхъестественного, смотрите сами.
image
  1. <Grid
  2.     x:Name="LayoutRoot"
  3.     Background="Transparent">
  4.     <Grid.ColumnDefinitions>
  5.       <ColumnDefinition Width="Auto" />
  6.       <ColumnDefinition Width="4*"/>
  7.       <ColumnDefinition Width="6*"/>
  8.     </Grid.ColumnDefinitions>
  9.     <Grid.RowDefinitions>
  10.       <RowDefinition Height="Auto" />
  11.       <RowDefinition Height="Auto" />
  12.     </Grid.RowDefinitions>
  13.     <Image
  14.       Source="../Images/gnome_face_devilish.png"
  15.       Width="48"
  16.       Height="48"
  17.       Margin="15, 0, 20, 0"
  18.       Grid.RowSpan="2"/>
  19.     <TextBlock
  20.       Text="Nickname:"
  21.       Grid.Column="1"/>
  22.     <TextBlock
  23.       Text="{Binding Path=NickName, Mode=OneWay}"
  24.       Grid.Column="2"
  25.       FontWeight="Bold"/>
  26.     <TextBlock
  27.       Text="Karma:"
  28.       Grid.Row="1"
  29.       Grid.Column="1" />
  30.     <TextBlock
  31.       Text="{Binding Path=Karma, Mode=OneWay}"
  32.       Grid.Row="1"
  33.       Grid.Column="2"
  34.       FontWeight="Bold" />
  35.   </Grid>

Задан источник (Source) для картинки и прописана привязка к данным (Binding).
Кстати, для самого пользователя есть отдельный класс. Вот его представление:
public class User
  {
    public string NickName { get; set; }
    public int Karma { get; set; }
  }

Теперь создадим по коллекции для каждого списка и сымитируем загрузку пользователей:
  1. public partial class MainPage : UserControl
  2.   {
  3.     public MainPage()
  4.     {
  5.       InitializeComponent();
  6.       this.DataContext = this;
  7.       InitializeCollections();
  8.       LoadUsers();
  9.     }
  10.  
  11.     //Объявление коллекций
  12.     public ObservableCollection<User> Users { get; set; }
  13.     public ObservableCollection<User> WhiteList { get; set; }
  14.     public ObservableCollection<User> BlackList { get; set; }
  15.  
  16.     //Инициализация коллекций
  17.     private void InitializeCollections()
  18.     {
  19.       Users = new ObservableCollection<User>();
  20.       WhiteList = new ObservableCollection<User>();
  21.       BlackList = new ObservableCollection<User>();
  22.     }
  23.  
  24.     //Метод, имитирующий загрузку данных в коллекцию Users
  25.     private void LoadUsers()
  26.     {
  27.       var r = new Random();
  28.  
  29.       //Добавление 20 пользователей со случайным значением кармы.
  30.       for (int i = 1; i < 21; i++)
  31.       {
  32.         var newUser = new User
  33.         {
  34.           NickName = "Username" + i,
  35.           Karma = r.Next(200) - 100
  36.         };
  37.  
  38.         Users.Add(newUser);
  39.       }
  40.     }
  41.   }

В качестве DataContext для MainPage установим сам MainPage. Сделал я это для того, чтобы можно было легко привязаться к данным. Выглядит это так:
<ListBox ItemsSource="{Binding Path=Users, Mode=TwoWay}">

Для работы Drag & Drop не хватает последнего штриха:
AllowDrop="True"

Установив этому свойству значение true, мы разрешаем элементу принимать данные с использованием drag & drop. Причем, установить его можно как у ListBox'а, так и у ListBoxDragDropTarget'а.

Вот что получилось:

image

Сейчас можно перетащить пользователя с отрицательной кармой в любой из списков.
По плану, нужно запретить перетаскивать такового в White List.

Сначала разберемся с событиями, которые генерируются при Drag-n-Drop'е:

image

DragEnter происходит, когда перетаскиваемый контейнер пересекает границу элемента назначения (срабатывает до события DragOver).
DragLeave — перетаскиваемый контейнер вытащили из элемента-источника.
DragOver — срабатывает, когда перетаскиваемый контейнер перемещается над элементом назначения.
Drop — перетаскиваемый контейнер был «дропнут» в элемент назначения.

Добавим обработчик события DragEnter для белого списка.
  1.     <toolkit:ListBoxDragDropTarget
  2.       AllowDrop="True"
  3.       Grid.Row="1"
  4.       Grid.Column="1"
  5.       HorizontalContentAlignment="Stretch"
  6.       VerticalContentAlignment="Stretch"
  7.       DragEnter="ListBoxDragDropTarget_DragEnter">
  8.       <ListBox
  9.         ItemsSource="{Binding Path=WhiteList, Mode=TwoWay}"
  10.         x:Name="lbWhiteList">
  11.         <ListBox.ItemTemplate>
  12.           <DataTemplate>
  13.             <my:UCUser />
  14.           </DataTemplate>
  15.         </ListBox.ItemTemplate>
  16.       </ListBox>
  17.     </toolkit:ListBoxDragDropTarget>

И присвоим ListBox'у имя, оно нам пригодится.

Сам обработчик:
  1. private void ListBoxDragDropTarget_DragEnter(object sender, Microsoft.Windows.DragEventArgs e)
  2.     {
  3.       //Узнали формат
  4.       var dataFormat = e.Data.GetFormats()[0];
  5.  
  6.       //Получили объект в формате ItemDragEventArgs
  7.       var dragEventArgs = e.Data.GetData(dataFormat) as ItemDragEventArgs;
  8.  
  9.       //Получили коллекцию перетаскиваемых элементов
  10.       SelectionCollection sc = dragEventArgs.Data as SelectionCollection;
  11.       
  12.       //Проверка каждого элемента коллекции
  13.       foreach (var item in sc)
  14.       {
  15.         //Если хоть один из перетаскиваемых элементов (пользователей) имеет отрицательную карму
  16.         //запрещаем добавлять эти элементы в белый список.
  17.         if ((item.Item as User).Karma < 0)
  18.         {
  19.           lbWhiteList.AllowDrop = false;
  20.           break;
  21.         }
  22.       }
  23.     }

DragEventArgs.Data возвращает объект IDataObject, который содержит в себе данные, связанные с соответствующим drag event'ом. Но напрямую к данным обратиться не получится, они помечены как Non-Public:

image

Для получения этих данных используется метод GetData(), который возвращает данные в формате, переданном в качестве параметра (формат можно задать строкой или типом). А узнать формат можно с помощью метода GetFormats().

В результате получился экземпляр класса ItemDragEventArgs. Этот класс содержит информацию с описанием события перетаскивания на UIElement. У него есть свойство Data, которое уже напрямую относится к перетаскиваемому контейнеру. Приводим этот объект к SelectionCollection.
Коллекция здесь используется потому что мы можем перетаскивать сразу несколько элементов
(проверить это можно очень просто, у ListBox'a свойство SelectionMode устанавливаем в Multiple).
Осталось пробежать по всей коллекции и проверить есть ли среди перетаскиваемых пользователей ребята с отрицательной кармой. Если есть, то просто запрещаем их «дропать» в белый список.

Вернуть AllowDrop в прежнее значение (в True) можно в обработчике события MouseLeave.

И последнее, для реализации возможности сортировать элементы в пределах одного списка c помощью drag-n-drop'а достаточно просто переопределить шаблон для ItemsPanel:
<ListBox.ItemsPanel>
  <ItemsPanelTemplate>
    <StackPanel />
  </ItemsPanelTemplate>
</ListBox.ItemsPanel>


Скачать проект можно здесь.
Tags:
Hubs:
Total votes 9: ↑3 and ↓6-3
Comments1

Articles