diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs
index 964a153c8b..da4e90fb66 100644
--- a/src/Avalonia.Controls/TextBox.cs
+++ b/src/Avalonia.Controls/TextBox.cs
@@ -18,6 +18,7 @@ using Avalonia.Media.TextFormatting;
using Avalonia.Media.TextFormatting.Unicode;
using Avalonia.Automation.Peers;
using System.Diagnostics;
+using Avalonia.Threading;
namespace Avalonia.Controls
{
@@ -159,18 +160,41 @@ namespace Avalonia.Controls
(o, v) => o.UndoLimit = v,
unsetValue: -1);
+ ///
+ /// Defines the event.
+ ///
public static readonly RoutedEvent CopyingToClipboardEvent =
RoutedEvent.Register(
nameof(CopyingToClipboard), RoutingStrategies.Bubble);
+ ///
+ /// Defines the event.
+ ///
public static readonly RoutedEvent CuttingToClipboardEvent =
RoutedEvent.Register(
nameof(CuttingToClipboard), RoutingStrategies.Bubble);
+ ///
+ /// Defines the event.
+ ///
public static readonly RoutedEvent PastingFromClipboardEvent =
RoutedEvent.Register(
nameof(PastingFromClipboard), RoutingStrategies.Bubble);
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent TextChangedEvent =
+ RoutedEvent.Register(
+ nameof(TextChanged), RoutingStrategies.Bubble);
+
+ ///
+ /// Defines the event.
+ ///
+ public static readonly RoutedEvent TextChangingEvent =
+ RoutedEvent.Register(
+ nameof(TextChanging), RoutingStrategies.Bubble);
+
readonly struct UndoRedoState : IEquatable
{
public string? Text { get; }
@@ -359,8 +383,8 @@ namespace Avalonia.Controls
///
public double LineHeight
{
- get { return GetValue(LineHeightProperty); }
- set { SetValue(LineHeightProperty, value); }
+ get => GetValue(LineHeightProperty);
+ set => SetValue(LineHeightProperty, value);
}
[Content]
@@ -376,11 +400,19 @@ namespace Avalonia.Controls
CaretIndex = CoerceCaretIndex(caretIndex, value);
SelectionStart = CoerceCaretIndex(selectionStart, value);
SelectionEnd = CoerceCaretIndex(selectionEnd, value);
- if (SetAndRaise(TextProperty, ref _text, value) && IsUndoEnabled && !_isUndoingRedoing)
+
+ var textChanged = SetAndRaise(TextProperty, ref _text, value);
+
+ if (textChanged && IsUndoEnabled && !_isUndoingRedoing)
{
_undoRedoHelper.Clear();
SnapshotUndoRedo(); // so we always have an initial state
}
+
+ if (textChanged)
+ {
+ RaiseTextChangeEvents();
+ }
}
}
@@ -564,6 +596,27 @@ namespace Avalonia.Controls
remove => RemoveHandler(PastingFromClipboardEvent, value);
}
+ ///
+ /// Occurs asynchronously after text changes and the new text is rendered.
+ ///
+ public event EventHandler? TextChanged
+ {
+ add => AddHandler(TextChangedEvent, value);
+ remove => RemoveHandler(TextChangedEvent, value);
+ }
+
+ ///
+ /// Occurs synchronously when text starts to change but before it is rendered.
+ ///
+ ///
+ /// This event occurs just after the property value has been updated.
+ ///
+ public event EventHandler? TextChanging
+ {
+ add => AddHandler(TextChangingEvent, value);
+ remove => RemoveHandler(TextChangingEvent, value);
+ }
+
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get("PART_TextPresenter");
@@ -1252,7 +1305,7 @@ namespace Avalonia.Controls
if (text != null && _wordSelectionStart >= 0)
{
- var distance = caretIndex - _wordSelectionStart;
+ var distance = caretIndex - _wordSelectionStart;
if (distance <= 0)
{
@@ -1539,11 +1592,39 @@ namespace Avalonia.Controls
return text.Substring(start, end - start);
}
+ ///
+ /// Raises both the and events.
+ ///
+ ///
+ /// This must be called after the property is set.
+ ///
+ private void RaiseTextChangeEvents()
+ {
+ // Note the following sequence of these events (following WinUI)
+ // 1. TextChanging occurs synchronously when text starts to change but before it is rendered.
+ // This occurs after the Text property is set.
+ // 2. TextChanged occurs asynchronously after text changes and the new text is rendered.
+
+ var textChangingEventArgs = new TextChangingEventArgs(TextChangingEvent);
+ RaiseEvent(textChangingEventArgs);
+
+ Dispatcher.UIThread.Post(() =>
+ {
+ var textChangedEventArgs = new TextChangedEventArgs(TextChangedEvent);
+ RaiseEvent(textChangedEventArgs);
+ }, DispatcherPriority.Normal);
+ }
+
private void SetTextInternal(string value, bool raiseTextChanged = true)
{
if (raiseTextChanged)
{
- SetAndRaise(TextProperty, ref _text, value);
+ bool textChanged = SetAndRaise(TextProperty, ref _text, value);
+
+ if (textChanged)
+ {
+ RaiseTextChangeEvents();
+ }
}
else
{
diff --git a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
index d39d964277..5d5ffcc381 100644
--- a/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
+++ b/src/Avalonia.Controls/TextBoxTextInputMethodClient.cs
@@ -64,7 +64,7 @@ namespace Avalonia.Controls
return new TextInputMethodSurroundingText
{
- Text = lineText ?? "",
+ Text = lineText ?? "",
AnchorOffset = anchorOffset,
CursorOffset = cursorOffset
};
diff --git a/src/Avalonia.Controls/TextChangedEventArgs.cs b/src/Avalonia.Controls/TextChangedEventArgs.cs
new file mode 100644
index 0000000000..77c609f19b
--- /dev/null
+++ b/src/Avalonia.Controls/TextChangedEventArgs.cs
@@ -0,0 +1,20 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data specific to a TextChanged event.
+ ///
+ public class TextChangedEventArgs : RoutedEventArgs
+ {
+ public TextChangedEventArgs(RoutedEvent? routedEvent)
+ : base (routedEvent)
+ {
+ }
+
+ public TextChangedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
+ : base(routedEvent, source)
+ {
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/TextChangingEventArgs.cs b/src/Avalonia.Controls/TextChangingEventArgs.cs
new file mode 100644
index 0000000000..4dedbc927b
--- /dev/null
+++ b/src/Avalonia.Controls/TextChangingEventArgs.cs
@@ -0,0 +1,20 @@
+using Avalonia.Interactivity;
+
+namespace Avalonia.Controls
+{
+ ///
+ /// Provides data specific to a TextChanging event.
+ ///
+ public class TextChangingEventArgs : RoutedEventArgs
+ {
+ public TextChangingEventArgs(RoutedEvent? routedEvent)
+ : base (routedEvent)
+ {
+ }
+
+ public TextChangingEventArgs(RoutedEvent? routedEvent, IInteractive? source)
+ : base(routedEvent, source)
+ {
+ }
+ }
+}