Book Reader (Pagination & List Picker)

0
236

To get the best reading experience, this app enables you to customize the foreground and background colors, the text size, and even the font family. Book Reader provides easy page navigation and enables jumping to any chapter or page number.

It might not be immediately obvious, but the biggest challenge to implementing this app is pagination—dividing the book’s contents into discrete pages based on the font settings. Avoiding this challenge by placing the entire book’s contents in a scroll viewer wouldn’t be a great user experience. It also wouldn’t be feasible without extra trickery due to the size limitation of UI elements. Therefore, this app shows one page of text at a time. The user can tap the screen to advance the page, or tap a button on the application bar to go back by one page.

The Main Page

The main page, pictured in Figure 25.1 with its application bar expanded, shows the current page and an application bar with a button to go back one page, a button to jump to any chapter or page, and a button to change settings. The application bar area also shows the current page number as well as the total number of pages in the book (based on the current font settings). Listing 25.1 contains this page’s XAML.

FIGURE 25.1 The main page, with its default Amazon Kindle-inspired color scheme that provides just enough contrast for reading.
FIGURE 25.1 The main page, with its default Amazon Kindle-inspired color scheme that provides just enough contrast for reading.

LISTING 25.1 MainPage.xaml—The User Interface for Book Reader’s Main Page

[code]

<phone:PhoneApplicationPage x:Class=”WindowsPhoneApp.MainPage”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
xmlns:phone=”clr-namespace:Microsoft.Phone.Controls;assembly=Microsoft.Phone”
xmlns:shell=”clr-namespace:Microsoft.Phone.Shell;assembly=Microsoft.Phone”
xmlns:local=”clr-namespace:WindowsPhoneApp”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”
SupportedOrientations=”Portrait”>
<!– The application bar, with three buttons –>
<phone:PhoneApplicationPage.ApplicationBar>
<shell:ApplicationBar Opacity=”0”>
<shell:ApplicationBarIconButton Text=”previous” IsEnabled=”False”
IconUri=”/Shared/Images/appbar.left.png” Click=”PreviousButton_Click”/>
<shell:ApplicationBarIconButton Text=”page jump”
IconUri=”/Images/appbar.book.png” Click=”JumpButton_Click”/>
<shell:ApplicationBarIconButton Text=”settings” IconUri=
“/Shared/Images/appbar.settings.png” Click=”SettingsButton_Click”/>
</shell:ApplicationBar>
</phone:PhoneApplicationPage.ApplicationBar>
<Grid x:Name=”LayoutRoot”>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height=”56”/>
</Grid.RowDefinitions>
<!– The document that takes up most of the page –>
<local:PaginatedDocument x:Name=”Document” Margin=”12”
Width=”456” Height=”720”/>
<!– The footer that shows the page number and total page count –>
<TextBlock x:Name=”Footer” Grid.Row=”1” Margin=”14,0,0,17”
HorizontalAlignment=”Left” VerticalAlignment=”Center”/>
<!– The full-screen panel with the text box and chapter list –>
<Grid x:Name=”JumpPanel” Grid.RowSpan=”2” Visibility=”Collapsed”>
<Grid.RowDefinitions>
<RowDefinition Height=”Auto”/>
<RowDefinition/>
</Grid.RowDefinitions>
<Rectangle Grid.RowSpan=”2” Fill=”{StaticResource PhoneBackgroundBrush}”
Opacity=”.9”/>
<!– Enter a page number –>
<StackPanel Orientation=”Horizontal” Margin=”12”>
<TextBlock Text=”Jump to page:” VerticalAlignment=”Center”/>
<TextBox x:Name=”PageTextBox” InputScope=”Number” MinWidth=”150”
GotFocus=”PageTextBox_GotFocus” KeyUp=”PageTextBox_KeyUp”/>
<Button Content=”Go” MinWidth=”150” local:Tilt.IsEnabled=”True”
Click=”GoButton_Click”/>
</StackPanel>
<!– Choose a chapter from the list box –>
<ListBox x:Name=”ChaptersListBox” Grid.Row=”1” Margin=”12”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”
SelectionChanged=”ChaptersListBox_SelectionChanged”>
<!– This is done so the chapter page numbers are right-aligned –>
<ListBox.ItemContainerStyle>
<Style TargetType=”ListBoxItem”>
<Setter Property=”HorizontalContentAlignment” Value=”Stretch”/>
</Style>
</ListBox.ItemContainerStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<Grid local:Tilt.IsEnabled=”True”>
<!– The left-aligned chapter title –>
<TextBlock Text=”{Binding Key}”/>
<!– The right-aligned page number –>
<TextBlock Text=”{Binding Value}” HorizontalAlignment=”Right”/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Grid>
</phone:PhoneApplicationPage>

[/code]

  • The Footer text block appears in the application bar area because it is placed underneath its area, and the application bar is marked with an opacity of 0.
  • The list box filled with chapters, shown in Figure 25.2, uses an important but hard-to-discover trick to enable the list box items to stretch to fill the width of the list box. This enables elements of each item (the page number, in this case) to be right-aligned without giving each item an explicit width.
FIGURE 25.2 The list box with chapters uses a HorizontalContentAlignment of Stretch, so the page numbers can be right-aligned without giving each item an explicit width.
FIGURE 25.2 The list box with chapters uses a HorizontalContentAlignment of Stretch, so the page numbers can be right-aligned without giving each item an explicit width.

To make the content of list box items stretch to fill the width of the list box, give the list box an ItemContainerStyle as follows:

[code]

<ListBox.ItemContainerStyle>
<Style TargetType=”ListBoxItem”>
<Setter Property=”HorizontalContentAlignment” Value=”Stretch”/>
</Style>
</ListBox.ItemContainerStyle>

[/code]

Listing 25.2 contains the code-behind for the main page.

LISTING 25.2 MainPage.xaml.cs—The Code-Behind for Book Reader’s Main Page

[code]

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Navigation;
using System.Windows.Resources;
using Microsoft.Phone.Controls;
using Microsoft.Phone.Shell;
namespace WindowsPhoneApp
{
public partial class MainPage : PhoneApplicationPage
{
IApplicationBarIconButton previousButton;
public MainPage()
{
InitializeComponent();
this.previousButton = this.ApplicationBar.Buttons[0]
as IApplicationBarIconButton;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
// Respect the saved settings
this.ApplicationBar.ForegroundColor = Settings.TextColor.Value;
this.LayoutRoot.Background = new SolidColorBrush(Settings.PageColor.Value);
this.Document.Foreground = this.Footer.Foreground =
new SolidColorBrush(Settings.TextColor.Value);
this.Document.FontSize = Settings.TextSize.Value;
this.Document.FontFamily = new FontFamily(Settings.Font.Value);
if (this.Document.Text == null)
{
// Load the book as one big string from the included file
LoadBook(delegate(string s)
{
// This happens on a background thread, but that’s okay
this.Document.Text = s;
UpdatePagination();
});
}
else if (this.State.ContainsKey(“TextSize”))
{
if (((int)this.State[“TextSize”] != Settings.TextSize.Value ||
(string)this.State[“Font”] != Settings.Font.Value))
{
// If the font family or size changed, the book needs to be repaginated
UpdatePagination();
}
else if ((Color)this.State[“TextColor”] != Settings.TextColor.Value)
{
// If only the color changed, simply re-render the current page
this.Document.RefreshCurrentPage();
}
}
// Remember the current text settings so we can detect if they
// were changed when returning from the settings page
this.State[“TextSize”] = Settings.TextSize.Value;
this.State[“Font”] = Settings.Font.Value;
this.State[“TextColor”] = Settings.TextColor.Value;
}
protected override void OnBackKeyPress(CancelEventArgs e)
{
base.OnBackKeyPress(e);
// If the page/chapter jump panel is open, make the back button close it
if (this.JumpPanel.Visibility == Visibility.Visible)
{
e.Cancel = true;
CloseJumpPanel();
}
}
protected override void OnMouseLeftButtonUp(MouseButtonEventArgs e)
{
base.OnMouseLeftButtonUp(e);
// Treat any tap as a page advance,
// unless the page/chapter jump panel is open
if (this.JumpPanel.Visibility == Visibility.Collapsed)
{
this.Document.ShowNextPage();
RefreshFooter();
}
}
// Retrieve the text from the included text file
public static void LoadBook(Action<string> callback)
{
string s = null;
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += delegate(object sender, DoWorkEventArgs e)
{
// Do this work on a background thread
StreamResourceInfo info = Application.GetResourceStream(
new Uri(“1342.txt”, UriKind.Relative));
using (info.Stream)
using (StreamReader reader = new StreamReader(info.Stream))
s = reader.ReadToEnd();
if (callback != null)
callback(s);
};
worker.RunWorkerAsync();
}
void UpdatePagination()
{
this.Document.UpdatePagination(delegate()
{
// Now that the book has been repaginated, refresh some UI
// on the main thread
this.Dispatcher.BeginInvoke(delegate()
{
// Move to the page we were previously on based on the character index
// in the string (because the old page numbers are now meaningless)
this.Document.ShowPageWithCharacterIndex(
Settings.CurrentCharacterIndex.Value);
RefreshFooter();
// Fill the chapters list box based on the current page numbers
this.ChaptersListBox.Items.Clear();
for (int i = 0; i < this.Document.Chapters.Count; i++)
{
this.ChaptersListBox.Items.Add(new KeyValuePair<string, string>(
“Chapter “ + (i + 1), // Title
this.Document.Chapters[i].ToString(“N0”) // Page number
));
}
});
});
}
void RefreshFooter()
{
// Because this is called whenever the page is changed, this is a good
// spot to store the current spot in the book
Settings.CurrentCharacterIndex.Value = this.Document.CurrentCharacterIndex;
this.Footer.Text = this.Document.CurrentPage.ToString(“N0”) + “ / “ +
this.Document.TotalPages.ToString(“N0”);
this.previousButton.IsEnabled = (this.Document.CurrentPage > 1);
}
void OpenJumpPanel()
{
this.JumpPanel.Visibility = Visibility.Visible;
this.ApplicationBar.IsVisible = false;
// Fill the text box with the current page number
// (without thousands separator)
this.PageTextBox.Text = this.Document.CurrentPage.ToString();
// Temporarily support landscape hardware keyboards
this.SupportedOrientations = SupportedPageOrientation.PortraitOrLandscape;
}
void CloseJumpPanel()
{
this.JumpPanel.Visibility = Visibility.Collapsed;
this.ApplicationBar.IsVisible = true;
this.SupportedOrientations = SupportedPageOrientation.Portrait;
}
void ChaptersListBox_SelectionChanged(object sender,
SelectionChangedEventArgs e)
{
if (this.ChaptersListBox.SelectedIndex >= 0)
{
// Jump to the selected page
this.Document.ShowPage(
this.Document.Chapters[this.ChaptersListBox.SelectedIndex]);
RefreshFooter();
// Clear the selection so consecutive taps on the same item works
this.ChaptersListBox.SelectedIndex = -1;
// Delay the closing of the panel so OnMouseLeftButtonUp
// doesn’t advance the page
this.Dispatcher.BeginInvoke(delegate() { CloseJumpPanel(); });
}
}
void PageTextBox_GotFocus(object sender, RoutedEventArgs e)
{
this.PageTextBox.SelectAll();
}
void PageTextBox_KeyUp(object sender, System.Windows.Input.KeyEventArgs e)
{
// Make pressing Enter do the same thing as tapping “Go”
if (e.Key == Key.Enter)
GoButton_Click(this, null);
}
void GoButton_Click(object sender, RoutedEventArgs e)
{
// If the page number is valid, jump to it
int pageNumber;
if (int.TryParse(this.PageTextBox.Text, out pageNumber))
{
this.Document.ShowPage(pageNumber);
RefreshFooter();
CloseJumpPanel();
}
}
// Application bar handlers
void PreviousButton_Click(object sender, EventArgs e)
{
this.Document.ShowPreviousPage();
RefreshFooter();
}
void JumpButton_Click(object sender, EventArgs e)
{
OpenJumpPanel();
}
void SettingsButton_Click(object sender, EventArgs e)
{
this.NavigationService.Navigate(new Uri(“/SettingsPage.xaml”,
UriKind.Relative));
}
}
}

[/code]

  • The book is a text file included as content (Build Action = Content), just like the database files in the preceding chapter. The filename is 1342.txt, matching the document downloaded from the Project Gutenberg website.
  • This app uses the following settings:

    [code]
    public static class Settings
    {
    // The current position in the book
    public static readonly Setting<int> CurrentCharacterIndex =
    new Setting<int>(“CurrentCharacterIndex”, 0);
    // The user-configurable settings
    public static readonly Setting<string> Font =
    new Setting<string>(“Font”, “Georgia”);
    public static readonly Setting<Color> PageColor =
    new Setting<Color>(“PageColor”, Color.FromArgb(0xFF, 0xA1, 0xA1, 0xA1));
    public static readonly Setting<Color> TextColor =
    new Setting<Color>(“TextColor”, Colors.Black);
    public static readonly Setting<int> TextSize =
    new Setting<int>(“TextSize”, 32);
    }
    [/code]

    The reader’s position in the book is stored as a character index—the index of the first character on the current page in the string containing the entire contents of the book. This is done because the page number associated with any spot in the book can vary dramatically based on the font settings. With this scheme, the user’s true position in the book is always maintained.

  • The key-value pair added to the chapters list box is a convenient type to use because it exposes two separate string properties that the data template in Listing 25.1 is able to bind to. The “key” is the left-aligned chapter title and the “value” is the rightaligned page number.

The Settings Page

Book Reader’s settings page is almost identical to the settings page for Notepad. The difference is a font picker on top of the other controls, shown in Figure 25.3. This font picker is created with the list picker control from the Silverlight for Windows Phone Toolkit.

FIGURE 25.3 The font picker shows ten fonts in a WYSIWYG picker.
FIGURE 25.3 The font picker shows ten fonts in a WYSIWYG picker.

A list picker is basically a combo box. It initially looks like a text box but, when tapped, it enables the user to pick one value out of a list of possible values.

To get the WYSIWYG font list inside the list picker, Book Reader’s settings page uses the following XAML:

[code]

<toolkit:ListPicker x:Name=”FontPicker” Header=”Font” Grid.ColumnSpan=”2”
SelectionChanged=”FontPicker_SelectionChanged” ItemCountThreshold=”10”>
<toolkit:ListPicker.ItemTemplate>
<DataTemplate>
<TextBlock FontFamily=”{Binding}” Text=”{Binding}”/>
</DataTemplate>
</toolkit:ListPicker.ItemTemplate>
<sys:String>Arial</sys:String>
<sys:String>Calibri</sys:String>
<sys:String>Georgia</sys:String>
<sys:String>Lucida Sans Unicode</sys:String>
<sys:String>Segoe WP</sys:String>
<sys:String>Segoe WP Black</sys:String>
<sys:String>Tahoma</sys:String>
<sys:String>Times New Roman</sys:String>
<sys:String>Trebuchet MS</sys:String>
<sys:String>Verdana</sys:String>
</toolkit:ListPicker>

[/code]

The data template binds both FontFamily and Text properties of each text block to display each string in the list.

List pickers support two different ways of presenting their list of items: an inline mode and a full mode. In the inline mode, the control expands and collapses with smooth animations. This is what is happening in Figure 25.3. In the full mode, the control displays a full-screen popup that presents its list of items. This is pictured in Figure 25.4.

Why does the ComboBox control look so strange when I try to use it in a Windows Phone app?

The ComboBox control is a core Silverlight control frequently used on the web, but it was never given a style that is appropriate for Windows Phone. It is not intended to be used. (The control should have been removed to avoid confusion.) If you find yourself wanting to use a combo box, use the list picker instead.

By default, a list picker uses its inline mode if there are five or fewer items; otherwise, it uses full mode. This is consistent with Windows Phone design guidelines. However, you can force either mode by setting the value of ItemCountThreshold appropriately. The list picker will stay in its inline mode as long as the number of items is less than or equal to ItemCountThreshold. Book Reader chooses to keep the font picker with 10 fonts in inline mode, so it sets this property to 10.

FIGURE 25.4 A variation of Book Reader’s font picker, configured to use full mode.
FIGURE 25.4 A variation of Book Reader’s font picker, configured to use full mode.

List picker defines a Header and corresponding HeaderTemplate property, and an ItemTemplate property for customizing the appearance of each item in the inline mode. Even if you use full mode, these properties are still important for the appearance of the list picker when the full-screen list isn’t showing. For the full-screen list, list picker also defines separate FullModeHeader and FullModeItemTemplate properties. The full-mode list picker shown in Figure 25.4 takes advantage of these two properties as follows:

[code]

<toolkit:ListPicker Header=”Font” FullModeHeader=”FONT”>
<toolkit:ListPicker.ItemTemplate>
<!– For displaying the selected item inline –>
<DataTemplate>
<TextBlock FontFamily=”{Binding}” Text=”{Binding}”/>
</DataTemplate>
</toolkit:ListPicker.ItemTemplate>
<toolkit:ListPicker.FullModeItemTemplate>
<!– For displaying each item in full mode –>
<DataTemplate>
<TextBlock FontFamily=”{Binding}” Text=”{Binding}” Margin=”12”
FontSize=”{StaticResource PhoneFontSizeExtraLarge}”/>
</DataTemplate>
</toolkit:ListPicker.FullModeItemTemplate>
<sys:String>Arial</sys:String>
<sys:String>Calibri</sys:String>
<sys:String>Georgia</sys:String>
<sys:String>Lucida Sans Unicode</sys:String>
<sys:String>Segoe WP</sys:String>
<sys:String>Segoe WP Black</sys:String>
<sys:String>Tahoma</sys:String>
<sys:String>Times New Roman</sys:String>
<sys:String>Trebuchet MS</sys:String>
<sys:String>Verdana</sys:String>
</toolkit:ListPicker>

[/code]

If you don’t specify a FullModeItemTemplate, the full mode will use ItemTemplate.

List pickers cannot contain UI elements when full mode is used!

If you directly place UI elements such as text blocks or the toolkit’s own ListPickerItem controls inside a list picker, an exception is thrown when attempting to display the full-mode popup.That’s because the control attempts to add each item to the additional full-screen list, but a single UI element can only be in one place at a time.The solution is to place nonvisual data items in the list picker then use item template(s) to control each item’s visual appearance.

Avoid putting an inline-mode list picker at the bottom of a scroll viewer!

List picker behaves poorly in this situation. When it first expands, the view is not shifted to ensure its contents are on-screen.Then, when attempting to scroll to view the off-screen contents, the list picker collapses!

For the best performance, elements below an inline-mode list picker should be marked with CacheMode=”BitmapCache”.That’s because the expansion and contraction of the list picker animates the positions of these elements.

The PaginatedDocument User Control

To determine where page breaks occur, the PaginatedDocument user control must measure the width and height of each character under the current font settings. The only way to perform this measurement is to place text in a text block and check the values of its ActualWidth and ActualHeight properties. Therefore, PaginatedDocument uses the following three-step algorithm:

  1. Find each unique character in the document. (The Pride and Prejudice document contains only 85 unique characters.)
  2. Measure the width and height of each character by placing each one in a text block, one at a time. The height of all characters is always the same (as the reported height is the line height, padding and all), so the height only needs to be checked once.
  3. Go through the document from beginning to end and, using the precalculated widths of each character, figure out where each line break occurs. With this information, and with the precalculated line height, we know where each page break occurs. Determining line breaks can be a bit tricky due to the need to wrap words appropriately.

The control renders any page by adding a text block for each line, based on the calculated page breaks and line breaks. This is done to ensure that every line break occurs exactly where we expect it to.

Listing 25.3 contains the user control’s XAML and Listing 25.4 contains its code-behind.

LISTING 25.3 PaginatedDocument.xaml—The User Interface for the PaginatedDocument User Control

[code]

<UserControl x:Class=”WindowsPhoneApp.PaginatedDocument”
xmlns=”http://schemas.microsoft.com/winfx/2006/xaml/presentation”
xmlns:x=”http://schemas.microsoft.com/winfx/2006/xaml”
FontFamily=”{StaticResource PhoneFontFamilyNormal}”
FontSize=”{StaticResource PhoneFontSizeNormal}”
Foreground=”{StaticResource PhoneForegroundBrush}”>
<Canvas>
<!– Contains the lines of text –>
<StackPanel x:Name=”StackPanel” Margin=”0,-6,0,0”/>
<!– Used for measurements –>
<TextBlock x:Name=”MeasuringTextBlock”/>
</Canvas>
</UserControl>

[/code]

LISTING 25.4 PaginatedDocument.xaml.cs—The Code-Behind for the PaginatedDocument User Control

[code]

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Windows.Controls;
namespace WindowsPhoneApp
{
public partial class PaginatedDocument : UserControl
{
Dictionary<char, double> characterWidths = new Dictionary<char, double>();
double characterHeight;
bool isUpdating;
int currentPageBreakIndex;
List<int> pageBreaks = new List<int>();
List<int> lineBreaks = new List<int>();
public List<int> Chapters = new List<int>();
public PaginatedDocument()
{
InitializeComponent();
}
public int CurrentPage
{
get { return this.currentPageBreakIndex + 1; }
}
public int TotalPages
{
get { return this.pageBreaks.Count – 1; }
}
public string Text { get; set; }
public int CurrentCharacterIndex { get; private set; }
public void UpdatePagination(Action doneCallback)
{
if (this.Text == null || this.isUpdating)
throw new InvalidOperationException();
this.isUpdating = true;
// Reset measurements
this.pageBreaks.Clear(); this.lineBreaks.Clear();
this.pageBreaks.Add(0); this.lineBreaks.Add(0);
this.Chapters.Clear();
this.characterWidths.Clear();
this.characterHeight = -1;
BackgroundWorker worker = new BackgroundWorker();
worker.DoWork += delegate(object sender, DoWorkEventArgs e)
{
// STEP 1: BACKGROUND THREAD
// Build up a dictionary of unique characters in the text
for (int i = 0; i < this.Text.Length; i++)
{
if (!this.characterWidths.ContainsKey(this.Text[i]))
this.characterWidths.Add(this.Text[i], -1);
}
// Copy the character keys so we can update the width values
// without affecting the enumeration
char[] chars = new char[this.characterWidths.Keys.Count];
this.characterWidths.Keys.CopyTo(chars, 0);
this.Dispatcher.BeginInvoke(delegate()
{
// STEP 2: MAIN THREAD
// Measure the height of all characters
// and the width of each character
foreach (char c in chars)
{
// The only way to measure the width is to place the
// character in a text block and ask for its ActualWidth
this.MeasuringTextBlock.Text = c.ToString();
this.characterWidths[c] = this.MeasuringTextBlock.ActualWidth;
// The height for all characters is the same
// (except for newlines, which are twice the height)
if (this.characterHeight == -1 && !Char.IsWhiteSpace(c))
this.characterHeight = this.MeasuringTextBlock.ActualHeight;
}
this.MeasuringTextBlock.Text = “”;
double pageWidth = this.Width + 1; // Allow one pixel more than width
double linesPerPage = this.Height / this.characterHeight;
BackgroundWorker worker2 = new BackgroundWorker();
worker2.DoWork += delegate(object sender2, DoWorkEventArgs e2)
{
// STEP 3: BACKGROUND THREAD
// Determine the index of each page break
int linesOnThisPage = 0;
double currentLineWidth = 0;
int lastWordEndingIndex = -1;
// Loop through each character and determine each line
// break based on character widths and text block wrapping behavior.
// A line break should then be a page break when the cumulative
// height of lines exceeds the page height.
for (int i = 0; i < this.Text.Length; i++)
{
char c = this.Text[i];
bool isLineBreak = false;
bool isForcedPageBreak = false;
if (c == ‘n’)
{
if (linesOnThisPage == 0 && currentLineWidth == 0)
continue; // Skip blank lines at the start of a page
isLineBreak = true;
lastWordEndingIndex = i;
}
else if (c == ‘r’)
{
isLineBreak = isForcedPageBreak = true;
lastWordEndingIndex = i;
// This is the start of a chapter
// Add 1 because the page break isn’t added yet
Chapters.Add(this.pageBreaks.Count + 1);
}
else
{
currentLineWidth += this.characterWidths[c];
// Check for a needed line break
if (currentLineWidth > pageWidth)
isLineBreak = true;
}
if (isLineBreak)
{
linesOnThisPage++;
if (lastWordEndingIndex<=this.lineBreaks[this.lineBreaks.Count-1])
{
// The last spot where the line can be broken was already
// used as a line break. Therefore, we have no choice but to
// force a line break right now.
}
else
{
// Move back to first character after the actual break, which
// we may have passed due to word wrapping
i = lastWordEndingIndex;
}
// Reset the width for the next line
currentLineWidth = 0;
// Skip the space between split words
int breakIndex;
if (i < this.Text.Length – 1 && this.Text[i + 1] == ‘ ‘)
breakIndex = i + 1;
else
breakIndex = i;
this.lineBreaks.Add(breakIndex);
// See if this is a page break.
// It is if the NEXT line would be cut off
bool isNaturalPageBreak = (linesOnThisPage + 1) > linesPerPage;
if (isForcedPageBreak || isNaturalPageBreak)
{
this.pageBreaks.Add(breakIndex);
// Reset
linesOnThisPage = 0;
}
}
else if (c == ‘ ‘ || c == ‘-’ || c == ‘–’)
lastWordEndingIndex = i; // This can be used as a line break
// if we run out of space
}
// Add a final line break and page break
// marking the end of the document
if (this.lineBreaks[this.lineBreaks.Count – 1] != this.Text.Length)
{
this.lineBreaks.Add(this.Text.Length);
this.pageBreaks.Add(this.Text.Length);
}
// We’re done!
doneCallback();
this.isUpdating = false;
};
worker2.RunWorkerAsync();
});
};
worker.RunWorkerAsync();
}
public void ShowPageWithCharacterIndex(int characterIndex)
{
if (characterIndex < 0 || characterIndex >= this.Text.Length ||
this.Text == null)
return;
int pageBreakIndex = this.pageBreaks.BinarySearch(characterIndex);
if (pageBreakIndex < 0)
{
// The characterIndex doesn’t match an exact page break, but BinarySearch
// has returned a negative number that is the bitwise complement of the
// index of the next element that is larger than characterIndex
// (or the list’s count if there is no larger element).
// By subtracting one, this gives the index of the smaller element, or
// the index of the last element if the index is too big.
// Because 0 is in the list, this will always give a valid index.
pageBreakIndex = ~pageBreakIndex – 1;
}
// If the page break index is the last one (signifying the last character
// of the book), go back one so we’ll render the whole last page
if (pageBreakIndex == this.pageBreaks.Count – 1)
pageBreakIndex–;
ShowPage(pageBreakIndex + 1); // 1-based instead of 0-based
}
public void ShowPage(int pageNumber)
{
if (pageNumber >= this.pageBreaks.Count || this.Text == null)
return;
this.currentPageBreakIndex = pageNumber – 1;
RefreshCurrentPage();
}
public void ShowPreviousPage()
{
if (this.currentPageBreakIndex == 0 || this.Text == null)
return;
this.currentPageBreakIndex–;
RefreshCurrentPage();
}
public void ShowNextPage()
{
if (this.currentPageBreakIndex >= this.pageBreaks.Count – 2 ||
this.Text == null)
return;
this.currentPageBreakIndex++;
RefreshCurrentPage();
}
public void RefreshCurrentPage()
{
// An exact match should always be found
int firstLineBreakIndex = this.lineBreaks.BinarySearch(
this.pageBreaks[this.currentPageBreakIndex]);
int lastLineBreakIndex = this.lineBreaks.BinarySearch(
this.pageBreaks[this.currentPageBreakIndex + 1]) – 1;
this.StackPanel.Children.Clear();
for (int i = firstLineBreakIndex; i <= lastLineBreakIndex; i++)
{
// We’re guaranteed that lastLineBreakIndex is always less than count – 1
string line = this.Text.Substring(this.lineBreaks[i],
this.lineBreaks[i + 1] – this.lineBreaks[i]);
line = line.Trim();
if (line.Length == 0)
line = “ “;
this.StackPanel.Children.Add(new TextBlock
{
Text = line,
Foreground = this.MeasuringTextBlock.Foreground,
FontSize = this.MeasuringTextBlock.FontSize,
FontFamily = this.MeasuringTextBlock.FontFamily
});
}
this.CurrentCharacterIndex = this.lineBreaks[firstLineBreakIndex];
}
}
}

[/code]

  • The index of each line break and page break is stored in respective lists. The list of page breaks is a subset of the list of line breaks, and this relationship is leveraged when a page must be rendered.
  • Inside UpdatePagination, as much work as possible is offloaded to a background thread. Because the actual measurement must be done on the UI thread, however, two background workers are used to transition from a background thread to the main thread then back to a background thread.
  • This control makes a few assumptions about the input text, and the Pride and Prejudice document included in the project has been preprocessed to make these assumptions true:
    • A line feed character (n) denotes a forced line break, which should only occur at the end of a paragraph. (The original text used a fixed line width and therefore places n characters at regular intervals, which defeats the purpose of the dynamic layout.)
    • A carriage return character (r) denotes the beginning of a chapter. This enables the automatic population of the chapters collection, which drives the population of the chapters list box on the main page.

The Finished Product

Book Reader (Pagination & List Picker)