• Microsoft .NET
  • WPF и Silverlight
  • Привязка элемента управления DataGrid (Silverlight) к динамическим данным при помощи IDictionary

Привязка элемента управления DataGrid (Silverlight) к динамическим данным при помощи IDictionary

В данной статье мы рассмотрим метод привязки элемента Silverlight DataGrid к динамическим данным, структура которых неизвестна во время компиляции. Данный метод был расширен, и включает в себя повестку об изменениях, тем самым элемент DataGrid может быть отредактирован.

Используя Silverlight, вы с легкостью сможете привязать данные к элементу DataGrid - процесс довольно прост. Имея источник данных в формате XML (datasource), вы можете запросто преобразовать ваши данные при помощи Linq to SQL в анонимный тип, а затем привязать полученный набор к табличной сетке - все можно выполнить при помощи нескольких простых строк кода. Но что делать, если данные, которые вы хотите привязать к вашей сетке, динамичны? То есть во время компиляции вы не знаете, сколько колонок требуется, или что представляет собой содержимое. Это стандартная проблема, с которой всякий раз сталкиваются пользователи Silverlight.

Наиболее приемлемым решением данной проблемы будет создание словаря для каждой строки вашего элемента DataGrid и привязка ваших колонок к данным при помощи строкового индекса словаря:

Binding="{Binding Path=[Name]}”

К сожалению, синтаксис PropertyPath, используемый для привязки, не понимает индексирование, что делает невозможным привязку к словарю.

Существует такое находчивое решение данной проблемы - путем динамического создания нового типа (Type), основанного на значениях, присутствующих в словаре. То есть если словарь содержит такие ключи, как “Name” или “Age”, то тип (Type) будет сгенерирован со свойствами Name и Age. Далее мы продемонстрируем вам альтернативный метод, который не использует никакого промежуточного языка и черной магии!

Мы начнем с простого примера класса, который может быть использован для хранения ‘динамических’ данных на обработку в нашем элементе DataGrid:

public class Row
{
  private Dictionary<string, object> _data = new Dictionary<string, object>();
  public object this [string index]
  {
    get { return _data[index]; }
    set { _data[index] = value; }
  }
}

Мы можем заполнить данными набор этих объектов и связать их с элементом DataGrid следующим образом:

Random rand = new Random();
 
var rows = new ObservableCollection<Row>();
for (int i = 0; i < 200; i++)
{
  Row row = new Row();
  row["Forename"] = s_names[rand.Next(s_names.Length)];
  row["Surname"] = s_surnames[rand.Next(s_surnames.Length)];
  row["Age"] = rand.Next(40) + 10;
  row["Shoesize"] = rand.Next(10) + 5;
  rows.Add(row);
}
 
_dataGrid.ItemsSource = rows;

Тем не менее, как уже упоминалось ранее, мы не можете создать путь привязки, который осуществляет доступ к нашим строковым индексам (Rows). Так как же привязать колонки к данным?

Классическим  решением .NET является создание специализированного деск(п)риптора свойства. При осуществлении привязки доступ к свойствам объекта не осуществляется напрямую - доступ будет выполнен при помощи связанного дескриптора свойств. Специализированный дескриптор может быть предоставлен для нашего класса Row, к примеру, при помощи интерфейса ICustomTypeDescriptor, который раскрывает свойства, доступ к которым привлекает использование нашего индексатора. Вот таким образом .NET DataRowView раскрывает свои свойства. К сожалению, есть некоторая загвоздка - Silverlight не включает в себя требуемые интерфейсы для создания специализированных свойств.

Простым решением данной проблемы является использование такой привязки, которая свяжет каждую строку напрямую с каждым элементом Row, вместо связывания с конкретным свойством элемента, а затем используется преобразователь значений для доступа к индексаторам и извлечения требуемого значения. Вот пример выполнения данных действий:

<data:DataGrid Name="_dataGrid" AutoGenerateColumns="False" Height="300" IsReadOnly="False">
  <data:DataGrid.Columns>
    <data:DataGridTextColumn Header="Forename" Binding="{Binding Converter={StaticResource RowIndexConverter}, ConverterParameter=Forename}"/>
    <data:DataGridTextColumn Header="Surname" Binding="{Binding Converter={StaticResource RowIndexConverter}, ConverterParameter=Surname}"/>
    <data:DataGridTextColumn Header="Age" Binding="{Binding Converter={StaticResource RowIndexConverter}, ConverterParameter=Age}"/>
    <data:DataGridTextColumn Header="Shoesize" Binding="{Binding Converter={StaticResource RowIndexConverter}, ConverterParameter=Shoesize}"/>
  </data:DataGrid.Columns>
</data:DataGrid>

А вот преобразователь значений:

public class RowIndexConverter : IValueConverter
{
  public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
  {
    Row row = value as Row;
    string index = parameter as string;
    return row[index];
  }
 
  public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
  {
    throw new NotImplementedException();
  }
}

В указанном выше XAML выражение привязки не обладает указанным путем (Path), поэтому источником привязки является экземпляр Row, а не свойство Row. Преобразователь значений RowIndexConverter просто использует предоставленный ConverterParameter для каждой колонки, чтобы получить доступ к индексатору Row и извлечь конкретное  значение. Это очень хорошо работает, и мы можем увидеть наши данные в сетке:


Тем не менее, существует одна проблема - если вы щелкнете по заголовку колонки, то таблица не будет отсортирована. Колонки элемента DataGrid обладают свойствами CanUserSort и SortMemberPath, тем не менее установка данных свойств не поможет нам, поскольку DataGrid ожидает от привязываемого свойства наличие определенного имени, которое оно, конечно же, не имеет. Для разрешения (без того, чтобы пересортировывать динамически созданные типы) нам необходимо более глубоко изучить способ привязки данных элементов DataGrid к данным.

Многое можно почерпнуть из документации к ICollectionView, где говорится, что элемент управления DataGrid использует данный интерфейс для осуществления доступа к указанной функциональности в источнике данных, назначенном его свойству ItemsSource. Если ItemsSource реализует IList, но не реализует ICollectionView, то элемент DataGrid оборачивает ItemsSource во внутреннюю реализацию ICollectionView.

Интерфейс ICollectionView отвечает за фильтрацию, сортировку и группировку данных, привязанных к элементу DataGrid. WPF также обладает интерфейсом ICollectionView, и о нем вы можете прочитать в различных источниках интернета. Тем не менее, в то время как WPF оборачивает DataContext в представление данных, которое разделяется между различными элементами управления, оказывается, Silverlight запрещает его использование в DataGrid. Это вызывает проблемы в случае, если, к примеру, вы хотите синхронизировать текущий элемент между элементами управления.

Итак, за сортировку данных отвечает ICollectionView. Внутренняя реализация данного интерфейса, который создает DataGrid, будет ожидать от нашего объекта экспозиции свойств привязки, что объясняет причину того, почему это не работает. Тем не менее, если мы предоставим наш собственный интерфейс ICollectionView, то мы можем получить контроль над сортировкой и самиможем реализовать ее при помощи осуществления доступа к значениям ‘property’ посредством строкового индексатора Row.

Интерфейс ICollectionView обладает множеством методов, событий и свойств. Данный интерфейс реализуем, и сделать это можно при помощи класса, который наследует ObservableCollection для того, чтобы привязать элемент DataGrid к набору, где перелистываются данные с сервера, поэтому сортировка должна быть выполнена со стороны сервера. Это предоставляет нам все, что необходимо - коллекцию, которая может сама обработать сортировку. Единственным обязательным изменением тут является метод ICollectiomView.Refresh(), который отвечает за обновление представления данных после того, как был изменен SortDescriptions.

Далее вам предоставляется реализация данного метода:

public class SortableCollectionView : ObservableCollection<Row>, ICollectionView
{
 
  ...
  public void Refresh()
  {
      IEnumerable<Row> rows = this;
      IOrderedEnumerable<Row> orderedRows = null;
 
      // использование OrderBy и ThenBy LINQ методы для
      // сортировки данных
      bool firstSort = true;
      for (int sortIndex = 0; sortIndex < _sort.Count; sortIndex++)
      {
          SortDescription sort = _sort[sortIndex];
          Func<Row, object> function = row => row[sort.PropertyName];
          if (firstSort)
          {
              orderedRows = sort.Direction == ListSortDirection.Ascending ?
                  rows.OrderBy(function) : rows.OrderByDescending(function);
 
              firstSort = false;
          }
          else
          {
              orderedRows = sort.Direction == ListSortDirection.Ascending ?
                  orderedRows.ThenBy(function) : orderedRows.ThenByDescending(function);
          }
      }
 
      _suppressCollectionChanged = true;
 
      // изменение последовательность набора на основании предыдущего результата
      int index = 0;
      foreach (var row in orderedRows)
      {
          this[index++] = row;
      }
 
      _suppressCollectionChanged = false;
 
      // вызов требуемое сообщение
      this.OnCollectionChanged(
          new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
   }
 
  ...
}

Когда пользователь нажимает на заголовок колонки сетки, то он изменяет привязки ICollectionView.SortDescription таким образом, чтобы они отражали данные изменения. Реализация ICollectiomView вызывает метод Refresh тогда, когда изменяется коллекция ICollectionView.SortDescription. В указанной выше реализации мы использовали методы OrderBy и ThenBy из LINQ для сортировки данных. Оказывается OrderBy является методом, работающим с IEnumerable, в то время как ThenBy является методом над IOrderedEnumerable, что объясняет веселую логику, включающую ‘firstSort’. Как только будет произведена сортировка, нижележащая коллекция будет соответственно отсортирована. Единственной хитростью является то, что мы используем поле типа boolean, _suppressCollectionChanged для того, чтобы скрыть множество событий CollectionChanged, которые будут вызваны ObservableCollection во время данного процесса. Наконец, мы вызываем NotifyCollectionChanged, что в результате выполнит обновление DataGrid и отображение желаемого порядка элементов.

Суммируя все вместе, мы просто заполняем наш SortableCollectionView и связываем его с DataGrid, изменяя XAML таким образом, чтобы он явно оповещал табличную сетку о том, что она может отсортировать данные и по какому свойству будет осуществлять сортировку каждая колонка (обычно это подразумевается из Binding Path):

<data:DataGrid Name="_dataGrid" AutoGenerateColumns="False"  IsReadOnly="False" Margin="5">
    <data:DataGrid.Columns>
        <data:DataGridTextColumn Header="Forename" CanUserSort="True" SortMemberPath="Forename"
                                  Binding="{Binding Converter={StaticResource RowIndexConverter},
                                    ConverterParameter=Forename}"/>
        <data:DataGridTextColumn Header="Surname" CanUserSort="True" SortMemberPath="Surname"
                                 Binding="{Binding Converter={StaticResource RowIndexConverter},
                                    ConverterParameter=Surname}"/>
        <data:DataGridTextColumn Header="Age" CanUserSort="True" SortMemberPath="Age"
                                 Binding="{Binding Converter={StaticResource RowIndexConverter},
                                    ConverterParameter=Age}"/>
        <data:DataGridTextColumn Header="Shoesize" CanUserSort="True" SortMemberPath="Shoesize"
                                 Binding="{Binding Converter={StaticResource RowIndexConverter},
                                    ConverterParameter=Shoesize}"/>               
    </data:DataGrid.Columns>
</data:DataGrid>

Вот табличная сетка в действии:

Здесь вы можете загрузить полноценный код решения: silverlightdynamicbinding.zip .

Colin Eberhardt