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
- Ensure every EndChange() call is paired with exactly one prior BeginChange() on the same TextBoxBase instance.
- Use a try/finally around BeginChange...EndChange so early exceptions cannot leave unbalanced state, or better, track the level yourself before calling EndChange.
- 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
- Wrap BeginChange/EndChange in try/finally
- Track change-block nesting in an instance counter
- Never expose EndChange on public helper APIs without ownership of the BeginChange
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
- SR.TextBoxBase_CantSetIsUndoEnabledInsideChangeBlock
- SR.UndoNoOpenUnit
- SR.UndoServiceDisabled
- SR.UndoUnitAlreadyOpen
- SR.UndoUnitCantBeOpenedTwice
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)