Gishu
Gishu

Reputation: 136613

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):

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)

Upvotes: 4

Views: 1885

Answers (4)

Joe White
Joe White

Reputation: 97696

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.

Upvotes: 3

Dean Chalk
Dean Chalk

Reputation: 20451

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

Upvotes: 0

GaryX
GaryX

Reputation: 737

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>

Upvotes: 0

Dean Chalk
Dean Chalk

Reputation: 20451

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>

Upvotes: 3

Related Questions