5

I am trying to limit the number of lines a user can enter in a textbox.

I have been researching - the closest I can find is this: Limit the max number of chars per line in a textbox .

And Limit the max number of chars per line in a textbox which turns out to be for winforms.

This isn't quite what I'm after... also worth mentioning that there is a misleading maxlines property which I have discovered only limits what is shown in the text box.

My requirements:

  • Do not require the use of a mono-spaced font
  • Limit the textbox to having a maximum of 5 lines
  • Accepts carriage return
  • Do not allow extra carriage returns
  • Stops text input when max length has been reached
  • Wraps text (don't particularly care if it does this in-between words or breaks up whole words)
  • Handles text being pasted into the control and will only paste in what will fit.
  • No scroll bars
  • Also - and this would nice - having the option of limiting the number of characters per line

These requirements are for creating a WYSIWYG textbox which will be used for capturing data that will eventually be printed, and the fonts need to be changeable - if the text gets cut off or is too big for a fixed size line - then it will come out that way in print (even if it does not look right).

I've had a stab at doing this myself by handling events - but am having a great deal of trouble getting this right. Here is my code so far.

XAML

 <TextBox TextWrapping="Wrap" AcceptsReturn="True"
        PreviewTextInput="UIElement_OnPreviewTextInput"
        TextChanged="TextBoxBase_OnTextChanged" />

Code Behind

 public int TextBoxMaxAllowedLines { get; set; }
    public int TextBoxMaxAllowedCharactersPerLine { get; set; }


    public MainWindow()
    {
        InitializeComponent();

        TextBoxMaxAllowedLines = 5;
        TextBoxMaxAllowedCharactersPerLine = 50;
    }

    private void TextBoxBase_OnTextChanged(object sender, TextChangedEventArgs e)
    {
        TextBox textBox = (TextBox)sender;

        int textLineCount = textBox.LineCount;

        if (textLineCount > TextBoxMaxAllowedLines)
        {
            StringBuilder text = new StringBuilder();
            for (int i = 0; i < TextBoxMaxAllowedLines; i++)
                text.Append(textBox.GetLineText(i));

            textBox.Text = text.ToString();
        }

    }

    private void UIElement_OnPreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        TextBox textBox = (TextBox)sender;

        int textLineCount = textBox.LineCount;


        for (int i = 0; i < textLineCount; i++)
        {
            var line = textBox.GetLineText(i);

            if (i == TextBoxMaxAllowedLines-1)
            {
                int selectStart = textBox.SelectionStart;
                textBox.Text = textBox.Text.TrimEnd('\r', '\n');
                textBox.SelectionStart = selectStart;

                //Last line
                if (line.Length > TextBoxMaxAllowedCharactersPerLine)
                    e.Handled = true;
            }
            else
            {
                if (line.Length > TextBoxMaxAllowedCharactersPerLine-1 && !line.EndsWith("\r\n"))
                    e.Handled = true;    
            }

        }
    }

This doesn't quite work right - I am getting strange behaviour on the last line and the selected position within the textbox keeps jumping about.

As an aside, maybe I am going down the wrong track... I was also wondering if this could be achieved by using a regular expression using something like this: https://stackoverflow.com/a/1103822/685341

I am open to any ideas as I have been struggling with this for a while. The requirements listed above are immutable - I am unable to change them.

Community
  • 1
  • 1
Jay
  • 9,561
  • 7
  • 51
  • 72

6 Answers6

5

Here is my final solution - I'd still like to hear if anyone can come up with a better way of doing this...

This just handles max number of lines - I haven't done anything with max characters yet - but it's logically a simple extension to what I have already done.

As I'm handling the textChanged event of the textbox - this also covers pasing into the control too - I haven't found a clean way to truncate text in this event (unless I handle the key_preview separately) - so I'm just not allowing invalid input by undoing.

XAML

<TextBox TextWrapping="Wrap" AcceptsReturn="True" 
             HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Disabled">
       <i:Interaction.Behaviors>
            <lineLimitingTextBoxWpfTest:LineLimitingBehavior TextBoxMaxAllowedLines="5" />

        </i:Interaction.Behaviors>
    </TextBox>

Code (for behaviour)

/// <summary> limits the number of lines the textbox will accept </summary>
public class LineLimitingBehavior : Behavior<TextBox>
{
    /// <summary> The maximum number of lines the textbox will allow </summary>
    public int? TextBoxMaxAllowedLines { get; set; }

    /// <summary>
    /// Called after the behavior is attached to an AssociatedObject.
    /// </summary>
    /// <remarks>
    /// Override this to hook up functionality to the AssociatedObject.
    /// </remarks>
    protected override void OnAttached()
    {
        if (TextBoxMaxAllowedLines != null && TextBoxMaxAllowedLines > 0)
            AssociatedObject.TextChanged += OnTextBoxTextChanged;
    }

    /// <summary>
    /// Called when the behavior is being detached from its AssociatedObject, but before it has actually occurred.
    /// </summary>
    /// <remarks>
    /// Override this to unhook functionality from the AssociatedObject.
    /// </remarks>
    protected override void OnDetaching()
    {
        AssociatedObject.TextChanged -= OnTextBoxTextChanged;
    }

    private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
    {
        TextBox textBox = (TextBox)sender;

        int textLineCount = textBox.LineCount;

        //Use Dispatcher to undo - http://stackoverflow.com/a/25453051/685341
        if (textLineCount > TextBoxMaxAllowedLines.Value)
            Dispatcher.BeginInvoke(DispatcherPriority.Input, (Action) (() => textBox.Undo()));
    }
}

This requires System.Windows.InterActivity to be added to the project and referenced in XAML thusly:

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
Jay
  • 9,561
  • 7
  • 51
  • 72
1

That is my simple soultion to set the MaxLines for TextBox and it is working fine, I hope it matches your requirements.

My_Defined_MaxTextLength is a property to set the MaxLenght

My_MaxLines is a property to set Maximum lines

My_TextBox.TextChanged += (sender, e) =>
                 {
                     if(My_TextBox.LineCount > My_MaxLines)
                     {
                         My_TextBox.MaxLength = My_TextBox.Text.Length;
                     }
                     else
                     {
                         My_TextBox.MaxLength = My_Defined_MaxTextLength;
                     }

                 };

Best Regards

Ahmed Nour

Ahmed Nour
  • 11
  • 2
0

I have been looking for answers to problems similar to this for awhile and every answer that I have found involves attaching event handlers and writing a lot of code. This didn't seem right to me, and seems to link the GUI much too tightly to the Codebehind for my tastes. Also, it doesn't seem to leverage the power of WPF.

Limiting the number of lines is actually part of the more generic question of: How do you limit anything in a textbox as it is being edited?

The answer is surprisingly simple: bind your textbox to a custom DependencyProperty, then use the CoerceCallback to limit/alter/change the content of the textbox.

Make sure to set your data context properly - the simplest (but not the best) way is to add the line: DataContext="{Binding RelativeSource={RelativeSource self}}" to the top of your Window or UserControl XAML code.

XAML

<TextBox TextWrapping="Wrap"
     Text="{Binding NotesText, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
     AcceptsReturn="True"
     HorizontalScrollBarVisibility="Disabled"
     VerticalScrollBarVisibility="Disabled">

Codebehind (C#)

    const int MaxLineCount = 10;
    const int MaxLineLength = 200;

    public static readonly DependencyProperty NotesTextProperty =
            DependencyProperty.Register(
                name: "NotesText",
                propertyType: typeof( String ),
                ownerType: typeof( SampleTextBoxEntryWindow ),
                typeMetadata: new PropertyMetadata(
                    defaultValue: string.Empty,
                    propertyChangedCallback: OnNotesTextPropertyChanged,
                    coerceValueCallback: CoerceTextLineLimiter ) );

    public string NotesText
    {
        get { return (String)GetValue( NotesTextProperty ); }
        set { SetValue( NotesTextProperty, value ); }
    }

    private static void OnNotesTextPropertyChanged(DependencyObject source,
        DependencyPropertyChangedEventArgs e)
    {
        // Whatever you want to do when the text changes, like 
        // set flags to allow buttons to light up, etc.
    }

    private static object CoerceTextLineLimiter(DependencyObject d, object value)
    {
        string result = null;

        if (value != null)
        {
            string text = ((string)value);
            string[] lines = text.Split( '\n' );

            if (lines.Length <= MaxLineCount)
                result = text;
            else
            {
                StringBuilder obj = new StringBuilder();
                for (int index = 0; index < MaxLineCount; index++)
                    if (lines[index].Length > 0)
                        obj.AppendLine( lines[index] > MaxLineLength ? lines[index].Substring(0, MaxLineLength) : lines[index] );

                result = obj.ToString();
            }
        }
        return result;
    }

(The line-limiting code is crude - but you get the idea).

The cool thing is, is this provides an easy framework to do other things as well, like limiting to numbers or alpha or special stuff - for instance, here is a simple (non-Regx) Phone number coerce method:

private static object CoercePhoneNumber(DependencyObject d, object value)
    {
        StringBuilder result = new StringBuilder();

        if (value != null)
        {
            string text = ((string)value).ToUpper();

            foreach (char chr in text)
                if ((chr >= '0' && chr <= '9') || (chr == ' ') || (chr == '-') || (chr == '(') || (chr == ')'))
                    result.Append( chr );

        }
        return result.ToString();
    }

This seems like a much cleaner and maintainable solution to me that can easily be refactored - while keeping the data and the presentation as separate as possible. The Coerce methods don't need to know anything about where the data came from or is going - it is just data.

Thomas Phaneuf
  • 306
  • 4
  • 9
  • I'm not sure that this is useful in my situation where I don't know how long the line is and am not expecting either carriage returns or new lines in the string (as this is just a user typing into a word-wrapping text box. Short of knowing which font I am using (and possibly only if I use a mono-spaced font) I am unable to calculate the line lengths in the code behind; the only way to get this that I have found is to ask the control itself how many lines have been entered. The solution I posted does just that and prevents any further input. it is hacky though. – Jay Feb 03 '16 at 15:33
  • I misunderstood what you were looking for when you were talking line counts. That is easily addressed - you can do something similar to the code in your OnTextBoxTextChanged event and adapt it for use in CoerceTextLineLimiter method. Cast the parameter d to Textbox: ((TextBox)d) to get the LineCount value, then you can operate directly the text data ((String)value), and the data in the textbox will respond. There should be no need to invoke, though. – Thomas Phaneuf Feb 10 '16 at 03:32
0

Thanks to Jay's answer I was able to find the best solution for me. It will undo paste and block typing.

public class LineLimitingBehavior : Behavior<TextBox>
{
    public int? TextBoxMaxAllowedLines { get; set; }

    protected override void OnAttached()
    {
        if (TextBoxMaxAllowedLines == null || !(TextBoxMaxAllowedLines > 0)) return;

        AssociatedObject.PreviewTextInput += OnTextBoxPreviewTextInput;
        AssociatedObject.TextChanged += OnTextBoxTextChanged;
    }

    private void OnTextBoxTextChanged(object sender, TextChangedEventArgs e)
    {
        var textBox = (TextBox)sender;

        if (textBox.LineCount > TextBoxMaxAllowedLines.Value)
            Dispatcher.BeginInvoke(DispatcherPriority.Input, (Action)(() => textBox.Undo()));
    }

    private void OnTextBoxPreviewTextInput(object sender, TextCompositionEventArgs e)
    {
        var textBox = (TextBox)sender;
        var currentText = textBox.Text;
        textBox.Text += e.Text;

        if (textBox.LineCount > TextBoxMaxAllowedLines.Value)
            e.Handled = true;

        textBox.Text = currentText;
        textBox.CaretIndex = textBox.Text.Length;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.PreviewTextInput -= OnTextBoxPreviewTextInput;
        AssociatedObject.TextChanged -= OnTextBoxTextChanged;
    }
}
Idan
  • 23
  • 1
  • 6
0

This should limit the lines and will show the last added lines, as opposed to showing the first lines:

XAML

<TextBox TextWrapping="Wrap" AcceptsReturn="True"
    PreviewTextInput="UIElement_OnPreviewTextInput"
    TextChanged="TextBoxBase_OnTextChanged" />

Code

const int MaxLineCount = 10;

private void TextBoxBase_OnTextChanged(object sender, TextChangedEventArgs e)
{
    TextBox textBox = (TextBox)sender;

    int textLineCount = textBox.LineCount;

    if (textLineCount > MaxLineCount)
    {
        StringBuilder text = new StringBuilder();
        for (int i = 0; i < MaxLineCount; i++)
        {
            text.Append(textBox.GetLineText((textLineCount - MaxLineCount) + i - 1));
        }
        textBox.Text = text.ToString();
    }
}
Jack Berkhout
  • 47
  • 1
  • 7
0
string prev_text = string.Empty;
    private void textBox1_TextChanged(object sender, TextChangedEventArgs e)
    {
        int MaxLineCount = 5;
        if (textBox1.LineCount > MaxLineCount)
        {
            int index = textBox1.CaretIndex;
            textBox1.Text = prev_text;
            textBox1.CaretIndex = index;
        }
        else
        {
            prev_text = textBox1.Text;
        }
    }
Danil
  • 701
  • 8
  • 7