views:

302

answers:

3

I'm having trouble filtering hierarchical data that's being displayed in nested xaml templates.

I've got a ObservableCollection<Foo> Foos, that I'm displaying in XAML.

Lets say Foo looks like:

class Foo
{
    public ObservableCollection<Bar> Bars;
}

class Bar
{
    public ObservableCollection<Qux> Quxes;
}

I'm displaying Foos with the following 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>

To put it into words, there's a tab control, with a tab for each Foo. Each Foo is a tab control, with each Bar it contains in it's own tab. Each Bar contain's a listbox of its Quxes.

or:

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

I also have a TextBox that I'd like to use to filter this breakdown. When I type in the text box, I'd like to filter the quxes so those not containing the text wouldn't be visible. Ideally Bar tabs would also be hidden if they have no visible quxes, and Foo tabs hidden when they have no visible Bars

I have considered two approaches:

Approach 1, reset the Filter property on the appropriate CollectionViewSources

On my text box's TextChanged event, I loop through my Foo's asking for the corresponding (static) TabControl's 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?
}

Approach 2, declare the ListView's ItemSource to a CollectionViewSource

I tried setting the Filter via xaml, by changing this line:

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

to this,

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

I've tried a number of things where I have "?????" but I cannot correctly bind to the ListBox's datacontext and appropriate Quxes member. Nothing I try results in the quxes being displayed, and I get no errors on the console. Even If I could get this approach to work, I'm not sure how I would re-trigger this filter when the text in the search box changed.

Any advice or direction would be appreciated.

A: 

I think you should expose an ICollectionView from your View-Model instead of (or in addition to) an ObservableCollection. This would bring all the business logic involved with filtering/sorting into the VM, which is the right place for it.

You can get the ICollectionView of a collection by creating a CollectionViewSource, setting its Source property to the collection and retrieving the View property.

(Update) Here's some sample code:

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; }
}
Aelij Arbel
But how does this help me with template-generated items?
luke
You can bind the TextBox with the filter to a property in your view model, and modify the collection view's filter accordingly. You can also modify the child item's collection view and so on.
Aelij Arbel
How does one get the View's of the child collections (Foos/Quxes) to be able to filter? Would you mind posting an example or some pseudo code?
luke
A possible solution, but I'd have to modify the definition for Foo/Bar which is less than ideal.
luke
You *should* change the definition. The view-model is the right place to put filtering logic. You can expose both the collection and the collection view.
Aelij Arbel
+1  A: 

Edit

Finally I've got it working with your requirements.

Here is the link to the updated project.


(edit by luke)

This is the (excellent) solution I ended up going with, so I'm going to extract the important parts, and actually make them part of the post here:

The key xaml portion ends up looking like this:

<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"/>

I set the respective control to each one of these views as the control's ItemSource. The magic is in the binding of each CVS. Each CVS gets the data context for the control/templated control in which appears, so you can use the the real name of the bound object's collection. I'm not sure I understand why binding the source of that source binding to itself (the CVS) works, but it does so beautifully.

The code for the filter TextBox then becomes something like:

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();
    }
}

Excellent solution as it requires no changing of the underlying objects or hierarchy.

Eugene Cheverda
I appreciate the example Eugene, but I was hoping to stay largely in the framework that I had outlined. If that's impossible, I can always define a custom list control like you suggest, but I was trying to avoid that.
luke
You may not create your custom list control. `ItemsCollection` which is represented by `Items` property of list control has the `Filter` property. You need to set it with your specified predicate and update this predicate with either changing `TextBox.Text` property or by internal (hided from user) logic of filtering. See the example of usage `ItemCollection.Filter` by MSDN link: http://msdn.microsoft.com/en-us/library/ms752348.aspx
Eugene Cheverda
That sounds fine- but that brings me right back to the original problem: How do I set the filter in the template (xaml), or access the template instantiation to set the filter in code?
luke
Check edit please. I think that this is exactly what you need.
Eugene Cheverda
It appears you're binding the Source of a CVS to itself, which is interesting. I'll give that a shot. FYI- your project does not compile as-is.
luke
It seems strange because i've just compiled it flawlessly (VS2010). If your IDE is less than VS2010 then simply copy `MainWindow.xaml` and `MainWindow.xaml.cs` contents to your sample project. Sorry for the inconvenience. Hope this solution will help you.
Eugene Cheverda
No problem. You're right, I'm using VS2008.
luke
I finally got back around to this issue in my code, and your solution works very well. I'm going to edit your post to include the important parts of the project you posted.
luke
A: 

I've had the similar problem at the work today and came out with the following solution:

  1. Add Visibility property to all your elements directly or through adapter pattern.

        Visibility Visibility
        {
            get { return visibility; }
            set { visibility = value; PropertyChanged("Visibility"); }
        }
    
  2. Bind Visibility property of controls to corresponding Visibility properties from step1.

  3. Implement simple filtering to your data through extension methods or inside them.

    void Filter(Func<Foo, bool> filterFunc)
    {
        foreach (var item in foos)
        {
            if (!filterFunc(item))
                item.Visibility = Visibility.Collapsed;
            else
                item.Visibility = Visibility.Visible;
        }
    }
    
  4. Add simple filter calls on TextChanged event of your TextBox.

    Filter(n => n.Name.ToLower().Contains(textBox.Text));

or a bit more advanced for you container controls:

Filter(c => c.Items.Any(i => i.Visibility == Visibility.Visible));
Grozz
That's an interesting idea, but this requires extra machinery that I feel like XAML/WPF should provide.
luke
Well, it's the fastest and easiest solution I came out with :)
Grozz