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
. DataTemplate
s 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 RelayCommand
s to invoke commands in the ViewModel
. The RelayCommand
is a custom command class which implements the ICommand
interface.
public class RelayCommand : ICommand {}
RelayCommand
s 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. RelayCommand
s 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 ViewModel
s in the application inherit from ViewModelBase
. The ViewModelBase
is a base class that contains common methods which can be used by the different ViewModel
s 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 backgroundworker
s, 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 backgroundworker
s, one that loads PDF files and another for the merge process. The initialization of the backgroundworker
s 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.
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