7

I have a TextBox in WPF. I want to restrict the length of the text in the TextBox. There is an easy way to restrict the number of characters by the property MaxLength.

In my use case I need to restrict the text not by the number of characters but by length of the binary representation of the text in a given encoding. As the program is used by germans there are some umlaut, that consume two byte.

I already have a method, that checks, if a given string fits into the given length:

public bool IsInLength(string text, int maxLength, Encoding encoding)
{
    return encoding.GetByteCount(text) < maxLength;
}

Does anybody has an idea how to tie this function to the textbox in a way, that the user has no possibility to enter too much characters to exceed the maximum byte length.

Solutions without EventHandler are prefered as the TextBox is inside a DataTemplate.

scher
  • 1,813
  • 2
  • 18
  • 39
  • do you use dataannotations and the validation interfaces (Inotfiy...) to validate the input? – Jehof Aug 01 '17 at 09:15
  • Yes, i do. Is there a way then? – scher Aug 01 '17 at 09:20
  • A Custom validation attribute on the property that is bound to the textbox – Jehof Aug 01 '17 at 09:22
  • This might be an idea for a static solution. In this case the maximum length and the encoding can be defined by the user in a settings file. – scher Aug 01 '17 at 09:24
  • not only. depends on what kind of settings file you are using? normal appsettings. The custom validation needs to be in a static method this can access the ConfigurationManager class to get the values from the settings file. But this depends on your system – Jehof Aug 01 '17 at 09:28

2 Answers2

4

A ValidationRule may be what fits the bill here. Here's an example implementation:

public sealed class ByteCountValidationRule : ValidationRule
{
    // For this example I test using an emoji () which will take 2 bytes and fail this rule.
    static readonly int MaxByteCount = 1;

    static readonly ValidationResult ByteCountExceededResult = new ValidationResult(false, $"Byte count exceeds the maximum allowed limit of {MaxByteCount}");

    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        var val = value as string;

        return val != null && Encoding.UTF8.GetByteCount(val) > MaxByteCount
            ? ByteCountExceededResult
            : ValidationResult.ValidResult;
    }
}

And the XAML use:

    <TextBox.Text>
        <Binding Path="Text" UpdateSourceTrigger="PropertyChanged">
            <Binding.ValidationRules>
                <local:ByteCountValidationRule />
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>

Now you can either put 1 emoji or 2 ascii characters to trigger failure (since either will exceed 1 byte limit).

Maverik
  • 5,619
  • 35
  • 48
  • Thanks for your response. This is a nice solution. To use the ValidationRule was new for me and will help me in further applications. However I was looking for a solution, where the user is not able to input too much characters. With the ValidationRule the user is able to input too much characters but gets an error. – scher Aug 02 '17 at 05:46
  • I strongly doubt you can achieve that using native textbox. The user can visually input the characters but your viewmodels don't get updated with the invalid information. That's just how the system works. A workaround would be to add a binding to MaxLength perhaps that dynamically reduces the number of characters that can be input as user types but even then that'd fail to achieve your desired result 100% besides being a confusing user experience. I'd strongly suggest not to go down this route of changing the default expected behavior of control. – Maverik Aug 02 '17 at 12:44
  • I am working on a solution with a [`Behavior`](https://msdn.microsoft.com/en-us/library/system.windows.interactivity.behavior(v=expression.40).aspx). I will post it if its possible. – scher Aug 02 '17 at 13:22
0

I have extended the solution of Alex Klaus to prevent input of too long texts.

public class TextBoxMaxLengthBehavior : Behavior<TextBox>
{
    public static readonly DependencyProperty MaxLengthProperty =
        DependencyProperty.Register(
            nameof(MaxLength),
            typeof(int),
            typeof(TextBoxMaxLengthBehavior),
            new FrameworkPropertyMetadata(0));

    public int MaxLength
    {
        get { return (int) GetValue(MaxLengthProperty); }
        set { SetValue(MaxLengthProperty, value); }
    }

    public static readonly DependencyProperty LengthEncodingProperty =
        DependencyProperty.Register(
            nameof(LengthEncoding),
            typeof(Encoding),
            typeof(TextBoxMaxLengthBehavior),
            new FrameworkPropertyMetadata(Encoding.Default));

    public Encoding LengthEncoding
    {
        get { return (Encoding) GetValue(LengthEncodingProperty); }
        set { SetValue(LengthEncodingProperty, value); }
    }

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

        AssociatedObject.PreviewTextInput += PreviewTextInputHandler;
        DataObject.AddPastingHandler(AssociatedObject, PastingHandler);
    }

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

        AssociatedObject.PreviewTextInput -= PreviewTextInputHandler;
        DataObject.RemovePastingHandler(AssociatedObject, PastingHandler);
    }

    private void PreviewTextInputHandler(object sender, TextCompositionEventArgs e)
    {
        string text;
        if (AssociatedObject.Text.Length < AssociatedObject.CaretIndex)
            text = AssociatedObject.Text;
        else
        {
            //  Remaining text after removing selected text.
            string remainingTextAfterRemoveSelection;

            text = TreatSelectedText(out remainingTextAfterRemoveSelection)
                ? remainingTextAfterRemoveSelection.Insert(AssociatedObject.SelectionStart, e.Text)
                : AssociatedObject.Text.Insert(AssociatedObject.CaretIndex, e.Text);
        }

        e.Handled = !ValidateText(text);
    }

    private bool TreatSelectedText(out string text)
    {
        text = null;
        if (AssociatedObject.SelectionLength <= 0)
            return false;

        var length = AssociatedObject.Text.Length;
        if (AssociatedObject.SelectionStart >= length)
            return true;

        if (AssociatedObject.SelectionStart + AssociatedObject.SelectionLength >= length)
            AssociatedObject.SelectionLength = length - AssociatedObject.SelectionStart;

        text = AssociatedObject.Text.Remove(AssociatedObject.SelectionStart, AssociatedObject.SelectionLength);
        return true;
    }

    private void PastingHandler(object sender, DataObjectPastingEventArgs e)
    {
        if (e.DataObject.GetDataPresent(DataFormats.Text))
        {
            var pastedText = Convert.ToString(e.DataObject.GetData(DataFormats.Text));
            var text = ModifyTextToFit(pastedText);

            if (!ValidateText(text))
                e.CancelCommand();
            else if (text != pastedText)
                e.DataObject.SetData(DataFormats.Text, text);

        }
        else
            e.CancelCommand();
    }

    private string ModifyTextToFit(string text)
    {
        var result = text.Remove(MaxLength);
        while (!string.IsNullOrEmpty(result) && !ValidateText(result))
            result = result.Remove(result.Length - 1);

        return result;
    }

    private bool ValidateText(string text)
    {
        return LengthEncoding.GetByteCount(text) <= MaxLength;
    }
}

In the XAML I can use it like this:

<DataTemplate DataType="{x:Type vm:StringViewModel}">
    <TextBox Text="{Binding Path=Value, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}">
        <i:Interaction.Behaviors>
            <b:TextBoxMaxLengthBehavior MaxLength="{Binding MaxLength}" LengthEncoding="{Binding LengthEncoding}" />
        </i:Interaction.Behaviors>
    </TextBox>
</DataTemplate>

where xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity". I hope that this will help somebody else.

scher
  • 1,813
  • 2
  • 18
  • 39
  • 1
    Why not use the ValidationRule approach...? It's smaller, easier to read, and works... And it's just as reusable as a behavior. – Lynn Crumbling Aug 03 '17 at 21:36
  • Because with the ValidationRule the user can input more characters into the textbox than allowed. He or she sees that this is not valid but that was not my expected behavior. I really wanted the user not o input too much characters. – scher Aug 04 '17 at 04:48
  • Not sure why people are downvoting you. The paste handler could be optimized a bit so it doesn't go character by character but otherwise this is a nice reusable solution to mimic `MaxLength` behavior. – Mike Marynowski Feb 01 '19 at 21:10