dotnet/wpf · error · InvalidOperationException

SR.UndoUnitCantBeOpenedTwice

Error message

SR.UndoUnitCantBeOpenedTwice

What it means

ParentUndoUnit.Open starts a new open undo unit nested inside this parent. The same undo unit cannot be opened twice while it is already part of the parent chain: if `newUnit` is found in this unit's parent chain, InvalidOperationException(SR.UndoUnitCantBeOpenedTwice) is thrown to prevent recursive/cyclic opening.

Solutions

  1. Call Close(unit, ...) for the currently open unit before calling Open on it (or any ancestor) again.
  2. Check OpenedUnit/DeepestOpenUnit state before calling Open and skip if the unit is already open.
  3. Ensure your edit transaction boundaries are balanced: every Open has exactly one matching Close, including in exception paths (try/finally).
  4. If the unit should merge, use Add/Merge instead of opening again.

Example fix

// before
if (currentUnit != null) parentUndoUnit.Open(currentUnit); // throws if already in chain
// after
if (parentUndoUnit.OpenedUnit != currentUnit) parentUndoUnit.Open(currentUnit);
Defensive patterns

Strategy: validation

Validate before calling

bool canOpen = parentUnit != null
    && parentUnit.OpenedUnit == null
    && !ReferenceEquals(parentUnit.OpenedUnit, newUnit);

Type guard

bool IsOpenInChain(ParentUndoUnit parent, IUndoUnit unit) =>
    parent != null && unit != null &&
    object.ReferenceEquals(parent.DeepestOpenUnit, unit);

Try / catch

try { parentUnit.Open(newUnit); }
catch (InvalidOperationException ex) when (ex.Message.Contains("opened twice"))
{
    // unit already open: skip or close the previous unit first
}

Prevention

When it happens

Trigger: Calling Open(newUnit) on a ParentUndoUnit when newUnit already appears in its parent unit chain (IsInParentUnitChain returns true), i.e. re-opening an already-open or ancestor unit.

Common situations: Custom text-editing undo implementations calling Open twice for the same unit without an intervening Close; re-entrant edit handlers (e.g. nested property-change events) opening the same unit again; forgetting that Open fails when no deeper unit is open but the unit is an ancestor.

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

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/PresentationFramework/MS/Internal/documents/ParentUndoUnit.cs:69

        /// </summary>
        /// <param name="newUnit">
        /// IParentUndoUnit to open
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Thrown if passed unit is null.
        /// </exception>
        public virtual void Open(IParentUndoUnit newUnit)
        {
            IParentUndoUnit deepestOpen;

            ArgumentNullException.ThrowIfNull(newUnit);

            deepestOpen = DeepestOpenUnit;
            if (deepestOpen == null)
            {
                if (IsInParentUnitChain(newUnit))
                {
                    throw new InvalidOperationException(SR.UndoUnitCantBeOpenedTwice);
                }

                _openedUnit = newUnit;
                newUnit?.Container = this;
            }
            else
            {
                newUnit?.Container = deepestOpen;

                deepestOpen.Open(newUnit);
            }
        }

        /// <summary>
        /// Closes the current open unit, adding it to the containing unit's undo stack if committed.
        /// </summary>
        public virtual void Close(UndoCloseAction closeAction)
        {

View on GitHub (pinned to 81131a70a4)