Показаны сообщения с ярлыком DataGrid. Показать все сообщения
Показаны сообщения с ярлыком DataGrid. Показать все сообщения

вторник, 2 августа 2011 г.

Сохранение порядка сортировки выборки в Linq

На MSDN’овском форуме задали вопрос о сохранении порядка сортировки выборки при использовании Linq. Задача достаточно типовая, поэтому решил написать небольшой мануал, как это сделать используя Dynamic Linq. Для упрощения пусть данные отображается у нас в DataGrid’e и сортируются нажатием мышкой на заголовок столбцов.

Dynamic Linq

Библиотека Dynamic Linq  входит в набор примеров кода для Visual Studio 2008 (C#, VB). После выкачивания можно найти исходный код, документацию и пример использования в папке \LinqSamples\DynamicQuery.

Проект с примером использования отлично собирается как под .Net Framework 3.5, так и под 4.0.  Сам код библиотеки вынесен в namespace System.Linq.Dynamic и находится в файле Dynamic.cs. Для использования можно скопировать код к себе в проект или создать отдельный проект с библиотекой.

Использование

Итак у нас есть форма, на ней DataGrid с гордым именем dataGrid1.

Простой класс:

class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string NickName { get; set; }
    public DateTime BirthDate { get; set; }
}

При инициализации формы подсовываем Grid’у в качестве источника данных массив Person:

var persons = new Person[] 
{
    new Person{FirstName = "Сидр", LastName = "Сидоров", NickName = "sid", BirthDate = DateTime.Today.AddYears(-22)},
    new Person{FirstName = "Петр", LastName = "Петров", NickName = "pit", BirthDate = DateTime.Today.AddYears(-21)},
    new Person{FirstName = "Иван", LastName = "Иванов", NickName = "ian", BirthDate = DateTime.Today.AddYears(-20)},
};
this.dataGrid1.ItemsSource = persons;

Данные отображаются и сортируются нажатием на заголовки колонок, но порядок сортировки не сохраняется между сеансами.

Итак, приступим.

Первое, что нам нужно сделать, это перенести код библиотеки Dynamic Linq  к себе в проект. Это можно сделать двумя способами:

  1. Создать класс и скопировать код из полученного ранее проекта;
  2. Сразу добавить файл в себе в проект (в Solution Explorer’e щелкнуть право кнопкой на проект, и выбрать Add->Existing Item).

У DataGrid’а добавляем в обработчик события Sorting код, который будет сохранять поле и порядок сортировки в конфигурационном файле

private void dataGrid1_Sorting(object sender, DataGridSortingEventArgs e)
{
    IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForAssembly();
    using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream("LastSort.txt", FileMode.Create, store))
    {
        using (StreamWriter writer = new StreamWriter(stream))
        {
            writer.WriteLine(String.Format("{0} {1}", e.Column.SortMemberPath,
                (e.Column.SortDirection != ListSortDirection.Ascending) ? ListSortDirection.Ascending : ListSortDirection.Descending));
        }
    }
}

Небольшое пояснение по поводу IsolatedStorage
IsolatedStorage – это механизм позволяющий сохранять данные в “виртуальные” папки в зависимости от выбранной области ограничения видимости. Использованная функция GetUserStoreForAssembly() возвращает нам “путь” который уникален для пользователя  и Assembly, т.е. другие приложения и пользователи не будут иметь доступ к файлу LastSort.txt в котором хранятся данные об используемом порядке сортировки.

Небольшое пояснение по поводу (e.Column.SortDirection != ListSortDirection.Ascending) ? ListSortDirection.Ascending : ListSortDirection.Descending
Дело в том, что Column.SortDirection является Nullable, то есть кроме ListSortDirection.Ascending и ListSortDirection.Descending принимает еще и значение NULL, которое и является значением по умолчанию. Помимо этого в  DataGridSortingEventArgs e содержится предыдущее состояние сортировки, поэтому приходится использовать такую конструкцию.

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

private void Window_Closing(object sender, CancelEventArgs e)
{
    foreach (var c in this.dataGrid1.Columns)
    {
        if (c.SortDirection.HasValue)
        {
            IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForAssembly();
            using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream("LastSort.txt", FileMode.Create, store))
            {
                using (StreamWriter writer = new StreamWriter(stream))
                {
                    writer.WriteLine(String.Format("{0} {1}", c.SortMemberPath, c.SortDirection));
                }
            }
            break;
        }
    }
}

Пишем функцию которая выдергивает значение из файла в изолированном хранилище

private string GetLastSort()
{
    IsolatedStorageFile store = IsolatedStorageFile.GetUserStoreForAssembly();
    using (IsolatedStorageFileStream stream = new IsolatedStorageFileStream("LastSort.txt", FileMode.OpenOrCreate, store))
    {
        using (StreamReader reader = new StreamReader(stream))
        {
            return reader.ReadLine() ?? "NickName Descending";
        }
    }
}

После этого меняем подключение данных для DataGrid’а на

this.dataGrid1.ItemsSource = persons.AsQueryable<Person>().OrderBy(GetLastSort()).Select("new (FirstName, LastName, NickName, BirthDate)");

понедельник, 21 марта 2011 г.

WPF DataGrid и раскраска (выделение цветом) ячеек и строк (HOWTO)

Иногда возникает необходимость выделить цветом какие-нибудь данные в DataGrid’е. Давайте рассмотрим, как это можно сделать в WPF.
Для примера создадим класс Person
public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}

Пишем небольшую заполнялку тестовых данных:
private List<Person> GenerateSampleData()
{
    Random r = new Random();
    List<Person> personList = new List<Person>();
    for (int i = 1; i <= 100; i++)
    {
        personList.Add(new Person { Id = i, Name = String.Format("Person Name {0}", i), Age = r.Next(60) });
    }
    return personList;
}

Кидаем на поверхность окна DataGrid и в конструкторе формы заполняем данными:
this.dataGrid1.ItemsSource = GenerateSampleData();

И получаем вот такую милую/унылую таблицу.


Допустим, у нас стоит задача выделить цветом те “персоны”, возраст которых не достиг 16 лет.


1.1. В определение окна добавляем ссылку на пространство имен текущего проекта

xmlns:local="clr-namespace:DataGridColor"
 

1.2. Создаем конвертер, который будет преобразовывать возраст в кисть, которой будем закрашивать фон
 
class AgeToColorConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // Все проверки для краткости выкинул
        return (int)value <= 16 ? 
            new SolidColorBrush(Colors.OrangeRed)
            : new SolidColorBrush(Colors.White);
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new Exception("The method or operation is not implemented.");
    }
}


1.3. В ресурсах окна определяем ключ конвертера
<local:AgeToColorConverter x:key="AgeToColorConverter">

1.4. И там же, в ресурсах окна, объявляем стиль, TargetType у которого будет DataGridRow.

<Style TargetType="{x:Type DataGridRow}">
    <Setter Property="Background" Value="{Binding Age, Converter={StaticResource AgeToColorConverter}}" />
</Style>

В результате получаем вот такую полосатую таблицу:



А что если нужно выделять цветом не всю строку, а только отдельные ячейки в колонке. Для примера, нужно выделить ячейки с идентификатором меньше либо равно 16.

Эта задача решается похожим способом.

2.1. Добавляем в проект еще один конвертер
class IdToColorConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        // Все проверки для краткости выкинул
        return (int)value <= 16 ?
            new SolidColorBrush(Colors.Yellow)
            : new SolidColorBrush(Colors.Green);
    }
    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        throw new Exception("The method or operation is not implemented.");
    }
}

2.2 Присваивам ключ
<local:IdToColorConverter x:Key="IdToColorConverter" />

2.3. Добавляем в ресурсы окна еще один стиль, TargetType’ом у которого является  DataGridCell. Так же стилю присваиваем ключ посредством указания x:Key="IdStyle", иначе стиль будет применен для всех ячеек во всех таблицах (а ведь нам этого не нужно).
<Style x:Key="IdStyle" TargetType="{x:Type DataGridCell}">
<Setter Property="Background" Value="{Binding Id, Converter={StaticResource IdToColorConverter}}" />
</Style> 


2.4. И для колонки, в которой хотим использовать раскраску отличную от всей строки указываем CellStyle:

<DataGridTextColumn Header="ID" Width="50" 
                    Binding="{Binding Id}" 
                    CellStyle="{StaticResource IdStyle}"/>


2.5. Если раздражает, что цвет Border’a ячейки теперь берется из цвета Background’а строки,  модифицируем стиль для ячейки устанавливая закраску Border’a белым цветом
<Style x:Key="IdStyle" TargetType="{x:Type DataGridCell}">
    <Setter Property="Background" Value="{Binding Id, Converter={StaticResource IdToColorConverter}}" />
    <Setter Property="BorderBrush"  Value="White" />
</Style>






Собственно, чтобы привлечь внимание к определенной ячейке не обязательно делать из DataGrid’а приведенного попугая, достаточно подкрашивать Border присваивая BorderBrush’у акцентирующую кисть.

Если есть вопросы – задавайте, будем разбираться вместе.

UPDATE: Если нужно раскрашивать строчки в зависимости от значений нескольких полей в таблице, то следует использовать MultiBinding и реализовывать интерфейс IMultiValueConverter. Для примера реализован вариант запрошенный io, когда нужно выделить строку, если идентификатор меньше 10 или возраст меньше 16.
Пример использования MultiBinding'а:
<Style TargetType="{x:Type DataGridRow}">
  <Setter Property="Background">
    <Setter.Value>
      <MultiBinding Converter="{StaticResource MultiBindingConverter}">
        <Binding Path="Id"/>
        <Binding Path="Age"/>
      </MultiBinding>
    </Setter.Value>
  </Setter>
</Style>

Пример реализации IMultiValueConverter'а:
    class MultiBindingConverter:IMultiValueConverter

    {
        public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            return ((int)values[0] <10 || (int)values[1]<16) ? 
                new SolidColorBrush(Colors.OrangeRed)
                : new SolidColorBrush(Colors.White);
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

PS. Выложил исходники.