Exercise 2: Printing a Schedule across Multiple Pages

Although a schedule is fairly likely to fit comfortably onto a single page, and we can force it to fit uncomfortably if necessary thanks to the Viewbox, squashing things to fit on one page is not a good general-purpose solution. In practice, splitting content across multiple pages is often necessary, so we’ll modify the printing code to support this.

Pagination Concerns

  1. Run the application
  2. Go into the schedule planner and subscribe to lots oftalks.
  3. Print the talks.
    Note:
    If you added a lot of talks, you may notice the content’s font is getting smaller when printed (there is no page 2). If you get 3 or more talks in every time slot for the whole day, you’ll see that when you print, the list of talks starts to get narrower, as the Viewbox has to shrink it down to make it fit vertically. This ensures we have enough items in the schedule to make paging important.

    To paginate the content, we’ll need to take control of how many items we show on screen at once. This means we need to stop relying on the ItemsControl to display the top-level list of groups.
  4. Stop the application.

Add Logic for Pagination

  1. Add a new user control to the Views folder called ScheduleTimeSlotPrintView.
  2. Replace ScheduleTimeSlotPrintView’s Grid named LayoutRoot with the following content:

    XAML

    <Grid Margin="20,5"> <Grid.RowDefinitions> <RowDefinition Height="20" /> <RowDefinition Height="Auto" /> </Grid.RowDefinitions> <TextBlock Text="{Binding Path=Name,StringFormat=HH:mm}" /> <StackPanel x:Name="talkPanel" Grid.Row="1" /> </Grid>
    Note:
    Notice the StackPanel. We are going to populate that with the talks for the time group, so this fills the role that was previously taken by the inner ItemsControl. So we’re getting rid of both ItemsControls. We’re not actually going to split individual time slots across page boundaries. All the talks from a given slot will be on the same page. However, we still can’t use the ItemsControl to generate the individual talks within a slot, albeit for a slightly different reason.

    We’re avoiding ItemsControl at the top level so that we can paginate, but we’re avoiding a nested ItemsControl here simply because it doesn’t work. AdataboundItemsControl defers the generation of its child items. While the information will be available in time to print, it won’t be available at the point at which we try to work out how many items will fit on a page. If the information for the nested items control isn’t available at that point, we can’t know how big it’s going to be. If we ask the ItemsControl how big it wants to be at that point, it will return a preferred height of 0. Since our pagination calculations need to know how big this control will be, we can’t use ItemsControl. Instead we’ll have to populate the panel manually.
  3. Add the following using statements to the ScheduleTimeSlotPrintView.xaml.cs or ScheduleTimeSlotPrintView.xaml.vb code behind:

    C#

    using SlEventManager.Web; using System.Windows.Data;
  4. Visual Basic

    Imports SlEventManager.Web Imports System.Windows.Data
  5. Add the following to the ScheduleTimeSlotPrintView.xaml.cs or ScheduleTimeSlotPrintView.xaml.vb code behind:

    C#

    public void SetData(CollectionViewGroup cvg) { this.DataContext = cvg; foreach (Talk item in cvg.Items) { talkPanel.Children.Add(new TextBlock { Text = item.TalkTitle, FontWeight = FontWeights.Bold }); talkPanel.Children.Add(new TextBlock { Text = item.TalkAbstract, TextWrapping = TextWrapping.Wrap }); } }

    Visual Basic

    Public Sub SetData(ByVal cvg As Object) Me.DataContext = cvg Dim item = CType(cvg, SlEventManager.Talk) talkPanel.Children.Add(New TextBlock With {.Text = item.TalkTitle, .FontWeight = FontWeights.Bold}) talkPanel.Children.Add(New TextBlock With {.Text = item.TalkAbstract,.TextWrapping = TextWrapping.Wrap}) End Sub
    Note:
    For each talk this will generate a title and an abstract, just like the template we had previously been using did.
  6. Open SchedulePrintView.xaml, find the Viewbox.
  7. Delete the Viewbox and its contents. Replace it with the following Border:

    XAML

    <Border x:Name="timeSlotContainer" Grid.Row="2" />
  8. Add the following using statements to the SchedulePrintView.xaml.cs or SchedulePrintView.xaml.vb code behind:

    C#

    Using System.Windows.Data;
  9. Visual Basic

    Imports System.Windows.Data
  10. In the SchedulePrintView.xaml.cs or SchedulePrintView.xaml.vb code behind, add this methodwhich builds a panel containing the talks:

    C#

    public int PopulatePage(IEnumerable<object> items, int startingPoint) { double containerWidth = timeSlotContainer.ActualWidth; double containerHeight = timeSlotContainer.ActualHeight; StackPanel timeSlotPanel = new StackPanel(); timeSlotPanel.Width = containerWidth; timeSlotContainer.Child = timeSlotPanel; int itemsAdded = 0; this.UpdateLayout(); foreach (object item in items.Skip(startingPoint)) { ScheduleTimeSlotPrintView uc = new ScheduleTimeSlotPrintView(); uc.SetData((CollectionViewGroup) item); uc.DataContext = item; timeSlotPanel.Children.Add(uc); this.UpdateLayout(); timeSlotPanel.Measure(new Size(containerWidth, double.PositiveInfinity)); if (timeSlotPanel.DesiredSize.Height>containerHeight&&itemsAdded>0) { timeSlotPanel.Children.Remove(uc); break; } itemsAdded += 1; } returnitemsAdded; }

    Visual Basic

    Public Function PopulatePage(ByVal items As ObservableCollection(Of SlEventManager.Talk), ByVal startingPoint As Integer) As Integer Dim containerWidth As Double = timeSlotContainer.ActualWidth Dim containerHeight As Double = timeSlotContainer.ActualHeight Dim timeSlotPanel As New StackPanel() timeSlotPanel.Width = containerWidth timeSlotContainer.Child = timeSlotPanel Dim itemsAdded As Integer = 0 Me.UpdateLayout() For Each item As Object In items.Skip(startingPoint) Dim uc As New ScheduleTimeSlotPrintView() uc.SetData(item) uc.DataContext = item timeSlotPanel.Children.Add(uc) Me.UpdateLayout() timeSlotPanel.Measure(New Size(containerWidth, Double.PositiveInfinity)) If timeSlotPanel.DesiredSize.Height > containerHeight AndAlso itemsAdded > 0 Then timeSlotPanel.Children.Remove(uc) Exit For End If itemsAdded += 1 Next item Return itemsAdded End Function
  11. Note:
    The purpose of this code is to put as many items onto the current page as will fit. The code relies on Silverlight’s layout system to do most of the work. It creates a StackPanel (timeSlotPanel), which will contain the items to be printed for this page, and adds child items one at a time. Each time round the loop, we call this.UpdateLayout() to get Silverlight to lay the page out based on its current content.

    Then we find out how much space is required by the items we’ve added so far by calling Measure on the StackPanel. It’s fairly unusual to call this directly, because Silverlight calls Measure for you on all your UI elements as part of the layout mechanism to find out how much space they would like. But even though Measure will have been called here during UpdateLayout, the layout process goes on to do further work, and by the time UpdateLayout returns, we will know how much space the StackPanel has been allocated in the final layout, whereas what we really want to know is how much it wanted: it may have been allocated less space than it desired. (And if that happens, it means we’ve tried to put more items on the page than will fit.)

    We call Measure ourselves, passing in double.PositiveInfinity as the height. This requests a vertically unconstrained layout: a mode of layout in which the element is required to work out how much space it would like in an ideal world, if there were no constraints. (Note that we constrain the width—we are essentially asking “If you were this wide, how tall would you need to be for all your contents to fit?”) If the StackPanel would like more space than is actually available, we know that adding this last item pushed us just past the point where the items will fit. We remove the last item to get back to a point where we’ve not overfilled the page, and then break out of the loop. The code returns the number of items that it added, so that the calling code knows whether it needs to print more pages, and if so, what item it should start at for the next page.
  12. In SchedulePlanner.xaml.cs or SchedulePlanner.xaml.vb, find the print button click handler.
  13. Add this variable to the start of the method (this tracks the number of items that have been printed so far)

    C#

    int totalItemsPrinted = 0;
  14. Visual Basic

    Dim totalItemsPrinted As Integer = 0
  15. after the line PrintWidth property, add this code:

    C#

    PageNumber = currentPage++

    Visual Basic

    .PageNumber = Function() As Integer currentPage += 1 Return (currentPage - 1) End Function.Invoke
  16. Then immediately after the line that sets the PageVisual property, add this code:

    C#

    totalItemsPrinted += printView.PopulatePage( printViewModel.Talks.Groups, totalItemsPrinted); pe.HasMorePages = totalItemsPrinted<printViewModel.Talks.Groups.Count;
  17. Visual Basic

    totalItemsPrinted += printView.PopulatePage(printViewModel.Talks.SourceCollection, totalItemsPrinted) pe.HasMorePages = totalItemsPrinted<CType(printViewModel.Talks.SourceCollection, ObjectModel.ObservableCollection(Of SlEventManager.Talk)).Count

    Note:
    This calls the code you just added to populate the current page, and then works out whether it needs more pages after this one.
  18. Run the application and print the schedule to an XPS file. You should now find it prints across two pages:

    Figure 2

    Printing Multiple Pages of Content

    Note:
    With multipage printouts it’s often useful to add page numbers. This is relatively straightforward to achieve.

Page Numbering

  1. Open the SchedulePrintViewModel class add the following property:

    C#

    public int PageNumber { get; set; }
  2. Visual Basic

    Public Property PageNumber() As Integer
  3. In SchedulePlanner.xaml.cs or SchedulePlanner.xaml.vb, go to the print click handler.
  4. After the totalItemsPrinted variable, add another variable to track the current page:

    C#

    int currentPage = 1;
  5. Visual Basic

    Dim currentPage As Integer = 1
  6. Find the code that constructs a SchedulePrintViewModel using the C# object initializer to set the properties.
  7. Add one more property initializer to set the PageNumber property to currentPage++.
    Note:
    The increment operator will ensure the page number goes up each time.
  8. In SchedulePrintView.xaml, find the Grid.RowDefinitions section near the top.
  9. Add a fourth RowDefinition with a Height of Auto.
  10. Inside the Grid, add the following TextBox:

    XAML

    <TextBlock Grid.Row="3" Text="{Binding Path=PageNumber}" HorizontalAlignment="Center" />
  11. Run the application and print again. This time you should see page numbers at the bottom of the page.