Yet Another Scrollable TextBlock for Windows Phone
I have a personal Windows Phone (WP8) project where I needed to display a large amount of scrollable text on the screen while ideally taking a fraction of a second for loading. While searching the web I came across a few solutions, the most popular being the one by Alex Yakhnin in his blog called "Creating Scrollable TextBlock for WP7" (https://blogs.msdn.com/b/priozersk/archive/2010/09/08/creating-scrollable-textblock-for-wp7.aspx). That's what I started using initially, however I immediately ran across performance issues. For a relatively large block of text it would take me somewhere around 5 seconds to finish rendering. Also, the memory footprint would be quite substantial for such large amounts of text. So I went ahead and have improved performance of Alex's control to load in under 500ms and utilize virtualization available via various ListBoxes (I've used RadDataBoundListBox from Telerik, but you can use any other similar control).
To accomplish my task I have encapsulated the "splitting" logic into a separate class that produces the output as a List of strings. You can then bind that list to your favorite ListBox control and voila, you have a ginormous text block. Few things to notice about the TextBlockSplitter class. First of all, since I'm using a TextBlock control for text measurement and splitting, execution of the splitting code has to be done on the UI thread, otherwise you will get a nasty runtime exception. Second, and probably more important, is that the code has been optimized for performance and not text flow. For example, you may see something like this:
The logic can be optimized for text flow, but the performance will probably go down slightly...
So here is the code for the TextBlockSplitter class:
public class TextBlockSplitter
{
private TextBlock measureBlock;
private const double maxHeight = 2048;
public FontFamily FontFamily { get; set; }
private TextBlockSplitter()
{
measureBlock = GenerateTextBlock();
}
private static TextBlockSplitter instance;
public static TextBlockSplitter Instance
{
get
{
if (instance == null)
instance = new TextBlockSplitter();
return instance;
}
}
private TextBlock GenerateTextBlock()
{
TextBlock textBlock = new TextBlock();
textBlock.TextWrapping = TextWrapping.Wrap;
textBlock.Margin = new Thickness(10);
return textBlock;
}
public IList<string> Split(string value, double fontSize, FontWeight fontWeight, double screenWidth)
{
List<string> parsedText = new List<string>();
StringReader reader = new StringReader(value);
measureBlock.FontSize = fontSize;
measureBlock.FontWeight = fontWeight;
measureBlock.Width = screenWidth;
int maxTextCount = this.GetMaxTextSize();
if (value.Length < maxTextCount)
{
parsedText.Add(value);
}
else
{
while (reader.Peek() > 0)
{
string line = reader.ReadLine();
parsedText.AddRange(ParseLine(line, maxTextCount));
}
}
return parsedText;
}
private IList<string> ParseLine(string line, int maxTextCount)
{
int maxLineCount = GetMaxLineCount();
string tempLine = line;
var parsedText = new List<string>();
try
{
while (tempLine.Trim().Length > 0)
{
int charactersFitted = GetCharactersThatFit(tempLine, maxTextCount);
parsedText.Add(tempLine.Substring(0, charactersFitted).Trim());
tempLine = tempLine.Substring(charactersFitted, tempLine.Length - (charactersFitted));
}
}
catch (Exception e)
{
// Ignore
}
return parsedText;
}
private int GetCharactersThatFit(string text, int maxTextCount)
{
int maxLineLength = maxTextCount > text.Length ? text.Length : maxTextCount;
for (int i = maxLineLength - 1; i > 1; i--)
{
if (text[i] == ' ')
{
var nHeight = MeasureString(text.Substring(0, i - 1)).Height;
if (nHeight <= maxHeight)
return i;
}
}
return maxLineLength;
}
private Size MeasureString(string text)
{
this.measureBlock.Text = text;
return new Size(measureBlock.ActualWidth, measureBlock.ActualHeight);
}
private int GetMaxTextSize()
{
// Get average char size
Size size = this.MeasureText(" ");
// Get number of char that fit in the line
int charLineCount = (int)(measureBlock.Width / size.Width);
// Get line count
int lineCount = (int)(maxHeight / size.Height);
return charLineCount * lineCount / 2;
}
private int GetMaxLineCount()
{
Size size = this.MeasureText(" ");
// Get number of char that fit in the line
int charLineCount = (int)(measureBlock.Width / size.Width);
// Get line count
int lineCount = (int)(maxHeight / size.Height) - 5;
return lineCount;
}
private Size MeasureText(string value)
{
measureBlock.Text = value;
return new Size(measureBlock.ActualWidth, measureBlock.ActualHeight);
}
}
To use the code simply call the Split method passing in the necessary parameters:
var splitText = TextBlockSplitter.Instance.Split(largeText, 20, FontWeights.Normal,
Application.Current.Host.Content.ActualWidth);
Paragraphs = new ObservableCollection<string>(splitText);
That’s it. Hope you will find this useful.
Comments
Anonymous
September 08, 2013
My Textblock datablinding problem are the same, after 2000px height are all blank .. can you just confirm me, the relerik Listbox control can solve the problem ? How many pixel it can cover ? I need almost 5000+ words to blind with this textblock.. thanks for the article, new hope to end my projectAnonymous
September 16, 2013
That's correct. Telerik ListBox will solve this problem as well as any control with scrolling content. As long as the control itself (not it's content) is within 2048px it will not get trimmed.Anonymous
November 24, 2013
im getting an Error The name "RadDataBoundListBox" does not exist in the namespace "clr-namespace:Telerik.Windows.Controls;assembly=Telerik.Windows.Controls.Primitives". C:UsersNaveendraDesktopNew Folder (2)MainPage.xaml 18 9 PhoneApp1Anonymous
November 25, 2013
Hi Naveen, most likely you're getting "The name 'RadDataBoundListBox' does not exist" error message because you do not have Telerik controls installed. For this sample to work you may as well use LongListSelector. Just replace RadDataBoundListBox control with this: <phone:LongListSelector ItemsSource="{Binding Paragraphs}"> <phone:LongListSelector.ItemTemplate> <DataTemplate> <ListBoxItem> <TextBlock Text="{Binding}" TextWrapping="Wrap" FontSize="20"/> </ListBoxItem> </DataTemplate> </phone:LongListSelector.ItemTemplate> </phone:LongListSelector>Anonymous
November 28, 2013
How would I go about integrating this into a Windows Phone App Studio app? There's a character limit there for RSS feeds.Anonymous
December 02, 2013
Sorry Hammy, but I don't have any experience with the App Studio.Anonymous
December 16, 2013
Amazing. Thank you !! my problem got solved which I have been facing so long. How to add images between the tests sir ? I am a beginner and I can finish an app if I get an answer to it !!!Anonymous
December 17, 2013
Vinay, do you mean to include images in the text? If so you would have to do some work. See this answer for an example of embedded Image in TextBlock. stackoverflow.com/.../5588866Anonymous
March 08, 2014
Vinay how you solved?Anonymous
March 20, 2014
How can we solve txt flow problem? Any clue?Anonymous
May 31, 2014
Can you please upload this example again blogs.msdn.com/.../creating-scrollable-textblock-for-wp7.aspx it would be really helpful for me!!!Anonymous
July 21, 2014
Hey can you me an idea how we can solve textflow problem?Anonymous
August 11, 2014
I really appreciate your work, it helped me. I would point out, though, that this mechanism (unnecessary) breaks small lines and ignores the empty ones. To avoid the first the first one, an extra MeasureString(text, maxTextCount); is needed in private int GetCharactersThatFit(string text, int maxTextCount), to determine if the text already fits the maxtTextCount.Anonymous
November 24, 2014
I can not find the function GetTextBlock GetTextBlock function, anyone can help meAnonymous
November 24, 2014
I can not find the function GetTextBlock GetTextBlock function, anyone can help me