Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Simulating MaxLines Property in a Silverlight TextBox

0.00/5 (No votes)
30 Aug 2011 1  
How to simulate the MaxLines property in a Silverlight TextBox.

Introduction

You must have noticed that Silverlight TextBox control is missing the infamous MaxLines property which is a quite useful property in the WPF TextBox, and that's why you're reading this.

I have had this requirement since last week about putting a limit on the number of lines you can write in a multiline (AcceptsReturn=true) TextBox in Silverlight 4 and was disappointed to know that there is currently no way you do it like in WPF. In WPF, the TextBox control has an integer property called MaxLines which you can set to limit the number of lines. In Silverlight, there seems to be no way because you wouldn’t possibly know when or where in the string a word wrap split has occurred. It would be simple if you are using a fixed-width font like Courier/CourierNew, because you can simply count the number of characters that would fit in one line, but in my case, it is Comic Sans. The client insisted that they wanted this feature so I decided to write my own logic to simulate the word wrap algorithm used in Silverlight TextBoxes. I had no choice but to take the challenge.

Using the code

The attached code contains a Visual Studio solution that demonstrates the behavior of the text box. I called the new control XTextBox for "Extended TextBox".

The second attachment XTextBoxControl contains the source code of the XTextBox control.

You may checkout the demo of the TextBox here: http://briggs69.blogspot.com/2011/08/solution-maxlines-property-in.html.

Points of interest

What I did was I extended the Silverlight TextBox control and added my own logic to simulate how the word wrap behaves. Here's how it works.

The most interesting part of the source code is the GetNumberOfLines() method. This method returns the number of lines occupied by a certain string in the TextBox.

First off, we need to know at which characters does the word wrap algorithm splits the string into "wrappable" words, or in this case I call them tokens. With a few experiments, I found out that the TextBox word wrap algorithm splits words on these characters:

//split string into tokens/words separated by these symbols
string tokenizers = @"(?<=[ /\\\$%\(\)\-\+\[\]\{\}\?])";

List<string> tokens = Regex.Split(strNoBreaks, tokenizers).ToList();

Here we use a Regular Expression to split the string instead of String.Split() so it will include the splitting characters such as a white space.

Now that we know where to split the words to simulate the word wrap operation, what we need next is a way to measure the actual width of a string. This is so we will know whether a certain group of tokens in the textbox will fit or not in a single line in the TextBox. We can use a TextBlock control to measure the width of the string. Because with TextBlocks, if you don’t set the width explicitly, the actual width will automatically adjust according to its contents. So, we can use them to measure the actual with of a string.

Here is how the method looks like:

protected int GetNumberOfLines(string theText = null)
{
    tbLineMeasurer.Text = "";
    string strInput = theText == null ? Text : theText;

    //split string into tokens/words separated by these symbols
    string tokenizers = @"(?<=[ /\\\$%\(\)\-\+\[\]\{\}\?])";

    List<string> tokens = Regex.Split(strInput, tokenizers).ToList();
    int tokensInNewLine = 0;

    lineCount = 1;
    for (var i = 0; i < tokens.Count; i++)
    {
        string token = tokens[i];

        if (token == string.Empty)
        {
            if (tokensInNewLine > 0)
            {
                tbLineMeasurer.Text = string.Empty;
                tokensInNewLine--;
            }

            continue;
        }

        //if the word contains a line break, add line count
        //then split the token by linebreak into multiple tokens
        //then insert each token to the list of tokens and set line count
        if (token.Contains("\r"))
        {
            string[] s = token.Split('\r');
    
            lineCount += s.Count() - 1;
            tokensInNewLine = s.Count() - 1;

            int j = i + 1;
            foreach (var tok in s)
            {
                tokens.Insert(j, tok);
                j++;
            }

            //Skip this token, proceed to next token.
            continue;
        }

        //append the current token to the measuring TextBlock
        tbLineMeasurer.Text += token;

        tbLineMeasurer.InvalidateMeasure();

        //if the length of the current line's string
        //plus the new token exceeds the line width,
        //increment line count, then carry
        //over the current token to the next line.
        if (tbLineMeasurer.ActualWidth >= lineWidth)
        {
            if (tbLineMeasurer.Text != token)
            //Increment line count only
            //if the token is NOT the very first token
            {
                //carry over this token to the next line
                tbLineMeasurer.Text = token;
                lineCount++; //increment line count
            }

            //If the current token's width exceeds
            //the line width, simulate token split
            //then insert the last remaining unsplitted
            //string to the list of tokens for the next iteration
            if (tbLineMeasurer.ActualWidth >= lineWidth)
            {
                char[] arrToken = tbLineMeasurer.Text.ToArray();
                tbLineMeasurer.Text = "";
                foreach (var ch in arrToken)
                {
                    tbLineMeasurer.Text += ch;
                    if (tbLineMeasurer.ActualWidth > lineWidth)
                    {
                        lineCount++;
                        tbLineMeasurer.Text = new String(ch, 1);
                    }
                }

                //If this current token wraps to a new line, 
                //clear the measurer then insert the token to the list
                //because this token will be in a new line
                if (tokensInNewLine <= 0)
                {
                    tokens.Insert(i + 1, tbLineMeasurer.Text);
                    tbLineMeasurer.Text = "";
                    continue;
                }
            }
        }

        //If the following tokens are part of a token
        //with linebreaks, clear the measurer
        //as if the new token is in a new line.
        if (tokensInNewLine > 0)
        {
            tbLineMeasurer.Text = string.Empty;
            tokensInNewLine--;
        }
    }

    //System.Diagnostics.Debug.WriteLine(
    //       string.Format("Line Count = {0}" ,lineCount));
    return lineCount;
}

We may however optimize the code because you will be calling this from the TextChanged event. You could add another method that would roughly estimate the number of lines and then calls this method only if the estimated number of lines is close enough to the MaxLines limit. This method has a worst case running time of O(n^2) where n is the number of tokens/words in the string for a normal scenario and n is the number of characters for the worst case scenario. So be careful, this may run slowly if you have thousands of words in your textbox.

History

  • 18 Aug, 2011: Created article.
  • 31 Aug, 2011: Added link to the demo. Updated algorithm. Fixed bugs.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here