Introduction
The PDF output format is widely used for publishing digital documents that are exchangeable across platforms while keeping the fidelity of the document's layout, fonts, and other visual styles intact, both on screens and printed media. One obvious example is that most of the US government forms are in PDF format.
There are some libraries and technologies that enable the use case that PDF is not just an output format, but works as a data capture (user data input) interface as well. The end user can fill out PDF forms directly within the Adobe Reader, and then submit it online. But, almost all of those approaches are server process oriented, generating PDF streams from the server and sending it down to the client; substantial server side application logics are required to enable using the PDF for data collection.
This article provides an alternative client oriented simple approach that blends a PDF and a Silverlight host application in a web browser, eliminating the need for server side logic of assembling user-specific PDF files for data collection. Especially, in the case of a group of PDF forms data that need to be collected, this technique will have higher scalability than the classic server centric technologies, because it makes it possible that the application server only deals with pure data and business logic, no PDF specific logic needs to run on the application server.
The downloadable source code has all the details in a working sample; it has a Visual Studio 2008 SP1 solution and project files, needs Silverlight 2 Release Candidate 0, and Adobe Reader 8.1.2 pre-installed. The Silverlight application, named SilverForm in the demo project, manages PDF groups as a template list (same for all users), streaming down as a static file from a Web server when the user requests, leveraging the client side Silverlight Plug-in and the Adobe Reader Plug-in to have them work together in a web browser, with tasks like data collection, form navigation, form data persistence, and populating PDF with user data all handled by the client side code.
If you have the Silverlight 2 Beta 2 runtime installed, you can run the sample application from here. For developers who have the Silverlight 2 Release Candidate 0 installed, the sample application is here. Both of them share almost the same source code, and require Adobe Reader 8.1.2 or above.
Overview
The word “blend” here has two-fold meanings, one is to render the PDF on top of the Silverlight hosting application, and reposition/resize the loaded PDF when the Silverlight application layout is updated or resized. Another aspect is to exchange data between Silverlight and PDF; the PDF can notify the hosting Silverlight application that it’s ready to read and populate forms data, and the hosting Silverlight application needs a channel that can request all the user data filled in the PDF and persist/restore them in a client-side isolated storage.
The basic idea to render PDF on top of Silverlight is to have a DIV
element for positioning in the Silverlight application's hosting HTML code. The content of the DIV
is an IFRAME
for resizing, while the source of the IFRAME
is another HTML file (mqzPDFContainer.htm, let's call it "loading HTML") that knows how to load a PDF by setting up the correct OBJECT
tag or EMBED
tag for different browsers.
The positioning DIV
is created in the hosting HTML (or the ASP.NET page) that wraps the Silverlight application (in the demo project, the hosting HTML file is SilverFormsTestPage.html), it has absolute positioned style and transparent background, no border, and is initially hidden with empty content.
The resizing IFRAME
will be set as the positioning DIV
’s content (innerHTML
) in run time when a specific PDF’s URL has been selected.
In order to let the positioning DIV
know its left and top coordinates and the width and height of the IFRAME
, we need a Silverlight User Control that serves as the “back” of the PDF. The IFRAME
content (source) will be positioned and resized exactly based on this control’s position and size. Whenever the user resizes the Silverlight application that results in this “back” User Control’s position or size changing, it would notify the DIV
and IFRAME
to reposition/resize accordingly via a Silverlight HTML Bridge.
When the JavaScript code from the loading HTML (mqzPDFContainer.htm, it’s the IFRAME
’s source) sets up the correct OBJECT
or EMBED
tag for the PDF file, the browser will take control from there. It would load and instantiate the Adobe Reader, then set the PDF URL as the source of the tag (data
attribute in the OBJECT
element, src
attribute in the EMBED
tag). When the Reader loads up, it would start to download the specified PDF file through HTTP.
The requested PDF file can reside in the same web server as the Silverlight host application is, all static files ---- template files that are the same for all users --- will be downloaded to the client and run within the browser; no application server logic is needed.
When the specified PDF is loaded into the Reader, it’ll initiate the data exchange process between the PDF and Silverlight. The data exchange flow is bi-directional, the PDF calls the hostContainer PostMessage
to interact with the hosting HTML, and Silverlight application’s managed code invokes the Silverlight HTML Bridge to read and write data.
As you can see, the hosting HTML is the middle man between Silverlight and PDF. As a matter of fact, Silverlight managed code never “knows” it’s working with PDF, it only talks with the HTML Bridge. And, PDF never interacts with the managed code directly; all it does is to post and process messages via the hostContainer as if there is no Silverlight application around it.
To blend PDF with Silverlight visually, with integrated exchangeable data, it’s really a combination of Silverlight, C#, HTML Bridge, JavaScript, hostContainer, and PDF. The demo project is built with the thread-safe version of Silverlight Cairngorm. Because of the length of all the details, this article will focus on the plumbing works that connects Silverlight and PDF; the following article will provide points of interests of the demo project that puts all the details together.
Blend visually
1. Create the positioning DIV
The positioning DIV
is created by inserting the following HTML code into the hosting HTML code (or the ASP.NET page markup). The hosting HTML file is SilverFormsTestPage.html in the demo project, it has the OBJECT
tag to create the Silverlight application. The following DIV
will be inserted right after the Silverlight OBJECT tag:
<!---->
<div id="silverlightControlHost">
<object data="data:application/x-silverlight,"
type="application/x-silverlight-2" width="100%" height="100%">
<param name="source" value="ClientBin/Debug/SilverForms.xap"/>
<param name="onerror" value="onSilverlightError" />
<param name="onLoad" value="pluginLoaded" />
<param name="background" value="white" />
<param name="enableHtmlAccess" value="true" />
<a href="http://go.microsoft.com/fwlink/?LinkID=124807"
style="text-decoration: none;">
<img src="http://go.microsoft.com/fwlink/?LinkId=108181"
alt="Get Microsoft Silverlight" style="border-style: none"/>
</a>
</object>
<iframe style='visibility:hidden;height:0;width:0;border:0px'></iframe>
</div>
<div style="position:absolute;background-color:transparent;
border:0px;visibility:hidden;" id="mqzIFrmParent">
</div>
Please note that enableHtmlAccess
is set to true for the HTML Bridge. Also, the ID
of the DIV
(mqzIFrmParent
) is crucial, because the JavaScript gets the reference of the DIV
object using this ID
.
2. Create IFRAME and position/size it
All the code that manipulates the properties of the positioning DIV
and the corresponding IFRAME
is encapsulated in a single JavaScript file: mqzJsCalls.js:
mqzJsCalls = new function()
{
var mqzJsCalls = this;
var _iFrameParentID = "mqzIFrmParent";
var _iFrameID = "myIFrame";
var _iRect = { left: 0, top: 0, width: 0, height: 0 };
mqzJsCalls.getHTMLZone = function() {
return document.getElementById(_iFrameParentID);
},
mqzJsCalls.getZoneFrame = function() {
return document.getElementById(_iFrameID);
},
mqzJsCalls.getZoneContent = function() {
var frmObj = document.getElementById(_iFrameID);
if (null == frmObj)
return null;
if (typeof (frmObj.contentWindow.getPDFBridgeBusy) != "function")
return null;
return frmObj.contentWindow;
},
mqzJsCalls.moveHTMLZone = function(x, y, w, h) {
_iRect.left = x;
_iRect.top = y;
_iRect.width = w;\
_iRect.height = h;
mqzJsCalls.moveHTMLRect();
},
mqzJsCalls.refreshHTMLZone = function() {
_iRect.left++;
_iRect.width--;
mqzJsCalls.moveHTMLRect();
_iRect.left--;
_iRect.width++;
mqzJsCalls.moveHTMLRect();
},
mqzJsCalls.moveHTMLRect = function() {
var zoneRef = mqzJsCalls.getHTMLZone();
if (null != zoneRef) {
zoneRef.style.left = _iRect.left + "px";
zoneRef.style.top = _iRect.top + "px";
}
var zoneFrmRef = mqzJsCalls.getZoneFrame();
if (null != zoneFrmRef) {
zoneFrmRef.width = _iRect.width + "px";
zoneFrmRef.height = _iRect.height + "px";
}
},
mqzJsCalls.showHTMLZone = function(bShow) {
var zoneRef = mqzJsCalls.getHTMLZone();
if (null != zoneRef)
zoneRef.style.visibility = bShow ? "visible" : "hidden";
if (bShow)
mqzJsCalls.refreshHTMLZone();
},
mqzJsCalls.setHTMLZoneSrc = function(url) {
var zoneRef = mqzJsCalls.getHTMLZone();
if (null != zoneRef)
zoneRef.innerHTML = "<iframe id='" + _iFrameID +
"' src='" + url +
"'frameborder='0'></iframe>";
},
mqzJsCalls.getPDFBridgeBusy = function() {
var frmObj = mqzJsCalls.getZoneContent();
if (null == frmObj)
return 0;
return frmObj.getPDFBridgeBusy();
}
};
The above JavaScript file is referenced by the hosting HTML SilverFormsTestPage.html. When the Silverlight managed code needs to set/update the position and size of the PDF (Reader), it calls mqzJsCalls.moveHTMLZone(x,y,w,h)
to reposition or resize it. When a new PDF URL is selected, the managed code will invoke mqzJsCalls.showHTMLZone(true)
to make the positioning DIV
visible, then call mqzJsCalls.setHTMLZoneSrc(pdfUrl)
to create the IFRAME
as the content of the DIV
tag (innerHTML
). We'll examine the SRC
attribute in the generated IFRAME
tag later in the "Load PDF into IFRAME" section.
3. Calling mqzJsCalls.moveHTMLZone(x,y,w,h) from the Silverlight User Control
In the demo project, the left hand side is the form navigation panel, implemented by a ListBox
and data bound to the form list data. The right hand side is populated by the ContentZone
User Control that serves as the "back" of the PDF (discussed in the Overview section). A GridSplitter
control sits between the navigation listbox and the content zone, so the user can change the widths of both controls by drag and drop. Also, the user could resize the browser window, which would affect the size of the ContentZone
control.
The sole responsibility of the ContentZone
User Control is to handle the Resize
and layoutUpdate
Silverlight events, then call mqzJsCalls.moveHTMLZone(x,y,w,h)
to position and size it correctly:
namespace SilverForms.View
{
public partial class ContentZone : UserControl
{
public Point LeftTopPt { get; set; }
public ContentZone()
{
InitializeComponent();
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
GetDisplayPosition();
}
private void OnLayoutUpdated(object sender, EventArgs e)
{
GetDisplayPosition();
}
private void GetDisplayPosition()
{
GeneralTransform gt =
this.TransformToVisual(Application.Current.RootVisual as UIElement);
LeftTopPt = gt.Transform(new Point(0, 0));
HtmlPage.Window.Invoke("moveHTMLZone", new object[] {
LeftTopPt.X, LeftTopPt.Y, this.ActualWidth, this.ActualHeight});
}
}
}
One trick worth noting is how to transform the left-top point (0,0) of the ContentZone
User Control coordinates to the the coordinates relative to the very left-top point of the web page. Surprisingly, Silverlight doesn't have an API like LocalToGlobal
or GlobalToLocal
, we have to rely on the GeneralTransform
object reference of the RootVisual
, then invoke the Transform
method to get the global coordinates.
Once we get the left-top coordinates and the width and the height, we can invoke the JavaScript method via HtmlPage.Window.Invoke
. The parameters are passed to the JavaScript through an object array.
4. Load PDF into IFRAME
Up till now, all the plumbing work have been done to load the PDF. The IFRAME
tag generated by mqzJsCalls.setHTMLZoneSrc(pdfUrl)
sets the pdfUrl
parameter to the SRC
of the IFRAME
. In the demo project, pdfUrl
is the URL of mqzPDFContainer.htm, and the PDF file is specified by query string parameters. For example, when the PDF name is ty08_w2.pdf under fed folder, pdfUrl
for the IFRAME
will be:
ClientBin/mqzPDFContainer.htm?fn=ClientBin/fed/ty08_w2.pdf
The reason for loading the PDF through the mqzPDFContainer.htm file is that the hostContainer
needs to set up for each PDF. Passing the PDF file name in the query string can enable mqzPDFContainer.htm to just reference the generic JavaScript code to set up hostContainer
, no form specific code is needed. Here is the HTML code for mqzPDFContainer.htm:
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>PDF Container and Communicator</title>
<style type="text/css">
body { margin: 0px; overflow:hidden }
</style>
<script language="javascript" type="text/javascript"
src="mqzQueryStringParser.js"></script>
<script language="javascript" type="text/javascript"
src="mqzLoadExternPDF.js"></script>
</head>
<body>
<div id="pdfCtrlHost"></div>
</body>
</html>
It's fairly simple, and only references two external JavaScript files and creates an empty DIV
. Both the external JavaScript files are in the demo project, mqzQueryStringParser.js makes parsing the query string and reading the specified pdfUrl
easier. I'll leave the details to the demo project, and focus on the core of the mqzLoadExternPDF.js file here:
var pdfFileName = findQueryString("fn");
...
function LoadPDFFile(fileName) {
var agentTest = /WebKit/;
var mimeType = "application/pdf";
if (agentTest.test(navigator.userAgent))
mimeType = "application/vnd.adobe.pdf";
var strObjTag = '<object id="ttPDFObj" height="100%" width="100%" type="';
strObjTag += mimeType;
strObjTag += '" style="position:absolute;background-color:' +
'transparent;border:0px;padding:0px;margin:0px;" data="';
strObjTag += fileName;
strObjTag += '"></object>';
var ffTest = /Firefox[\/\s](\d+\.\d+)/;
if (ffTest.test(navigator.userAgent)) {
var ffversion=new Number(RegExp.$1)
if (ffversion >= 3) {
strObjTag = '<embed id="ttPDFObj" name="ttPDFObj" ' +
'height="100%" width="100%" ' +
'align="middle" type="application/pdf" src="';
strObjTag += fileName;
strObjTag += '" allowscriptaccess="sameDomain" ' +
'style="position:absolute;border:0px;padding:0px;margin:0px;"/>';
}
}
document.getElementById("pdfCtrlHost").innerHTML = strObjTag;
}
function LoadContent() {
if (pdfFileName.length > 5) {
LoadPDFFile(pdfFileName);
setTimeout("onStartInit();", 10);
}
}
window.onload = LoadContent;
The pdfFileName
variable value is read from the query string. LoadContent
is wired up with the HTML page onload
event, it calls LoadPDFFile(pdfFileName)
when the HTML loads into the IRFRAME
. Inside the LoadPDFFile(pdfFileName)
function, it creates an OBJECT
tag for IE, Firefox 2.x (type=application/pdf), and Safari (type=application/vnd.adobe.pdf), and an EMDED
tag for Firefox 3. Once the OBJECT
tag or EMBED
tag are set to the innerHTML
of the empty DIV
, the web browser will start instantiating the Adobe Reader plug-in, and the plug-in will load the PDF specified in the data
(of the OBJECT
tag) or the SRC
(of the EMBED
tag) attribute. The end result is the PDF is rendered on top of Silverlight, on the exact location and size of the ContentZone
control ---- visually blended.
Data exchange between Silverlight and PDF
The data integration work has four important tasks:
- PDF notifies the host that it's ready to take data.
- Host sends data to PDF.
- Host notifies PDF it needs user data.
- PDF formats the user data and sends it back to the host.
Let's look at them one by one.
Before any "notification" can be sent or received by each end, the hostContainer
object needs to set up right after the PDF loads inside the Adobe Reader. The message handler needs to be set up in the loading HTML. In the managed code side, the scriptable object needs to be exposed to JavaScript.
1. Setting up the message handler and the hostContainer
The message handler needs to set up to post/process messages to and from the PDF document in the loading HTML file (mqzPDFContainer.htm). It's a JavaScript function attached to the OBJECT
/EMBED
object. The set up work is done by the referenced JavaScript file, mqzLoadExternPDF.js. In the JavaScript code shown in the "Load PDF into IFrame" section, onStartInit()
is "scheduled" to be invoked after 10 milliseconds, inside the method. It makes sure the dynamically inserted OBJECT
/EMBED
object is present before it sets up the hostContainer
object in the HTML. The following code is part of mqzLoadExternPDF.js:
function getUsablePDFObj() {
var PDFObject = document.getElementById("ttPDFObj");
if (typeof (PDFObject) == "undefined") {
return null;
}
if (typeof (PDFObject.postMessage) == "undefined") {
return null;
}
return PDFObject;
}
function onStartInit() {
var PDFObject = getUsablePDFObj();
if (PDFObject == null) {
setTimeout("onStartInit();", 10);
return;
}
PDFObject.targetframe = this;
PDFObject.messageHandler =
{
onMessage: function(aMessage) {
switch (aMessage[0]) {
case "_pdf_PageOpened": setTimeout("GetSavedFormFieldsData();", 100);
break;
case "_pdf_FormData": SetUserVals(aMessage); break;
}
return true;
},
onError: function(error, aMessage) {
alert("!error!" + aMessage);
}
};
}
"_pdf_PageOpened
" is the application defined customized message sent out from the PDF whenever the loading completes in the Adobe Reader. "_pdf_FormData
" is another customized message from PDF that notifies the host application it's form data is ready (included in aMessage
, a string array).
On the other hand, the hostContainer
object is the bridge between the HTML JavaScript and the PDF document. Each PDF document needs to set up a hostContainer
object within the Adobe Reader, then it can receive and post messages (in the format of a string array) from and to the external messageHandler
that is in the loading HTML. Here is the generic JavaScript (means it's not form specific, all PDF files share the same JavaScript file) code that sets up the object:
this.fnf = { FormName: "mqzXfa_Form" };
this.fnf.HostMessageNames = ["_host_FormData", "_host_GetAllFields"];
this.fnf.PageOpenDataPacket = [];
this.fnf.AllFieldDataPacket = ["_pdf_FormData", this.fnf.FormName];
this.nocache = true;
this.noautocomplete = true;
if (this.external && this.hostContainer) {
this.disclosed = true;
try {
if (!this.hostContainer.messageHandler)
this.hostContainer.messageHandler = new Object();
this.hostContainer.messageHandler.myDoc = this;
this.hostContainer.messageHandler.onMessage = fnfOnMessageFunc;
this.hostContainer.messageHandler.onError = fnfOnErrorFunc;
this.hostContainer.messageHandler.onDisclose = function() { return true; };
this.fnf.timeOutInitDoc = app.setTimeOut("fnfInitDoc();", 100);
}
catch (e) {
console.println("Exception: hostContainer:" + e);
app.alert("Exception: hostContainer:" + e);
}
}
else {
}
function fnfPostMessageToHost(stringArray) {
if (stringArray.length > 0) {
if (this.external && this.hostContainer) {
try {
this.hostContainer.postMessage(stringArray);
}
catch (e) {
app.alert("PostMessageToHost Error:" + e +
". Message=" + stringArray);
}
}
}
}
function fnfInitDoc() {
try {
app.clearTimeOut(this.fnf.timeOutInitDoc);
this.fnf.FormName = fnfGetFormName();
}
catch (e) {
app.alert("fnfInitDoc Error():" + e);
}
}
function fnfGetFormName() {
var pdfFileName = this.documentFileName;
var dotPos = pdfFileName.lastIndexOf(".");
return pdfFileName.substr(0, dotPos);
}
function fnfOnMessageFunc(stringArray) {
if (stringArray[0] == this.myDoc.fnf.HostMessageNames[0])
this.myDoc.fnfSetFieldsRawValue(stringArray);
else if (stringArray[0] == this.myDoc.fnf.HostMessageNames[1])
this.myDoc.fnfReadFieldsRawValue(stringArray);
}
function fnfOnErrorFunc(e) {
console.println("fnfOnMessageFunc Error: " + e);
app.alert("fnfOnMessageFunc Error: " + e);
}
function fnfSetFieldsRawValue(stringArray) {
try {
var formXMLDataStr = (stringArray.length > 1) ? stringArray[2] : "";
if (formXMLDataStr.length > 1)
xfa.datasets.data.loadXML(formXMLDataStr, 0, 1);
else
xfa.host.resetData();
}
catch (e) {
app.alert("fnfSetFieldsRawValue Error(" + stringArray[2] + "):" + e);
}
}
function fnfReadFieldsRawValue(stringArray) {
try {
this.fnf.AllFieldDataPacket = ["_pdf_FormData", this.fnf.FormName];
this.fnf.timeOutPostBack = app.setTimeOut("fnfPostBack();", 100);
}
catch (e) {
app.alert("fnfReadFieldsRawValue Error:" + e);
}
}
function fnfPostBack() {
app.clearTimeOut(this.fnf.timeOutPostBack);
this.fnf.AllFieldDataPacket[2] = xfa.data.saveXML();
fnfPostMessageToHost(this.fnf.AllFieldDataPacket);
}
xfa.data.saveXML()
is how to use the JavaScript to package the entire PDF XFA form data into XML format. Please note the fnfSetFieldsRawValue
function; when the passed in data is not empty, it calls xfa.datasets.data.loadXML(formXMLDataStr, 0, 1);
to populate the XFA form with the XML string; when it's empty, it invokes xfa.host.resetData();
to reset the XFA form. In the demo project, the "Reset" button will end up calling xfa.host.resetData();
to clear out all the user entered data in the currently loaded PDF.
2. Expose scriptable object from managed code
The ScriptableSilverForm
type has an attribute ScriptableType
, and it's intended to expose ScriptableMember
to the JavaScript that runs in the hosting HTML:
[ScriptableType]
public class ScriptableSilverForm
{
private SilverFormModel model = SilverFormModel.Instance;
private Dispatcher currentDispatcher =
Application.Current.RootVisual.Dispatcher;
[ScriptableMember()]
public void GetSavedFormFieldsData()
{
ThreadPool.QueueUserWorkItem(new
WaitCallback(DispatchSendHostFormDataToPDF), null);
}
[ScriptableMember()]
public void OnPDFFieldsDataReady(string pdfDataXML)
{
ThreadPool.QueueUserWorkItem(new WaitCallback(
DispatchReadPDFFormDataToHost), pdfDataXML);
}
public void DispatchReadPDFFormDataToHost(object rawData)
{
currentDispatcher.BeginInvoke(new Action<string>(ReadPDFFormDataToHost),
rawData);
}
...
...
}
The above type is registered to be exposed when the application starts (in the App.xaml.cs file):
private void Application_Startup(object sender, StartupEventArgs e)
{
this.RootVisual = new MainPage();
SilverForms.Model.ScriptableSilverForm silverForm =
new SilverForms.Model.ScriptableSilverForm();
HtmlPage.RegisterScriptableObject("silverForm", silverForm);
SilverFormController controller = SilverFormController.Instance;
}
3. PDF notifies the host that it's ready
As described in the "Setting up the hostContainer" section, whenever a PDF file completes loading into the Adobe Reader, it would send out a "_pdf_PageOpened
" message to the host application. The setTimeout
approach in the "_pdf_PageOpened
" message handler makes sure it returns immediately after the GetSavedFormFieldsData()
method is "scheduled" to call. By the time the GetSavedFormFieldsData()
method is executed, it will call the ScriptableSilverForm::
GetSavedFormFieldsData()
method in the managed code (details can be found at the downloadable demo project).
Once in ScriptableSilverForm::GetSavedFormFieldsData()
, it also returns right away after spinning off a thread-pool thread to initiate the process of "host sends data to PDF". The whole reason to use the thread-pool in order to return at once is to finish the control flow that initiates from the PDF, through the hotContainer
in the JavaScript, and ends at the managed code. At this point, the message generated from the PDF has reached the host application's managed code, and it's time for the host application to start the reverse trip: sending data to the PDF to populate the forms.
4. Host sends data to PDF
Inside ScriptableSilverForm
, the thread-pool thread will dispatch the actual method invocation back to the UI thread to execute, because the Silverlight HTML Bridge's mechanism to call the JavaScript method from the managed code (HtmlPage.Window.Invoke
) requires to run in the UI thread, or, it'll fail.
public void DispatchSendHostFormDataToPDF(object noUse)
{
currentDispatcher.BeginInvoke(new Action(SendHostFormDataToPDF), null);
}
public void SendHostFormDataToPDF()
{
if (model.SelectedForm != null)
{
string formDataXML = model.SelectedForm.fieldsXMLStr;
if (!String.IsNullOrEmpty(formDataXML))
HtmlPage.Window.Invoke("setFormData", formDataXML);
}
}
setFormData
is a JavaScript function defined in the hosting HTML page (SilverFormsTestPage.html):
function setFormData(formDataArrayXMLStr) {
var frmObj = mqzJsCalls.getZoneContent();
if (null == frmObj)
return;
var formDataMsg = ["_host_FormData", "formid"];
formDataMsg[2] = formDataArrayXMLStr;
frmObj.savedHostFormData = formDataMsg;
frmObj.PopulatePDFForm();
}
frmObj.PopulatePDFForm()
is implemented in the mqzLoadExternPDF.js file:
function PopulatePDFForm() {
try {
var objAcrobat = getUsablePDFObj();
objAcrobat.postMessage(savedHostFormData);
}
catch (e) {
alert("PopulatePDFForm Error: name=" + e.name +
" message=" + e.message);
}
setTimeout("pdfIsBridgeBusy = 0;", 100);
}
The formData
is sent to the PDF via the hostContainer
's postMessage
method. Up till now, the code finishes the reverse trip, data is sent to PDF, and the PDF will populate the data to let the user review or edit.
5. Host notifies the PDF it needs user data
Similarly, when the host application needs to read the user data from the PDF, the SilverFormModel
invokes the JavaScript function to notify the PDF that it needs user data:
public void BeginReadCurrentFormData()
{
if (null != FormsList)
HtmlPage.Window.Invoke("getFormData");
}
getFormData
is a JavaScript function defined in the hosting HTML page (SilverFormsTestPage.html):
function getFormData() {
var frmObj = mqzJsCalls.getZoneContent();
if (null == frmObj)
return;
frmObj.ReadPDFFieldsValue();
mqzJsCalls.refreshHTMLZone();
}
ReadPDFFieldsValue()
is implemented in the mqzLoadExternPDF.js file:
function ReadPDFFieldsValue() {
try {
var objAcrobat = getUsablePDFObj();
objAcrobat.postMessage(["_host_GetAllFields"]);
}
catch (e) {
alert("ReadPDFFieldsValue Error: name=" + e.name +
" message=" + e.message);
}
setTimeout("pdfIsBridgeBusy = 0;", 100);
}
Still, it goes through the hostContainer
post message to the PDF. When the PDF receives the message ("_host_GetAllFields
"), it would end the control flow that initiates from the managed code, through the JavaScript, and ends at the PDF. Then, it packages the user data in XML format (all PDF files in the demo project are XFA forms), and sends it back to the host application.
6. PDF sends the user data to the host
The PDF sends back user data in XML format via the message "_pdf_FormData
" (please refer to the "Setting up the message handler and the hostContainer" section for details). The JavaScript message handler will eventually end up calling the managed code through the exposed scriptable object (defined in the SilverFormsTestPage.html file):
function pdfFormDataReady(pdfFormDataXML) {
silverCtl.Content.silverForm.OnPDFFieldsDataReady(pdfFormDataXML);
}
Once again, the managed code will use the thread-pool to return the call from the JavaScript, then dispatch the actual method to the UI thread to execute:
public void DispatchReadPDFFormDataToHost(object rawData)
{
currentDispatcher.BeginInvoke(new Action<string>(ReadPDFFormDataToHost), rawData);
}
private void ReadPDFFormDataToHost(string pdfFormDataRawXML)
{
if (model.SelectedForm != null)
{
XDocument newXMLDoc = XDocument.Parse(pdfFormDataRawXML);
XElement rootElement = newXMLDoc.Descendants("topmostSubform").Single<XElement>();
string formXMLStr = rootElement.ToString();
formXMLStr = formXMLStr.Replace("\r\n ", "");
model.SelectedForm.fieldsXMLStr = formXMLStr;
if (null != model.toBeSetForm)
{
model.CommitToBeLoadedForm();
}
else
{
model.WriteUserData();
MessageBox.Show("All form data has been saved in Isolated Starage!",
"Blend PDF with Silverlight", MessageBoxButton.OK);
}
}
}
We have now finished up all the plumbing work for the data exchange between Silverlight and PDF.
Brief on the Demo Project
Some interesting spots can be found in the demo project, like:
- Using the Silverlight Cairngorm to request the Form List XML data at runtime and data bind to the
ListBox
for the navigation panel.
- When a new form is sleeted by the user, the currently opened PDF's data will be requested to read back to the host application's model (
SilverFormModel
), when it opens up the new PDF.
- The Save button will also read the currently loaded PDF's data first, then call the included
JSONCodec
helper method to serialize all the form data in JSON format, then persist them into isolated storage.
- When the application loads, the persisted JSON will be de-serialized back to the Form List, and we have some interesting code to incorporate the potential Form List XML data changes after the user saved their form data;
- ...
It's just too much details to cover in a single article, I leave the details to the downloadable code...
Note: The thread-safe version of the Silverlight Cairngorm and all the sample PDF files are included in the downloadable source code; it requires Visual Studio 2008 SP1 and Silverlight Tools for Visual Studio 2008 SP1 (includes Silverlight 2 Release Candidate 0). Before debugging, please be sure to make the SilverFormsWeb project as the start up project and set SilverFormsTestPage.html as the "specific page" in the start action of SilverFormsWeb project property dialog.
History
- 2008.10.01 ---- initial post, focuses on the details of the plumbing works that blends PDF and Silverlight visually, with bi-directional data exchange.