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, 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.
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?
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
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