dotnet/wpf · error · InvalidOperationException

SR.EnumeratorVersionChanged

Error message

SR.EnumeratorVersionChanged

What it means

RangeContentEnumerator implements IEnumerator and throws InvalidOperationException (SR.EnumeratorVersionChanged) when the underlying TextContainer's Generation has changed since the enumerator was created. The enumerator is invalidated by any modification to the text tree (insert/delete) because positions it holds may no longer be valid. An exception is made when a property-system tree walk is in progress (IsLogicalChildrenIterationInProgress), since inherited property changes can legitimately modify the TextContainer mid-iteration.

Solutions

  1. Complete the enumeration before modifying the TextContainer, or take a snapshot of the content (e.g. TextRange.Text) before editing
  2. Defer document modifications until after iteration completes (e.g. Dispatcher.BeginInvoke)
  3. Re-create the enumerator after the document changes instead of reusing the stale one
  4. Wrap the enumeration in try-catch for InvalidOperationException and restart enumeration with a fresh enumerator

Example fix

// before
foreach (var item in textRange.GetContentEnumerator()) { Process(item); ModifyDocument(); }
// after
var snapshot = CollectAll(textRange.GetContentEnumerator());
foreach (var item in snapshot) { Process(item); }
ModifyDocument();
Defensive patterns

Strategy: try-catch

Validate before calling

// no public generation API; guard by snapshotting before use
var snapshot = new List<object>();
var e = textRange.GetContentEnumerator();
while (e.MoveNext()) snapshot.Add(e.Current);

Try / catch

try { while (e.MoveNext()) Process(e.Current); }
catch (InvalidOperationException) { e = textRange.GetContentEnumerator(); /* retry with fresh enumerator */ }

Prevention

When it happens

Trigger: Calling Current on a RangeContentEnumerator after any edit was made to the associated TextContainer (e.g. Run.Text binding invalidated by a DataContext change on FlowDocument, or user/code edits to the document) between MoveNext calls.

Common situations: Iterating document content while the document is being modified: data binding updates text during a DataContext change, asynchronous edits, or re-entrant modification from within the enumeration loop.

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

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Documents/RangeContentEnumerator.cs:90

                if (_currentCache != null)
                {
                    return _currentCache;
                }

                if (_navigator.CompareTo(_end) >= 0)
                {
                    // IEnumerator.Current is documented to throw this exception
                    throw new InvalidOperationException(SR.EnumeratorReachedEnd);
                }

                // Throw if the tree has been modified since this enumerator was created unless a tree walk
                // by the property system is in progress. For example, changing DataContext on FlowDocument
                // can invalidate a binding on Run.Text during the inherited property change tree walk,
                // which in turn can modify the TextContainer.
                if (_generation != _start.TextContainer.Generation && !IsLogicalChildrenIterationInProgress)
                {
                    // IEnumerator.Current is documented to throw this exception
                    throw new InvalidOperationException(SR.EnumeratorVersionChanged);
                }

                switch (_navigator.GetPointerContext(LogicalDirection.Forward))
                {
                    case TextPointerContext.Text:
                        offset = 0;

                        // Merge all successive text runs into a single value.
                        do
                        {
                            runLength = _navigator.GetTextRunLength(LogicalDirection.Forward);
                            EnsureBufferCapacity(offset + runLength);
                            _navigator.GetTextInRun(LogicalDirection.Forward, _buffer, offset, runLength);
                            offset += runLength;
                            _navigator.MoveToNextContextPosition(LogicalDirection.Forward);
                        }
                        while (_navigator.GetPointerContext(LogicalDirection.Forward) == TextPointerContext.Text);

View on GitHub (pinned to 81131a70a4)