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

If you’ve liked the idea of the simple and quick PDF preview renderer I presented in my previous blog post and wish to avoid referencing third party licenses for displaying full PDF documents as well, this is for you – in this (and the following) article, I’ll show how to implement two types of PDF viewers to be used in UWP Windows Apps that both use the UWP-internal PDF rendering engine.

The first approach I’d like to suggest is similar to the thumbnail preview, as it only displays one page at a time. However, this time we start with a XAML user control, which means we build a custom UWP component consisting of a XAML and a code-behind part. In my example solution, I called it SinglePagePdfViewer. The XAML part contains (besides the necessary layout containert) three visual controls: An image (this one will contain the currently selected PDF page – remember that the Windows.Data.Pdf classes only allow image rendering, no vector- or text-based PDF display), and two buttons for navigating to the previous / next page within a PDF document:

<UserControl x:Class="MySampleApp.SinglePagePdfViewer" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
    <Grid>
        <Image x:Name="PageContainer" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
        
        <StackPanel Orientation="Horizontal" VerticalAlignment="Top" HorizontalAlignment="Right" Margin="10">
            <Button x:Name="PrevButton" IsEnabled="False" Width="40" Height="40">
                <SymbolIcon Symbol="Previous" />
            </Button>
            <Button x:Name="NextButton" IsEnabled="False" Width="40" Height="40" Margin="10 0 0 0">
                <SymbolIcon Symbol="Next" />
            </Button>
        </StackPanel>
    </Grid>
</UserControl>

In the sample snippet, I didn’t care too much about making these components look nicely, so feel free to style the two buttons in as much detail as you like!

The code-behind will – in contrast to our previous solution – contain two private members, one that stores the currently displayed page and one for keeping a reference to the full PDF document (both were not necessary in the PDF preview component, since it could only display the first page, and we disposed of the document directly after rendering the first page):

public sealed partial class SinglePagePdfViewer : UserControl
{
	private PdfDocument _document;
	private uint _currentPage;


	public SinglePagePdfViewer()
	{
		this.InitializeComponent();
	}
}

Obviously, also in this sample we’ll need a Source property – once again, I’ll realize that as dependency property to allow setting the source PDF file both from code as well as through data binding. Note that this time I use a string property, so that we can simply set it in XAML code for easy testing:

public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(
	"Source", typeof(string), typeof(SinglePagePdfViewer), new PropertyMetadata(default(string), SourceChangedCallback));

private static async void SourceChangedCallback(DependencyObject sender, DependencyPropertyChangedEventArgs args)
{
	var control = sender as SinglePagePdfViewer;
	var source = args.NewValue as string;

	await control.LoadDocument(source);
}

public string Source
{
	get => (string)GetValue(SourceProperty);
	set => SetValue(SourceProperty, value);
}

Again, whenever the Source property is changed from the outside, a method called LoadDocument is invoked. This one loads the PDF document, resets the page flag to show the first page (in case another document has previously been loaded), and assigns the rendered page to the existing Image control. In addition, it checks whether we are on the first or last page, in which case one of the navigation buttons needs to be disabled (the reason this is extracted to a separate method is that we’ll need it more often):

private async Task LoadDocument(string source)
{
	try
	{
		StorageFile file = await StorageFile.GetFileFromPathAsync(source);
		_document = await PdfDocument.LoadFromFileAsync(file);
	}
	catch (Exception e)
	{
		// TODO: Log and display exception
		return;
	}

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

private void CheckScrollButtons()
{
	PrevButton.IsEnabled = _currentPage > 0;
	NextButton.IsEnabled = _currentPage < _document.PageCount - 1;
}

You’ll notice that also the actual image rendering logic is encapsulated in its own method. Also this one is rather simple and won’t surprise you – open a given page, render it to an image, and return this image:

private async Task<BitmapImage> LoadPage(uint page)
{
	using (IRandomAccessStream stream = new MemoryStream().AsRandomAccessStream())
	{
		using (var pdfPage = _document.GetPage(page))
		{
			await pdfPage.RenderToStreamAsync(stream);
			BitmapImage bmp = new BitmapImage();
			bmp.SetSource(stream);
			return bmp;
		}
	}
}

At this point (although there are still a few parts missing) let’s take a quick look on how to include the SinglePagePdfViewer component in a UWP app. Obviously, you’ll need to adapt the actual file path to your system. Make sure you reference a file that is actually accessible from within the app, e.g. one that is located within the app’s local storage folder:

<Page x:Class="MySampleApp.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:local="using:MySampleApp">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <local:SinglePagePdfViewer Source="C:\Users\akits\AppData\Local\Packages\MyPackageName\LocalState\test.pdf" />
    </Grid>
</Page>

When starting the App, it should already load and display the PDF document’s first page! While this is nice, it’s not so different from what we achieved in our previous sample project, isn’t it? Therefore, it’s about time to add those parts of the code where the page scrolling magic is going on – the navigation buttons’ event handlers:

public SinglePagePdfViewer()
{
	this.InitializeComponent();

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

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

		CheckScrollButtons();
	};

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

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

		CheckScrollButtons();
	};
}

After stepping through the other parts of the code rather quickly, I’d like to analyze these event handler method in more detail. First of all, since they actually need to invoke the page rendering logic which is included within the LoadPage method which in turn is asynchronous, must mark both as async.

Then, even though the navigation buttons should be disabled in case we are displaying the first / last page, I’d strongly recommand to not rely on that view logic and again check the page index at the beginning of the event handlers, and leave them without switching pages just in case.

Of course, the core part of both event handler methods is to increment or decrement the _currentPage variable, and request a rendered image of the new page’s contents. This image is then assigned to our Image control, thus replacing the previously shown image.

Finally, we need to re-check whether we now are at the beginning or at the end of the document and if it is necessary to disable one of the navigation buttons.

Build and run the app again – if everything works fine so far, join me to the second part of this post to discuss potential optimizations!