dotnet/wpf · error · InvalidOperationException

SR.UndoUnitCantBeOpenedTwice

Error message

SR.UndoUnitCantBeOpenedTwice

What it means

UndoManager.Open throws this when the unit passed is already the deepest currently open unit (deepestOpen == unit). A single unit cannot be opened twice; nested Open calls must use distinct, newly created parent units.

Solutions

  1. Create a new IParentUndoUnit instance for each Open call instead of reusing one
  2. Check UndoManager.OpenedUnit/DeepestOpenUnit before opening and skip if already open
  3. Guard re-entrant event handlers with a boolean flag while the unit is open

Example fix

// before
var unit = _cachedUnit;
undoManager.Open(unit);
// after
var unit = new ParentUndoUnit(undoManager);
undoManager.Open(unit);
Defensive patterns

Strategy: validation

Validate before calling

if (undoManager.OpenedUnit == unit || undoManager.DeepestOpenUnit == unit) { /* already open: skip */ }

Try / catch

try { undoManager.Open(unit); } catch (InvalidOperationException ex) when (ex.Message.Contains("twice")) { /* unit already open: reuse it */ }

Prevention

When it happens

Trigger: Calling Open with the same IParentUndoUnit instance that is already open, typically from re-entrant calls in Do, Reopen, OpenCompositionUndoUnit, OpenTypingUndoUnit, or ResizeColumn.

Common situations: Re-entrant text change handlers (TextChanged/TextChanging) that open the same undo unit again, or custom code that opens a unit and then triggers an edit that re-opens it.

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/c2e1d12ed19136c0. Report an issue: GitHub.

Appendix: source

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

        /// </exception>
        /// <exception cref="ArgumentNullException">
        /// Thrown if passed unit is null.
        /// </exception>
        internal void Open(IParentUndoUnit unit)
        {
            IParentUndoUnit deepestOpen;

            if (!IsEnabled)
            {
                throw new InvalidOperationException(SR.UndoServiceDisabled);
            }

            ArgumentNullException.ThrowIfNull(unit);

            deepestOpen = DeepestOpenUnit;
            if (deepestOpen == unit)
            {
                throw new InvalidOperationException(SR.UndoUnitCantBeOpenedTwice);
            }

            if (deepestOpen == null)
            {
                if (unit != LastUnit)
                {
                    // Don't want to add the unit again if we're just reopening it
                    Add(unit as IUndoUnit);
                    SetLastUnit(unit as IUndoUnit);
                }
                SetOpenedUnit(unit);
                unit.Container = this;
            }
            else
            {
                unit.Container = deepestOpen;
                deepestOpen.Open(unit);
            }

View on GitHub (pinned to 81131a70a4)