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");
}
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.