Help With Old CancelEdit Problem

Help With Old CancelEdit Problem

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


Ian posted on Thursday, February 26, 2009

Sorry about reviving an old topic again, but I just can't seem to get it into my head why this won't work.  It's regarding deleting an item from a child collection, executing CancelEdit(), but not seeing the item return to its original position in the list.  The only old thread I found that directly addresses this is this year old post http://forums.lhotka.net/forums/thread/19023.aspx where Rocky explains that it is too complicated.  I've revisited the examples in Chapter 13 where he shows how complicated it can be, but I've convinced myself that it's not that difficult to fix.  Maybe I'm missing a critical concept here, but the following possible solution (with extremely little code) does appear to work with his examples. 

Suppose 2 methods in BusinessListBase were modified thus:

protected override void InsertItem(int index, C item)

{

// set parent reference

item.SetParent(this);

// set child edit level

Core.UndoableBase.ResetChildEditLevel(item, this.EditLevel, false);

// when an object is inserted we assume it is

// a new object and so the edit level when it was

// added must be set

item.EditLevelAdded = _editLevel;

InsertIndexItem(item);

base.InsertItem(index, item);

item.OriginalIndex = this.IndexOf(item);   < -- extra line

InsertIntoMap(item, index);

}

private void UnDeleteChild(C child)

{

// since the object is no longer deleted, remove it from

// the deleted collection

DeletedList.Remove(child);

// we are inserting an _existing_ object so

// we need to preserve the objects editleveladded value

// because it will be changed by the normal add process

int saveLevel = child.EditLevelAdded;

int i = this.Count;

while (i > 0 && (child.OriginalIndex < this[i-1].OriginalIndex ))

{

i--;

}

Insert(i, child);

InsertIndexItem(child);

//Add(child);

child.EditLevelAdded = saveLevel;

}

An extra property, OriginalIndex , would have to be added to each child object.  Using this property, it does seem clear as to where undeleted items need to be reinserted. 

Using the example on page 389 of the 2008 book, after items A, B and C get removed, then we call CancelEdit(), C (OriginalIndex = 2) is the only item in the list at index 0.  CancelEdit again undeletes item B (OriginalIndex =1).  Since B's OriginalIndex < C's, B is inserted before C instead of index [1] where is used to be.  This insures that B always gets placed back into the list ahead of C.  Likewise, a 3rd CancelEdit() will put A at the beginning of the list. 

I know Rocky mentioned how arbitrary Adds, Deletes and Clears make this a chore to do, but I don't see it.  At the time a CancelEdit needs to Undelete an object, all of the Adds, Deletes and Clears that happened after the initial deletion have been rolled back, so they are no longer relevent.  The list is in the exact same state it was when the item was deleted in the first place, so it IS possible to put it back where it came from.

Am I missing something here?  This seems much too simple of a solution to be overlooked for so long.  Can someone please provide an example where this doesn't work?  I would very much like to see this happen.

Regards,

Ian

Ian replied on Thursday, February 26, 2009

Before you restate that the solution is to use SortedBindingList, I'd like to first point out why this won't work for my scenario. 

SortedBindingList requires that each object has an editable field to sort by, eg LineNumber.  As items are deleted, added, undeleted, the developer has to write code that cycles through all of the items in the list, and maintain this field so that each object's LineNumber field is in sync.  It's a pain and I've never liked it.  Wouldn't it be easier if something already existed to do this chore for us?  As luck would have it, there is!  BusinessListBase already takes care of this for us.  Instead of maintaining my own field, I do the following:

private int _origLineNumber;

public int LineNumber

{

get

{

QuoteItems parentList = this.Parent as QuoteItems;

return parentList.IndexOf(this) + 1;

}

}

public override bool IsDirty

{

get

{

QuoteItems parentList = this.Parent as QuoteItems;

if (parentList != null)

if (LineNumber != _origLineNumber)

return true;

return base.IsDirty;

}

}

The LineNumber field is directly linked to the item's position in its parent's collection.  You can delete, add and cancel records in a grid with reckless abandon, and the LineNumber is always kept up to date, with no maintenance on my part.  It just simply works.  Of course, when originally loading the collection, you have to ORDER BY LineNumber when you query the database to insure the items are in the correct order in the first place.

The origLineNumber is set once, when the child object is first created from database values.  The only place it's used is to determine if the object is dirty.  Even if no data has been changed, if it's position in the collection has changed, it needs to be written.

I really like this code.  It's far better than maintaining my own CurrentLineNumber field, imo.  However, using a SortedBindingList will not solve the problem of sorting items in the list because it will be sorting using the item's index anyway, which is out of sync when an item gets undeleted.

Thoughts?

Ian replied on Monday, March 02, 2009

cricket

RockfordLhotka replied on Monday, March 02, 2009

You are correct, the order could be preserved within some margin of error. It would work fine if no other items were added or removed. If any other items had been added or removed there'd be ordering issues based on when items were un-added and un-removed.

In other words, it is relatively straightforward to fix the simple problem of un-removing one item, but it gets hard from there. Fixing the simple problem would just mean I'd have someone else (or maybe you :) ) coming back a little while later asking for the "real" fix.

Consider the case where two items are removed, both from position 3 (an arbitrary number).

Then un-remove them, and you'll find that the order in which they are un-removed matters, or they'll swap positions in the list.

Again, a solvable problem - probably using some sort of fix-up on either the remove or un-remove.

But it gets even more fun if two items are removed from position 3, and then a couple other items are inserted into positions 0 to 2. Now it matters whether items are un-added or un-removed first, because "position 3" has actually shifted down to some lower position in the list.

And then add in multi-level undo, where there are nested Begin/CancelEdit() steps with intermixed add/remove operations.

If position matters to the business domain, then position should be a property of the child object, and you really can use SortedBindingList or a LINQ query to ensure that the items appear in positional order.

Ian replied on Thursday, March 05, 2009

I understand what you're saying, but I think the reason your current logic doesn't work is that you're concerned with the position of deleted items at the time they are deleted, rather than at the time they are inserted into the list

You say, 2 items are removed from position 3.  Yes, but only one was at position 3 when the list was loaded.  The second item must have been at position 4, only moving to position 3 when the original position 3 item was deleted.  When the list is loaded from the database, item[3].OriginalIndex = 3, whereas item[4].OriginalIndex = 4.  Using these OriginalIndex values, it is clear which order these 2 items should be in after undeletion, regardless of the order they were undeleted.

Example #2, 2 are items deleted from position 3, 2 items are inserted into posistions 0 to 2.  "Position 3" has moved down the list.  Of course it matters which order the un-added or un-removed happens, but this order is predictable.  You roll back the main list first, then the deleted list.  So the items get un-added first, returning "position 3" back to its original posistion, then 2 items get undeleted, once again using their OriginalIndex to determine insertion order.  In combination with OriginalIndex, the edit level of the item that was added to the list can insure that, say, an item with OriginalIndex =3 at edit level 1, always gets inserted before and item with OriginalIndex=3 at edit level 2.

I don't _think_ N-level undo should cause a problem.  Any given edit level, say edit level 3, can predict its undo order.  Newly added items get removed first, deleted items get restored to their OriginalIndex position, coming after an item with the same OriginalIndex but an earlier edit level.  So by the time edit level 2 gets rolled back, the list should be the same as it was before a BeginEdit() caused edit level 3 to happen.  So on and so forth until the edit level that started the CancelEdit().

Unless there is a complex example that I haven't though of yet, I still think this is doable.  Whether it's worth it for you is a different question.  One of these days I'd like to tinker with the CSLA source code to see if I can get this to work.  It might be a while though.  Very unfortunately, software development is not my primary job so I get pulled away from it quite often.  I wish I got paid to do this all day every day, but I don't.  But hopefully one of these days I can bring something more concrete to the table.

Copyright (c) Marimer LLC