суббота, 4 февраля 2012 г.

Фильтрация элементов дерева

Задача простая: есть двухуровневый список (первоначально грузится из базы), надо построить по нему дерево, поддерживающее фильтрацию. Т.е. есть поле ввода текста, как только появляется текст, во втором уровне вложенности остаются только элементы в отображаемом тексте которых есть соответствующий текст. Первый уровень не фильтруется.
Порешаем?


Для отображения элементов будем использовать вот такой класс:
public class Node
{
  
public string Name { get; set; }
   public CollectionViewSource Items { get; set; }
}
Заполним его данными:
List<Node> roots = new List<Node>();
roots.Add(
new Node()
{
  Name =
"aaa",
  Items = new CollectionViewSource()
  {
  Source =
new Node[]
  {
   
new Node() { Name = "11"},
    new Node() { Name = "21"},
    new Node() { Name = "31"},
  }
}
});
roots.Add(
new Node()
{
  Name =
"bbb",
  Items = new CollectionViewSource()
  {
  Source =
new Node[]
  {
  
new Node() { Name = "12"},
   new Node() { Name = "22"},
   new Node() { Name = "32"},
  }
}
});
roots.Add(
new Node()
{
  Name =
"ccc",
  Items = new CollectionViewSource()
  {
  Source =
new Node[]
  {
   
new Node() { Name = "13"},
    new Node() { Name = "23"},
    new Node() { Name = "33"},
  }
}
});


Ну и собственно форма для демонстрации:
<StackPanel>
<TextBox x:Name="tbFilter" />
<TreeView x:Name="tvMain">
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Items.View}" >
<HierarchicalDataTemplate.ItemTemplate>
<HierarchicalDataTemplate>
<TextBlock Text="{Binding Name}" />
HierarchicalDataTemplate>
HierarchicalDataTemplate.ItemTemplate>
<TextBlock Text="{Binding Name}" />
HierarchicalDataTemplate>
TreeView.ItemTemplate>
TreeView>
StackPanel>

Вон уже сколько кода написали, а для поддержки фильтрации в изменение события TextChanged у нашего поля ввода надо всего лишь написать вот такую штуку:
private void tbFilter_TextChanged(object sender, TextChangedEventArgs e)
{
  foreach (var root in tvMain.Items.Cast<Node>())
  {
    root.Items.Filter -= Items_Filter;
    root.Items.Filter += Items_Filter;
  }
}

Ну и метод фильтрации соответственно:
void Items_Filter(object sender, FilterEventArgs e)
{
  Node filteredNode = e.Item as Node;
  e.Accepted = filteredNode.Name.ToLower().Contains(tbFilter.Text.ToLower());
}

Все. Работать будет вот так:



Вот с фильтром:
Ну или так:

Работающий пример можно взять здесь.

7 комментариев:

  1. Блин, сколько маленьких хитростей в таком небольшом коде. Без кружки кофе не разберешься)

    ОтветитьУдалить
  2. Да вроме кроме корявой инициализации исходного массива, все остальное нормально? Или нет?

    ОтветитьУдалить
    Ответы
    1. Этот комментарий был удален автором.

      Удалить
  3. Инициализация, вроде, инстинктивно понятна. Не совсем понятно, почему при каждом TextChanged нужно отписывать и подписывать обработчик Items_Filter?
    Еще вопрос: в разметке:
    [HierarchicalDataTemplate ItemsSource="{Binding Items.View}" >
    [HierarchicalDataTemplate.ItemTemplate>
    [HierarchicalDataTemplate>
    [TextBlock Text="{Binding Name}" />
    [/HierarchicalDataTemplate>
    [/HierarchicalDataTemplate.ItemTemplate>
    [TextBlock Text="{Binding Name}" />
    [/HierarchicalDataTemplate]

    верхнему шаблону ItemsSource задается как Items.View. Items.View я так понимаю, это какой-то тип привязки, а не коллекция. Если это так, то что такое View в привязываемом List roots? И почему шаблон для второго уровня дерева обходится без такой же строчки ItemsSource, берет ее у родителя?
    Пардоньте, если вопросы нубские)

    ОтветитьУдалить
    Ответы
    1. А еще blogspot съедает пробелы и разметку)

      Удалить
    2. Отписывание/подписывание: Проблема в том, что данный для фильтра являются внешними по отношению к CollectionViewSource, поэтому о том, что надо применить фильтр снова, он (источник) не знает. Я нашел такой способ применить фильтр заново.
      Про View: Дело в том, что Binding по умолчанию смотрит на объект который лежит у текущего компонента в DataContext. В данном же случае, у текущего элемента, для которого мы и описываем шаблон, в DataContext лежит объект типа Node. У этого объекта есть поле Items (типа CollectionViewSource), а .View говорит, что у объекта типа CollectionViewSource есть свойство View, из которого мы и пытаемся взять деток. Собственно в этом свойсте CollectionViewSource и выдает отфильтрованную/сгрупперованную/отсортированную часть коллекции переданной в конструктор.
      У дочернего шаблона нет ItemsSource, в связи с тем, что дерево двух уровневое. Соответственно элементам второго уровня источник дочерних элементов не нужен.
      Доступно? Если нет, то что? Я попробую подробнее объяснить...

      Удалить