dotnet/wpf · error · InvalidOperationException

SR.UndoNoOpenParentUnit

Error message

SR.UndoNoOpenParentUnit

What it means

UndoManager.Add throws this when the unit being added has no open parent unit to attach to (DeepestOpenUnit is null and the unit itself is not a top-level unit that can be pushed directly). Undo units must be added inside an open parent unit context; otherwise the manager cannot place them on a stack and fails fast.

Solutions

  1. Ensure a parent unit is open before Add: call Open(parentUnit) (or use the Create*UndoUnit flow that manages opening) and Close it in a finally block.
  2. Audit Open/Close pairing — make Close run even on exceptions so the open-unit stack stays balanced.
  3. For top-level units, use the API path designed for them instead of Add on a closed context.
  4. Do not cache an UndoManager open-unit context across user operations; open a fresh unit per edit.

Example fix

// before
var unit = undoManager.CreateInsertUndoUnit(...);
undoManager.Add(unit); // throws: no open parent
// after
using (new UndoScope(undoManager)) // helper that calls Open/Close in Dispose
{
    var unit = undoManager.CreateInsertUndoUnit(...);
    undoManager.Add(unit);
}
Defensive patterns

Strategy: try-catch

Validate before calling

if (undoManager.IsEnabled) { /* ensure unit open scope */ }

Try / catch

try { undoManager.Add(unit); }
catch (InvalidOperationException) { /* no open parent: open a scope and retry or log */ }

Prevention

When it happens

Trigger: Calling Add with an IUndoUnit when no parent unit is open: e.g. calling Create*UndoUnit/Add helpers outside an Open(unit) ... Close() scope, or after the parent unit was already closed, so DeepestOpenUnit returns null.

Common situations: Custom IUndoUnit implementations recorded after their parent unit was closed; unbalanced Open/Close (exception skipped Close, or Close called twice); adding a unit from a different thread or re-entrant call after the open unit completed.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of dotnet/wpf@81131a70a4 (2026-09-14). Data as JSON: /api/errors/d53adc0045c79457. Report an issue: GitHub.

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/PresentationFramework/MS/Internal/documents/UndoManager.cs:461

                            {
                                _bottomUndoIndex = 0;
                            }
                        }
                        UndoStack[_topUndoIndex] = unit;
                    }
                }
                else if (State == UndoState.Undo)
                {
                    RedoStack.Push(unit);
                }
                else if (State == UndoState.Rollback)
                {
                    // do nothing, throwing out the unit
                }
            }
            else
            {
                throw new InvalidOperationException(SR.UndoNoOpenParentUnit);
            }
        }

        /// <summary>
        /// Clear the undo and redo stacks, as well as LastUnit.
        /// </summary>
        /// <exception cref="InvalidOperationException">
        /// Thrown if UndoManager is disabled
        /// </exception>
        internal void Clear()
        {
            if (!IsEnabled)
            {
                throw new InvalidOperationException(SR.UndoServiceDisabled);
            }

            // In practice, we only clear when the public IsUndoEnabled property is set false.
            // We'll check that property again when _imeSupportModeEnabled transitions to false.

View on GitHub (pinned to 81131a70a4)