Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Limit number of lines entered in WPF textbox

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.

like image 542
Jay Avatar asked Oct 15 '15 11:10

Jay


2 Answers

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"
like image 153
Jay Avatar answered Oct 16 '22 21:10

Jay


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

like image 40
Ahmed Nour Avatar answered Oct 16 '22 21:10

Ahmed Nour