Skip to content

Commit 06862ad

Browse files
Fix issue with editlevel and clone (#4796)
* Fix edit level preservation when cloning object graphs with children When cloning an object graph during an active edit session, child objects were having their edit levels incorrectly reset. This occurred because InsertItem called UndoableBase.ResetChildEditLevel when adding deserialized children back to their parent collection, which cleared the child's already-restored state stack. The fix adds an _isDeserializing flag to BusinessListBase and BusinessBindingListBase that is set during OnSetChildren. When this flag is true, InsertItem skips the ResetChildEditLevel call, preserving the child's correctly deserialized edit level and undo state. This enables the clone-edit-save pattern where users can: - Begin editing an object - Clone it to show in a separate view - Continue editing or cancel on either the original or clone independently Co-Authored-By: Claude Opus 4.5 <[email protected]> * Update gitignore for claude * #4794 Add another unit test --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent c352e92 commit 06862ad

File tree

4 files changed

+241
-16
lines changed

4 files changed

+241
-16
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,4 @@ Source/Csla.Xaml.Uwp/project.lock.json
181181
/Samples/ProjectTracker/ProjectTracker.Ui.UWP/ProjectTracker.Ui.UWP.nuget.targets
182182
.vscode
183183
.idea
184+
.claude/settings.local.json

Source/Csla.test/BusinessListBase/BusinessListBaseTests.cs

Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,200 @@ static Root SimulateLocationTransfer(Root original)
266266
}
267267
}
268268

269+
[TestMethod]
270+
public void ClonePreservesChildEditLevel()
271+
{
272+
// Arrange: Create a root with children and begin editing
273+
var root = CreateRoot();
274+
var child = root.Children.AddNew();
275+
child.Data = "original";
276+
277+
root.BeginEdit();
278+
279+
// Make changes after BeginEdit
280+
child.Data = "modified";
281+
282+
// Verify edit levels before clone
283+
var rootEditLevel = ((Core.IUndoableObject)root).EditLevel;
284+
var childEditLevel = ((Core.IUndoableObject)child).EditLevel;
285+
var childListEditLevel = ((Core.IUndoableObject)root.Children).EditLevel;
286+
287+
Assert.AreEqual(1, rootEditLevel, "Root EditLevel should be 1 before clone");
288+
Assert.AreEqual(1, childEditLevel, "Child EditLevel should be 1 before clone");
289+
Assert.AreEqual(1, childListEditLevel, "ChildList EditLevel should be 1 before clone");
290+
291+
// Act: Clone the object graph
292+
var clonedRoot = root.Clone();
293+
294+
// Assert: Verify edit levels are preserved after clone
295+
var clonedRootEditLevel = ((Core.IUndoableObject)clonedRoot).EditLevel;
296+
var clonedChildEditLevel = ((Core.IUndoableObject)clonedRoot.Children[0]).EditLevel;
297+
var clonedChildListEditLevel = ((Core.IUndoableObject)clonedRoot.Children).EditLevel;
298+
299+
Assert.AreEqual(1, clonedRootEditLevel, "Cloned Root EditLevel should be 1");
300+
Assert.AreEqual(1, clonedChildEditLevel, "Cloned Child EditLevel should be 1");
301+
Assert.AreEqual(1, clonedChildListEditLevel, "Cloned ChildList EditLevel should be 1");
302+
303+
// Verify the modified data is preserved
304+
Assert.AreEqual("modified", clonedRoot.Children[0].Data, "Modified data should be preserved in clone");
305+
}
306+
307+
[TestMethod]
308+
public void ClonePreservesUndoStateForCancelEdit()
309+
{
310+
// Arrange: Create a root with children and begin editing
311+
var root = CreateRoot();
312+
var child = root.Children.AddNew();
313+
child.Data = "original";
314+
315+
root.BeginEdit();
316+
317+
// Make changes after BeginEdit
318+
child.Data = "modified";
319+
320+
// Act: Clone the object graph
321+
var clonedRoot = root.Clone();
322+
323+
// Cancel edit on the clone - this should restore the original value
324+
clonedRoot.CancelEdit();
325+
326+
// Assert: The cloned child should revert to original value
327+
Assert.AreEqual("original", clonedRoot.Children[0].Data, "CancelEdit on clone should restore original value");
328+
Assert.AreEqual(0, ((Core.IUndoableObject)clonedRoot).EditLevel, "Root EditLevel should be 0 after CancelEdit");
329+
}
330+
331+
[TestMethod]
332+
public void ClonePreservesUndoStateForApplyEdit()
333+
{
334+
// Arrange: Create a root with children and begin editing
335+
var root = CreateRoot();
336+
var child = root.Children.AddNew();
337+
child.Data = "original";
338+
339+
root.BeginEdit();
340+
341+
// Make changes after BeginEdit
342+
child.Data = "modified";
343+
344+
// Act: Clone the object graph
345+
var clonedRoot = root.Clone();
346+
347+
// Apply edit on the clone
348+
clonedRoot.ApplyEdit();
349+
350+
// Assert: The cloned child should keep the modified value
351+
Assert.AreEqual("modified", clonedRoot.Children[0].Data, "ApplyEdit on clone should keep modified value");
352+
Assert.AreEqual(0, ((Core.IUndoableObject)clonedRoot).EditLevel, "Root EditLevel should be 0 after ApplyEdit");
353+
354+
// Should be able to save without errors
355+
Assert.IsTrue(clonedRoot.IsDirty, "Clone should be dirty");
356+
clonedRoot = clonedRoot.Save();
357+
Assert.IsFalse(clonedRoot.IsDirty, "Clone should not be dirty after save");
358+
}
359+
360+
[TestMethod]
361+
public void ClonePreservesMultiLevelUndo()
362+
{
363+
// Arrange: Create a root with children
364+
var root = CreateRoot();
365+
var child = root.Children.AddNew();
366+
child.Data = "level0";
367+
368+
// First level of editing
369+
root.BeginEdit();
370+
child.Data = "level1";
371+
372+
// Second level of editing
373+
root.BeginEdit();
374+
child.Data = "level2";
375+
376+
// Verify edit level is 2 before clone
377+
Assert.AreEqual(2, ((Core.IUndoableObject)root).EditLevel, "Root EditLevel should be 2");
378+
Assert.AreEqual(2, ((Core.IUndoableObject)child).EditLevel, "Child EditLevel should be 2");
379+
380+
// Act: Clone the object graph
381+
var clonedRoot = root.Clone();
382+
var clonedChild = clonedRoot.Children[0];
383+
384+
// Assert: Edit levels preserved
385+
Assert.AreEqual(2, ((Core.IUndoableObject)clonedRoot).EditLevel, "Cloned Root EditLevel should be 2");
386+
Assert.AreEqual(2, ((Core.IUndoableObject)clonedChild).EditLevel, "Cloned Child EditLevel should be 2");
387+
388+
// Verify multi-level undo works
389+
Assert.AreEqual("level2", clonedChild.Data, "Should be at level2");
390+
391+
clonedRoot.CancelEdit();
392+
Assert.AreEqual("level1", clonedChild.Data, "After first CancelEdit should be at level1");
393+
Assert.AreEqual(1, ((Core.IUndoableObject)clonedRoot).EditLevel, "EditLevel should be 1");
394+
395+
clonedRoot.CancelEdit();
396+
Assert.AreEqual("level0", clonedChild.Data, "After second CancelEdit should be at level0");
397+
Assert.AreEqual(0, ((Core.IUndoableObject)clonedRoot).EditLevel, "EditLevel should be 0");
398+
}
399+
400+
[TestMethod]
401+
public void ClonePreservesChildAddedDuringEdit()
402+
{
403+
// Arrange: Create a root and begin editing before adding child
404+
var root = CreateRoot();
405+
406+
root.BeginEdit();
407+
408+
// Add child after BeginEdit - child should have EditLevelAdded = 1
409+
var child = root.Children.AddNew();
410+
child.Data = "new child";
411+
412+
// Act: Clone the object graph
413+
var clonedRoot = root.Clone();
414+
415+
// Assert: Clone should have the child
416+
Assert.AreEqual(1, clonedRoot.Children.Count, "Clone should have 1 child");
417+
Assert.AreEqual("new child", clonedRoot.Children[0].Data, "Clone should have child data");
418+
419+
// Cancel edit should remove the child (it was added after BeginEdit)
420+
clonedRoot.CancelEdit();
421+
Assert.AreEqual(0, clonedRoot.Children.Count, "After CancelEdit, child added during edit should be removed");
422+
}
423+
424+
[TestMethod]
425+
public void ClonePreservesChildEditLevelWhenUsingBindingSource()
426+
{
427+
// Arrange: Create a root with children and begin editing
428+
var root = CreateRoot();
429+
var child = root.Children.AddNew();
430+
child.Data = "original";
431+
432+
((System.ComponentModel.IEditableObject)root).BeginEdit();
433+
((System.ComponentModel.IEditableObject)child).BeginEdit();
434+
435+
// Make changes after BeginEdit
436+
child.Data = "modified";
437+
438+
// Verify edit levels before clone
439+
var rootEditLevel = ((Core.IUndoableObject)root).EditLevel;
440+
var childEditLevel = ((Core.IUndoableObject)child).EditLevel;
441+
var childListEditLevel = ((Core.IUndoableObject)root.Children).EditLevel;
442+
443+
Assert.AreEqual(1, rootEditLevel, "Root EditLevel should be 1 before clone");
444+
Assert.AreEqual(1, childEditLevel, "Child EditLevel should be 1 before clone");
445+
Assert.AreEqual(0, childListEditLevel, "ChildList EditLevel should be 0 before clone");
446+
447+
// Act: Clone the object graph
448+
var clonedRoot = root.Clone();
449+
450+
// Assert: Verify edit levels are preserved after clone
451+
var clonedRootEditLevel = ((Core.IUndoableObject)clonedRoot).EditLevel;
452+
var clonedChildEditLevel = ((Core.IUndoableObject)clonedRoot.Children[0]).EditLevel;
453+
var clonedChildListEditLevel = ((Core.IUndoableObject)clonedRoot.Children).EditLevel;
454+
455+
Assert.AreEqual(1, clonedRootEditLevel, "Cloned Root EditLevel should be 1");
456+
Assert.AreEqual(1, clonedChildEditLevel, "Cloned Child EditLevel should be 1");
457+
Assert.AreEqual(0, clonedChildListEditLevel, "Cloned ChildList EditLevel should be 0");
458+
459+
// Verify the modified data is preserved
460+
Assert.AreEqual("modified", clonedRoot.Children[0].Data, "Modified data should be preserved in clone");
461+
}
462+
269463
private Root CreateRoot()
270464
{
271465
IDataPortal<Root> dataPortal = _testDIContext.CreateDataPortal<Root>();

Source/Csla/BusinessBindingListBase.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -300,9 +300,13 @@ protected virtual void EditChildComplete(IEditableBusinessObject child)
300300

301301
#region Insert, Remove, Clear
302302

303+
[NonSerialized]
304+
[NotUndoable]
305+
private bool _isDeserializing;
306+
303307
/// <summary>
304308
/// Override this method to create a new object that is added
305-
/// to the collection.
309+
/// to the collection.
306310
/// </summary>
307311
protected override object AddNewCore()
308312
{
@@ -354,12 +358,15 @@ protected override void InsertItem(int index, C item)
354358
// ensure child uses same context as parent
355359
if (item is IUseApplicationContext iuac)
356360
iuac.ApplicationContext = ApplicationContext;
357-
// set child edit level
358-
UndoableBase.ResetChildEditLevel(item, EditLevel, false);
359-
// when an object is inserted we assume it is
360-
// a new object and so the edit level when it was
361-
// added must be set
362-
item.EditLevelAdded = EditLevel;
361+
if (!_isDeserializing)
362+
{
363+
// set child edit level
364+
UndoableBase.ResetChildEditLevel(item, EditLevel, false);
365+
// when an object is inserted we assume it is
366+
// a new object and so the edit level when it was
367+
// added must be set
368+
item.EditLevelAdded = EditLevel;
369+
}
363370
base.InsertItem(index, item);
364371
}
365372
else
@@ -667,7 +674,15 @@ protected override void OnSetChildren(Serialization.Mobile.SerializationInfo inf
667674
{
668675
_deletedList = (MobileList<C>)formatter.GetObject(child.ReferenceId);
669676
}
670-
base.OnSetChildren(info, formatter);
677+
_isDeserializing = true;
678+
try
679+
{
680+
base.OnSetChildren(info, formatter);
681+
}
682+
finally
683+
{
684+
_isDeserializing = false;
685+
}
671686
}
672687

673688
/// <inheritdoc/>

Source/Csla/BusinessListBase.cs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -307,9 +307,13 @@ protected virtual void EditChildComplete(IEditableBusinessObject child)
307307

308308
#region Insert, Remove, Clear
309309

310+
[NonSerialized]
311+
[NotUndoable]
312+
private bool _isDeserializing;
313+
310314
/// <summary>
311315
/// Override this method to create a new object that is added
312-
/// to the collection.
316+
/// to the collection.
313317
/// </summary>
314318
protected override C AddNewCore()
315319
{
@@ -373,12 +377,15 @@ protected override void InsertItem(int index, C item)
373377
// ensure child uses same context as parent
374378
if (item is IUseApplicationContext iuac)
375379
iuac.ApplicationContext = ApplicationContext;
376-
// set child edit level
377-
UndoableBase.ResetChildEditLevel(item, EditLevel, false);
378-
// when an object is inserted we assume it is
379-
// a new object and so the edit level when it was
380-
// added must be set
381-
item.EditLevelAdded = EditLevel;
380+
if (!_isDeserializing)
381+
{
382+
// set child edit level
383+
UndoableBase.ResetChildEditLevel(item, EditLevel, false);
384+
// when an object is inserted we assume it is
385+
// a new object and so the edit level when it was
386+
// added must be set
387+
item.EditLevelAdded = EditLevel;
388+
}
382389
base.InsertItem(index, item);
383390
}
384391
else
@@ -701,7 +708,15 @@ protected override void OnSetChildren(SerializationInfo info, MobileFormatter fo
701708
{
702709
_deletedList = (MobileList<C>)formatter.GetObject(child.ReferenceId);
703710
}
704-
base.OnSetChildren(info, formatter);
711+
_isDeserializing = true;
712+
try
713+
{
714+
base.OnSetChildren(info, formatter);
715+
}
716+
finally
717+
{
718+
_isDeserializing = false;
719+
}
705720
}
706721

707722
/// <inheritdoc/>

0 commit comments

Comments
 (0)