fix(TextBox): clear undo history on external text updates - #21839
Merged
MrJul merged 3 commits intoJul 24, 2026
Merged
Conversation
TwoWay-bound TextBox instances retain prior user-edit snapshots when the binding source replaces the text, allowing undo to write values from a previous model context back into the current source. Cover the external replacement and the normal TwoWay source echo separately so a fix cannot clear valid user undo history after each edit.
Binding and direct property updates currently enter the same undo timeline as user edits, so a reused TwoWay-bound TextBox can write text from a previous model back into the current source. Track TextBox-initiated mutations through synchronous source notifications, preserve those edits in the undo timeline, and establish a fresh baseline for all external updates. MaskedTextBox routes its own edits through the same boundary so its existing undo behavior remains intact.
NathanDrake2406
marked this pull request as ready for review
July 23, 2026 16:56
|
You can test this PR using the following package version. |
MrJul
reviewed
Jul 24, 2026
Comment on lines
+378
to
+379
| private bool _needsUndoRedoBaseline; | ||
| private bool _needsUndoRedoSnapshotAfterTextChange; |
Member
There was a problem hiding this comment.
Are the two flags really necessary? They gate the exact same code path.
âĶ history Replace the boolean text-mutation flag with a three-state TextMutationKind (ExternalReplacement / Edit / InternalSynchronization) so MaskedTextBox's prompt-character reformatting on focus/blur and mask-provider refreshes no longer create undo entries, while actual keyboard edits remain undoable.
NathanDrake2406
force-pushed
the
nathan/fix-21578-textbox-binding-undo
branch
from
July 24, 2026 16:53
358ca9f to
652a008
Compare
|
You can test this PR using the following package version. |
MrJul
enabled auto-merge
July 24, 2026 18:24
MrJul
pushed a commit
that referenced
this pull request
Jul 29, 2026
* test(TextBox): reproduce stale undo history after binding update TwoWay-bound TextBox instances retain prior user-edit snapshots when the binding source replaces the text, allowing undo to write values from a previous model context back into the current source. Cover the external replacement and the normal TwoWay source echo separately so a fix cannot clear valid user undo history after each edit. * fix(TextBox): reset undo history for external text updates Binding and direct property updates currently enter the same undo timeline as user edits, so a reused TwoWay-bound TextBox can write text from a previous model back into the current source. Track TextBox-initiated mutations through synchronous source notifications, preserve those edits in the undo timeline, and establish a fresh baseline for all external updates. MaskedTextBox routes its own edits through the same boundary so its existing undo behavior remains intact. * fix(TextBox): distinguish edits from internal synchronization in undo history Replace the boolean text-mutation flag with a three-state TextMutationKind (ExternalReplacement / Edit / InternalSynchronization) so MaskedTextBox's prompt-character reformatting on focus/blur and mask-provider refreshes no longer create undo entries, while actual keyboard edits remain undoable.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does the pull request do?
This PR restores the
TextBoxundo boundary between user edits and externalTextupdates.Mutations to
TextPropertyare now classified into three kinds, tracked internally as aTextMutationKind:Clear(),SelectedTextreplacement. Undoable, belongs to the current document's history.MaskedTextBoxtoggling prompt characters on focus/blur, or refreshing its display after the mask configuration changes). Leaves undo/redo history untouched.This restores the behavior originally introduced for #336, which regressed when #9490 made every
Textcoercion create an undo snapshot.What is the current behavior?
Every
Textchange is added to the same undo timeline, including values supplied by a binding.In a master-detail editor that reuses one TwoWay-bound
TextBox, changing the selected model leaves the previous model's text in the undo stack. Undo can then write that stale text into the newly selected model.What is the updated/expected behavior with this PR?
External
Textupdates clear both undo and redo history and establish the new value as the baseline. Normal typing remains undoable, including the synchronous source notification produced by a TwoWay binding.MaskedTextBox's own display-only reformatting (prompt characters shown/hidden on focus and blur, and the text re-parsed afterMask,PromptChar,Culture, etc. change) does not create undo entries, since it isn't an edit to the logical document. Actual masked edits (typing, paste, Delete/Space/Backspace) remain undoable, andClear()/SelectedTextreplacement are deliberately kept undoable as editing commands.Regression coverage verifies:
CanUndoandCanRedoMaskedTextBoxdoes not create an undo operationClear()andSelectedTextreplacement remain undoableValidation
dotnet build src/Avalonia.Controls/Avalonia.Controls.csproj -p:AvsSkipBuildingLegacyTargetFrameworks=True --no-restore: succeeded with 0 warnings and 0 errorsdotnet run --project tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj -p:AvsSkipBuildingLegacyTargetFrameworks=True -- --filter-class "Avalonia.Controls.UnitTests.TextBoxTests" "Avalonia.Controls.UnitTests.MaskedTextBoxTests": 225 passedAvalonia.Controls.UnitTestsrun: 3653 passed, 0 failed, 1 pre-existing unrelated skipHow was the solution implemented (if it's not obvious)?
TextBoxtracks aTextMutationKind(Edit/InternalSynchronization/ExternalReplacement) while callingSetCurrentValue(TextProperty, ...)through two internal entry points,SetTextFromEditandSetTextFromInternalSynchronization. The kind stays set through synchronous TwoWay source notifications, so a binding echo of an edit is not mistaken for an external replacement. Any coercion that doesn't go through either entry point (bindings, direct property sets) is treated as an external replacement, clearing history and starting a fresh baseline.MaskedTextBoxroutes its own mutations through whichever entry point matches their intent:SetTextFromEditfor keyboard editing and paste,SetTextFromInternalSynchronizationfor prompt-character display toggling on focus/blur and for reformatting after mask configuration changes.This replaces an earlier single-bool version of this seam (
_isTextChangeFromTextBox) that conflated "should this clear history" with "should this create an undo entry" and routed allMaskedTextBoxmutations, including focus-only display changes, through the undoable path.The behavior change is intentionally limited to undo/redo state. There are no public API or serialization changes. The compatibility risk is that programmatic
Textassignments are no longer undoable; they now match binding updates and the non-user-input semantics established by #336.Checklist
Breaking changes
None.
Obsoletions / Deprecations
None.
Fixed issues
Fixes #21578