KeyNotFoundException occurring in PositionMap<T> class.

KeyNotFoundException occurring in PositionMap<T> class.

Old forum URL: forums.lhotka.net/forums/t/6802.aspx


Alva posted on Thursday, April 16, 2009

I am writing an application that keeps a collection of business objects in a collection that inherits the BusinessListBase class.  The name of this collection is ContactCollection.

I am receiving a KeyNotFoundException in the PositionMap<T> class in the file named PositionMap.cs.  This exception happens in the RemoveFromMap(T item) method of the class at the following instruction: '_map[_list[ i ]]--;'

An inspection of the '_map' variable (declared as Dictionary<T,int> _map) and the _list variable (declared as _Ilist<T> _list) using a debugger reveal that the objects in _map do not map properly to the object index positions in _list at the time of the KeyNotFoundException.  The exception seems to occur when an attempt is made to find the business object in _map that was just deleted from _map (and therefore in not in _map).

The objects in _map map properly to the object index positions in _list when the ContactCollection is first populated with data residing on disk.  After its initial population, the ContactCollection is bound to a control that is a derivative of the DataGridView control.  A sort occurs through this control on a column that contains non-unique values before any deletions from the ContactCollection occur.  It is my belief that _map and _list get out of sync when this sort occurs.

My question is: How do I get _map to stay in sync with _list when performing a sort on the ContactCollection object (or any object that inherits BusinessListBase)?

Any help on this issue is appreciated.

Alva

RockfordLhotka replied on Thursday, April 16, 2009

What version of CSLA? Several bugs were fixed in this area a while ago, so if you aren't running the current code, this might explain the issue.

Alva replied on Wednesday, April 22, 2009

I'm using version 3.6.2.

RockfordLhotka replied on Thursday, April 23, 2009

I've asked Aaron to take a look at this, hopefully he can find a resolution.

AaronErickson replied on Thursday, April 23, 2009

Hi there.

I went ahead and tried to reproduce this in WinForms - and was unable to using the older means of creating a SortedBindingList from the BLB derived class.

I must assume that you are then binding to a LinqBindingList of some type. If that is the case, I will need something to go by here so I can get to your specific case.

1.) In the best of all worlds, if you can provide us with a failing unit test that makes the exception happen - we can get to fixing the framework code pretty quickly (and as an added bonus, your test will go into the test suite).

2.) If that is not possible - if you can give us some sample code you have in your derived classes - especially the derived list, we might be able to re-create this condition.

Thanks!

-Aaron

Alva replied on Tuesday, April 28, 2009

     I believe that I may have found the answer to my problem.  I was not using a SortedBindingList or a LinqBindingList when binding my collection to the data grid view.  My collection class inherited the BusinessListBase class and after creating it and loading it I would bind it directly to the data grid view without creating a SortedBindingList or a LinqBindingList class.  I came across a section in "C# 2008 Business Objects" that states that the CSLA list base classes do not support sorting through the IBindingList interface (Page 73 last paragraph).

    When I created a SortedBindingList from my object, and bound that to my data grid view, the KeyNotFoundException that I had been receiving went away.

    Thank you for your help and your patience.

-Alva

tchimev replied on Friday, April 24, 2009

I had the same problem.
The _map and _list collection get out of sync if you provide wrong implementation for GetHashCode() and Equals() in your business objects. So two business objects cannot compare correctly to each other.


RockfordLhotka replied on Friday, April 24, 2009

Oh is that the problem?

 

Prior to version 3.0 CSLA supported (and almost required) the concept of logical equality where you’d override GetIdValue() and CSLA would override Equals() and GetHashCode() for you.

 

Starting with CSLA 3.0 that stopped. The reason is that WPF (and Silverlight) don’t support the concept of logical equality. I think this was a bad choice on Microsoft’s part, but it is their platform and we have to live with it.

 

What this means, is that you can’t allow Equals() to say that two instances are the same – even if those two instances really are logically the same.

 

So starting with CSLA 3.0, support for logical equality has not been a design goal within CSLA either. If you use logical equality, you can never move to WPF or Silverlight. Since those are the UI technologies of the future, my view is that you should not use logical equality, as it will ultimately dead-end your app.

 

Now if you really need logical equality, I understand. It is a valuable concept. And you can use it, just not by overriding Equals().

 

Instead, if you need logical equality, you should define and implement a new interface such as ILogicalEquality

 

public interface ILogicalEquality

{

  bool LogicalEquals(object obj)

}

 

These days, with extension methods, you can make this all pretty smooth, by adding extensions to any objects that implement this interface.

 

But the point is, you need to invent your own comparison subsystem separate from the .NET one because the .NET concepts of equality will not work with logical equality.

 

Rocky

 

 

From: tchimev [mailto:cslanet@lhotka.net]
Sent: Friday, April 24, 2009 7:08 AM
To: rocky@lhotka.net
Subject: Re: [CSLA .NET] KeyNotFoundException occurring in PositionMap<T> class.

 

I had the same problem.
The _map and _list collection get out of sync if you provide wrong implementation for GetHashCode() and Equals() in your business objects. So two business objects cannot compare correctly to each other.




bgilbert replied on Monday, May 11, 2009

Rocky,

I'm struggling with this exception as well and I'm not sure I understand how to implement what you've described.

I have a BusinessListBase-derived collection in which I've defined a Sort method that does a bubble sort like this:
        For i As Integer = Me.Count - 1 To 0 Step -1
            For j As Integer = 1 To i
                '   Use custom CompareTo for comparison.
                If Me(j - 1).CompareTo(Me(j)) = 1 Then
                    tempObj = Me(j - 1)
                    Me(j - 1) = Me(j)
                    Me(j) = tempObj
                End If
            Next j
        Next i

It implements IComparable with a custom CompareTo method. When it hits Me(j - 1) = Me(j), it fails inside the InsertIntoMap method in PositionMap. My BusinessBase class overriddes GetIdValue with the Id of the object. At first I thought it may be because the Id is an integer and I had the potential for several zeros in my dirty collection, so I initialized the Id with a random integer. This didn't help.

I'm not sure if I understand your previous comment about logical equality and what the solution is.

Thanks for your help,
Barry

RockfordLhotka replied on Monday, May 11, 2009

If you have a small sample using 3.6.2 that illustrates the issue I'm sure Aaron would find that useful.

In 3.6.2 overriding GetIdValue() really has no impact, because CSLA doesn't replace the default Equals() or GetHashCode() implementations. I think GetIdValue() just changes the default ToString() implementation and that's it.

bgilbert replied on Tuesday, May 12, 2009

Here's some code that may be useful in debugging the issue. This is obviously incomplete.
These classes are based on our own base classes, which in turn derive from BusinessBase and BusinessListBase. These also implement ITypedList. This code was originally written against CSLA 2.0, but we're now using 3.6.1.

<Serializable()> _
Public Class EmployeeEvents
    Inherits KaizenBusinessListBase(Of EmployeeEvents, EmployeeEvent)
    Protected Friend Sub Sort()
        Dim tempEvent As EmployeeEvent = Nothing
        RaiseListChangedEvents = False
        For i As Integer = Me.Count - 1 To 0 Step -1
            For j As Integer = 1 To i
                '   Use custom CompareTo for comparison.
                If Me(j - 1).CompareTo(Me(j)) = 1 Then
                    tempEvent = Me(j - 1)
                    Me(j - 1) = Me(j)
                    Me(j) = tempEvent
                End If
            Next j
        Next i
        RaiseListChangedEvents = True
        ResetBindings()
        OnCollectionSorted()
    End Sub
'Standard DataPortal stuff here
End Class

<Serializable(), _
TypeDescriptionProvider(GetType(EmployeeEventsDescriptionProvider))> _
Public Class EmployeeEvent
    Inherits KaizenBusinessBase(Of EmployeeEvent)
    Implements IComparable(Of EmployeeEvent)
   
    Protected _id As Integer
    Protected _timeStart As DateTime = Nothing
    Protected _timeEnd As DateTime = Nothing

    <System.ComponentModel.Browsable(False)> _
    <System.ComponentModel.DataObjectField(True, True)> _
    <Description("Id")> _
    Public Property Id() As Integer    <System.Runtime.CompilerServices.MethodImpl(Runtime.CompilerServices.MethodImplOptions.NoInlining)> _
        Get
            CanReadProperty("Id", True)
            Return _id
        End Get    <System.Runtime.CompilerServices.MethodImpl(Runtime.CompilerServices.MethodImplOptions.NoInlining)> _
        Set(ByVal value As Integer)
            If _id <> value Then
                CanWriteProperty("Id", True)
                _id = value
                PropertyHasChanged("Id")
            End If
        End Set
    End Property
<Description("Start time")> _
    Public Property TimeStart() As DateTime
        <System.Runtime.CompilerServices.MethodImpl(Runtime.CompilerServices.MethodImplOptions.NoInlining)> _
        Get
            CanReadProperty("TimeStart", True)
            Return _timeStart
        End Get
        <System.Runtime.CompilerServices.MethodImpl(Runtime.CompilerServices.MethodImplOptions.NoInlining)> _
        Set(ByVal value As DateTime)
            If Not _timeStart.Equals(value) Then
                CanWriteProperty("TimeStart", True)
                _timeStart = value
                If Parent IsNot Nothing Then
                    ' In order to evaluate certain rules, the existing collection must first be sorted by TimeStart.
                    CType(Me.Parent, EmployeeEvents).Sort()
                End If
                PropertyHasChanged("TimeStart")
            End If
        End Set
    End Property

    <Description("End time")> _
    Public Property TimeEnd() As DateTime        <System.Runtime.CompilerServices.MethodImpl(Runtime.CompilerServices.MethodImplOptions.NoInlining)> _
        Get
            CanReadProperty("TimeEnd", True)
            Return _timeEnd
        End Get      <System.Runtime.CompilerServices.MethodImpl(Runtime.CompilerServices.MethodImplOptions.NoInlining)> _
        Set(ByVal value As DateTime)
            _timeEndNull = (_timeEnd = DateTime.MinValue)
            If Not _timeEnd.Equals(value) Then
                CanWriteProperty("TimeEnd", True)
                _timeEnd = value
                PropertyHasChanged("TimeEnd")
                If value > DateTime.MinValue Then
                    If Not CloseEvent(_timeEnd) Then
                        Me.CancelEdit()
                        MarkClean()
                    End If
                End If
            End If
        End Set
    End Property

    Public Function CompareTo(ByVal other As EmployeeEvent) As Integer _
      Implements System.IComparable(Of EmployeeEvent).CompareTo
        If other Is Nothing Then
            Return 1
        ElseIf _timeStart < other.TimeStart Then
            Return -1
        ElseIf _timeStart > other.TimeStart Then
            Return 1
        ElseIf _timeStart = other.TimeStart AndAlso _timeEnd < other.TimeEnd Then
            Return -1
        ElseIf _timeStart = other.TimeStart AndAlso _timeEnd > other.TimeEnd Then
            Return 1
        ElseIf other.TimeStart = _timeStart And other.TimeEnd = _timeEnd Then
            Return 0
        End If
    End Function

' Standard data_portal stuff, factory methods, validation rules, and type descriptors here
End Class

Thanks for any help you can offer.

Barry

RockfordLhotka replied on Tuesday, May 12, 2009

The problem is that we’ve tried to replicate this issue and can’t. So at this point we’re only debugging if someone provides us with a complete failure example.

 

Rocky

 

bgilbert replied on Thursday, May 14, 2009

Aaron,

I've created a stripped down app and db that reproduces this issue. I've uploaded the stuff here. Let me know when you want to look at this and I can tell you how to set this up and reproduce the error.

Barry

Copyright (c) Marimer LLC