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 TextBox
es. 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:
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 TextBlock
s, 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;
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 (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++;
}
continue;
}
tbLineMeasurer.Text += token;
tbLineMeasurer.InvalidateMeasure();
if (tbLineMeasurer.ActualWidth >= lineWidth)
{
if (tbLineMeasurer.Text != token)
{
tbLineMeasurer.Text = token;
lineCount++; }
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 (tokensInNewLine <= 0)
{
tokens.Insert(i + 1, tbLineMeasurer.Text);
tbLineMeasurer.Text = "";
continue;
}
}
}
if (tokensInNewLine > 0)
{
tbLineMeasurer.Text = string.Empty;
tokensInNewLine--;
}
}
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.