Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

PDF Split and Merge Tool using itextsharp

0.00/5 (No votes)
19 Dec 2015 1  
Pdf split and merge tool using itextsharp

Introduction

I recently followed the 70-511 WPF course, the skills I have learned I want to share with you. In this article, I will discuss a PDF split and merge tool that I created in WPF using Visual Studio 2015.

The most recent code can be found in my Github repository here, the Window Installer can be found here
and the ClickOnce deployment can be launched here.

Background

Inspired by several commercial products, I decided to create a PDF split and merge tool. This tool makes use of the iTextSharp library, which allows you to create and modify documents in the Portable Document Format (PDF). The source of the PDF split and merge tool can be found on top of this article. The two screenshots below show how the UI of the application looks like, simple and easy to use.

Application Architecture

The PDF split and merge tool is created using the MVVM pattern. At startup, the MainView is created and its DataContext set, after which the MainView is shown to the user.

protected override void OnStartup(StartupEventArgs e)
{
    try
    {
        MainView mainView = new MainView();
        mainView.DataContext = new MainViewModel();
        mainView.Show();
    }
    catch (Exception ex)
    {
        Debug.WriteLine("OnStartup" + ex.ToString());
    }
}

The MainView contains a TabControl, each TabItem of this control is bound to a ViewModel. DataTemplates are used to render (tell WPF how to draw) each ViewModel with a specific UserControl. This approach keeps the business logic (ViewModels) completely separate from the UI (Views). The TabControl is defined in the MainView as follows:

<TabControl ItemsSource="{Binding TabControls, Mode=OneWay}"
            SelectedItem="{Binding SelectedTab, 
            Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
    <TabControl.Resources>
        <DataTemplate DataType="{x:Type vm:SplitPdfViewModel}">
            <v:SplitPdfView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:MergePdfViewModel}">
            <v:MergePdfView />
        </DataTemplate>
        <DataTemplate DataType="{x:Type vm:AboutViewModel}">
            <v:AboutView />
        </DataTemplate>
    </TabControl.Resources>
    <TabControl.ItemContainerStyle>
        <Style TargetType="{x:Type TabItem}" >
            <Setter Property="Header" 
            Value="{Binding Header}"></Setter>
        </Style>
    </TabControl.ItemContainerStyle>
</TabControl>

The TabControl ItemsSource is bound to a collection of ViewModels. This collection is created in the MainViewModel. Each ViewModel object is the DataContext of a TabItem. The SelectedTab property is used to keep track of which TabItem is selected.

public MainWindowViewModel()
{
    InitializeProperties();
}    

private void InitializeProperties()
{
    TabControls = new ObservableCollection<ITabViewModel>();
    TabControls.Add(new SplitPdfViewModel("Split"));
    TabControls.Add(new MergePdfViewModel("Merge"));
    TabControls.Add(new AboutViewModel("About"));
    SelectedTab = TabControls.First(); 
}

private ITabViewModel selectedTab = null;
public ITabViewModel SelectedTab
{
    get { return selectedTab; }
    set { SetProperty(ref selectedTab, value); }
}

private ObservableCollection<ITabViewModel> tabControls = null;
public ObservableCollection<ITabViewModel> TabControls
{
    get { return tabControls; }
    private set { SetProperty(ref tabControls, value); }
}

Each TabItem ViewModel needs to implement the interfaces ITabViewModel. This interface mandates that the ViewModel implements a Header property which is used for the Header property of the TabItem.

public interface ITabViewModel
{
    string Header { get; set; }
}

When a user selects a password protected PDF file, a lock icon appears in the DataGrid instead of a green tick icon. The user can click on this icon after which an MVVM based modal PDF unlock dialog will appear prompting the user to enter a password to unlock the PDF file. The modal PDF unlock dialog is a child Window of the MainView and requires you to enter a password before you can return to the MainView.

The modal PDF unlock dialog is created as follows:

private void OnUnlockCmdExecute()
{
    PdfLoginView pdfLoginView = new PdfLoginView();
    pdfLoginView.DataContext = new PdfLoginViewModel(pdfLoginView, PdfFiles.First());
    pdfLoginView.Owner = App.Current.MainWindow;
    bool? dialogResult = pdfLoginView.ShowDialog();
    if (dialogResult.HasValue && dialogResult.Value)
    {
        ProgressStatus = "PDF file successfully unlocked.";
    }
    else
    {
        ProgressStatus = "Failed to unlock PDF file.";
    }
}

Note that the LoginView is passed in the constructor of the LoginViewModel. In this way, the LoginViewModel has a reference to the LoginView which allows it to set the dialogResult of the LoginView and close it, as shown in the code below.

private void OnOkCmdExecute(object parameter)
{
    if (LockedFile.Open(((System.Windows.Controls.PasswordBox)parameter).Password) == Define.Success)
    {
        HasError = false;
        ErrorContent = string.Empty;
        pdfLoginView.DialogResult = true;
        pdfLoginView.Close();
    }
    else
    {
        ((System.Windows.Controls.PasswordBox)parameter).Password = string.Empty; 
        ErrorContent = "Failed to unlock PDF file, please try again.";
        HasError = true;
    }
}

In addition to the modal unlock dialog, the PDF split and merge tool also uses two non-modal dialogs. Non-modal or modeless dialogs are used to display information that is not essential for continuation of the application, thus allowing the dialog to remain open while work in the application continues. A non-modal dialog is used to display PDF file properties when the user double clicks on a PDF entry in the DataGrid or right-clicks on a PDF entry in the DataGrid and chooses Show PDF file properties from the context menu. The second non-modal dialog is used to open a PDF file in a WebBrowser control. You can open a PDF file by right-clicking on a PDF entry in the DataGrid and choosing Open PDF file from the context menu. The code to open and close a non-modal dialog is shown below.

private void OnShowFilePropertiesCmdExecute()
{
    Dictionary<string, string> properties = null;
    Dictionary<string, string> security = null;
    Dictionary<string, string> info = null;

    if (SelectedFile.GetProperties(out properties, out info, out security) == Define.Success)
    {
        FilePropertiesViewModel filePropertiesViewModel =
                                new FilePropertiesViewModel(SelectedFile.Info.FullName, 
                                properties, info, security);
        FilePropertiesView propertiesView = new FilePropertiesView();
        propertiesView.DataContext = filePropertiesViewModel;
        propertiesView.Owner = App.Current.MainWindow;
        propertiesView.Show();
    }
}

In order to close the FilePropertiesView from the FilePropertiesViewModel, the FilePropertiesView passes its reference as a command parameter to the close command.

<Window.InputBindings>
    <KeyBinding Key="Escape" 
                Command="{Binding CloseWindowCmd}"
                CommandParameter="{Binding 
                RelativeSource={RelativeSource AncestorType={x:Type Window}}}"/>
</Window.InputBindings>

The close command implementation in the FilePropertiesViewModel receives the FilePropertiesView window as an object parameter, which can then be used to close it. Note that this approach is an alternative to the close procedure used in the modal unlock dialog.

public void OnCloseWindowCmdExecute(object parameter)
{
    SystemCommands.CloseWindow((Window)parameter);
}

As you can see in the screenshots on top of this article, the MainView implements a status bar. The status bar contains a progress bar and a label. The status bar items are bound to the selected TabItem. For the About TabItem, the visibility property of the status bar is set to hidden in the AboutViewModel.

<StatusBar DockPanel.Dock="Bottom" 
VerticalAlignment="Bottom" Background="LightGray">
    <StatusBarItem Visibility="{Binding Path=SelectedTab.StatusBarIsVisible}">
        <Grid>
            <ProgressBar x:Name="pbStatus"
                         Width="100" 
                         Height="20"                                 
                         Value="{Binding Path=SelectedTab.ProgressBarValue, Mode=OneWay}"
                         IsIndeterminate="{Binding Path=SelectedTab.ProgressBarIsIndeterminate, 
                         Mode=OneWay}"/>
            <TextBlock Text="{Binding ElementName=pbStatus, 
                       Path=Value, StringFormat={}{0:0}%}" 
                       HorizontalAlignment="Center" 
                       VerticalAlignment="Center"></TextBlock>
        </Grid>
    </StatusBarItem>
    <Separator Visibility="
    {Binding Path=SelectedTab.StatusBarIsVisible}"></Separator>
    <StatusBarItem Visibility="{Binding Path=SelectedTab.StatusBarIsVisible}">
        <Label Content="{Binding Path=SelectedTab.ProgressStatus}" 
               HorizontalContentAlignment="Left" 
               VerticalAlignment="Bottom" 
               MinWidth="200" 
               Height="26"></Label>
    </StatusBarItem>
</StatusBar>

I use RelayCommands to invoke commands in the ViewModel. The RelayCommand is a custom command class which implements the ICommand interface.

public class RelayCommand : ICommand {}

RelayCommands enable you to use a centralized architecture for tasks. You can associate any number of UI controls or input gestures with a RelayCommand and bind that RelayCommand to a handler that is executed when controls are activated or gestures are performed. RelayCommands also keep track of whether they are available. If a RelayCommand is disabled, UI elements associated with that RelayCommand are disabled, too. The usage of a RelayCommand is illustrated below with the implementation of the split command. The OnSplitPdfCmdCanExecute returns only true when the IsBusy flag is false, thereby enabling the split command. The OnSplitPdfCmdExecute is the handler that contains the split logic. This handler is called when the split command is executed.

public RelayCommand SplitPdfCmd { get; private set; }
SplitPdfCmd = new RelayCommand(OnSplitPdfCmdExecute, OnSplitPdfCmdCanExecute);

private bool OnSplitPdfCmdCanExecute()
{
    return !IsBusy;
}

private void OnSplitPdfCmdExecute()
{
    PageRangeParser pageRangeParser = null;

    IsBusy = true;
    bool isValid = ViewIsValid(out pageRangeParser);
    if (isValid)
    {
        splitBackgroundWorker.RunWorkerAsync(new object[] { PdfFiles.First(),
            pageRangeParser, DestinationPath, OverwriteFile });
    }
    else
    {
        IsBusy = false;
    }
}

A final note to make about the architecture, is that all ViewModels in the application inherit from ViewModelBase. The ViewModelBase is a base class that contains common methods which can be used by the different ViewModels in the application, it implements the interfaces IDisposable, INotifyPropertyChanged and INotifyDataErrorInfo. By using this base class, duplicate code is avoided and maintainability is increased.

Controls and Layout

The MainView uses a DockPanel control to layout its controls. The DockPanel control is a container that enables you to dock contained controls to the edges of the dock panel. It provides docking for contained controls by providing an attached property called Dock. The following code demonstrates how the MainView layout is set using a DockPanel, implementation details are not shown and can be found in the included code.

<DockPanel LastChildFill="True">
    <Border DockPanel.Dock="Top"> 
        <StackPanel Orientation="Horizontal">
            <Image/>
            <TextBlock/>            
            <TextBlock/>
        </StackPanel>
    </Border>
    <StatusBar DockPanel.Dock="Bottom"></StatusBar>
    <TabControl></TabControl>
</DockPanel>

The DockPanel.Dock property has four possible values: Top, Bottom, Left, and Right, which indicate docking to the top, bottom, left, and right edges of the DockPanel control, respectively. The DockPanel control exposes a property called LastChildFill, which can be set to True or False. When set to True (the default setting), the last control added to the layout will fill all remaining space. So the layout of the MainView is as follows: it has a header containing the header text. The header text is embedded in a border control that is docked on top (DockPanel.Dock="Top") of the MainView. The status bar is docked to the bottom (DockPanel.Dock="Bottom") of the MainView and the TabControl will fill all remaining space because it is the last child added to the DockPanel.

The split, merge and about view use a Grid to layout all controls. Grid is the most commonly used panel for creating user interfaces in WPF. With the Grid control, you can define columns and rows in the Grid. Then you can assign child controls to designated rows and columns to create a more structured layout. The merge view contains a GridSplitter control that enables the user to resize Grid rows at run time. Using the GridSplitter, the user can expand the DataGrid containing the PdfFile entries by grabbing the row containing the GridSplitter with the mouse and move it to adjust the size of the Grid rows.

<GridSplitter Grid.Row="1" 
              Height="5" 
              Width="Auto"
              HorizontalAlignment="Stretch"
              VerticalAlignment="Center"
              ResizeBehavior="PreviousAndNext"
              ResizeDirection="Rows"></GridSplitter>

PdfFile entries in the split and merge view are displayed in a DataGrid. This is accomplished by binding an ObservableCollection of PdfFile objects to the DataGrid. The PDF split and merge tool can merge any number of PDF files but it can split only one PDF file at the time. Hence the DataGrid in the merge View can contain any number of PdfFile entries, while the DataGrid in the split View can contain only one PdfFile entry.

The IntegerTextBox used to enter the page split interval, is a custom control that inherits from TextBox. It checks if the entered value can be converted to an unsigned 16 bit integer, if so it accepts the input else it ignores the input by setting the OnPreviewTextInput tunneling event to handled, thereby stopping further tunneling and bubbling of this event.

Each radio button in the split view is bound to an enum property using a converter. For each enum value, a specific radio button in the group is checked. The two radio buttons specifying the split method are bound to the enum property SplitMethod, a definition of the enum property is shown below:

public enum DocSplitMethod
{
    None,
    Interval,
    Range,
}

private DocSplitMethod splitMethod = DocSplitMethod.None;
public DocSplitMethod SplitMethod
{
    get { return splitMethod; }
    set { SetProperty(ref splitMethod, value); }
}

Implementation of a radio button specifying the split method is shown below. The conversion of the enum value to the checked status of the radio button is made possible by the EnumToBoolConverter and the ConverterParameter.

<RadioButton x:Name="rBtnRange"
             ToolTip="Split PDF document into files containing different ranges of pages per file"
             GroupName="SplitMethod">
    <RadioButton.IsChecked>
        <Binding Path="SplitMethod" 
                 Mode="TwoWay" 
                 UpdateSourceTrigger="PropertyChanged"
                 ConverterParameter="{x:Static root:DocSplitMethod.Range}">
            <Binding.Converter>
                <converter:EnumToBoolConverter></converter:EnumToBoolConverter>
            </Binding.Converter>
        </Binding>
    </RadioButton.IsChecked>
    <StackPanel>
        <AccessText Text="By page _range"></AccessText>
        <Label Content="_Page range separated by comma, semicolon to separate output files"
               Target="{Binding ElementName=txtRange}"></Label>
    </StackPanel>
</RadioButton>

The enum to bool converter is shown below. As you can see in the implementation of the converter, if the passed ConverterParameter is equal to the bound enum value, the Convert method returns true thereby setting the IsChecked property of the corresponding radio button to true.

[ValueConversion(typeof(bool), typeof(Enum))]
public class EnumToBoolConverter : IValueConverter
{
    public object Convert(object value, Type targetType, 
        object parameter, CultureInfo culture)
    {
        if (value == null || parameter == null)
            return false;
        else
            return value.Equals(parameter);
    }

    public object ConvertBack(object value, 
        Type targetTypes, object parameter, CultureInfo culture)
    {
        if (value == null || parameter == null)
            return false;
        else if ((bool)value)
            return parameter;
        else
            return Binding.DoNothing;
    }
}

In order to make the merge DataGrid more user friendly, I added a behavior that displays a row number for each PDF entry. Using the row numbers, you can easily view and rearrange PDF entries in the merge DataGrid.

Backgroundworker

The split and merge process consume relatively a large amount of CPU time. The backgroundworker provides an easy way to run these processes in the background, thereby leaving the user interface responsive and available for user input. The SplitViewModel uses two backgroundworkers, the first one loads the PDF file. The second worker is used for the PDF split process. Similar to the SplitViewModel, the MergeViewModel also uses two backgroundworkers, one that loads PDF files and another for the merge process. The initialization of the backgroundworkers in the SplitViewModel is shown below:

private void InitializeBackgroundWorker()
{
    splitBackgroundWorker = new BackgroundWorker();
    fileBackgroundWorker = new BackgroundWorker();

    fileBackgroundWorker.DoWork += new DoWorkEventHandler(FileWorkerDoWork);
    fileBackgroundWorker.RunWorkerCompleted += 
        new RunWorkerCompletedEventHandler(FileWorkerRunWorkerCompleted);

    splitBackgroundWorker.WorkerReportsProgress = true;
    splitBackgroundWorker.WorkerSupportsCancellation = true;
    splitBackgroundWorker.DoWork += 
        new DoWorkEventHandler(SplitWorkerDoWork);
    splitBackgroundWorker.ProgressChanged += 
        new ProgressChangedEventHandler(SplitWorkerProgressChanged);
    splitBackgroundWorker.RunWorkerCompleted += 
        new RunWorkerCompletedEventHandler(SplitWorkerRunWorkerCompleted);
}

The backgroundworker that loads the PDF file implements two event handlers, catching the DoWork and the RunWorkerCompleted event. In the DoWork handler, a PdfFile object is created and its properties are set after which the RunWorkerCompleted event is raised by the backgroundworker. The RunWorkerCompleted handler updates the status bar and displays errors that occurred during the file load process. The backgroundworker that performs the file splitting implements one more event handler then the worker that loads the PDF file. In the code above, you can also see that the split backgroundworker supports cancellation. By clicking the cancel button or pressing the escape key during the split process, the CancelAsync method is called. This call in return sets the CancellationPending property of the split backgroundworker to true. By polling the CancellationPending property in the DoWork handler, the split logic can determine when to cancel the split operation. During the split process, the ProgressChanged event is periodically raised to update the progress status. When the split worker has finished, the RunWorkerCompleted event handler is called which updates the status and displays errors that occurred during the split process.

The PdfFile collection is accessed by two different threads, the first is the UI thread and the second thread is the backgroundworker. Because of multiple threads accessing the same collection, I used a ObservableCollection and wrapped the code responsible for updating the collection in Dispatcher.Invoke.

Drag and Drop Support

The PDF split and merge tool allows you to drag and drop files into the DataGrid. This functionality is implemented as a behavior, the behavior has one dependency property indicating whether multiple PDF files are allowed to be dropped into the DataGrid. This dependency property was necessary because the split DataGrid allows only one PDF file to be dropped, while the merge DataGrid allows multiple PDF files to be dropped. Drag and drop functionality is implemented by handling the following events:

protected override void OnAttached()
{
    base.OnAttached();

    this.AssociatedObject.DragEnter += AssociatedObject_DragEnter;
    this.AssociatedObject.DragLeave += AssociatedObject_DragLeave;
    this.AssociatedObject.DragOver += AssociatedObject_DragOver;
    this.AssociatedObject.Drop += AssociatedObject_Drop;
}

To enable drag and drop functionality, the AllowDrop property must be set to true for the DataGrid in the split and merge View. As mentioned earlier, the DataGrid in the split View accepts only one PDF file, because of this restriction the AllowDrop property is set to false when the DataGrid contains a PDF file (SelectedFile != null).

<DataGrid.AllowDrop>
    <Binding Path="SelectedFile" Mode="OneWay">
        <Binding.Converter>
            <converter:NullToBoolValueConverter/>
        </Binding.Converter>
    </Binding>
</DataGrid.AllowDrop>

The DragEnter event fires when the mouse enters the DataGrid in which the drag and drop functionality is enabled. This DragEnter event passes a DragEventArgs object to the method that handles it, and the DragEventArgs can be inspected to see if the DataObject being dragged onto the DataGrid is a PDF file and whether the source allows copying of the data. If the data is indeed a PDF file and copying of the data is allowed, the drag effect is set to copy. The following example demonstrates how to examine the data format of the DataObject and set the Effect property:

void AssociatedObject_DragEnter(object sender, DragEventArgs e)
{
    e.Effects = DropAllowed(e);
    e.Handled = true;
}

private DragDropEffects DropAllowed(DragEventArgs e)
{
    DragDropEffects dragDropEffects = DragDropEffects.None;

    if (e.Data.GetDataPresent(DataFormats.FileDrop) &&
        (e.AllowedEffects & DragDropEffects.Copy) == DragDropEffects.Copy)
    {
        string[] Dropfiles = (string[])e.Data.GetData(DataFormats.FileDrop);
        if ((AllowMultipleFiles && Dropfiles.Length > 0) ||
            (!AllowMultipleFiles && Dropfiles.Length == 1))
        {
            int fileCnt = 0;
            dragDropEffects = DragDropEffects.Copy;
            do
            {
                if (string.Compare(GetExtension(Dropfiles[fileCnt]).ToLower(), ".pdf") != 0)
                    dragDropEffects = DragDropEffects.None;

            } while (++fileCnt < Dropfiles.Length &&
                dragDropEffects == DragDropEffects.Copy);
        }
    }

    return dragDropEffects;
}

The DragOver Event occurs when an object is dragged over the DataGrid. The handler for this event receives a DragEventArgs object. In our case, the DragOver Event is used to highlight the potential drop location by setting the IsSelected property of the DataGrid row to true for the row under the mouse pointer.

void AssociatedObject_DragOver(object sender, DragEventArgs e)
{
    if (DropAllowed(e) == DragDropEffects.Copy)
    {
        var row = UIHelpers.TryFindFromPoint<DataGridRow>((DataGrid)sender, 
            e.GetPosition(AssociatedObject));
        if (row != null) { ((DataGridRow)row).IsSelected = true; }
        e.Effects = DragDropEffects.Copy;
    }
    else
    {
        e.Effects = DragDropEffects.None;
    }
    e.Handled = true;
}

When the mouse button is released over a target control during the drag and drop operation, the DragDrop event is raised. The handler for this event receives a DragEventArgs object, using the GetData method, the copied data is retrieved and passed to the corresponding ViewModel which implements the IDropable interface. In addition to the file path, the row index at which the drop took place is also passed to the ViewModel. This allows the merge ViewModel to append PDF file entries at a certain position in the PDF file collection (ObservableCollection<PdfFile>).

void AssociatedObject_Drop(object sender, DragEventArgs e)
{
    IDropable target = this.AssociatedObject.DataContext as IDropable;
    int index = -1;
    if (target != null)
    {
        if (((DataGrid)sender) != null) { index = ((DataGrid)sender).SelectedIndex; }
        target.Drop(e.Data.GetData(DataFormats.FileDrop), index);
    }
    e.Handled = true;
}

iTextSharp Library

You can install iTextSharp using the Nuget Package Manager in Visual Studio 2015, which can be found in the top menu via Tools->Nuget Package Manager. The iTextSharp library is well documented and you can find a lot of code examples in which this library is used. During the usage of the library, I found out that if you dispose an empty PDF document, an error is thrown in the library. This is a known bug in the library, because the error only occurred when the split or merge process is canceled by the user, I implemented a workaround by skipping cleanup when the user cancels the split or merge process.

if (destinationDoc != null &&
    !splitBackgroundWorker.CancellationPending)
{
    destinationDoc.Close();
    destinationDoc.Dispose();
    destinationDoc = null;
}

The code line in the iTextSharp library that throws the error:

if (pages.Count == 0) 
{ 
    throw new IOException("The document has no pages.") 
};   

As mentioned earlier, secured PDF files need to be first unlocked before they can be processed by the PDF merge and split tool. There are two different types of passwords which can be used to secure a PDF file. The user password, if set, this password is what you need to provide in order to open a PDF. The owner password is the password that you specify when you want to apply permissions to a PDF. For example, if you don't want to allow printing of the PDF or if you don't want to allow pages to be extracted, then you can specify permissions which prevent that from happening. However, when the unethicalreading flag is set to true for the iTextSharp PdfReader, the PdfReader ignores the security settings entirely and will automatically process a PDF if a user password is not set.

PdfReader.unethicalreading = true;

Hence the PDF split and merge tool can process any PDF file which has no user password set. In addition, it can also process any PDF file for which a user or owner password is provided.

Validation

Validation in the split and merge view are performed using the INotifyDataErrorInfo interface. Validation errors are shown to the user using an error template based on Silverlight. Details about the INotifyDataErrorInfo interface and the Silverlight based error template can be found here. Validation of input controls is done conditionally, which means that only enabled controls are validated.

One challenging aspect of validation was the validation of the entered page range. Each specified page range results in one PDF export file. The page range can be specified in two different ways, a closed page range in which the start page and end page are separated by a dash (for example 1-7 or 23-8). The second way to specify a page range is by comma separated page numbers (for example 3,4,5,6 or 2,45,23,33 or just one page 66). When specifying a page range, you can combine the two notations as often as you want, you only need to separate each page range by the ';' character. For example the following page range is valid: (1-7;3,4,5,6,1;8;9-5;3-5;4,5,1,2;). By using a regular expression, I was able to validate the page range. The resulting expression is shown below:

@"^((\d{1,4}-\d{1,4};)*|((\d{1,4},)*(\d{1,4};))*)*(((\d{1,4},)*(\d{1,4}))|(\d{1,4}-\d{1,4})){1};?$" 

where:
'^' and '$' = Indicate respectively the start and end of the input line.
\d{1,4} = Matches an integer in the range 0 to 9999.
(\d{1,4}-\d{1,4};)* = Closed page range "d-d;" can occur 0 or more times.     
((\d{1,4},)*(\d{1,4};))* = Comma separated page numbers. 
(((\d{1,4},)*(\d{1,4}))|(\d{1,4}-\d{1,4})){1};?$ = makes the ';' char optional at the end

The entered page range and page interval are also validated by checking if they contain page numbers that are beyond the end of the PDF document or zero. If so, the user is prompted by a validation error to adjust the entered value.

For illustrative purposes, I created a custom validation for the LoginView. When the user enters a wrong password to unlock the PDF, the HasError property is set to true and the ErrorContent is set. In the LoginView, the error is displayed to the user.

A final note about validation is that during the split and merge process, the Grid becomes disabled, using a datatrigger. In this way, no input can be changed when the application is busy.

<Grid.Style>
    <Style TargetType="{x:Type Grid}">
        <Setter Property="IsEnabled" Value="True"></Setter>
        <Style.Triggers>
            <DataTrigger Binding="{Binding IsBusy}" Value="True">
                <Setter Property="IsEnabled" Value="False"></Setter>
            </DataTrigger>
        </Style.Triggers>
    </Style>
</Grid.Style>

Key Gestures

Key gestures are used to make the application keyboard friendly. When the DataGrid receives focus, the following key gestures can be used.

<DataGrid.InputBindings>
    <KeyBinding Command="{Binding AddFileCmd}" 
    		Key="Insert"/>
    <KeyBinding Command="{Binding RemoveFileCmd}" 
    		Key="Delete" />
    <KeyBinding Command="{Binding UnlockCmd}" 
    		Key="U" Modifiers="Control" />
    <KeyBinding Command="{Binding ShowFilePropertiesCmd}" 
    		Key="P" Modifiers="Control"/>
    <KeyBinding Command="{Binding OpenFileCmd}" 
    		Key="O" Modifiers="Control"/>
    <KeyBinding Command="{Binding MoveUpCmd}" 
    		Modifiers="Control" Key="Up" />
    <KeyBinding Command="{Binding MoveDownCmd}" 
    		Modifiers="Control" Key="Down" />
    <KeyBinding Command="{Binding ScrollUpCmd}" 
    		Key="Up"/>
    <KeyBinding Command="{Binding ScrollDownCmd}" 
    		Key="Down"/>
</DataGrid.InputBindings>

In addition to key gestures, I also used mnemonic keys to make the application keyboard friendly. Labels controls support mnemonic keys, which move the focus to a designated control when the Alt key is pressed with the mnemonic key. The mnemonic key is specified by preceding the desired key with the underscore (_) symbol and appears underlined at run time when the Alt key is pressed. You can designate a target control by setting the target property of the Label control. The following example demonstrates how the mnemonic key of the page interval textbox is set. The focus shifts to the page interval textbox when Alt+P is pressed.

<Label VerticalAlignment="Center" 
       Margin="10,0,0,0" 
       Target="{Binding ElementName=txtInterval}" 
       Content="_Pages"></Label>

As mentioned earlier, all the mnemonic keys used in the application are underlined at run time when the Alt key is pressed, so the user can easily recognize and make use of these keys. In addition to mnemonic keys, button controls also expose properties that make an application more keyboard friendly which are the IsDefault property and the IsCancel property. The IsDefault property determines whether a particular button is considered the default button for the View. When IsDefault is set to true, the button’s click event is raised when the user presses Enter in the View. Similarly, the IsCancel property determines whether the button should be considered a Cancel button. When IsCancel is set to true, the button’s click event is raised when Esc is pressed.

<Button ToolTip="Start PDF split process"
        VerticalAlignment="Center" 
        Command="{Binding SplitPdfCmd}"
        Margin="0,0,2,0"
        IsDefault="True"/>

<Button ToolTip="Cancel PDF split process"
        Margin="2,0,5,0"
        VerticalAlignment="Center"
        Command="{Binding CancelSplitPdfCmd}"
        IsCancel="True"/>

Buttons controls support access keys, which are similar to the mnemonic keys supported by labels. When a letter in a button’s content is preceded by an underscore symbol (_), that letter will appear underlined when the Alt key is pressed, and the button will be clicked when the user presses Alt and that key together. For example, the overwrite checkbox (which inherits from buttonbase) is defined as follows:

<CheckBox ToolTip="Overwrite resulting PDF files if they exist in location"
          IsChecked="{Binding OverwriteFile, 
          Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" 
          Margin="0,5,0,0" 
          Content="_Overwrite file(s) if exists"></CheckBox>

The overwrite mode appears as "Overwrite file(s) if exists", where the first letter 'O' is underlined when Alt is pressed, and the checkbox is toggled when Alt+O is pressed.

The different tabs can be switched using the default gesture, which is Ctrl+Tab or Ctrl+Shift+Tab.

References

  • MCTS Self Paced Training Kit Exam 70-511

Points of Interest

If you have any improvement requests about the tool or this article, please let me know.

Note that inherited properties used for bindings must be public, protected properties cannot be used for bindings.

The webbrowser control uses the acrobat PDF reader plugin to display PDF files. If the user's computer has Adobe Reader installed on it, it also has the acrobat PDF reader plugin installed on it. One problem that I faced when displaying PDF files in the WebBrowser control is that they remained locked even after closing the WebBrowser control. I found the solution to this problem in this article.

//Avoid PDF lock by AcroRd32.dll after closure
private void OnCleanUpCmdExecute(object parameter)
{
    WebBrowser webBrowser = (WebBrowser)parameter;
    if (webBrowser != null)
    {
        App.Current.Dispatcher.BeginInvoke(new Action(delegate ()
        {
            webBrowser.NavigateToString("about:blank");
        }));
    }
}
<i:Interaction.Triggers>
    <i:EventTrigger EventName="Closing">
        <i:InvokeCommandAction Command="{Binding CleanUpCmd}" 
                    CommandParameter="{Binding ElementName=mainWebBrowser}"/>
    </i:EventTrigger>
</i:Interaction.Triggers>

An improvement can be made to this application by embedding a PDF viewer in the application, so that the application is not dependent on a local installed PDF plugin.

History

  • October 31st, 2015: Version 1.0.0.0 - Created the article
  • December 19th, 2015: Version 1.0.0.0 - Link to code and Installer kits added

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here