c# – Proper way to use CollectionViewSource in ViewModel

c# – Proper way to use CollectionViewSource in ViewModel

You have two options to use CollectionViewSource properly with MVVM –

  1. Expose an ObservableCollection of items (Categories in your case) through your ViewModel and create CollectionViewSource in XAML like this –

    <CollectionViewSource Source={Binding Path=Categories}>
        <CollectionViewSource.SortDescriptions>
           <scm:SortDescription PropertyName=CategoryName />
        </CollectionViewSource.SortDescriptions>
    </CollectionViewSource>
    

    scm: xmlns:scm=clr-namespace:System.ComponentModel;assembly=Wind‌​owsBase

    see this – Filtering collections from XAML using CollectionViewSource

  2. Create and Expose an ICollectionView directly from your ViewModel

    see this – How to Navigate, Group, Sort and Filter Data in WPF

Following example shows how to create a collection view and
bind it to a ListBox

View XAML:

<Window 
    xmlns=http://schemas.microsoft.com/winfx/2006/xaml/presentation
    xmlns_x=http://schemas.microsoft.com/winfx/2006/xaml
    xmlns_scm=clr-namespace:System.ComponentModel;assembly=WindowsBase
    x_Class=CustomerView>
    <ListBox ItemsSource={Binding Customers} />
</Window>

View Codebehind:

public class CustomerView : Window
{
   public CustomerView()
   {
       DataContext = new CustomerViewModel();
   }
}

ViewModel:

public class CustomerViewModel
{
    private readonly ICollectionView customerView;

    public ICollectionView Customers
    {
        get { return customerView; }
    }

    public CustomerViewModel()
    {
        IList<Customer> customers = GetCustomers();
        customerView = CollectionViewSource.GetDefaultView( customers );
    }
}

Update:

Q. If there is no property to sort on? e.g. if there is an ObservableCollection of string or int?

A. In that case you can Simply use . as the property name:

<scm:SortDescription PropertyName=. />

I found that it is handy to have a CollectionViewSource in my ViewModel and bind the ListBox (in my case) to the CollectionViewSource.View while setting the CollectionViewSource.Source to be the list I want to use.

Like so:

ViewModel:

    public DesignTimeVM()  //Im using this as a Design Time VM 
    {
        Items = new List<Foo>();
        Items.Add(new Foo() { FooProp= 1, FooPrep= 20.0 });
        Items.Add(new Foo() { FooProp= 2, FooPrep= 30.0 });

        FooViewSource = new CollectionViewSource();
        FooViewSource.Source = Items;

        SelectedFoo = Items.First();

        //More code as needed
    }

XAML:

<ListBox ItemsSource={Binding FooViewSource.View} SelectedItem={Binding SelectedFoo}/>

This means I can do neat stuff in the VM as needed (from https://blogs.msdn.microsoft.com/matt/2008/08/28/collectionview-deferrefresh-my-new-best-friend/ ):

        using (FooViewSource.DeferRefresh())
        {
            //Remove an old Item
            //add New Item
            //sort list anew, etc. 
        }

I suppose this is possible when using the ICollectionView object also, but the demo code in the blog link seems to be some codebehind stuff, refering the listbox directly, which Im trying to avoid.

BTW before you ask, heres how you use a Design Time VM: WPF Design Time View Model

c# – Proper way to use CollectionViewSource in ViewModel

Just for reference, another way is to use an attached property on the CollectionViewSource which then pipes the functions to the ViewModel (Implementing an Interface).

This is a very basic Demonstration just for filtering, it would need some work for e.g. a second Collection on the VM but i think its enough to show the general technique.

If this is better or worse than the other methods is up for discussion, i just wanted to point out, that theres another way of doing this

Definition of attached Property:

public static class CollectionViewSourceFilter
{
    public static IFilterCollectionViewSource GetFilterObject(CollectionViewSource obj)
    {
        return (IFilterCollectionViewSource)obj.GetValue(FilterObjectProperty);
    }

    public static void SetFilterObject(CollectionViewSource obj, IFilterCollectionViewSource value)
    {
        obj.SetValue(FilterObjectProperty, value);
    }

    public static void FilterObjectChanged(object sender, DependencyPropertyChangedEventArgs e)
    {
        if (e.OldValue is IFilterCollectionViewSource oldFilterObject
            && sender is CollectionViewSource oldCvs)
        {
            oldCvs.Filter -= oldFilterObject.Filter;
            oldFilterObject.FilterRefresh -= (s, e2) => oldCvs.View.Refresh();
        }

        if (e.NewValue is IFilterCollectionViewSource filterObject
            && sender is CollectionViewSource cvs)
        {
            cvs.Filter += filterObject.Filter;
            filterObject.FilterRefresh += (s,e2) => cvs.View.Refresh();
        }
    }

    public static readonly DependencyProperty FilterObjectProperty = DependencyProperty.RegisterAttached(
        FilterObject,
        typeof(Interfaces.IFilterCollectionViewSource),
        typeof(CollectionViewSourceFilter),
        new PropertyMetadata(null,FilterObjectChanged)
    );
}

Interface:

public interface IFilterCollectionViewSource
{
    void Filter(object sender, FilterEventArgs e);
    event EventHandler FilterRefresh;
}

usage in xaml:

<CollectionViewSource
        x_Key=yourKey
        Source={Binding YourCollection}
        classes:CollectionViewSourceFilter.FilterObject={Binding} />

and usage in the ViewModel:

class YourViewModel : IFilterCollectionViewSource
{
    public event EventHandler FilterRefresh;

    private string _SearchTerm = string.Empty;
    public string SearchTerm
    {
        get { return _SearchTerm; }
        set {
            SetProperty(ref _SearchTerm, value);
            FilterRefresh?.Invoke(this, null);
        }
    }

    private ObservableCollection<YourItemType> _YourCollection = new ObservableCollection<YourItemType>();
    public ObservableCollection<YourItemType> YourCollection
    {
        get { return _YourCollection; }
        set { SetProperty(ref _YourCollection, value); }
    }

    public void Filter(object sender, FilterEventArgs e)
    {
        e.Accepted = (e.Item as YourItemType)?.YourProperty?.ToLower().Contains(SearchTerm.ToLower()) ?? true;
    }
}

Leave a Reply

Your email address will not be published.