Vijay Chavda
Vijay Chavda

Reputation: 862

ArgumentException with ListView SelectedItem's property and TextBox binding

EDIT:

I figured out that the source of this problem is the Equals() and GetHashCode() implementation in Bar.

Especially the properties (like Name in Bar) which participates in GetHashCode() and is also binded to a TextBox. On removing these override methods everything just works fine (except that I want to keep them)

What I don't understand is WHY is this happening??


I have a TextBox, a ListView, and some data bindings with following ViewModel:

[PropertyChanged.ImplementPropertyChanged]
public class ViewModel
{
    public ObservableCollection<Foo> Foos { get; set; }

    public Foo SelectedFoo { get; set; }
}

[PropertyChanged.ImplementPropertyChanged]
public class Foo
{
    public Bar FooBar { get; set; }
}

[PropertyChanged.ImplementPropertyChanged]
public class Bar
{
    public string Name { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as Bar;

        if (other != null)
        {
            return other.Name == Name;
        }

        return false;
    }

    public override int GetHashCode()
    {
        return Name.GetHashCode();
    }
}

This is my List:

<ListView x:Name="V_List" SelectedItem="{Binding SelectedFoo}"  ItemsSource="{Binding Path=Foos}" SelectionMode="Single">
    ...
</ListView>

And this is my TextBox:

<TextBox Text="{Binding Path=SelectedFoo.FooBar.Name, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />

And here's the thing:

When I select the first Foo from the List, the binding works, and Name of the FooBar property of selected Foo appears in the TextBox. No matter how many times I change my selection, the appropriate value appears in the TextBox.

But now if I change the Name using the TextBox (which, after focus lost, because of TwoWay data binding, is working and I checked with Debugging) and then change my selection from the list, the TextBox still shows the previously selected item's value.

Moreover, upon selecting that same item again, and then selecting some other item, I get the following exception (which surprisingly, the debugger didn't report, I had to log it to a file. Probably maybe because the exception was not raised from my code.)

Here's the log:

The Exception is:-Exception :: System.ArgumentException: An item with the same key has already been added. at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource) at System.Collections.Generic.Dictionary2.Insert(TKey key, TValue value, Boolean add) at System.Collections.Generic.Dictionary2..ctor(IDictionary2 dictionary, IEqualityComparer1 comparer) at System.Windows.Controls.Primitives.Selector.InternalSelectedItemsStorage..ctor(InternalSelectedItemsStorage collection, IEqualityComparer`1 equalityComparer) at System.Windows.Controls.Primitives.Selector.SelectionChanger.ApplyCanSelectMultiple() at System.Windows.Controls.Primitives.Selector.SelectionChanger.End() at System.Windows.Controls.Primitives.Selector.SetSelectedHelper(Object item, FrameworkElement UI, Boolean selected) at System.Windows.Controls.Primitives.Selector.NotifyIsSelectedChanged(FrameworkElement container, Boolean selected, RoutedEventArgs e) at System.Windows.Controls.Primitives.Selector.OnSelected(Object sender, RoutedEventArgs e) at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs) at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised) at System.Windows.UIElement.RaiseEventImpl(DependencyObject sender, RoutedEventArgs args) at System.Windows.UIElement.RaiseEvent(RoutedEventArgs e) at System.Windows.Controls.ListBoxItem.OnSelected(RoutedEventArgs e)
at System.Windows.Controls.ListBoxItem.OnIsSelectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) at System.Windows.DependencyObject.OnPropertyChanged(DependencyPropertyChangedEventArgs e) at System.Windows.FrameworkElement.OnPropertyChanged(DependencyPropertyChangedEventArgs e) at System.Windows.DependencyObject.NotifyPropertyChange(DependencyPropertyChangedEventArgs args) at System.Windows.DependencyObject.UpdateEffectiveValue(EntryIndex entryIndex, DependencyProperty dp, PropertyMetadata metadata, EffectiveValueEntry oldEntry, EffectiveValueEntry& newEntry, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType) at System.Windows.DependencyObject.SetValueCommon(DependencyProperty dp, Object value, PropertyMetadata metadata, Boolean coerceWithDeferredReference, Boolean coerceWithCurrentValue, OperationType operationType, Boolean isInternal) at System.Windows.DependencyObject.SetCurrentValueInternal(DependencyProperty dp, Object value) at System.Windows.Controls.ListBox.NotifyListItemClicked(ListBoxItem item, MouseButton mouseButton) at System.Windows.Controls.ListBoxItem.HandleMouseButtonDown(MouseButton mouseButton) at System.Windows.Controls.ListBoxItem.OnMouseLeftButtonDown(MouseButtonEventArgs e) at System.Windows.UIElement.OnMouseLeftButtonDownThunk(Object sender, MouseButtonEventArgs e) at System.Windows.Input.MouseButtonEventArgs.InvokeEventHandler(Delegate genericHandler, Object genericTarget) at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target) at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs) at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised) at System.Windows.UIElement.ReRaiseEventAs(DependencyObject sender, RoutedEventArgs args, RoutedEvent newEvent) at System.Windows.UIElement.OnMouseDownThunk(Object sender, MouseButtonEventArgs e) at System.Windows.Input.MouseButtonEventArgs.InvokeEventHandler(Delegate genericHandler, Object genericTarget) at System.Windows.RoutedEventArgs.InvokeHandler(Delegate handler, Object target) at System.Windows.RoutedEventHandlerInfo.InvokeHandler(Object target, RoutedEventArgs routedEventArgs) at System.Windows.EventRoute.InvokeHandlersImpl(Object source, RoutedEventArgs args, Boolean reRaised) at System.Windows.UIElement.RaiseEventImpl(DependencyObject sender, RoutedEventArgs args) at System.Windows.UIElement.RaiseTrustedEvent(RoutedEventArgs args) at System.Windows.UIElement.RaiseEvent(RoutedEventArgs args, Boolean trusted) at System.Windows.Input.InputManager.ProcessStagingArea() at System.Windows.Input.InputManager.ProcessInput(InputEventArgs input) at System.Windows.Input.InputProviderSite.ReportInput(InputReport inputReport) at System.Windows.Interop.HwndMouseInputProvider.ReportInput(IntPtr hwnd, InputMode mode, Int32 timestamp, RawMouseActions actions, Int32 x, Int32 y, Int32 wheel) at System.Windows.Interop.HwndMouseInputProvider.FilterMessage(IntPtr hwnd, WindowMessage msg, IntPtr wParam, IntPtr lParam, Boolean& handled) at System.Windows.Interop.HwndSource.InputFilterMessage(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled) at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled) at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o) at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs) at System.Windows.Threading.ExceptionWrapper.TryCatchWhen(Object source, Delegate callback, Object args, Int32 numArgs, Delegate catchHandler)

Another thing worth mentioning that I noticed is, the exception is not raised if I set the SelectionMode of ListView to Multiple. In that case, the item in List whose FooBar's value I changed from the TextBox, remains selected, along with any other items that I may select then after.

Note: I am using Fody/PropertyChanged to implement INotifyCollectionChanged.

Upvotes: 2

Views: 754

Answers (2)

Jacob Proffitt
Jacob Proffitt

Reputation: 12768

What I don't understand is WHY is this happening?

I can't give you a definitive answer, but looking at the error, it looks like the ListView is keeping an internal Dictionary of the control's items. Further, I'd bet the key for that internal dictionary is based on the value of GetHashCode. The problem is that you are allowing your users to change that key (by changing the name property) while the object is still part of the control. I'd guess that on selection changing it's maintaining the dictionary by adding and removing items as necessary. Since the effective "key" for your item has changed, it may be trying to readd it to the internal dictionary, only to find that the item is already in the dictionary (but under the original key).

You can test that theory by removing and readding the changed item to the Foos collection when the name changes. That should clear and reinsert the item in the internal dictionary with the correct, new key.

Upvotes: 3

Brandon Kramer
Brandon Kramer

Reputation: 1118

Bar also needs to implement PropertyChanged, if you want Name to be updated.

[PropertyChanged.ImplementPropertyChanged]
public class Bar
{
    public string Name { get; set; }
}

Also, it is generally preferable to have the selected item binding done via a property on your VM.

For example:

[PropertyChanged.ImplementPropertyChanged]
public class ViewModel
{
    public ObservableCollection<Foo> Foos { get; set; }
    //This is the property to hold the selected item.
    public Foo SelectedFoo { get; set; }
}

Then change your ListView binding to:

<ListView x:Name="V_List" SelectedItem="{Binding SelectedFoo}" ItemsSource="{Binding Path=Foos}" SelectionMode="Single">
    ...
</ListView>

And your TextBox becomes:

<!--No need for binding the DataContext of the Grid.-->
<Grid>
    <TextBox Text="{Binding Path=SelectedFoo.FooBar.Name, Mode=TwoWay, UpdateSourceTrigger=LostFocus}" />
</Grid>

Upvotes: 2

Related Questions