wpfmvvmfiltericollectionview

Filter a ICollectionView based on the selected items Property in the same ListView - MVVM


I can't seem to come to a solution. Any help is appreciated.

EDIT: The solution was reached with a combination of Filter logic provided by @BionicCode , implementing a ListView selection in the XMAL per this post and adding a simple bool Converter er for visibility. I have also discovered that Items Control would also work for this situation and allow greater control per this post.

I have a ListView:

EDIT: One isn't enough unfortunately. Two ListViews will work. Please note the added namespace xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity". Additionally, I have added a "Back" button to allow the user to go back to select a different Category.

<Window x:Class="WpfApp1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"  
        xmlns:vm="clr-namespace:WpfApp1.ViewModels" 
        Title="MainWindow" Height="300" Width="500" WindowStartupLocation="CenterScreen">
    <Window.DataContext>
        <vm:MainViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="100"/>
            <ColumnDefinition/>
            <ColumnDefinition/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="15"/>
            <RowDefinition/>
        </Grid.RowDefinitions>

        <Grid Grid.Column="0" Grid.Row="1">
            <Button x:Name="BackBtn" Content="Back" Command="{Binding BacktoCatView}"/>
        </Grid>

        <Grid Grid.Column="1" Grid.Row="1"  >
            <ListView x:Name="CategoryListView" ItemsSource="{Binding CategoryView}" SelectedItem="{Binding SelectedCategory}">
                <ListView.ItemTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Category}"/>
                    </DataTemplate>
                </ListView.ItemTemplate>
                <i:Interaction.Triggers>
                    <i:EventTrigger EventName="SelectionChanged">
                        <i:InvokeCommandAction Command="{Binding SelectedCategoryCommand}" CommandParameter="{Binding SelectedItem, ElementName=CategoryListView}"/>
                    </i:EventTrigger>
                </i:Interaction.Triggers>
            </ListView>
        </Grid>

        <Grid Grid.Column="1" Grid.Row="1" Visibility="{Binding IsBrandView, Converter={StaticResource VisibilityConverter}}">
            <ListView ItemsSource="{Binding BrandView}">
                <ListView.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Brand}"/>
                </DataTemplate>
                </ListView.ItemTemplate>
            </ListView>
        </Grid>
    </Grid> </Window>

I have a Model:

public class Car : NotifyPropertyChanged
{
    private string _brand;
    public string Brand
    {
        get => _brand;
        set
        {
            _brand = value;
            OnPropertyChanged();
        }
    }

    private string _category;
    public string Category
    {
        get => _category;
        set
        {
            _category = value;
            OnPropertyChanged();
        }
    }
}

The ListView is bound to an ICollectionView called "CategoryView" that is based upon an ObservableCollection "CarsCollection" in the MainViewModel.

EDIT: Each Listview is bound to it's respective ICollectionView,alas, "CategoryView" and "BrandView". SelectedCategory is bound to SelectedItem in "CategoryView".

The MainViewModel:

 public class MainViewModel : NotifyPropertyChanged
        {
            private ObservableCollection<Car> _carsCollection;
            public ObservableCollection<Car> CarsCollection
            {
                get { return _carsCollection; }
                set
                {
                    _carsCollection = value;
                    OnPropertyChanged();
                }
            }
    
            private ICollectionView _categoryView;
            public ICollectionView CategoryView
            {
                get { return _categoryView; }
                set
                {
                    _categoryView = value;
                    OnPropertyChanged();
                }
            }
    
            private ICollectionView _brandView;
            public ICollectionView BrandView
            {
                get { return _brandView; }
                set
                {
                    _brandView = value;
                    OnPropertyChanged();
                }
            }
    
            private Car _selectedCategory;
            public Car SelectedCategory
            {
                get { return _selectedCategory; }
                set
                {
                    _selectedCategory = value;
                    OnPropertyChanged();
                }
            }
            public ICommand BacktoCatView { get; private set; }
            public ICommand SelectedCategoryCommand { get; private set; }
    
            
            private bool _isBrandView;
            public bool IsBrandView
            {
                get { return _isBrandView; }
                set
                {
                    _isBrandView = value;
                    OnPropertyChanged();
                }
            }
    
            public MainViewModel()
            {
                CarsCollection = new ObservableCollection<Car>
                    {
                        new Car { Brand = "Chevy", Category = "Sedan" },
                        new Car { Brand = "Mazda", Category = "Sports Car" },
                        new Car { Brand = "Toyota", Category = "Sedan" },
                        new Car { Brand = "Honda", Category = "Sports Car" },
                        new Car { Brand = "Volkswagon", Category = "Sedan" },
                        new Car { Brand = "Tesla", Category = "Sedan" },
                        new Car { Brand = "Dodge", Category = "Sports Car" },
                        new Car { Brand = "Jeep", Category = "Off Road" },
                    };
    
                BacktoCatView = new RelayCommand(ShowCategoryView);
                SelectedCategoryCommand = new RelayCommand(FilterbyBrand);
                //Filter by Category on app start
                CategoryView = CollectionViewSource.GetDefaultView(CarsCollection);
                CategoryView.Filter = item => !IsDuplicate((IEnumerable<Car>)CategoryView.SourceCollection, (Car)item);
                //Setup BrandView for filtering
                BrandView = (CollectionView)new CollectionViewSource { Source = CarsCollection }.View;
    
                OnPropertyChanged("CategoryView");
                OnPropertyChanged("BrandView");
            }
    
            static bool IsDuplicate(IEnumerable<Car> collection, Car target)
            {
                foreach (var item in collection)
                {
                    // NOTE: Check only the items BEFORE the one in question
                    if (ReferenceEquals(item, target)) break;
                    // If more than one Category is present, only show one instance of it
                    if (item.Category == target.Category) return true;
                }
                return false;
            }
    
            private void ShowCategoryView()
            {
                IsBrandView = false;
            }
    
            public void FilterbyBrand()
            {
                IsBrandView = true;
                BrandView.Filter = item => (item as Car).Category.Equals(_selectedCategory.Category, StringComparison.OrdinalIgnoreCase);
                OnPropertyChanged("BrandView");
            }
        }
    }  

EDIT: Added IValueConverter to a class called "Converters" in a new namespace called "Helpers" to handle Visibility. I also added a RelayCommand class in the Utility namespace to simplify the use of ICommands.

namespace WpfApp1.Helpers
{
    public class BoolToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            var boolValue = (bool)value;

            if (boolValue)
                return Visibility.Visible;

            return Visibility.Collapsed;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }
}

The first filter occurs in the Constructor:

    // Filter by Category
    CategoryView = CollectionViewSource.GetDefaultView(CarsCollection);
    CategoryView.Filter = item => !IsDuplicate((IEnumerable<Car>)CategoryView.SourceCollection, (Car)item);

Which uses the following to remove the duplicate instances of Category:

static bool IsDuplicate(IEnumerable<Car> collection, Car target)
    {
        foreach (var item in collection)
        {
            // NOTE: Check only the items BEFORE the one in question
            if (ReferenceEquals(item, target)) break;
            // If more than one Category is present, only show one instance of it
            if (item.Category == target.Category) return true;
        }
        return false;
    }

This filter work perfectly and produces the following in the CategoryListView:

Sedan
Sports Car
Off Road

Challenge:

When a user selects a "Category" from the ListView, I wish to next apply a second filter to the IListCollection that filters the ListView such that each "Brand" within the "Category" the user has selected is displayed in the ListView.

EDIT: now, when the user selects a category, they are presented with the list of Brand of that specific Category. The filtering occurs seamlessly to the user. Most importantly, this provides the user with an immediate view of available Categories on app startup and does not require the user to locate the desired Category within a Combobox.

Big thanks to @BionicCode for helping with the Filter Logic.

The Brand Filter and IsBrandView bool:

private void ShowCategoryView()
{
    IsBrandView = false;
}

public void FilterbyBrand()
{
    IsBrandView = true;
    BrandView.Filter = item => (item as Car).Category.Equals(_selectedCategory.Category, StringComparison.OrdinalIgnoreCase);
    OnPropertyChanged("BrandView");
}

Solution

  • I solved this with a combination of Filter logic provided by @BionicCode , implementing a ListView selection in the XMAL per this post and adding a simple bool Converter er for visibility.

    The solution has been added to the original post.