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" (http://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.

TextBlockSplitter.zip

Comments (15)

  1. Microasif says:

    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 project  

  2. 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.

  3. naveen says:

    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 PhoneApp1

  4. 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>

  5. Hammy Havoc says:

    How would I go about integrating this into a Windows Phone App Studio app? There's a character limit there for RSS feeds.

  6. Sorry Hammy, but I don't have any experience with the App Studio.

  7. The Great Vinay says:

    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 !!!

  8. 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/…/5588866

  9. Antonio says:

    Vinay how you solved?

  10. Zeeshan says:

    How can we solve txt flow problem?

    Any clue?

  11. naveen says:

    Can you please upload this example again blogs.msdn.com/…/creating-scrollable-textblock-for-wp7.aspx it would be really helpful for me!!!

  12. Aditya says:

    Hey can you me an idea how we can solve textflow problem?

  13. Bianca says:

    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.

  14. Hoang says:

    I can not find the function GetTextBlock

    GetTextBlock function, anyone can help me

  15. Caudam16 says:

    I can not find the function GetTextBlock

    GetTextBlock function, anyone can help me

Skip to main content