dotnet/wpf · error · InvalidOperationException

SR.TextBoxBase_UnmatchedEndChange

Error message

SR.TextBoxBase_UnmatchedEndChange

What it means

TextBoxBase.EndChange() closes a change block opened with BeginChange(). The change-block level in the underlying TextEditor.Selection must be greater than zero; if it is zero, there is no matching BeginChange(), so an InvalidOperationException is thrown. Change blocks group a series of text edits into a single undo unit.

Solutions

  1. Ensure every EndChange() call is paired with exactly one prior BeginChange() on the same TextBoxBase instance.
  2. Use a try/finally around BeginChange...EndChange so early exceptions cannot leave unbalanced state, or better, track the level yourself before calling EndChange.
  3. Refactor to use TextEditor/TextSelection edit APIs (e.g. TextEditor.BeginChange scope) that manage block lifetime automatically.

Example fix

// before
textBox.BeginChange();
if (invalid) return; // EndChange never called -> later EndChange throws
textBox.EndChange();
// after
textBox.BeginChange();
try
{
    if (invalid) throw new InvalidOperationException();
}
finally
{
    textBox.EndChange();
}
Defensive patterns

Strategy: try-catch

Validate before calling

bool canEndChange = textBox.GetType().GetProperty("TextSelectionInternal", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance) == null; // track your own level instead:
int changeBlockLevel = 0; // increment on BeginChange, decrement on EndChange; only call EndChange when changeBlockLevel > 0

Type guard

static bool CanEndChange(int localChangeBlockLevel) => localChangeBlockLevel > 0;

Try / catch

try
{
    textBox.EndChange();
}
catch (InvalidOperationException)
{
    // no open change block; log and continue
}

Prevention

When it happens

Trigger: Calling textBoxBase.EndChange() without a preceding, still-open BeginChange() on the same control; calling EndChange() twice for one BeginChange(); calling EndChange() after the change block was already closed by an earlier EndChange() or by undo logic resetting the level.

Common situations: Programmatic text manipulation code that brackets edits with BeginChange/EndChange but has unbalanced early-return or exception paths; helper methods that call EndChange defensively without tracking whether they opened the block.

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

Appendix: source

Thrown at src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/Primitives/TextBoxBase.cs:396

        //
        //.........................................................

        /// <summary>
        /// Begins a change block.
        /// </summary>
        public void BeginChange()
        {
            this.TextEditor.Selection.BeginChange();
        }

        /// <summary>
        /// Ends a change block.
        /// </summary>
        public void EndChange()
        {
            if (this.TextEditor.Selection.ChangeBlockLevel == 0)
            {
                throw new InvalidOperationException(SR.TextBoxBase_UnmatchedEndChange);
            }

            this.TextEditor.Selection.EndChange();
        }

        /// <summary>
        /// Creates and returns a change block
        /// </summary>
        /// <returns>IDisposable</returns>
        public IDisposable DeclareChangeBlock()
        {
            return this.TextEditor.Selection.DeclareChangeBlock();
        }

        #endregion Public Methods

        //------------------------------------------------------
        //

View on GitHub (pinned to 81131a70a4)