TextBox TextChanged event on programmatic versus user change of text contents
I would like to differentiate between changing the text programmatically (for example in a button click handler event开发者_C百科) and user input (typing, cutting and pasting text).
Is it possible?User input in a TextBox
can be identified with
- Typing : PreviewTextInput event
- Backspace, Delete, Enter : PreviewKeyDown event
- Pasting : DataObject.PastingEvent
Combining these three events with a bool flag to indicate if any of the above occured before the TextChanged event and you'll know the reason for the update.
Typing and Pasting are easy, but Backspace doesn't always trigger TextChanged
(if no text is selected and the cursor is at position 0 for example). So some logic is needed in PreviewTextInput.
Here is an Attached Behavior that implements the logic above and executes a command with a bool flag when TextChanged
is raised.
<TextBox ex:TextChangedBehavior.TextChangedCommand="{Binding TextChangedCommand}" />
And in code you can find out the source for the update like
private void TextChanged_Executed(object parameter)
{
object[] parameters = parameter as object[];
object sender = parameters[0];
TextChangedEventArgs e = (TextChangedEventArgs)parameters[1];
bool userInput = (bool)parameters[2];
if (userInput == true)
{
// User input update..
}
else
{
// Binding, Programatic update..
}
}
Here is a small sample project demonstrating the effect: SourceOfTextChanged.zip
TextChangedBehavior
public class TextChangedBehavior
{
public static DependencyProperty TextChangedCommandProperty =
DependencyProperty.RegisterAttached("TextChangedCommand",
typeof(ICommand),
typeof(TextChangedBehavior),
new UIPropertyMetadata(TextChangedCommandChanged));
public static void SetTextChangedCommand(DependencyObject target, ICommand value)
{
target.SetValue(TextChangedCommandProperty, value);
}
// Subscribe to the events if we have a valid command
private static void TextChangedCommandChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
TextBox textBox = target as TextBox;
if (textBox != null)
{
if ((e.NewValue != null) && (e.OldValue == null))
{
textBox.PreviewKeyDown += textBox_PreviewKeyDown;
textBox.PreviewTextInput += textBox_PreviewTextInput;
DataObject.AddPastingHandler(textBox, textBox_TextPasted);
textBox.TextChanged += textBox_TextChanged;
}
else if ((e.NewValue == null) && (e.OldValue != null))
{
textBox.PreviewKeyDown -= textBox_PreviewKeyDown;
textBox.PreviewTextInput -= textBox_PreviewTextInput;
DataObject.RemovePastingHandler(textBox, textBox_TextPasted);
textBox.TextChanged -= textBox_TextChanged;
}
}
}
// Catches User input
private static void textBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
{
TextBox textBox = sender as TextBox;
SetUserInput(textBox, true);
}
// Catches Backspace, Delete, Enter
private static void textBox_PreviewKeyDown(object sender, KeyEventArgs e)
{
TextBox textBox = sender as TextBox;
if (e.Key == Key.Return)
{
if (textBox.AcceptsReturn == true)
{
SetUserInput(textBox, true);
}
}
else if (e.Key == Key.Delete)
{
if (textBox.SelectionLength > 0 || textBox.SelectionStart < textBox.Text.Length)
{
SetUserInput(textBox, true);
}
}
else if (e.Key == Key.Back)
{
if (textBox.SelectionLength > 0 || textBox.SelectionStart > 0)
{
SetUserInput(textBox, true);
}
}
}
// Catches pasting
private static void textBox_TextPasted(object sender, DataObjectPastingEventArgs e)
{
TextBox textBox = sender as TextBox;
if (e.SourceDataObject.GetDataPresent(DataFormats.Text, true) == false)
{
return;
}
SetUserInput(textBox, true);
}
private static void textBox_TextChanged(object sender, TextChangedEventArgs e)
{
TextBox textBox = sender as TextBox;
TextChangedFired(textBox, e);
SetUserInput(textBox, false);
}
private static void TextChangedFired(TextBox sender, TextChangedEventArgs e)
{
ICommand command = (ICommand)sender.GetValue(TextChangedCommandProperty);
object[] arguments = new object[] { sender, e, GetUserInput(sender) };
command.Execute(arguments);
}
#region UserInput
private static DependencyProperty UserInputProperty =
DependencyProperty.RegisterAttached("UserInput",
typeof(bool),
typeof(TextChangedBehavior));
private static void SetUserInput(DependencyObject target, bool value)
{
target.SetValue(UserInputProperty, value);
}
private static bool GetUserInput(DependencyObject target)
{
return (bool)target.GetValue(UserInputProperty);
}
#endregion // UserInput
}
Depending on your exact demands you can use TextBox.IsFocused
in the TextChanged
event to determine manual input. This will obviously not cover all ways of programmatical changes, but works for a lot of examples just fine and is a pretty clean and save way of doing so.
Basically this works if:
...the programmatical changes are all based on a manual change (e.g. a Button press).
It will not work if:
...the programmatical changes are completely based on code (e.g. a Timer).
Code example:
textBox.TextChanged += (sender, args) =>
if (textBox.IsFocused)
{
//do something for manual input
}
else
{
//do something for programmatical input
}
}
Similar to JHunz's answer, just add a boolean member variable to your control:
bool programmaticChange = false;
When you are making programmatic changes do this:
programmaticChange = true;
// insert changes to the control text here
programmaticChange = false;
In your event handlers, you just need to inspect the value of programmaticChange
to determine if its a programmatic change or not.
Fairly obvious and not very elegant but its also workable and simple.
If you just want to use the built-in WPF TextBox, then I don't believe it's possible.
There is a similar discussion on the Silverlight forums here: http://forums.silverlight.net/p/119128/268453.aspx It's not exactly the same question, but I think the idea similar to that in the original post might do the trick for you. Have a SetText method on a subclassed TextBox, that set a flag before changing the text and then set it back after. You could then check for the flag inside the TextChanged event. This would of course require all of your programmatic text changes to use that method, but if you have enough control over the project to mandate that I think it would work.
I have cleaned up and modified the TextChangedBehavior
class from Fredrik answer so that it also correctly handles the cut command (ctr+X).
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
public class TextChangedBehavior
{
public static readonly DependencyProperty TextChangedCommandProperty =
DependencyProperty.RegisterAttached("TextChangedCommand",
typeof (ICommand),
typeof (TextChangedBehavior),
new UIPropertyMetadata(TextChangedCommandChanged));
private static readonly DependencyProperty UserInputProperty =
DependencyProperty.RegisterAttached("UserInput",
typeof (bool),
typeof (TextChangedBehavior));
public static void SetTextChangedCommand(DependencyObject target, ICommand value)
{
target.SetValue(TextChangedCommandProperty, value);
}
private static void ExecuteTextChangedCommand(TextBox sender, TextChangedEventArgs e)
{
var command = (ICommand)sender.GetValue(TextChangedCommandProperty);
var arguments = new object[] { sender, e, GetUserInput(sender) };
command.Execute(arguments);
}
private static bool GetUserInput(DependencyObject target)
{
return (bool)target.GetValue(UserInputProperty);
}
private static void SetUserInput(DependencyObject target, bool value)
{
target.SetValue(UserInputProperty, value);
}
private static void TextBoxOnPreviewExecuted(object sender, ExecutedRoutedEventArgs e)
{
if (e.Command != ApplicationCommands.Cut)
{
return;
}
var textBox = sender as TextBox;
if (textBox == null)
{
return;
}
SetUserInput(textBox, true);
}
private static void TextBoxOnPreviewKeyDown(object sender, KeyEventArgs e)
{
var textBox = (TextBox)sender;
switch (e.Key)
{
case Key.Return:
if (textBox.AcceptsReturn)
{
SetUserInput(textBox, true);
}
break;
case Key.Delete:
if (textBox.SelectionLength > 0 || textBox.SelectionStart < textBox.Text.Length)
{
SetUserInput(textBox, true);
}
break;
case Key.Back:
if (textBox.SelectionLength > 0 || textBox.SelectionStart > 0)
{
SetUserInput(textBox, true);
}
break;
}
}
private static void TextBoxOnPreviewTextInput(object sender, TextCompositionEventArgs e)
{
SetUserInput((TextBox)sender, true);
}
private static void TextBoxOnTextChanged(object sender, TextChangedEventArgs e)
{
var textBox = (TextBox)sender;
ExecuteTextChangedCommand(textBox, e);
SetUserInput(textBox, false);
}
private static void TextBoxOnTextPasted(object sender, DataObjectPastingEventArgs e)
{
var textBox = (TextBox)sender;
if (e.SourceDataObject.GetDataPresent(DataFormats.Text, true) == false)
{
return;
}
SetUserInput(textBox, true);
}
private static void TextChangedCommandChanged(DependencyObject target, DependencyPropertyChangedEventArgs e)
{
var textBox = target as TextBox;
if (textBox == null)
{
return;
}
if (e.OldValue != null)
{
textBox.PreviewKeyDown -= TextBoxOnPreviewKeyDown;
textBox.PreviewTextInput -= TextBoxOnPreviewTextInput;
CommandManager.RemovePreviewExecutedHandler(textBox, TextBoxOnPreviewExecuted);
DataObject.RemovePastingHandler(textBox, TextBoxOnTextPasted);
textBox.TextChanged -= TextBoxOnTextChanged;
}
if (e.NewValue != null)
{
textBox.PreviewKeyDown += TextBoxOnPreviewKeyDown;
textBox.PreviewTextInput += TextBoxOnPreviewTextInput;
CommandManager.AddPreviewExecutedHandler(textBox, TextBoxOnPreviewExecuted);
DataObject.AddPastingHandler(textBox, TextBoxOnTextPasted);
textBox.TextChanged += TextBoxOnTextChanged;
}
}
}
Partial credits for dodgy_coder (agreed not conform the beautiful design you hope for, but imo the best compromis). Consider everything you want to cover:
- move text by dragging with mouse from TB2 to TB1
- cut (ctrl-x, programmatic cut, mouse-menu-cut)
- paste (ctrl-v, programmatic paste, mouse-menu-paste)
- undo (ctrl-z, programmatic undo)
- redo (ctrl-Y, programmatic redo)
- delete & backspace
- keyboard text (alfanumeric + symbols + space)
Consider what you want to exclude:
- programmatic setting of Text
Code
public class MyTB : TextBox
{
private bool _isTextProgrammaticallySet = false;
public new string Text
{
set
{
_isTextProgrammaticallySet = true;
base.Text = value;
_isTextProgrammaticallySet = false;
}
}
protected override void OnTextChanged(TextChangedEventArgs e)
{
base.OnTextChanged(e);
// .. on programmatic or on user
// .. on programmatic
if (_isTextProgrammaticallySet)
{
return;
}
// .. on user
OnTextChangedByUser(e);
}
protected void OnTextChangedByUser(TextChangedEventArgs e)
{
// Do whatever you want.
}
}
The following is discouraged, but the results of an attempt to cover all:
The alternatives for catching all the events were:
- DataObject.AddPastingHandler(MyTextBox, MyPasteCommand);
Covers 1 & 3 - OnPreviewTextInput
Covers 7 but not space - OnKeyDown
Covers 7-space
Trying to cover 2, 4, 5, 6 & 8 I figured I should go with the easier and consistent solution above :)
Thanks to Tim for pointing in the right direction, but for my needs, the check for the IsFocus worked like a charm. It's so simple....
if (_queryField.IsKeyboardFocused && _queryField.IsKeyboardFocusWithin)
{
//do your things
}
else
{
//whatever
}
I had this problem too, but for my case it was enough to listen to the (Preview)TextInput event instead of using Meleak's rather complex solution. I realize that's not a complete solution if you have to listen for programmatic changes aswell, but in my case it worked fine.
精彩评论