I'm binding a ListView to an ICollectionView in my viewmodel. The ICollectionView has some predefined filters that are applied when you click some buttons. However I cannot seem to find any way to (auto) select the first item in the ListView after the collection has been filtered.
I've tried to set SelectedIndex=0, add both Target and Source notification to the binding, but all are ineffective when the filter applies.
Any pointers on how to achieve this?
EDIT: Below code illustrates my issue I'd say.
XAML:
<Window x:Class="CollectionViewTest.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:CollectionViewTest"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<!-- MENU -->
<StackPanel Orientation="Vertical">
<Button Content="Numbers below 4" Click="Below4_Click" Width="100"/>
<Button Content="Numbers below 7" Click="Below7_Click" Width="100"/>
<Button Content="All numbers" Click="All_Click" Width="100"/>
</StackPanel>
<!-- LIST -->
<ListView
Grid.Column="1"
SelectedIndex="0"
ItemsSource="{Binding Numbers, Mode=OneWay}"
SelectedItem="{Binding SelectedNumber, Mode=TwoWay}">
<ListView.Resources>
<DataTemplate DataType="{x:Type local:Number}">
<TextBlock Text="{Binding Value}" />
</DataTemplate>
</ListView.Resources>
</ListView>
<!-- DETAILS -->
<TextBlock Grid.Column="2" Text="{Binding SelectedNumber.Text}" Width="100"/>
</Grid>
</Window>
Code-Behind:
using System.Windows;
namespace CollectionViewTest
{
public partial class MainWindow : Window
{
private MainViewModel vm;
public MainWindow()
{
InitializeComponent();
vm = (MainViewModel)DataContext;
}
private void Below4_Click(object sender, RoutedEventArgs e)
{
vm.MenuFilter = f => f.Value < 4;
}
private void Below7_Click(object sender, RoutedEventArgs e)
{
vm.MenuFilter = f => f.Value < 7;
}
private void All_Click(object sender, RoutedEventArgs e)
{
vm.MenuFilter = f => true;
}
}
}
ViewModel:
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Windows.Data;
using System.Collections.ObjectModel;
namespace CollectionViewTest
{
public class MainViewModel : PropertyChangedBase
{
public MainViewModel()
{
Numbers = new ObservableCollection<Number>();
NumberCollection = CollectionViewSource.GetDefaultView(Numbers);
NumberCollection.Filter = Filter;
NumberCollection.SortDescriptions.Add(new SortDescription("Value", ListSortDirection.Ascending));
for (int i = 0; i < 10; i++)
Numbers.Add(new Number { Value = i, Text = $"This is number {i}." });
}
private Func<Number, bool> menuFilter;
public Func<Number, bool> MenuFilter
{
get => menuFilter;
set
{
menuFilter = value;
NumberCollection.Refresh();
}
}
private bool Filter(object item)
{
var number = (Number)item;
return MenuFilter == null ? true : MenuFilter(number);
}
public ObservableCollection<Number> Numbers { get; set; }
public ICollectionView NumberCollection { get; set; }
private Number selectedNumber;
public Number SelectedNumber { get => selectedNumber; set => Set(ref selectedNumber, value); }
}
public class Number : PropertyChangedBase
{
public int Value { get; set; }
private string text;
public string Text { get => text; set => Set(ref text, value); }
}
public class PropertyChangedBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void Set<T>(ref T field, T newValue = default(T), [CallerMemberName] string propertyName = null)
{
field = newValue;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
}
As you can see, pressing one of the buttons changes the Filter and calls Refresh on the collection. What I would like to have, is that the first item in the list (here '0') is selected automatically which then would display the text "This is number 0" in the text in column 2.
I have tried both the SelectedIndex=0 and also MoveCurrentToFirst but nothing is selected.
Don't set SelectedIndex
when binding to an ICollectionView
. Instead, set its CurrentItem
via MoveCurrentTo() or MoveCurrentToFirst():
myCollectionView.MoveCurrentTo(someItem);
...
myCollectionView.MoveCurrentToFirst();
Also, set IsSynchronizedWithCurrentItem on your ListView
:
<ListView IsSynchronizedWithCurrentItem="True" ...
When the filter is evaluated, the collection view is refreshed which in turn resets the collection. To detect this, listen for the CollectionChanged
event and look for the NotifyCollectionChangedAction.Reset
flag. Please refer to the CollectionView
source code for more details.