Issue with BusinessListBase during special circumstance.

Issue with BusinessListBase during special circumstance.

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


rxelizondo posted on Monday, October 16, 2006

I have a collection that contains objects that define a SortOrder property. The purpose of this SortOrder property is to let the users of the collection know what the position of the objects should when they are displayed on a list or report.

 

For example, the object with SortOrder = 1 should be listed before the object with SortOrder = 2 etc.

 

The user has the ability to delete any object from the collection but every time an object is deleted, all of the SortOrder values must be adjusted so that there are no gaps between them.

 

For example if a child object with SortOrder number = 2 is deleted then the child object with SortOrder = 3 will be modified to have its SortOrder = 2, the child object with SortOrder = 4 will be modified to have its SortOrder = 3 etc.

 

It should also be mentioned that the collection exposes a method that allows the user to change the sort order of the objects. This is basically accomplished by swapping two SortOrder values.

 

Ok, now let’s get to the problem. The BusinessListBase object exposes the Remove() and RemoveAt() inherited methods, as we all know these methods are there to allow you to delete collection objects. The problem with these methods is that they are public and are not virtual so there is no way to override them and add the necessary code on them to adjust the SortOrder values as these methods are being called.

 

In order to get around this limitation, I am overriding the “void RemoveItem(int index)” method of the collection and adding all the necessary code to do the adjustment to the SortOrder values there.

 

For the most part, everything works pretty well except that when the time comes to do a CancelEdit all hell breaks loose. This is because while the undoing is happening, any objects marked as new in the collection is removed and in turn the RemoveItem method that I overrode is being called and I get errors because as the undoing is going on the state ob the object is undetermined.

 

Personally, I think this is a bug in the CSLA because in my opinion, none of the collection virtual methods should be fired when a CancelEdit is happening. This is because all that we are doing is restoring the previous state.

 

Of course, we all know that you can’t just whish for the methods you override not to be called so how are we supposed to solve this issue?

 

Well, I propose to seal all of these methods at the BusinessListBase so that they can’t be overridden:

 

protected virtual void ClearItems();

protected virtual void InsertItem(int index, T item);

protected virtual void RemoveItem(int index);

protected virtual void SetItem(int index, T item);

 

and create a new set of methods that can be overridden. After doing that , we should add code to the BusinessListBase class so that these methods are not called when a CancelEdit is being performed.

 

Does this make sense? Is this a real problem or am I going through an acid flashback?

 

Thanks.

cash_pat replied on Monday, October 16, 2006

    Public Overloads Sub Remove(ByVal TranID As Guid)

            For Each Obj As Transaction In Me
                If Obj.TransactionID = TranID Then

                    Dim Pos As Integer = Me.IndexOf(Obj)
                    Remove(Obj)

                    For i As Integer = Pos To Me.Count - 1
                        Me.Item(i).SortOrder = i + 1
                    Next

                   Exit For

                End If
            Next

        End Sub

        Protected Overrides Function AddNewCore() As Object

            Dim _child As Transaction = Transaction.NewChild
            _child.SortOrder = Me.Count + 1
        
            Add(_child)

            Return _child

        End Function

rxelizondo replied on Monday, October 16, 2006

cash_pat, I appreciate your response however, I wasn’t really looking for a workaround to do what I need to do since I already have a way to accomplish it.

 

The primary purpose of my post was to expose what I consider to be a flaw on the CSLA. What I would like to hear is opinion from other people regarding if my thoughts are somewhat right or wrong and if the CSLA should account for the problem I describe above.

 

Brian Criswell replied on Monday, October 16, 2006

Why are you sorting on some arbitrary numbers instead of values in the list?  What I mean is why are you sorting by a manually maintained sort order instead of a string, date, etc. in the list?

rxelizondo replied on Monday, October 16, 2006

The SortOrder property is what dictates the sorting of the collection objects in the UI, this values are there to allow the user to arrange the collection anyway he or she pleases and be able to persist the value so when they come back and see the same info the sorting of the collection object will remain the same.

 

If you are asking why am I not using the physical order of the collection as my sorting criteria is because there is no way (at least I don’t know of none) to be able to swap object on a collection, furthermore, the CSLA will not remember the physical position of an object on the list when the transaction is undone and a child objects has to be restored on the original collection.

 

Brian, do you agree with me that the methods:

 

protected virtual void ClearItems();

protected virtual void InsertItem(int index, T item);

protected virtual void RemoveItem(int index);

protected virtual void SetItem(int index, T item);

 

should not be fired during cancel edit? Think about it, when you are undoing things you are asking the CSLA to restore the object to a previous state so these event are not applicable during that time because while restoring takes place the object is on an undetermined state and you should not be running your typical business logic during that time.

 

Do you agree?

 

JoeFallon1 replied on Monday, October 16, 2006

From my Codesmith Templates I sometimes add these 2 methods if I have a field for LineNumber.

e.g. A list of invoice lines and each has a line number. I can Move an Item up or down the list by calling one of these methods.

===================================================================

 'method to move a selected child up one position in the collection (requires swapping and re-numbering.)
    Public Function MoveUp(ByVal obj As <%= ItemName %>) As Integer
      If Not Contains(obj) Then Throw New Exception("Item does not exist in the List")
      Dim position As Integer = IndexOf(obj)
      'do not move it up if it is already the first item
      If position > 0 Then
        'store a reference to the previous item
        Dim prevItem As <%= ItemName %> = Me.Item(position - 1)
        'avoid having the same object in both locations in our List
        Me.Item(position) = CType(New Object, <%= ItemName %>)
        'do the swap
        Me.Item(position - 1) = obj
        Me.Item(position) = prevItem
        'set the new positions in the objects
        prevItem.<%= LineNoFieldName %> = (position + 1)
        obj.<%= LineNoFieldName %> = (position)
        Return position - 1
      Else
        Return position
      End If
    End Function

===================================================================

    'method to move a selected child up one position in the collection (requires swapping and re-numbering.)
    Public Function MoveDown(ByVal obj As <%= ItemName %>) As Integer
      If Not Contains(obj) Then Throw New Exception("Item does not exist in the List")
      Dim position As Integer = Me.IndexOf(obj)
      'do not move it down if it is already the last item
      If position < (Count - 1) Then
        'store a reference to the next item
        Dim nextItem As <%= ItemName %> = Me.Item(position + 1)
        'avoid having the same object in both locations in our List
        Me.Item(position) = CType(New Object, <%= ItemName %>)
        'do the swap
        Me.Item(position + 1) = obj
        Me.Item(position) = nextItem
        'set the new positions in the objects
        nextItem.<%= LineNoFieldName %> = (position + 1)
        obj.<%= LineNoFieldName %> = (position + 2)
        Return position + 1
      Else
        Return position
      End If
    End Function

===================================================================

Joe

Michael replied on Wednesday, October 14, 2009

Hi Joe and everyone else

I've implemented the the move up and down, but I've had to access the protected Items collection in BLB to get it to work properly as suggested here: http://forums.lhotka.net/forums/thread/11842.aspx.

When the BLB is bound to binding source and I do the swap by indexing directly into the BLB via this[] I get EditLevelMismatchException on Save.

Is the Items solution acceptable? Are there any unwanted side effects with N-level undo etc?

Thanks for your help
Michael


Brian Criswell replied on Tuesday, October 17, 2006

My question was more along the line of whether or not there was some aspect of the data other than the SortOrder that you could base your sort on, but it seems like you have a use case for allowing users to set an arbitrary sort.  Personally, I would go more towards something that Joe has shown as it would work with N-Level undo instead of fighting it.  You will need to take a snapshot of the whole collection and not just a single item, because changing one item will shift the others.

I really think this is a case where you have a specific use case that requires you to add some functionality, but that modification should not be at the core of how BusinessListBase works.  I do not agree that those methods should not be called as they are central to how n-level undo works on BusinessListBase.  CancelEdit() calls the RemoveAt and Add methods on the base list.  BindingList<T> then calls InsertItem, RemoveItem, etc. to give you an opportunity to modify the behaviour of the Insert, Remove, etc.

CancelEdit() should put your list back the way it was before the corresponding ApplyEdit().  I think that all you really need is to know whether or not you are in a cancel and not adjust the SortOrder properties during a cancel.  If your list has a parent object, the parent could set the property on the list as to when it is in a cancel process.  Otherwise, you might have a small hack to make to BusinessListBase.UndoChanges to make it overrideable.  You can then have it set and clear whether it is in a cancel.

Brad Harrison replied on Tuesday, October 17, 2006

I accomplished the same thing in a couple different projects.  I created a property called "SeqNo" that is the same as your "SortOrder". Whenever "SeqNo" is changed in BaseObject, an event is raised to let the list know that something has changed.  This allows doing things like 
  list(1).SeqNo = 1 and the list resequences the items to move the specified object to the top of list. Not sure how this code would work in the canceledit situation. 

Here's the basics

In the BusinessBase object.....

        Public Event BeforeSeqNoChange(ByVal obj As BaseObject, ByRef NewSeqNo As Integer)
       
        Public Overridable Property SeqNo() As Integer
            Get
                Return mSeqNo
            End Get
            Set(ByVal Value As Integer)
                If mSeqNo <> Value Then
                    RaiseEvent BeforeSeqNoChange(Me, Value)
                    mSeqNo = Value
                    'Insert the Rule for the column here ..........
                    'BrokenRules.Assert("", "Rule for SeqNo has been Broken.", "SeqNo", False)
                    MarkDirty()
                End If
            End Set
        End Property

And in the BusinessBaseList object....

        Private Sub HandleSeqNoChange(ByVal obj As BaseObject, ByRef NewSeqNo As Integer)
            If mIgnoreSeqNoChanges Then Return
            '
            Dim ix As Integer
            Dim existingSeqNo As Integer
            If NewSeqNo < 1 Then NewSeqNo = 1
            If NewSeqNo > list.Count Then NewSeqNo = List.Count
            ' --- here we go
            mIgnoreSeqNoChanges = True  ' don't call the event every time we make a change here...
            existingSeqNo = obj.SeqNo
            obj.SeqNo = -2  ' so that we don't change it here....
            If NewSeqNo > existingSeqNo Then
                For ix = existingSeqNo + 1 To NewSeqNo
                    ItemBySeqNo(ix).SeqNo = ix - 1
                Next
            Else
                For ix = existingSeqNo - 1 To NewSeqNo Step -1
                    ItemBySeqNo(ix).SeqNo = ix + 1
                Next
            End If
            obj.SeqNo = NewSeqNo
            mIgnoreSeqNoChanges = False  ' look for SeqNo changes again
        End Sub

        ' same sort of thing also done on fetch..
       
Public Sub Add(ByVal obj As BaseObject)
            If Not Contains(obj) Then
                List.Add(obj)
            End If
            AddHandler obj.BeforeSeqNoChange, AddressOf HandleSeqNoChange
        End Sub

 

 

Copyright (c) Marimer LLC