Introduction
This article shows how to use the Adobe PDF Reader COM Component in a WPF application with no code-behind (thanks to a little help from Expression Blend Behaviours). Although Expression Blend is used in this article, it is not essential as the same result can be achieved by downloading the free Expression Blend SDK and typing in the required XAML manually (it just requires a bit more effort- importing the correct namespaces etc).
Background
The application we will be creating is very simple- it allows users to view a PDF in a WPF Window, invoke a print action and change the current PDF displayed. This is straightforward enough to demonstrate the concepts in this article.
It is assumed you already know the benefits of using MVVM; understand basic details such as the usage of INotifyPropertyChanged and ICommand and the advantages of avoiding writing logic in the code behind (although sometimes code-behind is appropriate). If you don’t understand the basics of MVVM, I’d suggest checking out some of the links in the external links section first.
This article is in the format of a tutorial- it is assumed you are using Visual Studio 2010 and Expression Blend 4 (or have the Expression Blend 4 SDK installed) as well Adobe PDF Reader. For those without a paid licence for Visual Studio, Visual C# 2010 Express should be sufficient.
Step 1 Create a New WPF Application in Visual Studio
The first step is to create a new WPF application in Visual Studio. When you have done this you will need to create a Windows Forms User Control to host the Adobe PDF Reader.
You may be wondering why this is necessary? WPF cannot directly use an ActiveX control. To make an ActiveX control usable in WPF it must be hosted in a Windows Forms control. To use Windows Forms controls in WPF they themselves must be hosted in the WindowsFormsHost element. This relationship is depicted on the diagram below:
To create the Windows Forms User Control, choose add new item from the project’s context menu, enter "User Control" in the Search Installed Templates textbox and then name the file PdfViewer.cs:
As an alternative you could put this control in a Windows Forms Control Library, but to keep things simple we won’t.
Once this control is added to the project, the Windows Forms Designer should be open with a blank canvas. You will need to open the tool box (CTRL + W, X). As a first step it is a good idea to add a new tab for custom controls- this is an option from the context menu on the toolbox. With this new tab expanded, select “choose items” from the context menu. When the Choose Toolbox Items dialog appears, select the COM Components tab and select Adobe PDF Reader (this will add the AcroPDF.DLL to the toolbox).
Press OK- you should now see Adobe PDF Reader in the toolbox. Simply double click this for the component to be added to the user control.
Select the property Window for the component (F4), change the name to something more meaningful (I called it acrobatViewer) and change the Dock property to Fill.
We have now successfully added the Adobe ActiveX control to our Windows Forms UserControl. The next step is to add a custom property and method to this control, so it will be usable. Open the code behind for this control (F7 from the designer) and add the following code shown in bold:
public partial class PdfViewer : UserControl
{
private string pdfFilePath;
public PdfViewer()
{
InitializeComponent();
acrobatViewer.setShowToolbar(false);
acrobatViewer.setView("FitH");
}
public string PdfFilePath
{
get
{
return pdfFilePath;
}
set
{
if (pdfFilePath != value)
{
pdfFilePath = value;
ChangeCurrentDisplayedPdf();
}
}
}
public void Print()
{
acrobatViewer.printWithDialog();
}
private void ChangeCurrentDisplayedPdf()
{
acrobatViewer.LoadFile(PdfFilePath);
acrobatViewer.src = PdfFilePath;
acrobatViewer.setViewScroll("FitH", 0);
}
}
In the constructor we tell the control to hide the toolbar and set the reader to use FitH (Fit Horizontal). In the rest of the class we add a property “PdfFilePath” so that the current PDF that is showing can be changed as well as a method that allows the PDF to be printed showing the printer options dialog. The private method ChangeCurrentDisplayedPdf simply changes the current PDF displayed in the Viewer and sets the scroll position to be the top of the document.
At this stage we have enough functionality in the Windows Forms component to allow it to be used from WPF. The next step is to subclass WindowsFormsHost to wrap our custom Windows Forms Component and add a DependencyProperty so that our custom WPF element is easy to use with WPF DataBinding.
First add a reference in the project to WindowsFormsIntegration assembly. Create a new class called PdfViewerHost, and add
the following code:
public class PdfViewerHost : WindowsFormsHost
{
public static readonly DependencyProperty PdfPathProperty = DependencyProperty.Register(
"PdfPath", typeof(string), typeof(PdfViewerHost), new PropertyMetadata(PdfPathPropertyChanged));
private readonly PdfViewer wrappedControl;
public PdfViewerHost()
{
wrappedControl = new PdfViewer();
Child = wrappedControl;
}
public string PdfPath
{
get
{
return (string)GetValue(PdfPathProperty);
}
set
{
SetValue(PdfPathProperty, value);
}
}
public void Print()
{
wrappedControl.Print();
}
private static void PdfPathPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
PdfViewerHost host = (PdfViewerHost)d;
host.wrappedControl.PdfFilePath = (string)e.NewValue;
}
}
We now have a class that wraps the Windows Forms User Control, which in turn wraps the Adobe Reader COM component:
The main part of this control is a dependency property that wraps the Windows Forms Control’s PdfFilePath property. This is a standard DependencyProperty that uses a PropertyChangedCallback Delegate to change the wrapped object property each time the value of the dependency property changes. It is worth noting that when using XAML at runtime the SetValue method is called directly, hence there is no code to change the Windows Forms User control property in the .NET property wrapper. The control also has a Print method, which simple delegates to the underlying Windows Forms Control.
Now that we have our classes to make the ActiveX control play nicely with WPF and its binding system, we need to create a simple ViewModel. In a real world application, I would recommended at least creating a base class for your ViewModels or better yet, use one of the popular frameworks like MVVM Light or Microsoft PRISM. For the purposes of this article though we will create a contrived ViewModel that simply implements INotifyPropertyChanged.
Before creating the ViewModel you need to add a reference to the assembly Microsoft.Expression.Interactions.dll (available via the Expression Blend SDK). This library contains the class ActionCommand, an ICommand implementation- similar to Josh Smith’s RelayCommand or PRISM’s DelegateCommand. Normally I don’t use this and favour using the RelayCommand (as this is more widely used and supports generics), however as we will be referencing Expression Blend SDK assemblies, I figured we might as well use it in our app.
Our ViewModel class in all of its glory is as follows:
public class MainWindowViewModel : INotifyPropertyChanged
{
private readonly string[] pdfChoices;
private string currentPdf;
private int currentPdfChoiceIndex;
public MainWindowViewModel()
{
if (DesignerProperties.GetIsInDesignMode(new DependencyObject()))
{
return;
}
string currentFolder = Path.GetDirectoryName(Assembly.GetExecutingAssembly().GetName().CodeBase);
pdfChoices = new string[]
{
Path.Combine(currentFolder, "SamplePDF1.pdf"),
Path.Combine(currentFolder, "SamplePDF2.pdf"),
};
CurrentPdf = pdfChoices[currentPdfChoiceIndex];
SwapPdfsCommand = new ActionCommand(() =>
{
currentPdfChoiceIndex ^= 1;
CurrentPdf = pdfChoices[currentPdfChoiceIndex];
});
}
public event PropertyChangedEventHandler PropertyChanged;
public string CurrentPdf
{
get
{
return currentPdf;
}
set
{
if (currentPdf != value)
{
currentPdf = value;
OnPropertyChanged(new PropertyChangedEventArgs("CurrentPdf"));
}
}
}
public ICommand SwapPdfsCommand
{
get;
private set;
}
protected void OnPropertyChanged(PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, e);
}
}
}
On a class diagram, it is easier to see that there are only two things of interest to a View- the CurrentPdf and the SwapPdfsCommand properties.
The DesignerProperties.GetIsInDesignMode code is just a hack to allow us to do our wiring in Blend and prevent the Visual Studio/Blend designers from exploding. In production code you would not want this in your ViewModel. For the correct technique, search for "ViewModelLocator", "Blendability" and "Design Time Data" in your favourite search engine (I may implement this properly in a future version of this article). It is worth remembering any viewmodels you instantiate in XAML will have their constructor called when in design mode in both Visual Studio and Expression Blend.
In the constructor the code populates an array with the paths of two PDF documents that are in the same directory as the executable (the code download contains these). This code is just for demonstration purposes- in the real world you’d probably have a service that is injected into the ViewModel (using a dependency injection framework) to create these.
We wire up an ActionCommand that toggles the string Property CurrentPdf between the two values put into the array (simple XOR logic is used to achieve this). The class also contains a property containing the Command (to allow it to be bound to the View), and a string property containing the CurrentPdf.
We have now written all of the C# code necessary to allow the Adobe Reader Control to work nicely in WPF. As a final step simply add two PDFs to the project called SamplePDF1.pdf and SamplePDF2.pdf and set the Copy to Output Directory” to Copy if newer.
Also as there is not currently an x64 version of the Acrobat DLL it is wise to ensure that the target platform is set to x86 (this is on the build tab of the project properties), otherwise an error along the lines of {"Class not registered (Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG))"} will occur – pretty cryptic if you ask me!
To be sure that everything works at this stage you should build the solution in Visual Studio.
Step 2 Wire everything up in Expression Blend
Open the solution file in Expression Blend, and Select the Window object in the Objects and Timeline panel. Then in the properties panel select the DataContext property under common properties (it is easier if you use the Search Functionality):
To the right of the DataContext property, select New and then choose MainWindowViewModel class. This simple adjusts our XAML, so the Window's Data Context is set:
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
Next add the following XAML (replacing the default Grid) to define the look of our page (feel free to use the designer or type the XAML in directly):
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="3">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Margin" Value="3" />
</Style>
</StackPanel.Resources>
<Button Content="Print PDF" />
<Button Content="Change PDF File"/>
</StackPanel>
<local:PdfViewerHost x:Name="PdfViewer" />
</DockPanel>
This XAML defines the layout of our Window- a DockPanel that is filled by our Custom PdfViewHost and a StackPanel with two buttons- one for printing and the other for changing the PDF- pretty simple. In the Objects and Timeline Panel, select our PDFViewerHost and then find the PDFPath property (comes under Miscellaneous). Choose advanced options, Data Binding and then CurrentPdf:
If you run the project now, you will see a PDF is displayed in the viewer, however neither of the two buttons do anything. To change this select the Change PDF File button in the Objects and Timeline Panel, properties --> Miscellaneous --> command --> advanced options --> Data binding and choose the SwapPdfsCommand.
Finally in the Assets Panel select CallMethodAction in the Behaviors section and drag this on top of the Print PDF button. Select the properties Panel for this action. Under Common Properties choose Target Object --> Data Bind --> Element Property --> PdfViewer --> OK. In the method name type Print.
The final generated XAML inside the Window Element, should
look something like this:
<Window.DataContext>
<local:MainWindowViewModel/>
</Window.DataContext>
<DockPanel>
<StackPanel DockPanel.Dock="Bottom" Orientation="Horizontal" HorizontalAlignment="Right" Margin="3">
<StackPanel.Resources>
<Style TargetType="Button">
<Setter Property="Margin" Value="3" />
</Style>
</StackPanel.Resources>
<Button Content="Print PDF">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<behaviours:CallMethodAction TargetObject="{Binding ElementName=PdfViewer}" MethodName="Print" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Button Command="{Binding SwapPdfsCommand}" Content="Change PDF File"/>
</StackPanel>
<local:PdfViewerHost x:Name="PdfViewer" PdfPath="{Binding CurrentPdf}" />
</DockPanel>
Behind the scenes Expression Blend has added a reference to System.Windows.Interactivity.dll, but you can easily achieve the same result by referencing the same DLL in Visual Studio and typing the code by hand. That way is slower but is a good option if you don't have Expression Blend available. In a few steps we have allowed a legacy COM Component to be integrated fully with WPFs powerful data binding mechanism.
Using the code
I haven't test the code with the Express version of Visual Studio, but can't see why it wouldn't work.
Acknowledgements
I'd like to thanks Jason Hunt for checking this article.
External Links
Microsoft Expression- Working with built-in behaviors.
MSDN Walkthrough- hosting an ActiveX Control in WPF.
MSDN Documentation- WindowsFormsHost.
C# Online WPF Concepts- Dependency Property Implementation.
Microsoft Downloads- Expression Blend SDK for .NET 4.
MSDN Magazine- WPF Apps with the Model-View-ViewModel Design Pattern.
In the Box- MVVM Training.
MVVM Framework- MVVM Light.
Microsoft Patterns and Practises- PRISM Framework.
History
2012-05-12 - released article on code project.