Troubleshooting WPF DataGrid Row Selection Errors in .NET Framework

Table of Contents

WPF DataGrid Selection Error

This article details a common issue encountered in Windows Presentation Foundation (WPF) applications using the DataGrid control, specifically when selecting rows. It addresses the System.ArgumentException that may occur and provides a comprehensive understanding of its root cause and resolution within the context of the .NET Framework 4.5 and later versions. Understanding the interaction between custom data objects and WPF’s internal mechanisms is key to resolving this problem.

The DataGrid control is a powerful component for displaying and editing tabular data in WPF applications. Developers often bind its ItemsSource property to a collection of custom business objects. This allows the DataGrid to automatically generate columns and display data based on the properties of the objects in the collection. However, when certain patterns are followed in defining these custom objects, issues can arise, particularly related to object identity and hashing.

Symptoms

When developing an application targeting Microsoft .NET Framework 4.x that utilizes the WPF DataGrid control, you might encounter an exception during user interaction. This scenario typically occurs after a user has modified data within a row presented in the DataGrid. Subsequently, when the user attempts to select a different row, the application unexpectedly throws a System.ArgumentException.

The specific error message associated with this exception is “An item with the same key has already been added.” This indicates a conflict when trying to insert an element into a collection that does not allow duplicate keys, such as a Dictionary or a HashTable. The issue is specifically observed when running the application on systems where Microsoft .NET Framework 4.5 or a later version is installed. Applications running on earlier versions of the framework might not exhibit this behavior, even with the same application code and data objects.

The exception is often accompanied by a stack trace similar to the following:

System.ArgumentException was unhandled
HResult=-2147024809
Message=An item with the same key has already been added.
Source=mscorlib
StackTrace:
   at System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   at System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   at System.Collections.Generic.Dictionary`2..ctor(IDictionary`2 dictionary, IEqualityComparer`1 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.SelectedItemCollection.EndUpdateSelectedItems()
   at System.Windows.Controls.Primitives.MultiSelector.EndUpdateSelectedItems()
   at System.Windows.Controls.DataGrid.MakeFullRowSelection(ItemInfo info, Boolean allowsExtendSelect, Boolean allowsMinimalSelect)

This stack trace points towards operations happening within the System.Windows.Controls.Primitives.Selector and System.Windows.Controls.DataGrid classes, specifically during the process of managing selected items. The presence of Dictionary and HashTable operations within the stack trace is a strong indicator of the underlying problem related to key management in hash-based collections. The issue arises during selection because the Selector needs to identify and track selected items efficiently, and in .NET 4.5+, it relies more heavily on hash-based data structures for this purpose.

Cause

The root cause of this issue lies in how the custom object type used as the ItemsSource for the DataGrid is defined, specifically regarding its implementation of the Object.GetHashCode method. It is common practice to override GetHashCode when also overriding Object.Equals to maintain consistency between object equality and hash code generation. However, if the custom object’s type definition includes an overridden GetHashCode method that calculates the hash code based on mutable properties—properties whose values can change after the object is created—a problem can arise.

When a mutable property, which was used to calculate the object’s initial hash code, is modified (e.g., through DataGrid editing), the result of calling GetHashCode on that object again will yield a different hash code. This violates a fundamental contract of GetHashCode: if two objects are equal according to the Equals method, they must return the same hash code. While the reverse is not strictly required (different objects can have the same hash code, leading to collisions), the hash code for a single object should ideally remain constant throughout its lifetime if it’s ever going to be used as a key in a hash-based collection.

This application bug, related to an incorrectly implemented GetHashCode, existed in earlier versions of the .NET Framework as well. However, it was not exposed in this specific scenario because the WPF Selector class’s internal implementation for managing selected items in versions prior to .NET Framework 4.5 relied less heavily on hash-based collections like Dictionary or HashTable.

Starting with Microsoft .NET Framework 4.5, performance optimizations were introduced in the WPF Selector class. These optimizations involved making more extensive use of HashTable and Dictionary to improve the efficiency of operations like tracking selected items, especially in scenarios with large collections. When an object with a mutable hash code is used as a key in such a collection, and its relevant property changes after it has been added to the collection, the object can no longer be correctly located using its original hash code. The Selector’s logic might then attempt to re-add or update the object’s information in its internal data structures using the new hash code. This process can lead to the system trying to insert an entry into a dictionary with a key that already exists (either the original key is still present in some form, or the logic gets confused), resulting in the System.ArgumentException.

Essentially, the problem is caused by the user’s custom object violating the GetHashCode contract by relying on mutable state, and this underlying issue is triggered and exposed by the change in WPF’s internal data structure usage in .NET 4.5+.

Resolution

The resolution to this System.ArgumentException is to correct the implementation of the custom object’s type definition, specifically its overridden GetHashCode method. The GetHashCode implementation must adhere to the general guidelines for overriding this method, the most critical being that the hash code should remain constant for the object’s lifetime if the object is used as a key in a hash table or if its equality is based on fields that might change.

If the custom object’s equality (Equals method) relies on certain properties, the GetHashCode method should compute the hash based on the same properties. However, if those properties are mutable, this creates a conflict. The recommended practice is to compute the hash code based only on immutable fields or properties of the object. If all relevant fields for equality are mutable, this might indicate a design issue with the object itself if it’s intended to be used in hash-based lookups after mutation.

Consider an object representing a user with an Id (immutable) and a Name (mutable). If equality is based only on Id, GetHashCode should use only Id. If equality is based on both Id and Name, and Name can change, using instances of this object as keys in dictionaries or in scenarios relying on stable hash codes (like WPF’s selection logic in .NET 4.5+) is problematic.

If the mutable properties must be included in the equality comparison, and thus in the hash code calculation, and the object must be used in scenarios requiring stable hash codes (like being an item in a bound collection where selection is tracked), then the object’s design is fundamentally incompatible with mutable-state-based hashing. In such cases, you might need to consider alternative approaches, such as:

  1. Making the relevant properties immutable: If possible, change the design of the object so that the properties used for equality (and thus hashing) are set only during object creation and cannot be changed afterward.
  2. Using a wrapper object: If you cannot modify the original class (e.g., it’s from a library), you can create a wrapper class that contains an instance of the original object. The wrapper class would have an immutable identifier or calculate its hash code based on a stable representation of the wrapped object’s state at the time it was added to the collection. This wrapper object is then used in the collection bound to the DataGrid.
  3. Re-evaluating the need for mutable data in place: Can the editing process create a new object instead of modifying the existing one in the collection? This is often complex with data binding but might be viable in some architectures.

The simplest and most direct solution is typically to fix the GetHashCode implementation in the custom object type to ensure it produces a consistent hash code for the object’s lifetime, ideally by relying only on immutable components.

Let’s look at an example. Suppose you have a class:

public class MyDataItem
{
    public int Id { get; set; } // This should ideally be immutable for hashing
    public string Name { get; set; } // This is mutable

    // ... other properties ...

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }
        var other = (MyDataItem)obj;
        // Assume equality is based on Id and Name
        return Id == other.Id && Name == other.Name;
    }

    public override int GetHashCode()
    {
        // BAD implementation if Name can change!
        // Hashes based on mutable property 'Name'
        return (Id.GetHashCode() * 397) ^ Name.GetHashCode();
    }
}

In this example, if you change the Name property of a MyDataItem object while it is in the collection bound to the DataGrid, its hash code will change. This is the source of the problem.

A correct GetHashCode implementation for this class, assuming Id uniquely identifies the object and is immutable, would be:

public class MyDataItem
{
    public int Id { get; } // Made Id immutable
    public string Name { get; set; } // Name is still mutable

    public MyDataItem(int id, string name)
    {
        Id = id;
        Name = name;
    }

    // ... other properties ...

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }
        var other = (MyDataItem)obj;
        // Assume equality is based only on Id (common for primary keys)
        return Id == other.Id;
        // OR if equality MUST include Name: return Id == other.Id && Name == other.Name;
        // In the latter case, using this object in a hash table where Name changes is problematic.
    }

    public override int GetHashCode()
    {
        // GOOD implementation (if Id is immutable and used for equality/identity)
        // Hashes ONLY based on immutable property 'Id'
        return Id.GetHashCode();
    }
}

In the revised example, Id is immutable, and GetHashCode is based only on Id. This ensures that the hash code for a given MyDataItem object never changes, even if its Name property is modified. This satisfies the contract required by hash-based collections used internally by WPF’s Selector in .NET 4.5+. If equality must include Name and Name is mutable, then MyDataItem is fundamentally unsuitable for scenarios requiring stable hash codes, and a different approach (like the wrapper) would be necessary.

More Information

Understanding the relationship between Equals, GetHashCode, and hash-based collections (Dictionary<TKey, TValue>, HashSet<T>) is crucial for avoiding issues like this. The .NET documentation provides clear guidelines:

  1. If you override Equals, you should also override GetHashCode.
  2. If Equals(object a, object b) returns true, then a.GetHashCode() must be equal to b.GetHashCode().
  3. The reverse is not required: if a.GetHashCode() equals b.GetHashCode(), a.Equals(object b) might return false.
  4. GetHashCode should return a consistent value for the lifetime of an object if the object is used as a key in a hash table or if the object’s equality is based on mutable fields. For immutable types, this is straightforward. For mutable types, this constraint is difficult to meet if equality depends on mutable state.

When a type computes its hash code based on mutable properties, and the value of one or more of those properties changes after an instance of that type has been added to a hash-based collection (like the internal structures used by WPF’s Selector in .NET 4.5+), the object’s hash code also changes.

Let’s trace what might happen in the DataGrid selection scenario:

  1. The DataGrid is bound to a collection of custom objects with a bad GetHashCode (based on mutable properties).
  2. WPF’s Selector internally tracks selected items, likely storing them or information about them in a hash-based structure (like a Dictionary mapping an item to its selection state or related data). When an item is selected, its hash code is computed and used to store data in this structure.
  3. The user edits a mutable property (e.g., Name) of a selected item directly in the DataGrid. This changes the object’s state.
  4. Because the Name property was used in the GetHashCode calculation, calling GetHashCode on the modified object now returns a different value than when it was initially added to the internal hash table.
  5. The user selects a different row. This action triggers WPF’s selection logic, which needs to update its internal state. It might iterate through the items, perform lookups in its internal hash table, or attempt to add/remove items.
  6. When WPF tries to access or update information about the modified object in its internal hash table using its new hash code, it might find nothing there (because it was stored using the old hash code).
  7. The WPF logic might then, depending on the exact operation (e.g., rebuilding a collection of selected items or updating internal indices), attempt to add information related to this object into a Dictionary using a key derived from the object.
  8. Since the object (or a related identifier) was perhaps already present in the dictionary’s key set (due to complex internal state management and the discrepancy between the old and new hash codes, potentially leaving a ‘stale’ entry or causing a collision in complex updates), attempting to insert it again with the same logical key results in the System.ArgumentException: An item with the same key has already been added.

The critical point is that the GetHashCode method must return a consistent value for the lifetime of the object if that object’s identity (used for hashing) is based on mutable state and it’s used in scenarios requiring stable hashes. This is why basing GetHashCode on mutable state is generally discouraged unless you are absolutely certain the object will not be mutated while it is participating in hash-based operations.

The change in WPF’s Selector in .NET 4.5+ did not create the bug; it merely exposed a pre-existing flaw in the custom object’s design. By adhering to the guidelines for overriding GetHashCode, developers can ensure their objects behave correctly when used in scenarios involving hash-based collections, whether explicitly in their own code or implicitly within framework components like WPF.

To further illustrate the concept of stable hash codes, consider using an immutable key field, like a unique identifier generated when the object is first created, as the basis for the hash code.

public class MyDataItemWithStableHash
{
    private readonly Guid _id = Guid.NewGuid(); // Immutable identifier
    public string Name { get; set; } // Mutable property
    public int Value { get; set; } // Mutable property

    // Properties for data binding
    public string DisplayName => Name;
    public int DisplayValue => Value;

    // ... other properties ...

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }
        // Equality based on the immutable identifier
        var other = (MyDataItemWithStableHash)obj;
        return _id == other._id;
    }

    public override int GetHashCode()
    {
        // Hash code based ONLY on the immutable identifier
        return _id.GetHashCode();
    }
}

In this refined design, the object’s identity, as determined by Equals and GetHashCode, is based solely on the immutable _id field. Changes to mutable properties like Name or Value do not affect the hash code or the object’s equality comparison. This makes instances of MyDataItemWithStableHash safe to use as keys in hash-based collections, resolving the System.ArgumentException in the WPF DataGrid scenario. DataGrid binding can still work with the mutable Name and Value properties for display and editing, but the underlying object tracking mechanism (Selector) relies on the stable _id and its corresponding hash code.

The issue highlights the importance of understanding the contracts of base class methods like Equals and GetHashCode and considering how object mutability interacts with collection types, especially in modern framework versions that leverage performance optimizations.

This problem is not unique to WPF DataGrid; any scenario where a mutable object is used as a key in a Dictionary, HashSet, or other hash-based collection, and the properties used for hashing change while the object is in the collection, can lead to similar issues. The WPF DataGrid merely provides a common context where developers encounter this due to data binding and the framework’s internal implementation details.

By ensuring your custom data objects provide a stable and correct implementation of GetHashCode that aligns with their definition of equality, particularly when dealing with mutable state, you can avoid this System.ArgumentException and ensure your WPF applications function correctly on .NET Framework 4.5 and later versions. Pay close attention to which properties define an object’s identity and whether those properties can change after the object is created. This is a fundamental principle for writing robust applications that interact with collection types and data binding frameworks.

Were you able to resolve the issue by updating your GetHashCode implementation? Share your experience or any alternative solutions you found in the comments below!

Post a Comment