问题
In a previous question I described a problem where I couldn't figure out how to remember and set the SelectedItem
on a ListView
. The problem is, when I set the SelectedItem
from my ViewModel, the ListView does not show the SelectedItem
as highlighted.
The question remained unanswered, so I tried to reproduce the problem in a small example. To my big surprise, I just learned that the problem is solved when I create a copy of the original selected item (which I stored in my ViewModel), and set that as selected item. If I set the exact same object which was already set, then the listview will NOT show the selected item.
A short intro:
I have a ListView
which shows items which are data bound to ItemSource
. The SelectedValue
is also data bound to a property on the ViewModel. I have two buttons, previous and next, to navigate through different object sets, each object set contains a different ObersevableCollection
which is bound to the ListView
s ItemSource
.
The idea is that the selection made in a ListView
is stored, so that when the same object set is shown again, the item which was selected previously is selected again.
Below the (ugly testing) code that brought me to the point where I am now:
Note; the for me surprising part is covered in method ShowList1CommandOnExecute()
. I added comments which explain what seems weird to me, while it seems to be the only way to get it working properly.
MainViewModel:
public class MainViewModel : INotifyPropertyChanged
{
private int _index;
private Person _selectedPerson1;
private Person _selectedPerson2;
private Person _selectedPerson3;
private ObservableCollection<Person> _list1;
private ObservableCollection<Person> _list2;
private ObservableCollection<Person> _list3;
public RelayCommand ShowList1Command { get; set; }
public RelayCommand ShowList2Command { get; set; }
private Person _selectedPerson;
public Person SelectedPerson
{
get
{
switch (_index)
{
case 0:
return _selectedPerson1;
case 1:
return _selectedPerson2;
case 2:
return _selectedPerson3;
}
return null;
}
set
{
if (value != null)
{
switch (_index)
{
case 0:
_selectedPerson1 = value;
break;
case 1:
_selectedPerson2 = value;
break;
case 2:
_selectedPerson3 = value;
break;
}
}
_selectedPerson = value;
OnPropertyChanged();
}
}
private ObservableCollection<Person> _persons;
public ObservableCollection<Person> Persons
{
get { return _persons; }
set
{
_persons = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
ShowList1Command = new RelayCommand(ShowList1CommandOnExecute, ShowList1CommandOnCanExecute);
ShowList2Command = new RelayCommand(ShowList2CommandOnExecute, ShowList2CommandOnCanExecute);
_list1 = new ObservableCollection<Person>();
_list1.Add(new Person { Name = "Bas" });
_list1.Add(new Person { Name = "Anke" });
_list1.Add(new Person { Name = "Suus" });
_list2 = new ObservableCollection<Person>();
_list2.Add(new Person { Name = "Freek" });
_list2.Add(new Person { Name = "Ina" });
_list2.Add(new Person { Name = "Liam" });
_list3 = new ObservableCollection<Person>();
_list3.Add(new Person { Name = "aap" });
_list3.Add(new Person { Name = "noot" });
_list3.Add(new Person { Name = "mies" });
Persons = new ObservableCollection<Person>();
}
private void ShowList1CommandOnExecute()
{
if (_index < 3)
{
_index++;
}
else
{
_index = 0;
}
switch (_index)
{
case 0:
Persons = _list1;
if (_selectedPerson1 != null)
{
// This is what surprised me, this DOES work. Why do I need a copy of an object??
SelectedPerson = new Person(_selectedPerson1.Name);
}
break;
case 1:
Persons = _list2;
// This did work, which is why I tried the copied object (see case 0)
SelectedPerson = new Person {Name = "freek"};
break;
case 2:
Persons = _list3;
if (_selectedPerson3 != null)
{
// This will NEVER result in the selected item to be visualized as selected
// However, when you will check while debugging, the ListView DOES contain the correct selected item
SelectedPerson = _selectedPerson3;
}
break;
}
}
private bool ShowList1CommandOnCanExecute()
{
return true;
}
private void ShowList2CommandOnExecute()
{
switch (_index)
{
case 0:
SelectedPerson = new Person {Name = "bas"};
break;
case 1:
SelectedPerson = new Person {Name = "Freek"};
break;
case 2:
SelectedPerson = new Person{Name = "mieS"};
break;
}
}
private bool ShowList2CommandOnCanExecute()
{
return true;
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
The very simple entity class Person
:
public class Person
{
public string Name { get; set; }
public Person()
{
}
public Person(string name)
{
Name = name;
}
public override bool Equals(object obj)
{
var other = obj as Person;
if (other != null)
{
return Name.ToLowerInvariant().Equals(other.Name.ToLowerInvariant());
}
return false;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
A very simple UI, two test buttons and a ListView
:
<Grid.RowDefinitions>
<RowDefinition Height="100"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<ToolBar Grid.Row="0">
<Button Height="90" Width="90" Command="{Binding ShowList1Command}">Show List1</Button>
<Button Height="90" Width="90" Command="{Binding ShowList2Command}">Show List2</Button>
</ToolBar>
<ListView
x:Name="_matchingTvShowsFromOnlineDatabaseListView"
Grid.Row="1"
Grid.Column="0"
ItemsSource="{Binding Persons}"
SelectedItem="{Binding SelectedPerson, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
Again, the question:
Why do I need to create a copy of the previously SelectedItem
and set that copy again as selected item in order for the ListView to visualize (and highlight) the SelectedItem in the ListView?
回答1:
I have investigated your issue, but have yet to pinpoint exactly what the problem is. What I can tell
you is that while it appears that nothing is selected, the value of SelectedItem on the ListView is actually correct. I can come to no other conclusion than this is unexpected behaviour of the ListView/ListBox in WPF. So it appears that you are indeed correct - it is a bug.
Now, in regards to solving your issue, here is what I would suggest:
The View (I removed the RelayCommand for simplicity):
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication1"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:MainViewModel/>
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="100"></RowDefinition>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="auto"></RowDefinition>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Button Height="90" Width="90" Click="Button_Click">Toggle List</Button>
<ListView
DataContext="{Binding Persons}"
x:Name="_matchingTvShowsFromOnlineDatabaseListView"
Grid.Row="1"
Grid.Column="0"
ItemsSource="{Binding}"
SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
<ListView.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
<TextBlock Text="{Binding Index}" Grid.Row="2"/>
</Grid>
</Window>
The ViewModel:
public class MainViewModel : INotifyPropertyChanged
{
private int _index;
public int Index
{
get { return _index; }
set
{
_index = value;
OnPropertyChanged();
}
}
private SelectionCollection<Person> _list1;
private SelectionCollection<Person> _list2;
private SelectionCollection<Person> _list3;
private SelectionCollection<Person> _persons;
public SelectionCollection<Person> Persons
{
get { return _persons; }
set
{
_persons = value;
OnPropertyChanged();
}
}
public MainViewModel()
{
_list1 = new SelectionCollection<Person>();
_list1.Add(new Person { Name = "Bas" });
_list1.Add(new Person { Name = "Anke" });
_list1.Add(new Person { Name = "Suus" });
_list2 = new SelectionCollection<Person>();
_list2.Add(new Person { Name = "Freek" });
_list2.Add(new Person { Name = "Ina" });
_list2.Add(new Person { Name = "Liam" });
_list3 = new SelectionCollection<Person>();
_list3.Add(new Person { Name = "aap" });
_list3.Add(new Person { Name = "noot" });
_list3.Add(new Person { Name = "mies" });
Persons = new SelectionCollection<Person>();
}
public void ShowList1CommandOnExecute()
{
if (Index < 2)
{
Index++;
}
else
{
Index = 0;
}
switch (Index)
{
case 0:
Persons = _list1;
break;
case 1:
Persons = _list2;
break;
case 2:
Persons = _list3;
break;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}
}
And finally, a newly introduced collection that is also able to track selection:
public class SelectionCollection<T> : ObservableCollection<T>, INotifyPropertyChanged
{
private T _selectedItem;
public T SelectedItem
{
get { return _selectedItem; }
set
{
_selectedItem = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Please note that this can also be accomplished by using CollectionViews, but personally, I prefer not having these in my ViewModel layer.
Also note that the _index field was converted to a property only to allow me to bind to it, as I got confused by it ranging between 0-3, instead of 0-2 (also fixed above).
回答2:
A bit late to the game, but I had been jumping through hoops to solve this Problem in a similar setup. Setting the SelectedItem in a ListView using a bound Property in the Viewmodel or similar using a bound SelectedIndex just would not work. Until I tried to do it async:
Task.Factory.StartNew(() =>
{
BoundSelectedIndex = index;
});
Seems to work - more advanced contributors may answer why...
来源:https://stackoverflow.com/questions/25919112/listview-remember-selecteditem-not-working-as-i-expected-a-bug-in-wpfs-listvi