1

I've got a dilemma here. So I have mutiple expanders stacked on top of one another. Inside each expander is a ListBox, data bound, where each listitem displays a name of an object.

I've bound the search to filter the list items based on their name. However since I have two observable objects, the filtered items and unfiltered, the UI doesn't appear to get populated until someone searches. Whats the best way to fix this. I find it redundant to add items to both lists each time a new Person gets created. Using an mvvm approach.

The two collections are called People and PeopleFiltered. When i create people I add them to the list called People. When the search is applied it populates the PeopleFiltered list, which is the list the UI is bound to. How can I maintain this list be init to mimic People.

At the end of the day the PeopleFiltered collection should mimic People unless a search is being applied.

MainWindow.xaml

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfApplication1"
        WindowStartupLocation="CenterScreen"
        Title="MainWindow" Height="400" Width="200">

    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>

    <Grid Margin="5">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="*" />
        </Grid.RowDefinitions>

        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>

            <Label Content="Search:"/>
            <TextBox Grid.Column="1" Background="Gold" Text="{Binding SearchString, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
        </Grid>


        <StackPanel Grid.Row="1">
            <Expander Header="People" IsExpanded="{Binding IsExpanded, Mode=OneWay}">
                <ListView ItemsSource="{Binding PeopleFiltered}">
                    <ListView.ItemTemplate>
                        <DataTemplate>
                            <WrapPanel>
                                <Ellipse Width="8" Height="8" Fill="Green" Margin="0,0,5,0"/>
                                <TextBlock Text="{Binding Name}"/>
                            </WrapPanel>
                        </DataTemplate>
                    </ListView.ItemTemplate>
                </ListView>
            </Expander>
        </StackPanel>
    </Grid>
</Window>

MainWindowViewModel.cs

using System.Collections.ObjectModel;
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Linq;
using System.Collections.Generic;

namespace WpfApplication1
{
    public class MainWindowViewModel : NotifyBase
    {
        // search text
        private string searchString;
        public string SearchString
        {
            get { return searchString; }
            set
            {
                this.searchString = value;
                NotifyPropertyChanged("SearchString");
                ApplySearchFilter();
            }
        }

        private void ApplySearchFilter()
        {
            if (string.IsNullOrWhiteSpace(SearchString))
            {
                IsExpanded = false;
                PeopleFiltered.Clear();
                foreach (DisplayItem displayItem in People)
                {
                    PeopleFiltered.Add(displayItem);
                }
            }
            else
            {
                // open expanders and apply search
                IsExpanded = true;
                PeopleFiltered.Clear();
                foreach (DisplayItem displayItem in People)
                {
                    if (displayItem.Name.ToLowerInvariant().Contains(SearchString.ToLowerInvariant()))
                    {
                        PeopleFiltered.Add(displayItem);
                    }
                }
            }
        }

        // used to to open and close expanders
        private bool isExpanded;
        public bool IsExpanded
        {
            get { return this.isExpanded; }
            set
            {
                this.isExpanded = value;
                NotifyPropertyChanged("IsExpanded");
            }
        }

        // data collections for each expander
        private ObservableCollection<DisplayItem> people;
        public ObservableCollection<DisplayItem> People
        {
            get { return people ?? (people = new ObservableCollection<DisplayItem>()); }
            set
            {
                people = value;
                NotifyPropertyChanged("People");
            }
        }

        private ObservableCollection<DisplayItem> peopleFiltered;
        public ObservableCollection<DisplayItem> PeopleFiltered
        {
            get { return peopleFiltered ?? (peopleFiltered = new ObservableCollection<DisplayItem>()); }
            set
            {
                peopleFiltered = value;
                NotifyPropertyChanged("PeopleFiltered");
            }
        }

        // init
        public MainWindowViewModel()
        {
            // People
            People.Add(new DisplayItem() { Name="John" });
            People.Add(new DisplayItem() { Name="Veta"});
            People.Add(new DisplayItem() { Name="Sammy"});
            People.Add(new DisplayItem() { Name = "Sarah" });
            People.Add(new DisplayItem() { Name = "Leslie" });
            People.Add(new DisplayItem() { Name = "Mike" });
            People.Add(new DisplayItem() { Name = "Sherry" });
            People.Add(new DisplayItem() { Name = "Brittany" });
            People.Add(new DisplayItem() { Name = "Kevin" });
        }
    }

    // class used to display all items
    public class DisplayItem
    {
        public string Name { get; set; }
    }

    //observable object class
    public class NotifyBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}
JokerMartini
  • 5,674
  • 9
  • 83
  • 193
  • You can try this:- https://joshsmithonwpf.wordpress.com/2007/06/12/searching-for-items-in-a-listbox/ – yash Jan 09 '16 at 05:20
  • Handle TextChanged of TextBox, and Filter the lists. You can see my answer here for filtering a combobox. http://stackoverflow.com/questions/34382459/simple-wpf-combobox-filter/34390669#34390669. – AnjumSKhan Jan 09 '16 at 05:39

1 Answers1

2

Here's a nice approach.

Create a Query property in your View Model, this will be bound to your filter TextBox.

private string _Query;

public string Query
{
    get { return _Query; }
    set
    {
        _Query = value;

        Filter();

        //Notify property changed.
    }
}

One thing to note here is the Filter() method. This will be called every time the property changes, I'll get back to this later. Firstly, make sure your TextBox binding is TwoWay, it'll look like this:

<TextBox Text="{Binding Query}" ... />

In your View Model, you will need a collection for each ListBox.

private List<object> _Collection1; //The original collection
private List<object> _FilteredCollection1; //The filtered collection

public List<object> FilteredCollection1
{
    get { return _FilteredCollection1; }
    set
    {
        _FilteredCollection1 = value;

        //Notify property changed.
    }
}

//Some more collections
...

It's important to note here that there is a variable for the original unfiltered collection. This is important because we want to filter this list into a new collection, otherwise we'll just keep filtering over and over and eventually have nothing in the collection.

You'll need to bind the ItemsSource property in your ListBox to the collection(s).

<ListBox ItemsSource="{Binding FilteredCollection1}" ... />

Now, your Filter method can simply filter the _Collection1 variable into the FilteredCollection1 property.

private void Filter()
{
    //Perform the filter here for all collections.
    FilteredCollection1 = _Collection1.Where(x => x.Something == Query);

    //Do the same for all other collections...
}

Note: The above Linq is just an example, I expect yours will be slightly more complicated than that, but you get the idea.

So. Whenever Query gets updated, the Filter collection will fire, update the FilteredCollection properties which will in turn call property changed and update the view.


Alternative Approach

Here's another way.

Instead of using a Filter method, you can instead put your filter code inside the get block in the FilteredCollection properties, like this:

public List<object> FilteredCollection1
{
    get 
    {
        return _Collection1.Where(...);
    }
}

Then, in your Query property, simply call INotifyPropertyChanged for the collection:

private string _Query;

public string Query
{
    get { return _Query; }
    set
    {
        _Query = value;

        //Notify property changed.

        OnPropertyChanged("FilteredCollection1");
    }
}

This will force the view to refresh the FilteredCollection1 property.

Mike Eason
  • 9,525
  • 2
  • 38
  • 63