Search code examples
c#wpfxamltextboxmaxlength

Constraint length of Text in TextBox by its encoded representation


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.


Solution

  • 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.