TextBox TextChanged event on programmatic versus user change of text contents

27,708

Solution 1

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.

Solution 2

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
}

Solution 3

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
    }
}

Solution 4

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.

Solution 5

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;
        }
    }
}
Share:
27,708

Related videos on Youtube

Greg
Author by

Greg

Programming, cursing, riding my bikes and learning foreign languages.

Updated on July 04, 2020

Comments

  • Greg
    Greg almost 4 years

    I would like to differentiate between changing the text programmatically (for example in a button click handler event) and user input (typing, cutting and pasting text).
    Is it possible?

  • dansan
    dansan over 10 years
    so frustrating when people downvote without explanation.
  • Gebb
    Gebb over 10 years
    Thanks! I would suggest handling the case of Key.Space in TextBoxOnPreviewKeyDown, because typing a space does not trigger the PreviewTextInput event, and therefore is not recognized by the provided implementation as user input.
  • EricG
    EricG over 9 years
    i didnt downvote but imo these are really bad programming styles.. flaws are easy made. If other parties were to influence the Text then you dont want them to make mistakes either.
  • EricG
    EricG over 9 years
    Update: I think this is the most solid solution, ill post a more elaborate answer :)
  • EricG
    EricG over 9 years
    and redo and undo and space, see my post :)
  • dodgy_coder
    dodgy_coder over 9 years
    +1 I like this solution. One comment - is there be a logic bug on this line? should it be ... if(!_isTextProgrammaticallySet) { OnTextChangedByUser(e); ... } ?
  • Sivasubramanian
    Sivasubramanian over 8 years
    Hi, unsubscription of the events inside the method "TextChangedCommandChanged" has never happened. why so ??
  • Vlad Gonchar
    Vlad Gonchar about 5 years
    All solutions fall in two categories: 1. detecting user input; 2. detecting programmatic input. First one (detecting user input) is difficult to catch all verity of possible user inputs. Second one is more controllable because a programmer usually knows what of his code changes the value. I usually did "unassign event - do programmatic change - assign event back". But dodgy_coder's solution looks to me more elegant.
  • Eftekhari
    Eftekhari almost 4 years
    How to use it in code behind? This doesn't work!? TextBox tb = new TextBox(); tb.TextChangedBehavior += tb_TextChangedBehavior;