Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TextBox does not always update

Tags:

c#

mvvm

wpf

I have the following TextBox:

<TextBox Text="{Binding SearchString,
               UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" />

Bound to the following property:

private string _searchString;
public string SearchString
{
    get 
    { 
        return _searchString; 
    }
    set
    {
        value = Regex.Replace(value, "[^0-9]", string.Empty);             
        _searchString = value;
        DoNotifyPropertyChanged("SearchString");
    }
}

The class inherits from a base class that implements INotifyPropertyChanged

public event PropertyChangedEventHandler PropertyChanged;
protected void DoNotifyPropertyChanged(string propertyName)
{
    if (PropertyChanged != null) 
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}

All I want is a quick and dirty way to disallow non-numerical characters for an integer-only text box (I know it's not complete, just for demonstration). I don't want a mere notification that there is illegal text or anything, I want to discard right away all characters on input that are disallowed.

However, the TextBox is behaving weirdly. I can still enter any text I want, it will display as entered, e.g. "1aaa". Even though the property has been properly cleaned to "1" in this example, the Textbox still shows "1aaa". Only when I enter an actual digit that would cause _searchString to change does it also update the displayed text, for example when I have "1aaa2" it will correctly update to "12". What's the matter here?

like image 960
Hackworth Avatar asked Oct 21 '22 09:10

Hackworth


1 Answers

This sounds like view-specific logic, so I see no reason not to use code-behind the view to control it. Personally I would implement this kind of behavior with a PreviewKeyDown on the TextBox that discards any non-numeric characters.

It probably wouldn't hurt to have something generic that you could reuse, such as a custom NumbersOnlyTextBox control, or an AttachedProperty you could attach to your TextBox to specify that it only allows numbers.

In fact, I remember creating an attached property that allows you to specify a regex for a textbox, and it will limit character entry to just that regex. I haven't used it in a while, so you'll probably want to test it or possible update it, but here's the code.

// When set to a Regex, the TextBox will only accept characters that match the RegEx
#region AllowedCharactersRegex Property

/// <summary>
/// Lets you enter a RegexPattern of what characters are allowed as input in a TextBox
/// </summary>
public static readonly DependencyProperty AllowedCharactersRegexProperty =
    DependencyProperty.RegisterAttached("AllowedCharactersRegex",
                                        typeof(string), typeof(TextBoxProperties),
                                        new UIPropertyMetadata(null, AllowedCharactersRegexChanged));

// Get
public static string GetAllowedCharactersRegex(DependencyObject obj)
{
    return (string)obj.GetValue(AllowedCharactersRegexProperty);
}

// Set
public static void SetAllowedCharactersRegex(DependencyObject obj, string value)
{
    obj.SetValue(AllowedCharactersRegexProperty, value);
}

// Events
public static void AllowedCharactersRegexChanged(DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
    var tb = obj as TextBox;
    if (tb != null)
    {
        if (e.NewValue != null)
        {
            tb.PreviewTextInput += Textbox_PreviewTextChanged;
            DataObject.AddPastingHandler(tb, TextBox_OnPaste);
        }
        else
        {
            tb.PreviewTextInput -= Textbox_PreviewTextChanged;
            DataObject.RemovePastingHandler(tb, TextBox_OnPaste);
        }
    }
}

public static void TextBox_OnPaste(object sender, DataObjectPastingEventArgs e)
{
    var tb = sender as TextBox;

    bool isText = e.SourceDataObject.GetDataPresent(DataFormats.Text, true);
    if (!isText) return;

    var newText = e.SourceDataObject.GetData(DataFormats.Text) as string;
    string re = GetAllowedCharactersRegex(tb);
    re = "[^" + re + "]";

    if (Regex.IsMatch(newText.Trim(), re, RegexOptions.IgnoreCase))
    {
        e.CancelCommand();
    }
}

public static void Textbox_PreviewTextChanged(object sender, TextCompositionEventArgs e)
{
    var tb = sender as TextBox;
    if (tb != null)
    {
        string re = GetAllowedCharactersRegex(tb);
        re = "[^" + re + "]";

        if (Regex.IsMatch(e.Text, re, RegexOptions.IgnoreCase))
        {
            e.Handled = true;
        }
    }
}

#endregion // AllowedCharactersRegex Property

It would be used like this:

<TextBox Text="{Binding SearchString, UpdateSourceTrigger=PropertyChanged}" 
         local:TextBoxHelpers.AllowedCharactersRegex="[0-9]" />

But as to why it won't update the UI. The UI knows the value hasn't actually changed, so doesn't bother re-evaluating the binding when it receives the PropertyChange notification.

To get around that, you could try temporarily setting the value to something else before setting it to the regex value, and raising a PropertyChange notification so the UI re-evaluates the bindings, but honestly that isn't really an ideal solution.

private string _searchString;
public string SearchString
{
    get 
    { 
        return _searchString; 
    }
    set
    {
        value = Regex.Replace(value, "[^0-9]", string.Empty);    

        // If regex value is the same as the existing value,
        // change value to null to force bindings to re-evaluate
        if (_searchString == value)
        {
            _searchString = null;
            DoNotifyPropertyChanged("SearchString");
        }

        _searchString = value;
        DoNotifyPropertyChanged("SearchString");
    }
}
like image 88
Rachel Avatar answered Oct 28 '22 14:10

Rachel