.NET MAUI - Working with PDF Files Pt. 2

Search for a command to run...

No comments yet. Be the first to comment.
In this series I will be building a PDF splitting application for Windows using .NET MAUI.
Introduction In the earlier articles in my PDF Splitter series, we have relied on the inbuild .NET libraries to handle working with PDF files in MAUI, but these only support opening and displaying of PDF files. Now that we are looking to save our new...
Introduction Semantic Kernel is an open-source framework by Microsoft that integrates machine learning and natural language processing to build AI applications. It offers tools for tasks like language understanding, text generation, and semantic sear...

Introduction In the earlier articles in my PDF Splitter series, we have relied on the inbuild .NET libraries to handle working with PDF files in MAUI, but these only support opening and displaying of PDF files. Now that we are looking to save our new...

Introduction For the fourth article in my PDF Splitter series, we will start working with PDF files in .NET MAUI. Helpfully, MAUI contains a lot of useful PDF tools for reading PDF files out of the box, so we do not need to use any additional librari...

Introduction In this article, I'm going to introduce basic navigation into my .NET MAUI PDF Splitter application. If you're following my PDF Splitter Application series, you will have a simple application based on the out-the-box .NET template with a...

In the previous article, we created a PDF model and service to allow us to work with and track PDF pages. The next step is to build the UI to allow us to interact with the service.
To do this we will create a single 'view' and a 'view model' (see article on MVVM for more detail on the design patterns), and controls for displaying the list of pages, page content and any selected pages.
Before we can do any of the above, we should first update our file menu to allow us to select a PDF file to load and display.
Add the following to our AppShellViewModel.cs to inject out pdf service and use the inbuilt MAUI FilePicker control to allow us to select and load a PDF file.
Note: The code also includes navigation to route to our PDF View page that we previously created. We'll be adding code to this during this article so we should start to see everything come together.
public class AppShellViewModel
{
private IPdfService _pdfService;
public ICommand OnMenuItemClickedCommand { get; private set; }
public ICommand OnMenuItemClosePdfClickedCommand { get; private set; }
public ICommand OnMenuItemAboutClickedCommand { get; private set; }
private bool PdfOpen = false;
public AppShellViewModel(IPdfService pdfService)
{
_pdfService= pdfService;
OnMenuItemClickedCommand = new Command<string>(async (s) => await OnMenuItemClicked(s));
OnMenuItemClosePdfClickedCommand = new Command<string>(async (s) => await OnMenuItemClicked(s), (s) => PdfOpen);
OnMenuItemAboutClickedCommand = new Command(DisplayAbout);
}
public async Task OnMenuItemClicked(string item)
{
switch (item) {
case "Exit":
Application.Current.Quit();
break;
case "Open":
await SelectFile();
break;
case "Close":
await CloseFile();
break;
default:
break;
}
}
public async Task SelectFile()
{
var options = new PickOptions()
{
FileTypes = FilePickerFileType.Pdf
};
var fileResult = await FilePicker.PickAsync(options);
if (fileResult != null)
{
await _pdfService.LoadPdf(fileResult.FullPath);
PdfOpen = true;
(OnMenuItemClosePdfClickedCommand as Command).ChangeCanExecute();
await Shell.Current.GoToAsync(nameof(PdfViewPage));
}
}
public async Task CloseFile()
{
_pdfService.UnloadPdf();
PdfOpen = false;
(OnMenuItemClosePdfClickedCommand as Command).ChangeCanExecute();
await Shell.Current.GoToAsync($"//{nameof(MainPage)}");
}
public void DisplayAbout()
{
var about = new Window(new About(new AboutViewModel()));
about.Width = 650;
about.Height = 300;
Application.Current.OpenWindow(about);
}
}
The first thing we need to do is create a control to display the preview of pages from within our PDF File.
By breaking our page down into controls we can build quite complex UI's with simple building blocks. Our page list control is simply a list view nested in a scroll view, within which we provide a custom data template that is repeated for each page.
The list view is bound to our pages list, and we will also provide a function to allow an item to be selected, in this case, we've limited it to a single item. These bindings will come from our PdfViewPage xaml view and PdfViewPageViewModel which we will complete at the end of this article.
The template itself is just a grid with the image control to bind to our page item preview and a label that binds to the page item name.
It is important to mention how the binding context of controls is set to match the x:Name in the ContentView (usually just set to 'this') to handle the scope of the bindings.
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Class="PdfSplitter.Controls.PdfPages"
x:Name="this">
<ScrollView BindingContext="{x:Reference this}">
<ListView ItemsSource="{Binding Pages}" SelectionMode="Single" SelectedItem="{Binding SelectedPage}">
<ListView.ItemTemplate>
<DataTemplate>
<ViewCell>
<Grid Padding="10" MaximumWidthRequest="120">
<Grid.RowDefinitions>
<RowDefinition Height="6*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Row="0" Grid.Column="0" Margin="0, 5, 0, 0" Source="{Binding Preview}" />
<Label Grid.Row="1" Grid.Column="0" Text="{Binding PageName}" HorizontalOptions="Center" Margin="0" />
</Grid>
</ViewCell>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</ScrollView>
</ContentView>
In a similar fashion to the page list control for displaying the preview, we can create another control for displaying the 'selected' page. In this control, we simply create a 2-row grid, as want to display an image control which we bind to our page 'PageView' ImageSource function created in the previous article and add a button to allow us to select and add the page to our 'extracted' pdf.
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
x:Name="this"
x:Class="PdfSplitter.Controls.PdfPageContent">
<Grid BindingContext="{x:Reference this}">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Image Grid.Row="0" Margin="0, 10, 0, 0" Source="{Binding Page.PageView}" />
<Button
x:Name="SelectPage"
Grid.Row="1"
Margin="5"
Text="Add to Extracted File"
Command="{Binding SelectPageCommand}"
SemanticProperties.Hint="Add page to extracted document."
HorizontalOptions="Center" />
</Grid>
</ContentView>
Finally, we need a control to display our selected pages. This is very similar to our preview control in that it will display the preview image and the page name of selected pages. However, in this instance, we will display the pages horizontally rather than vertically, and also add a button to allow us to remove a selected page and return it to our original document page list, in case we select a page incorrectly.
<?xml version="1.0" encoding="utf-8" ?>
<ContentView xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:fonts="clr-namespace:PdfSplitter.Fonts"
x:Name="this"
x:Class="PdfSplitter.Controls.PdfSelectedPages">
<ScrollView Orientation="Horizontal">
<StackLayout BindingContext="{x:Reference this}" BindableLayout.ItemsSource="{Binding SelectedPages}" Orientation="Horizontal">
<BindableLayout.ItemTemplate>
<DataTemplate>
<Grid Padding="5,2,0,2" MaximumHeightRequest="180">
<Grid.RowDefinitions>
<RowDefinition Height="1*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Row="0" Grid.Column="0" Margin="0" Source="{Binding Preview}" ZIndex="0" />
<Label Grid.Row="1" Grid.Column="0" Text="{Binding PageName}" HorizontalOptions="Center" Margin="0" />
<Button
Grid.Row="0"
Grid.Column="0"
Margin="2"
Padding="0"
CornerRadius="10"
BackgroundColor="DarkRed"
BorderColor="Black"
BorderWidth="1"
TextColor="White"
FontAttributes="Bold"
Text="{x:Static fonts:MaterialDesignRegular.Delete}"
FontFamily="Icons"
FontSize="20"
WidthRequest="10"
HeightRequest="10"
HorizontalOptions="End"
VerticalOptions="Start"
ZIndex="1"
SemanticProperties.Hint="Remove page from document to extract."
Command="{Binding Source={RelativeSource AncestorType={x:Type StackLayout}}, Path=BindingContext.RemovePageCommand}"
CommandParameter="{Binding PageName}"/>
</Grid>
</DataTemplate>
</BindableLayout.ItemTemplate>
</StackLayout>
</ScrollView>
</ContentView>
When originally building this application the plan was to use the CollectionView component for the display of the collection of the selected pages above, as this is more powerful than the list view. However, at the time of writing, there is a well-documented error in this component which means it does not redraw correctly in the display when the underlying observable collection is updated.
Thankfully, we can work around the issue by using a StackLayout component as an alternative, setting the orientation to Horizontal as shown in the above control.
I have decided to call this a workaround, rather than a solution because the above is a bug rather than a feature, and as such may at some point be fixed, although for now, that does not seem to be a priority.
All that remains to do to display our PDF is to add our newly created controls to our PdfViewPage xaml:
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:Controls="clr-namespace:PdfSplitter.Controls"
x:Class="PdfSplitter.Views.PdfViewPage"
x:Name="pdfExtractorViewPage">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="4*" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5*" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Controls:PdfPageContent Grid.Row="0" Grid.Column="0" Page="{Binding SelectedPageItem}" SelectPageCommand="{Binding OnItemSelectedCommand}" />
<Controls:PdfPages Grid.Row="0" Grid.Column="1" Grid.RowSpan="2" Pages="{Binding Items}" SelectedPage="{Binding SelectedPageItem, Mode=TwoWay}" />
<Controls:PdfSelectedPages Grid.Row="1" Grid.Column="0" SelectedPages="{Binding SelectedItems}" RemovePageCommand="{Binding OnItemRemovedCommand}" />
</Grid>
</ContentPage>
And to update our view model to include the properties and methods that our controls will bind to:
public class PdfViewPageViewModel : BindableModelBase
{
private IPdfService _pdfService;
private PdfPageItem _selectedPageItem;
public ICommand OnItemSelectedCommand { get; private set; }
public ICommand OnItemRemovedCommand { get; private set; }
public PdfViewPageViewModel(IPdfService pdfService, ISavePdfService savePdfService)
{
_pdfService = pdfService;
_savePdfService = savePdfService;
OnItemSelectedCommand = new Command(SelectPage);
OnItemRemovedCommand = new Command<string>(RemovePage);
}
public ObservableCollection<PdfPageItem> Items => _pdfService.Items;
public ObservableCollection<PdfPageItem> SelectedItems => _pdfService.SelectedItems;
public PdfPageItem SelectedPageItem
{
get
{
if(_selectedPageItem == null && _pdfService.Items.Count > 0)
{
_selectedPageItem = _pdfService.Items[0];
}
return _selectedPageItem;
}
set
{
_selectedPageItem = value;
NotifyPropertyChanged(() => SelectedPageItem);
}
}
public bool PdfOpen { get; private set; }
public void SelectPage()
{
if(_selectedPageItem != null)
{
_pdfService.SelectItem(_selectedPageItem);
int selectedItemPageNo = _selectedPageItem.PageNumber;
SelectedPageItem = _pdfService.Items.FirstOrDefault(x => x.PageNumber > selectedItemPageNo);
NotifyPropertyChanged(() => DisplaySaveButton);
}
}
public void RemovePage(string pageName)
{
var pageToRemove = SelectedItems.FirstOrDefault(x => x.PageName == pageName);
if (pageToRemove != null)
{
_pdfService.UnselectItem(pageToRemove);
NotifyPropertyChanged(() => Items);
}
}
}
We should now be able to launch our application and it should look similar to the below. I have added some UI tweaks and changed the background image, implementation can be obtained from the GitHub repository.

And after opening a PDF file:

The final step is to implement the 'save' PDF functionality, which can be triggered as in the above image by way of clicking the green button. This will be the topic of my next article.