dotnet/wpf · error · InvalidOperationException

SR.UndoUnitCantBeAddedTwice

Error message

SR.UndoUnitCantBeAddedTwice

What it means

ParentUndoUnit.Add pushes a completed undo unit onto this parent's unit stack. A unit that is already part of this parent's chain (IsInParentUnitChain) must not be added again, so InvalidOperationException(SR.UndoUnitCantBeAddedTwice) is thrown to prevent the same unit appearing twice in the undo stack.

Solutions

  1. Do not call Add for a unit already closed into this parent — Close already adds it via the internal path.
  2. Remove the unit from its current parent before adding it to a different one.
  3. Track added units (e.g. a HashSet) and skip duplicates in custom undo code.
  4. If merging behavior is intended, rely on the Merge mechanism rather than re-adding.

Example fix

// before
parentUnit.Close(unit, action);
parentUnit.Add(unit); // duplicate add
// after
parentUnit.Close(unit, action); // Close already registers the unit
Defensive patterns

Strategy: validation

Validate before calling

bool alreadyRegistered = addedUnits.Contains(unit);
if (!alreadyRegistered) parentUnit.Add(unit);

Type guard

bool IsNotInChain(ParentUndoUnit p, IUndoUnit u) => !p.IsInParentUnitChain(u);

Try / catch

try { parentUnit.Add(unit); }
catch (InvalidOperationException ex) when (ex.Message.Contains("added twice"))
{
    // unit already on the stack: ignore
}

Prevention

When it happens

Trigger: Calling Add(unit) with a unit already contained in this parent unit (or an ancestor), typically by closing a unit and re-adding it, or adding a parent to one of its own descendants.

Common situations: Custom undo transaction code that both calls Close (which implicitly adds the unit) and then Add explicitly; moving units between stacks without removing them from the original parent; cyclic parent/child wiring.

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

Appendix: source

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

        /// </exception>
        public virtual void Add(IUndoUnit unit)
        {
            IParentUndoUnit parentUndoUnit;

            ArgumentNullException.ThrowIfNull(unit);

            parentUndoUnit = DeepestOpenUnit;

            // If we have an open unit, call Add on it
            if (parentUndoUnit != null)
            {
                parentUndoUnit.Add(unit);
                return;
            }

            if (IsInParentUnitChain(unit))
            {
                throw new InvalidOperationException(SR.UndoUnitCantBeAddedTwice);
            }

            if (Locked)
            {
                throw new InvalidOperationException(SR.UndoUnitLocked);
            }

            if (!Merge(unit))
            {
                _units.Push(unit);
                if (LastUnit is IParentUndoUnit)
                {
                    ((IParentUndoUnit)LastUnit).OnNextAdd();
                }

                SetLastUnit(unit);
            }
        }

View on GitHub (pinned to 81131a70a4)