A simple page-by-page PDF viewer component #2

As a follow-up to the simple SinglePagePdfViewer proposed in the previous blog post, let me suggest a potential performance improvement. At the moment, whenever one of the navigation buttons is tapped, we load the appropriate page, render it and display it as an image. Depending on the PDF document and each page’s content, this might take some time, resulting in a visible delay between tapping the button and seeing pages switch.

If we assume that the most frequent operation during scrolling through a PDF document is tapping the next button in order to scroll forward, the easiest approach towards a slightly better user experience is pre-loading the page that is (presumably) to be displayed next.

I’d like to suggest a real simple solution – all we need is a third member variable that will hold the next page’s pre-rendered image representation:

private BitmapImage _nextPageBmp;

The idea is to always render two pages, the current one and the subsequent one (except on the document’s last page, of course), and store the second image within this temporary member variable. However, it is important to invoke the two calls to the rendering method in the correct order (first the current page, then the temporary one), and to assign the current page image to the visual component as soon as possible, meaning directly after its rendering has finished and before starting the second rendering operation. For example, we modify the final code lines of the LoadDocument method in order to always render the second page after the first one has been scheduled for display:

_currentPage = 0;
PageContainer.Source = await LoadPage(_currentPage); // Initially show first page...
CheckScrollButtons(); // ...and activate / deactivate nav buttons

if (_currentPage < _document.PageCount - 1)
	_nextPageBmp = await LoadPage(_currentPage + 1);

Note that, as mentioned before, all the core business logic of the method should be invoked before calling the second rendering operation, because it’s the very last thing to do – if this can not be finished because, it does not really matter – in this case, switching to the next page would take a little longer because it needs to be done in the old-fashioned way after hitting the button, but it will still work. (The best solution would be to cancel the second call to LoadPage when any of the buttons is pressed, because in this case the whole context changes anyway and we won’t need the pre-loaded image any more, but unfortunately the PdfPage.RenderToStreamAsync method does not support cancellation.)

In addition, note the check whether we are on the last page before pre-loading the next page – if we actually are, there is nothing to pre-load and we can skip this step.

How would the navigation button event handlers change? The previous button is a bit easier to adapt: If we assume that after navigating to the previous page the user will probably want to navigate even further backwards, it doesn’t make sense to pre-render the next page, so we do nothing – except for removing a potentially existing pre-loaded image:

PrevButton.Tapped += async (sender, args) =>
{
	if (_currentPage == 0) return;

	_currentPage--;
	PageContainer.Source = await LoadPage(_currentPage);

	CheckScrollButtons();

	_nextPageBmp = null;
};

Of course, it might make sense to pre-load the previous page instead of the next one instead, but I decided to keep the example as simple as possible.

Within the NextButton tapped event, there are two things to take into account: At the very end, we need to pre-load the page after the next one (in a similar way as we did at the end of the LoadDocument method). Also here, the exception of being already on the document’s last page must be checked – in this case we should clear the temporary variable to avoid wrong image assignments.

In addition, at the beginning of the method we should check if there is a pre-loaded temporary image, in which case we can skip the call to LoadPage and instantly use it:

NextButton.Tapped += async (sender, args) =>
{
	if (_currentPage >= _document.PageCount - 1) return;

	_currentPage++;
	PageContainer.Source = _nextPageBmp ?? await LoadPage(_currentPage);

	CheckScrollButtons();

	if (_currentPage <span style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" data-mce-type="bookmark" class="mce_SELRES_start"></span>< _document.PageCount - 1)
		_nextPageBmp = await LoadPage(_currentPage + 1);
	else
		_nextPageBmp = null;
};

That’s it – build the app, run it loading a PDF file including some heavy contents, and check whether you notice an improvement in performance! Of course, this approach is not applicable to all scenarios – it will work quite well for e-books and other documents that are typically read from the beginning to the end, while it won’t have any benefits for users who jump through the document searching for a specific page, e.g. in PDF files containing construction plans. However, based on this example you should be able to realize different types of pre-loading scenarios, based on your PDF viewer component’s anticipated use case.