Фильтрация иерархического объекта, отображаемого с помощью вложенных шаблонов данных xaml

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

У меня есть ObservableCollection<Foo> Foos, который я показываю в XAML.

Допустим, Foo выглядит так:

class Foo
{
    public ObservableCollection<Bar> Bars;
}

class Bar
{
    public ObservableCollection<Qux> Quxes;
}

Я показываю Foos со следующим xaml:

<Grid>
    <Grid.Resources>
        <CollectionViewSource x:Key="MyCVS" Source="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type ListView}}, Path=DataContext.UnifiedSymbols}" Filter="MyCVS_Filter" />

        <DataTemplate x:Key="NestedTabHeaderTemplate">
            <TextBlock Text="{Binding Path=Name}"/>
        </DataTemplate>
        <DataTemplate x:Key="NestedTabContentTemplate">
            <ListBox ItemsSource="{Binding Path=Quxes}" DisplayMemberPath="Name"/>
        </DataTemplate>

        <DataTemplate x:Key="TopLevelTabHeaderTemplate">
            <TextBlock Text="{Binding Path=Name}"/>
        </DataTemplate>
        <DataTemplate x:Key="TopLevelTabContentTemplate">
            <TabControl ItemsSource="{Binding Path=Bars}"
                        ItemTemplate="{StaticResource NestedTabHeaderTemplate}" 
                        ContentTemplate="{StaticResource NestedTabContentTemplate}"
                        />
        </DataTemplate>
    </Grid.Resources>

    <TabControl ItemSource="{Binding correct binding for my control's collection of Foos}"
                ItemTemplate="{StaticResource TopLevelTabHeaderTemplate}" 
                ContentTemplate="{StaticResource TopLevelTabContentTemplate}"
                            x:Name="tabControl"
                />
</Grid>

Чтобы выразить это словами, есть элемент управления вкладками, с вкладкой для каждого Foo. Каждый Foo представляет собой элемент управления вкладками, и каждый элемент Bar находится на отдельной вкладке. Каждый бар содержит список своих Quxes.

or:

 ______ ______ ______  
| Foo1 | Foo2 | Foo3 |  
|______ ______       |  
| Bar1 | Bar2 |______|  
| | qux1            ||  
| | qux2            ||  
| | qux3            ||  
---------------------- 

У меня также есть TextBox, который я хотел бы использовать для фильтрации этой разбивки. Когда я ввожу текст в текстовое поле, я хочу фильтровать quxes, чтобы те, которые не содержат текста, не были видны. В идеале Bar вкладок также должны быть скрыты, если на них нет видимых quxes, и Foo вкладок скрыты, если на них нет видимых Bar вкладок

Я рассмотрел два подхода:

Подход 1, сброс свойства Filter в соответствующем CollectionViewSources.

В событии TextChanged моего текстового поля я перебираю запрос Foo о соответствующем (статическом) TabControl CollectionViewSource:

foreach(Foo foo in tabControl.Items)
{
    var tabItem = tabControl.ItemContainerGenerator.ContainerFromItem(foo);    // This is always of type TabItem
    // How do I get the TabControl that will belong to each of Foo's Bar's?
}

Подход 2, объявите ItemSource ListView в CollectionViewSource

Я попытался настроить фильтр через xaml, изменив эту строку:

<ListBox ItemsSource="{Binding Path=Quxes}" DisplayMemberPath="Name">

к этому,

<CollectionViewSource x:Key="MyCVS" Source="?????" Filter="MyCVS_Filter" />
...
<ListBox ItemsSource="{Binding Source={StaticResource MyCVS}}" DisplayMemberPath="Name">

Я пробовал несколько вещей, где у меня есть "?????" но я не могу правильно привязаться к контексту данных ListBox и соответствующему члену Quxes. Ничто из того, что я пытаюсь сделать, не приводит к отображению quxs, и я не получаю никаких ошибок на консоли. Даже если бы я мог заставить этот подход работать, я не уверен, как бы я повторно активировал этот фильтр, когда текст в поле поиска изменился.

Любые советы или направления будут оценены.


person luke    schedule 23.07.2010    source источник


Ответы (3)


Изменить

Наконец-то у меня это работает с вашими требованиями.

Вот ссылка на обновленный проект.


(отредактировано Люком)

Это (отличное) решение, к которому я пришел, поэтому я собираюсь извлечь важные части и фактически сделать их частью поста здесь:

Ключевая часть xaml в итоге выглядит так:

<CollectionViewSource x:Key="FooCVS" x:Name="_fooCVS" Source="{Binding Foos, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type WpfApplication1:MainWindow}}}" Filter="_fooCVS_Filter"/>
<CollectionViewSource x:Key="BarCVS" x:Name="_barCVS" Source="{Binding Bars, Source={StaticResource FooCVS}}" Filter="_barCVS_Filter"/>
<CollectionViewSource x:Key="QuxCVS" x:Name="_quxCVS" Source="{Binding Quxs, Source={StaticResource BarCVS}}"  Filter="_quxCVS_Filter"/>

Я установил соответствующий элемент управления для каждого из этих представлений как элемент управления ItemSource. Магия заключается в привязке каждого CVS. Каждый CVS получает контекст данных для элемента управления/шаблона элемента управления, в котором появляется, поэтому вы можете использовать реальное имя коллекции связанных объектов. Я не уверен, что понимаю, почему привязка источника этой привязки источника к самому себе (CVS) работает, но это прекрасно.

Затем код фильтра TextBox становится примерно таким:

private void filterTextBox_TextChanged(object sender, TextChangedEventArgs e)
{
    var cvs = TryFindResource("FooCVS") as CollectionViewSource;
    if (cvs != null)
    {
        if (cvs.View != null)
            cvs.View.Refresh();
    }
    cvs = TryFindResource("QuxCVS") as CollectionViewSource;
    if (cvs != null)
    {
        if (cvs.View != null)
            cvs.View.Refresh();
    }
    cvs = TryFindResource("BarCVS") as CollectionViewSource;
    if (cvs != null)
    {
        if (cvs.View != null)
            cvs.View.Refresh();
    }
}

Отличное решение, поскольку оно не требует изменения базовых объектов или иерархии.

person Eugene Cheverda    schedule 27.07.2010
comment
Я ценю пример Евгения, но я надеялся остаться в основном в рамках, которые я обрисовал. Если это невозможно, я всегда могу определить собственный элемент управления списком, как вы предлагаете, но я пытался этого избежать. - person luke; 27.07.2010
comment
Вы не можете создать свой собственный элемент управления списком. ItemsCollection, который представлен свойством Items элемента управления списком, имеет свойство Filter. Вам нужно установить его с указанным предикатом и обновить этот предикат либо с изменением свойства TextBox.Text, либо с помощью внутренней (скрытой от пользователя) логики фильтрации. См. пример использования ItemCollection.Filter по ссылке MSDN: msdn.microsoft.com/en- нас/библиотека/ms752348.aspx - person Eugene Cheverda; 27.07.2010
comment
Звучит неплохо, но это возвращает меня к исходной проблеме: как установить фильтр в шаблоне (xaml) или получить доступ к экземпляру шаблона, чтобы установить фильтр в коде? - person luke; 27.07.2010
comment
Проверьте редактирование, пожалуйста. Я думаю, что это именно то, что вам нужно. - person Eugene Cheverda; 27.07.2010
comment
Похоже, вы привязываете источник CVS к самому себе, что интересно. Я попробую. К вашему сведению, ваш проект не компилируется как есть. - person luke; 28.07.2010
comment
Это кажется странным, потому что я только что скомпилировал его безупречно (VS2010). Если ваша IDE меньше, чем VS2010, просто скопируйте содержимое MainWindow.xaml и MainWindow.xaml.cs в свой пример проекта. Простите за неудобства. Надеюсь, это решение поможет вам. - person Eugene Cheverda; 28.07.2010
comment
Наконец-то я вернулся к этой проблеме в своем коде, и ваше решение работает очень хорошо. Я собираюсь отредактировать ваш пост, чтобы включить важные части опубликованного вами проекта. - person luke; 02.08.2010

Я думаю, вы должны выставить ICollectionView из своей модели представления вместо (или в дополнение к) ObservableCollection. Это перенесет всю бизнес-логику, связанную с фильтрацией/сортировкой, в виртуальную машину, которая является подходящим местом для нее.

Вы можете получить ICollectionView коллекции, создав CollectionViewSource, задав для свойства Source коллекцию и получив свойство View.

(Обновление) Вот пример кода:

class Foo
{
    public Foo()
    {
        _bars = new ObservableCollection<Bar>();
        Bars = new CollectionViewSource { Source = _bars }.View;
    }

    private ObservableCollection<Bar> _bars;
    public ICollectionView Bars { get; private set; }

    public void Filter(string quxName)
    {
        Bars.Filter = o => ((Bar)o).Quxes.Any(q => q.Name == quxName);

        foreach (Bar bar in Bars)
        {
            bar.Filter(quxName);
        }
    }
}   

class Bar
{
    private ObservableCollection<Qux> _quxes;
    public ICollectionView Quxes { get; private set; }

    public void Filter(string quxName)
    {
        Quexs.Filter = o => ((Qux)o).Name == quxName;
    }
}

class Qux
{
    public string Name { get; set; }
}
person Eli Arbel    schedule 26.07.2010
comment
Но как это поможет мне с элементами, созданными на основе шаблона? - person luke; 26.07.2010
comment
Вы можете привязать TextBox с фильтром к свойству в вашей модели представления и соответствующим образом изменить фильтр представления коллекции. Вы также можете изменить вид коллекции дочернего элемента и так далее. - person Eli Arbel; 26.07.2010
comment
Как получить представление дочерних коллекций (Foos/Quxes), чтобы иметь возможность фильтровать? Не могли бы вы опубликовать пример или какой-нибудь псевдокод? - person luke; 26.07.2010
comment
Возможное решение, но мне придется изменить определение для Foo/Bar, которое далеко не идеально. - person luke; 30.07.2010
comment
Вы должны изменить определение. Модель представления — это правильное место для размещения логики фильтрации. Вы можете открыть как коллекцию, так и представление коллекции. - person Eli Arbel; 02.08.2010

Сегодня у меня была аналогичная проблема на работе, и я нашел следующее решение:

  1. Добавьте свойство видимости ко всем вашим элементам напрямую или через шаблон адаптера.

        Visibility Visibility
        {
            get { return visibility; }
            set { visibility = value; PropertyChanged("Visibility"); }
        }
    
  2. Привяжите свойство видимости элементов управления к соответствующим свойствам видимости из шага 1.

  3. Реализуйте простую фильтрацию ваших данных с помощью методов расширения или внутри них.

    void Filter(Func<Foo, bool> filterFunc)
    {
        foreach (var item in foos)
        {
            if (!filterFunc(item))
                item.Visibility = Visibility.Collapsed;
            else
                item.Visibility = Visibility.Visible;
        }
    }
    
  4. Добавьте простые вызовы фильтра для события TextChanged вашего TextBox.

    Фильтр(n => n.Name.ToLower().Contains(textBox.Text));

или немного более продвинутый для вас контейнер управления:

Filter(c => c.Items.Any(i => i.Visibility == Visibility.Visible));
person Grozz    schedule 29.07.2010
comment
Это интересная идея, но для этого требуется дополнительный механизм, который, как мне кажется, должен предоставить XAML/WPF. - person luke; 29.07.2010
comment
Ну, это самое быстрое и простое решение, которое я придумал :) - person Grozz; 29.07.2010