dotnet/wpf · error · InvalidOperationException

SR.IllegalTreeChangeDetectedPostAction

Error message

SR.IllegalTreeChangeDetectedPostAction

What it means

StructuralCache.DetectInvalidOperation throws InvalidOperationException when an illegal modification of the document tree was detected while the layout engine was measuring or arranging content. WPF's text layout caches structural information about the FlowDocument; mutating the tree mid-layout corrupts that cache, so the flag set by NoteIllegallTreeChangeDetected is surfaced on the next pass. This is a guard against undefined behavior, not a recoverable state.

Solutions

  1. Defer tree modifications with Dispatcher.BeginInvoke at a lower priority so they run after the layout pass completes
  2. Move content mutations out of layout-synchronous events (LayoutUpdated, SizeChanged handlers)
  3. Use DocumentPage/ICustomDocumentPaginator hooks only for read-only inspection; batch edits between layout passes
  4. Re-create the FlowDocumentPage/reader after edits instead of reusing a partially laid-out document

Example fix

// before
void OnLayoutUpdated(object s, EventArgs e) { doc.Blocks.Add(p); }
// after
void OnLayoutUpdated(object s, EventArgs e) {
    Dispatcher.BeginInvoke(DispatcherPriority.Background, new Action(() => doc.Blocks.Add(p)));
}
Defensive patterns

Strategy: validation

Validate before calling

// Guard: ensure no tree mutation is queued while layout is in progress
bool layoutInProgress = element.IsMeasureValid == false || element.IsArrangeValid == false;
if (layoutInProgress)
    Dispatcher.BeginInvoke(DispatcherPriority.Background, () => doc.Blocks.Add(newBlock));
else
    doc.Blocks.Add(newBlock);

Try / catch

try
{
    paginator.ComputePageCount();
}
catch (InvalidOperationException ex) when (ex.Message.Contains("tree change"))
{
    // re-run after layout settles
    Dispatcher.BeginInvoke(DispatcherPriority.Background, () => paginator.ComputePageCount());
}

Prevention

When it happens

Trigger: Modifying the element tree (adding/removing Blocks, Run text, TableCell content) inside a measure/arrange pass of a FlowDocument-based control, or from a handler that runs synchronously during layout (e.g. LayoutUpdated, OnMeasure override), and then calling DetectInvalidOperation (via pagination or collection operations).

Common situations: Data binding or event handlers updating document content during pagination/printing; running document edits inside MeasureOverride/ArrangeOverride; DocumentPaginator or print pipeline triggering layout while UI code mutates the FlowDocument.

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

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/PresentationFramework/MS/Internal/PtsHost/StructuralCache.cs:113

        /// <summary>
        /// Sets current page context to be used in the cache's queries
        /// </summary>
        /// <param name="currentPage">Document page to become current in the context</param>
        /// <returns>Reference to object compatible with IDisposable to re-initialize page context</returns>
        internal IDisposable SetDocumentVisualValidationContext(FlowDocumentPage currentPage)
        {
            return (new DocumentVisualValidationContext(this, currentPage) as IDisposable);
        }

        /// <summary>
        /// Detects if illegal tree change operation has been performed, but hidden by external
        /// code through try-catch statement.
        /// </summary>
        internal void DetectInvalidOperation()
        {
            if (_illegalTreeChangeDetected)
            {
                throw new InvalidOperationException(SR.IllegalTreeChangeDetectedPostAction);
            }
        }

        /// <summary>
        /// Notes the fact that world has changed while in measure / arrange.
        /// </summary>
        internal void OnInvalidOperationDetected()
        {
            if (_currentPage != null)
            {
                _illegalTreeChangeDetected = true;
            }
        }

        /// <summary>
        /// Invalidate format caches accumulated in the NameTable.
        /// </summary>
        internal void InvalidateFormatCache(bool destroyStructure)

View on GitHub (pinned to 81131a70a4)