How to define TextBox input restrictions?

Viewed 47860

How can I restrict TextBox to accept only capital letters, or for example digits, or forbid to put any special character?

Sure it's a piece of cake to catch TextInput event and handle the text here, but is it the proper way to do this?

6 Answers

I've done this in the past with an attached behavior, which can be used like this:

<TextBox b:Masking.Mask="^\p{Lu}*$"/>

The attached behavior code looks like this:

/// <summary>
/// Provides masking behavior for any <see cref="TextBox"/>.
/// </summary>
public static class Masking
{
    private static readonly DependencyPropertyKey _maskExpressionPropertyKey = DependencyProperty.RegisterAttachedReadOnly("MaskExpression",
        typeof(Regex),
        typeof(Masking),
        new FrameworkPropertyMetadata());

    /// <summary>
    /// Identifies the <see cref="Mask"/> dependency property.
    /// </summary>
    public static readonly DependencyProperty MaskProperty = DependencyProperty.RegisterAttached("Mask",
        typeof(string),
        typeof(Masking),
        new FrameworkPropertyMetadata(OnMaskChanged));

    /// <summary>
    /// Identifies the <see cref="MaskExpression"/> dependency property.
    /// </summary>
    public static readonly DependencyProperty MaskExpressionProperty = _maskExpressionPropertyKey.DependencyProperty;

    /// <summary>
    /// Gets the mask for a given <see cref="TextBox"/>.
    /// </summary>
    /// <param name="textBox">
    /// The <see cref="TextBox"/> whose mask is to be retrieved.
    /// </param>
    /// <returns>
    /// The mask, or <see langword="null"/> if no mask has been set.
    /// </returns>
    public static string GetMask(TextBox textBox)
    {
        if (textBox == null)
        {
            throw new ArgumentNullException("textBox");
        }

        return textBox.GetValue(MaskProperty) as string;
    }

    /// <summary>
    /// Sets the mask for a given <see cref="TextBox"/>.
    /// </summary>
    /// <param name="textBox">
    /// The <see cref="TextBox"/> whose mask is to be set.
    /// </param>
    /// <param name="mask">
    /// The mask to set, or <see langword="null"/> to remove any existing mask from <paramref name="textBox"/>.
    /// </param>
    public static void SetMask(TextBox textBox, string mask)
    {
        if (textBox == null)
        {
            throw new ArgumentNullException("textBox");
        }

        textBox.SetValue(MaskProperty, mask);
    }

    /// <summary>
    /// Gets the mask expression for the <see cref="TextBox"/>.
    /// </summary>
    /// <remarks>
    /// This method can be used to retrieve the actual <see cref="Regex"/> instance created as a result of setting the mask on a <see cref="TextBox"/>.
    /// </remarks>
    /// <param name="textBox">
    /// The <see cref="TextBox"/> whose mask expression is to be retrieved.
    /// </param>
    /// <returns>
    /// The mask expression as an instance of <see cref="Regex"/>, or <see langword="null"/> if no mask has been applied to <paramref name="textBox"/>.
    /// </returns>
    public static Regex GetMaskExpression(TextBox textBox)
    {
        if (textBox == null)
        {
            throw new ArgumentNullException("textBox");
        } 

        return textBox.GetValue(MaskExpressionProperty) as Regex;
    }

    private static void SetMaskExpression(TextBox textBox, Regex regex)
    {
        textBox.SetValue(_maskExpressionPropertyKey, regex);
    }

    private static void OnMaskChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
    {
        var textBox = dependencyObject as TextBox;
        var mask = e.NewValue as string;
        textBox.PreviewTextInput -= textBox_PreviewTextInput;
        textBox.PreviewKeyDown -= textBox_PreviewKeyDown;
        DataObject.RemovePastingHandler(textBox, Pasting);

        if (mask == null)
        {
            textBox.ClearValue(MaskProperty);
            textBox.ClearValue(MaskExpressionProperty);
        }
        else
        {
            textBox.SetValue(MaskProperty, mask);
            SetMaskExpression(textBox, new Regex(mask, RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace));
            textBox.PreviewTextInput += textBox_PreviewTextInput;
            textBox.PreviewKeyDown += textBox_PreviewKeyDown;
            DataObject.AddPastingHandler(textBox, Pasting);
        }
    }

    private static void textBox_PreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        var textBox = sender as TextBox;
        var maskExpression = GetMaskExpression(textBox);

        if (maskExpression == null)
        {
            return;
        }

        var proposedText = GetProposedText(textBox, e.Text);

        if (!maskExpression.IsMatch(proposedText))
        {
            e.Handled = true;
        }
    }

    private static void textBox_PreviewKeyDown(object sender, KeyEventArgs e)
    {
        var textBox = sender as TextBox;
        var maskExpression = GetMaskExpression(textBox);

        if (maskExpression == null)
        {
            return;
        }

        //pressing space doesn't raise PreviewTextInput - no idea why, but we need to handle
        //explicitly here
        if (e.Key == Key.Space)
        {
            var proposedText = GetProposedText(textBox, " ");

            if (!maskExpression.IsMatch(proposedText))
            {
                e.Handled = true;
            }
        }
    }

    private static void Pasting(object sender, DataObjectPastingEventArgs e)
    {
        var textBox = sender as TextBox;
        var maskExpression = GetMaskExpression(textBox);

        if (maskExpression == null)
        {
            return;
        }

        if (e.DataObject.GetDataPresent(typeof(string)))
        {
            var pastedText = e.DataObject.GetData(typeof(string)) as string;
            var proposedText = GetProposedText(textBox, pastedText);

            if (!maskExpression.IsMatch(proposedText))
            {
                e.CancelCommand();
            }
        }
        else
        {
            e.CancelCommand();
        }
    }

    private static string GetProposedText(TextBox textBox, string newText)
    {
        var text = textBox.Text;

        if (textBox.SelectionStart != -1)
        {
            text = text.Remove(textBox.SelectionStart, textBox.SelectionLength);
        }

        text = text.Insert(textBox.CaretIndex, newText);

        return text;
    }
}

Here is yet another version of the existing solutions here. It's a Behavior simulating a "TextChanging" event providing the old text, the new text and a flag to cancel the change. This way we can implement whatever filter we want.

I got rid of the "PreviewKeyDown" handler used only for the space character. TextBox seems to manage everything with routed commands and "Space" does have its own commmand, though it's not public. I also added support for additionals keyboard shortcuts like "Ctrl+Backspace" (delete previous word).

public class TextChangingBehavior : Behavior<TextBox>
{
    public event EventHandler<TextChangingEventArgs> TextChanging;

    protected override void OnAttached()
    {
        base.OnAttached();

        AssociatedObject.PreviewTextInput += OnPreviewTextInput;
        CommandManager.AddPreviewExecutedHandler(AssociatedObject, OnPreviewExecutedHandler);
        DataObject.AddCopyingHandler(AssociatedObject, OnCopying);
        DataObject.AddPastingHandler(AssociatedObject, OnPasting);
    }

    protected override void OnDetaching()
    {
        base.OnDetaching();

        AssociatedObject.PreviewTextInput -= OnPreviewTextInput;
        CommandManager.RemovePreviewExecutedHandler(AssociatedObject, OnPreviewExecutedHandler);
        DataObject.RemoveCopyingHandler(AssociatedObject, OnCopying);
        DataObject.RemovePastingHandler(AssociatedObject, OnPasting);
    }

    #region Text

    private enum CharCategory
    {
        LetterOrDigit,
        Whitespace,
        Other
    }

    private CharCategory GetCharCategory(char c)
    {
        if (char.IsLetterOrDigit(c))
            return CharCategory.LetterOrDigit;
        else if (char.IsWhiteSpace(c))
            return CharCategory.Whitespace;
        else
            return CharCategory.Other;
    }

    private string GetText(string input = null)
    {
        var box = AssociatedObject;
        var text = box.Text ?? string.Empty;
        if (input != null)
        {
            // Delete selection
            var deleteCount = box.SelectionLength;
            if (deleteCount > 0)
                text = text.Remove(box.SelectionStart, deleteCount);
            // Insert input
            if (input.Length > 0)
                text = text.Insert(box.CaretIndex, input);
        }
        return text;
    }

    #endregion

    private void OnPreviewExecutedHandler(object sender, ExecutedRoutedEventArgs e)
    {
        var box = AssociatedObject;
        var selectionExists = box.SelectionLength > 0;
        var caretIndex = box.CaretIndex;
        string newText = null;

        if (e.Command == ApplicationCommands.Cut)
        {
            if (selectionExists)
                newText = GetText(string.Empty);
        }
        else if (e.Command == EditingCommands.Backspace)
        {
            if (selectionExists)
                newText = GetText(string.Empty);
            else if (caretIndex > 0)
                newText = GetText().Remove(caretIndex - 1, 1);
        }
        else if (e.Command == EditingCommands.Delete)
        {
            if (selectionExists)
                newText = GetText(string.Empty);
            else
            {
                newText = GetText();
                if (caretIndex >= newText.Length)
                    newText = null;
                else
                    newText = newText.Remove(caretIndex, 1);
            }
        }
        else if (e.Command == EditingCommands.DeletePreviousWord)
        {
            if (selectionExists)
                newText = GetText(string.Empty);
            else if (caretIndex > 0)
            {
                newText = GetText();
                var startIndex = caretIndex;
                // Include whitespaces
                do
                    startIndex--;
                while (startIndex > 0 && char.IsWhiteSpace(newText[startIndex]));
                // Include the next block
                var currentCategory = GetCharCategory(newText[startIndex]);
                while (startIndex > 0 && GetCharCategory(newText[startIndex - 1]) == currentCategory)
                    startIndex--;

                newText = newText.Remove(startIndex, caretIndex - startIndex);
            }
        }
        else if (e.Command == EditingCommands.DeleteNextWord)
        {
            if (selectionExists)
                newText = GetText(string.Empty);
            else
            {
                newText = GetText();
                if (caretIndex >= newText.Length)
                    newText = null;
                else
                {
                    var endIndex = caretIndex + 1;
                    // Include the current block
                    var currentCategory = GetCharCategory(newText[caretIndex]);
                    while (endIndex < newText.Length && GetCharCategory(newText[endIndex]) == currentCategory)
                        endIndex++;
                    // Include whitespaces
                    while (endIndex < newText.Length && char.IsWhiteSpace(newText[endIndex]))
                        endIndex++;

                    newText = newText.Remove(caretIndex, endIndex - caretIndex);
                }
            }
        }
        else if (e.Command is RoutedUICommand cmd && new[] { "Space", "ShiftSpace" }.Contains(cmd.Name))
        {
            newText = GetText(" ");
        }

        if (newText != null && OnProcessChange(newText))
            e.Handled = true;
    }

    private void OnCopying(object sender, DataObjectCopyingEventArgs e)
    {
        if (e.IsDragDrop)
        {
            if (OnProcessChange(GetText(string.Empty)))
                e.CancelCommand();
        }
    }

    private void OnPasting(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(typeof(string)))
        {
            if (OnProcessChange(GetText((string)e.DataObject.GetData(typeof(string)))))
                e.CancelCommand();
        }
    }

    private void OnPreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        if (!string.IsNullOrEmpty(e.Text))
        {
            if (OnProcessChange(GetText(e.Text)))
                e.Handled = true;
        }
    }

    private bool OnProcessChange(string newValue)
    {
        var oldValue = GetText();

        if (string.Equals(oldValue, newValue, StringComparison.Ordinal))
            return false;
        else
        {
            var args = new TextChangingEventArgs(oldValue, newValue);
            OnTextChanging(args);
            return args.Cancel;
        }
    }

    protected virtual void OnTextChanging(TextChangingEventArgs e)
    {
        TextChanging?.Invoke(this, e);
    }
}

public class TextChangingEventArgs : EventArgs
{
    public string OldValue { get; }

    public string NewValue { get; }

    public bool Cancel { get; set; }

    public TextChangingEventArgs(string oldValue, string newValue)
    {
        OldValue = oldValue;
        NewValue = newValue;
    }
}
Related