12

I have a ListView control that displays items from an observable collection. These items need to be filtered. I am able to do that with a CollectionViewSource, but the filter needs to be updated each time an item changes.

My items looks like this:

enum Status {Done, Failed, Skipped, ...}

class Project {
  public string Name {get;set;}
  public Status Status {get;set;}
  // etc. etc.
}

class ProjectViewModel : INotifyPropertyChanged {
  private Project project;

  public ProjectBuildInfoViewModel(ProjectBuildInfo project)
  {
    this.project = project;
  }

  public string Name
  {
     get { return project.Name; }
     set { project.Name = value; OnPropertyChanged("Name"); }
  }

  // etc. etc.
}

class CollectionViewModel {
  private ObservableCollection<ProjectViewModel> projects = 
             new ObservableCollection<ProjectViewModel>();

  public ObservableCollection<ProjectViewModel> Collection
  {
     get { return projects; }
     private set {projects = value; }
  } 
}

Then I have this ListView whose ItemSource is bound to the collection.

// member of the user control class
private CollectionViewModel collection = new CollectionViewModel();

// in the constructor
listView.ItemSource = collection.Collection.

This doesn't filter anything. So I have these check boxes and they should indicate which items (depending of the state) should be displayed. I have used then a CollectionViewSource:

private void UpdateView()
{
  var source = CollectionViewSource.GetDefaultView(collection.Collection);
  source.Filter = p => Filter((ProjectViewModel)p);
  listStatus.ItemsSource = source;
}

The filter method looks like this:

private bool Filter(ProjectViewModel project)
{
     return (ckFilterDone.IsChecked.HasValue && ckFilterDone.IsChecked.Value && project.Status == Status.Done) ||
            (ckFilterFailed.IsChecked.HasValue && ckFilterFailed.IsChecked.Value && project.Status == Status.Failed) ||
            (ckFilterSkipped.IsChecked.HasValue && ckFilterSkipped.IsChecked.Value && project.Status == Status.Skipped);
}

This has the disadvantage that it captures the values of the checkboxes, so I have to call this method (UpdateView) each time a checkbox is checked. But it works.

However, the item state does change and if "done" is not checked for instance, when an item goes into "done" it should be removed from the view. Obviously that doesn't change unless I again call UpdateView. So I need to call this method each time something changes. That looks ugly and inefficient to me.

So my question is, can this be done in a nicer way?

wonea
  • 4,783
  • 17
  • 86
  • 139
Marius Bancila
  • 16,053
  • 9
  • 49
  • 91
  • I posted another approach as an answer but I recall making filters work without a call to Update(). Try implementing NotifyPropertyChanged in Project - the binding is not aware of a change without it. – paparazzo Mar 08 '12 at 13:33

3 Answers3

25

Bind your ListView directly to the filtered collection instead of the ObservableCollection by creating a property -

public ICollectionView YourFilteredCollection
{
   get
   {      
      var source = CollectionViewSource.GetDefaultView(collection.Collection);
      source.Filter = p => Filter((ProjectViewModel)p);
      return source;
   }
}

So, now simply you need to call Refresh() on your collection on your check boxes state changed event like this -

YourFilteredCollection.Refresh();

To refresh the collection based on any state change in the item class, you can generalize it by hooking the PropertyChanged event of your item class (for this your class need to implement INotifyPropertyChanged) and from there you can call refresh like this -

foreach (YourClass item in collection.Collection)
{
  item.PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}

void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
  YourFilteredCollection.Refresh();
}

So, whenever any property changes in your item class, your collection will be filtered.

Rohit Vats
  • 79,502
  • 12
  • 161
  • 185
  • 1
    but I would still have to call Refresh each time the state of an item, or the check boxes change. – Marius Bancila Mar 08 '12 at 13:58
  • I have update my answer regarding your concern for state change. So this way you need to call refresh only from two places now. Hope this helps. Moreover, you can subclass the ObservableCollection and move this logic there and can use your subclasss in place of your ObservableCollection. – Rohit Vats Mar 08 '12 at 15:42
  • 1
    This might be of your help to subclass the ObservableCollection - http://msdn.microsoft.com/en-us/library/ee696421.aspx – Rohit Vats Mar 08 '12 at 15:44
6

I like to use DataTriggers for that. For your logic would need to use a multivalue converter.

 <ListView Grid.Row="3" Grid.Column="2" ItemsSource="{Binding Path=GabeLib.DocFieldsAll}">
        <ListView.ItemContainerStyle>
            <Style TargetType="{x:Type ListViewItem}"  >
                <Style.Triggers>
                    <DataTrigger Binding="{Binding Path=Active}" Value="False">
                        <Setter Property="Visibility" Value="Collapsed"/>
                    </DataTrigger>
                    <DataTrigger Binding="{Binding Path=FieldDef.ID}" Value="0">
                        <Setter Property="Visibility" Value="Collapsed"/>
                    </DataTrigger>
                </Style.Triggers>
            </Style>
        </ListView.ItemContainerStyle>
paparazzo
  • 44,497
  • 23
  • 105
  • 176
0

Use a tool like ContinuousLinq. You bind your listview to a query that will reevaluate when an item in the list (or the list itself) changes.

Joel Lucsy
  • 8,520
  • 1
  • 29
  • 35