Selection bug for listbox where the list items are value types / structs and contain duplicates?
I turned an Horizontal ItemsControl to a Listbox so that I am able to select individual items but found that the selection was broken. Took some time to distill out the problematic bit.
Books = new[] { new Book{Id=1, Name="Book1"},
new Book{Id=2, Name="Book2"},
new Book{Id=3, Name="Book3"},
new Book{Id=4, Name="Book4"},
new Book{Id=3, Name="Book3"},
};
<DataTemplate DataType="{x:Type WPF_Sandbox:Book}">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
<ListBox ItemsSource="{Binding Books}"/>
If Book is a struct, the listbox selection (default mode : single) goes awry if you select an item which has an equivalent struct in the list. e.g Book3
If Book is turned into a class (with non-value type semantics), selection is fixed.
Choices (so far, don't like any of them):
- I chose structs because its a small data structure and the value type semantics are useful in comparing 2 instances for equality. Changing it to a class causes me to lose value-type semantics.. I can't use the default Equals anymore or override it for开发者_运维技巧 memberwise comparison.
- Add a differentiating Book attribute purely for the listbox selection to work (e.g. an Index).
- Eliminate Duplicates.. Not possible.
WPF listbox : problem with selection : states that the Listbox is setting SelectedItem and while updating the UI for this, it just lights up all items in the list that Equal(SelectedItem)
. Not sure why.. highlighting SelectedIndex would make this problem go away; maybe I am missing something.
ListBox is selecting many items even in SelectionMode="Single" : shows the same problem when list items are strings (value type semantics)
Why not simply use a better collection class as your datasource to overcome the problem
var collection = new[]
{
new Book {Id = 1, Name = "Book1"},
new Book {Id = 2, Name = "Book2"},
new Book {Id = 3, Name = "Book3"},
new Book {Id = 4, Name = "Book4"},
new Book {Id = 3, Name = "Book3"},
};
var Books = collection.ToDictionary(b => Guid.NewGuid(), b => b);
DataContext = Books;
And this will be your DataTemplate
<ListBox ItemsSource="{Binding}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Value.Name}"/>
</DataTemplate>
</ListBox.ItemTemplate>
I'm not clear on why you have duplicates in your list, if they're absolutely identical (i.e., if duplicates have all the same content and return true from Equals). You won't have any way to tell which of the duplicates the user has selected. Neither will the ListBox, which is probably why you're having problems.
Maybe, instead of binding directly to a collection of structs, you could wrap each struct in a class? Just define a BookWrapper class that contains a Book struct, and bind to a collection of BookWrappers instead of a collection of Books. You fix the problem of WPF not being able to tell the instances apart, but the rest of your code could continue to have the benefits of a struct.
Thanks to Dean Chalk for his idea.
I extend it so that it is easier to user for other structs
The idea is to use a converter to cast the original struct collection to a custom collection, which in turn override the equal to compare with Guid ID. You still has the original order
public class StructListItem
{
private Guid _id = Guid.NewGuid();
public Guid ID
{
get
{
return _id;
}
set
{
_id = value;
}
}
private object _core = default(object);
public object Core
{
get
{
return _core;
}
set
{
_core = value;
}
}
public StructListItem(object core)
{
Core = core;
}
public override bool Equals(object obj)
{
return ID.Equals(obj);
}
public override int GetHashCode()
{
return ID.GetHashCode();
}
}
public class StructToCollConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (value is IEnumerable)
{
List<StructListItem> _ret = new List<StructListItem>();
if (value != null)
{
IEnumerator i = ((IEnumerable)value).GetEnumerator();
while (i.MoveNext())
{
_ret.Add(new StructListItem(i.Current));
}
}
return _ret.ToArray();
}
return null;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
<ListBox ItemsSource="{Binding Books, Converter={StaticResource converter}}" SelectionMode="Single">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Core.Name}"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Garyx
Something a bit simpler maybe ?
public class StructListItem<T> where T : struct
{
public T Item { get; private set; }
public readonly Guid Id = Guid.NewGuid();
public StructListItem(T item)
{
Item = item;
}
public static IEnumerable<StructListItem<U>>
GetStructList<U>(IEnumerable<U> originalList) where U : struct
{
return originalList.Select(i => new StructListItem<U>(i));
}
}
精彩评论