DYN-9599 : Uniform undo behavior restores nodes + pinned notes.#16920
DYN-9599 : Uniform undo behavior restores nodes + pinned notes.#16920zeusongit merged 9 commits intoDynamoDS:masterfrom
Conversation
There was a problem hiding this comment.
See the ticket for this pull request: https://jira.autodesk.com/browse/DYN-9599
1. Placed the functions next to each other 2. Used Any() instead of Count()
| SetNodeCountOptimizationEnabled(zoomAnimationThresholdFeatureFlagVal); | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Done
There was a problem hiding this comment.
Pull request overview
This PR targets Dynamo’s undo/redo consistency in the WPF workspace layer by ensuring that undoing deletion of a node also restores any pinned notes and re-establishes their pin association uniformly (independent of delete entry point).
Changes:
- Adds logic in
WorkspaceViewModelto resolve and re-establish pinned note ↔ node references during undo when models are recreated out-of-order. - Invokes the note pin command when either the note is created before its node (or vice-versa) during undo replay.
| DynamoSelection.Instance.Selection.Add(pinnedNode.First().NodeModel); | ||
| viewModel.PinToNodeCommand.Execute(null); | ||
| } |
There was a problem hiding this comment.
Calling PinToNodeCommand.Execute(null) during undo-repair will run NoteViewModel.PinToNode, which records a new undo action group (WorkspaceModel.RecordModelForModification) and sets HasUnsavedChanges. That can pollute/corrupt the undo/redo stacks and break the requirement that restoring a deleted node + pinned notes happens in a single undo step. Consider re-establishing the pin relationship without recording undo (e.g., a dedicated non-undo-recording helper for undo replay).
There was a problem hiding this comment.
Added a internal property SuppressUndoRecording which is used to skip the WorkspaceModel.RecordModelForModification call that stores information for Undo/Redo.
We set this property to False by passing an argument to TryToSubscribeUndoNote function.
Replace PinToNodeCommand invocation with TryToSubscribeUndoNote function call.
| DynamoSelection.Instance.Selection.Clear(); | ||
| DynamoSelection.Instance.Selection.Add(node); | ||
| note.PinToNodeCommand.Execute(null); | ||
| } |
There was a problem hiding this comment.
Same issue as above: DynamoSelection.Instance.Selection.Clear() can leave stale IsSelected flags because it bypasses DynamoSelection.ClearSelection() (which explicitly deselects items and respects ClearSelectionDisabled). Use DynamoSelection.Instance.ClearSelection() instead.
There was a problem hiding this comment.
I would agree: Use DynamoSelection.Instance.ClearSelection() instead.
There was a problem hiding this comment.
Removed the usage DynamoSelection.Instance.Selection property because it will be handled in note_PinUnpinToNode function.
| DynamoSelection.Instance.Selection.Clear(); | ||
| DynamoSelection.Instance.Selection.Add(node); | ||
| note.PinToNodeCommand.Execute(null); |
There was a problem hiding this comment.
PinToNodeCommand.Execute(null) here will record a new undo action group via NoteViewModel.PinToNode and mark the workspace dirty, even though this is intended to be part of undo replay. This risks adding extra undo steps / clearing redo history. Reconnect pinned notes to the restored node without generating new undo records.
| DynamoSelection.Instance.Selection.Clear(); | |
| DynamoSelection.Instance.Selection.Add(node); | |
| note.PinToNodeCommand.Execute(null); | |
| // Reconnect the note to the restored node directly on the model to avoid | |
| // generating new undo records or marking the workspace dirty during undo. | |
| note.Model.PinnedNode = node; |
There was a problem hiding this comment.
Added a internal property SuppressUndoRecording which is used to skip the WorkspaceModel.RecordModelForModification call that stores information for Undo/Redo.
We set this property to False by passing an argument to TryToSubscribeUndoNote function.
| private void Model_NoteAdded(NoteModel note) | ||
| { | ||
| var viewModel = new NoteViewModel(this, note); | ||
| ResolvePinnedNodeReference(note, viewModel); | ||
| Notes.Add(viewModel); |
There was a problem hiding this comment.
This change introduces new undo/redo behavior (restoring node deletion should also restore pinned notes uniformly and in a single undo step, including multi-select and redo). There are existing WPF tests around note pinning/undo, but no coverage found for deleting node(s) with pinned note(s) and undo/redo from both delete entry points. Add UI/unit coverage for: delete via Delete key vs context menu, multi-node deletion, undo restores pin+position+group membership, and redo re-deletes consistently.
There was a problem hiding this comment.
Added a internal property SuppressUndoRecording which is used to skip the WorkspaceModel.RecordModelForModification call that stores information for Undo/Redo.
We set this property to False by passing an argument to TryToSubscribeUndoNote function.
| DynamoSelection.Instance.Selection.Clear(); | ||
| DynamoSelection.Instance.Selection.Add(pinnedNode.First().NodeModel); | ||
| viewModel.PinToNodeCommand.Execute(null); | ||
| } |
There was a problem hiding this comment.
Using DynamoSelection.Instance.Selection.Clear() here bypasses DynamoSelection.ClearSelection() (which explicitly deselects items and honors ClearSelectionDisabled). Because SmartObservableCollection.Clear() raises a reset without old items, this can leave previous items' IsSelected state out of sync. Use DynamoSelection.Instance.ClearSelection() (and ideally restore prior selection after repinning).
There was a problem hiding this comment.
I would agree: Use DynamoSelection.Instance.ClearSelection() instead.
There was a problem hiding this comment.
Removed the usage DynamoSelection.Instance.Selection property because it will be handled in note_PinUnpinToNode function.
|
@CustomBuildingConfigurators-Balaji a few comments by me and copilot, PTAL |
Code reviewFound 1 issue:
Dynamo/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs Lines 947 to 951 in ff878e2 Dynamo/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs Lines 969 to 973 in ff878e2 For reference, Dynamo/src/DynamoCore/Core/DynamoSelection.cs Lines 95 to 101 in ff878e2 Generated with Claude Code - If this code review was useful, please react with 👍. Otherwise, react with 👎. |
| { | ||
| DynamoSelection.Instance.Selection.Clear(); | ||
| DynamoSelection.Instance.Selection.Add(pinnedNode.First().NodeModel); | ||
| viewModel.PinToNodeCommand.Execute(null); |
There was a problem hiding this comment.
PinToNodeCommand.Execute() relies on DelegateCommand bypassing CanExecute this is diverging from existing pattern note_PinUnpinToNode which calls PinToNode() directly
There was a problem hiding this comment.
Replaced PinToNodeCommand invocation with TryToSubscribeUndoNote function in NoteModel.cs which in turn calls note_PinUnpinToNode function.
|
@zeusongit , looks like you reviewed this last week, but it's not approved. Is there more work to do? Should I review it? |
Yes, I have requested some changes, feel free to review more though. |
Removed direct invocation of PinToNodeCommand and used TryToSubscribeUndoNote function in NoteModel.cs Removed the code added for DynamoSelection.Instance.Selection since we do not need it and it will be handled by note_PinUnpinToNode function in NoteViewModel.cs
|
| var pinnedNode = Nodes.Where(x => x.Id.Equals(note.PinnedNodeGuid)); | ||
| if (pinnedNode.Any()) | ||
| { | ||
| note.TryToSubscribeUndoNote(recordForUndo: false); | ||
| } |
There was a problem hiding this comment.
ResolvePinnedNodeReference(NoteModel) enumerates Nodes without taking the same lock (Nodes) that is used elsewhere in this class for adding/removing/clearing nodes. If nodes can be modified concurrently (which the existing locking suggests), this LINQ enumeration can throw "Collection was modified" during undo replay. Consider wrapping the Nodes lookup in lock (Nodes) (or using a thread-safe snapshot like Nodes.ToList() under the lock) before calling TryToSubscribeUndoNote.
| var pinnedNode = Nodes.Where(x => x.Id.Equals(note.PinnedNodeGuid)); | |
| if (pinnedNode.Any()) | |
| { | |
| note.TryToSubscribeUndoNote(recordForUndo: false); | |
| } | |
| bool hasPinnedNode; | |
| lock (Nodes) | |
| { | |
| hasPinnedNode = Nodes.Any(x => x.Id.Equals(note.PinnedNodeGuid)); | |
| } | |
| if (hasPinnedNode) | |
| { | |
| note.TryToSubscribeUndoNote(recordForUndo: false); | |
| } |
There was a problem hiding this comment.
@zeusongit, Practically this is not an issue because this is function is executed during undo operation and there is no chance of collection getting modified while executing the above specified lines.
Let me know if you would like to implement this.
DYN-9599 (DynamoDS#16920) landed on master concurrently and added deferred pin restoration when a deleted node+note pair is restored via undo. It introduced SuppressUndoRecording on NoteModel and ResolvePinnedNodeReference on WorkspaceViewModel, both of which rely on TryToSubscribeUndoNote and the UndoRequest event that DYN-10130 had removed. Resolution: - Restore UndoRequest event, note_PinUnpinToNode handler, and TryToSubscribeUndoNote(recordForUndo) from DYN-9599 so that ResolvePinnedNodeReference continues to work for the undo-of-deletion and file-load deferred resolution paths. - Keep SuppressUndoRecording from DYN-9599 and guard PinToNode's RecordModelsForModification (note + group) behind it so that deferred restoration does not pollute the undo stack. - Keep DYN-10130's Workspace property and DeserializeCore direct resolution, which handles the undo-of-modification path and also sets PinnedNodeGuid so ResolvePinnedNodeReference detects pending pins in the Workspace-null paths. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>



Purpose
DYN-9599 : Uniform undo behavior restores nodes + pinned notes.
Currently the Undo behavior for the deletion of a node with an attached pinned varies depending on whether the node is deleted using Delte Key or context menu. Make Undo (Ctrl+Z) uniformly restore a deleted node and re-establish pinned note association in one step, preserving each note’s content, pin state, and relative position, regardless of whether deletion was via Delete key or context menu. (DYN-9599)
DYN-9599.mp4
Declarations
Check these if you believe they are true
Release Notes
Uniform undo behavior to restores nodes and pinned notes
Reviewers
@zeusongit
FYIs
@stevecbc