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

Blend PDF with Silverlight

0.00/5 (No votes)
1 Oct 2008 5  
Details & demo project of plumbing works that blend PDF and Silverlight visually with bi-directional data exchange.

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:

<!--Inserted positiong DIV tag follows 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()
// anonymous class/function enforces namespace behavior
{

  var mqzJsCalls = this; // JSDoc purposes
  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)) {
  //test for Firefox/x.x or Firefox x.x (ignoring remaining digits);
    var ffversion=new Number(RegExp.$1)
    // capture x.x portion and store as a number

    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"];
//empty Array will make sure the message is sent just once
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; //for hostContainer

  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; };

    //HostContainerDisclosurePolicy.SameOriginPolicy;
    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) {
  //app.alert("Recv’d Msg[ " + stringArray[0] + "]: " + stringArray, 1, 1);
  if (stringArray[0] == this.myDoc.fnf.HostMessageNames[0])
  //set user fields values
    this.myDoc.fnfSetFieldsRawValue(stringArray);
  else if (stringArray[0] == this.myDoc.fnf.HostMessageNames[1])
  //get user fields values
    this.myDoc.fnfReadFieldsRawValue(stringArray);
}

function fnfOnErrorFunc(e) {
  console.println("fnfOnMessageFunc Error: " + e);
  app.alert("fnfOnMessageFunc Error: " + e);
}


function fnfSetFieldsRawValue(stringArray) {
  //app.alert("fnfSetFieldsRawValue : " + stringArray);
  try {
    var formXMLDataStr = (stringArray.length > 1) ? stringArray[2] : "";
    if (formXMLDataStr.length > 1)
      xfa.datasets.data.loadXML(formXMLDataStr, 0, 1);
    else //no XML data, reset the form
      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);
  }
  ...//other code ommited to illustrate Scriptable attributes
  ...
}

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);

  //create the instance of Controller
  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);
    // Get the first child element from contacts xml tree.
    XElement rootElement = newXMLDoc.Descendants("topmostSubform").Single<XElement>();
    string formXMLStr = rootElement.ToString();
    formXMLStr = formXMLStr.Replace("\r\n ", "");
    model.SelectedForm.fieldsXMLStr = formXMLStr;

    if (null != model.toBeSetForm)
    {
      //navigational form selection will come here
      model.CommitToBeLoadedForm();
    }
    else
    {
      //Save button will come here
      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.

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