混合 PDF 与 Silverlight






4.81/5 (30投票s)
详细介绍和演示项目,展示了如何以视觉方式将 PDF 和 Silverlight 结合起来,并实现双向数据交换。
引言
PDF 输出格式广泛用于发布数字文档,可以在各种平台之间交换,同时保持文档布局、字体和其他视觉风格的完整性,无论是在屏幕上还是打印介质上。一个明显的例子是,大多数美国政府表格都是 PDF 格式的。
有一些库和技术使得 PDF 不仅仅是输出格式,还可以作为数据捕获(用户数据输入)界面。最终用户可以直接在 Adobe Reader 中填写 PDF 表格,然后在线提交。但是,几乎所有这些方法都是面向服务器进程的,从服务器生成 PDF 流并将其发送到客户端;需要大量的服务器端应用程序逻辑才能使用 PDF 进行数据收集。
本文提供了一种替代的、面向客户端的简单方法,该方法将 PDF 和 Silverlight 宿主应用程序在 Web 浏览器中进行混合,无需服务器端逻辑即可组装特定于用户的 PDF 文件以进行数据收集。特别是,当需要收集一组 PDF 表格数据时,此技术比传统的以服务器为中心的技v术具有更高的可伸缩性,因为它使得应用程序服务器可以只处理纯数据和业务逻辑,而无需在应用程序服务器上运行任何 PDF 特定逻辑。
下载的源代码包含了一个可用的示例,其中包含所有细节;它有一个 Visual Studio 2008 SP1 的解决方案和项目文件,需要预装 Silverlight 2 Release Candidate 0 和 Adobe Reader 8.1.2。Silverlight 应用程序(在演示项目中命名为 SilverForm)将 PDF 组作为模板列表(对所有用户都一样)进行管理,当用户请求时,作为静态文件从 Web 服务器流式传输下来,利用客户端的 Silverlight 插件 和 Adobe Reader 插件 在 Web 浏览器中协同工作,数据收集、表单导航、表单数据持久化以及使用用户数据填充 PDF 等任务都由客户端代码处理。
如果您已安装 Silverlight 2 Beta 2 运行时,可以 从此处运行示例应用程序。对于已安装 Silverlight 2 Release Candidate 0 的开发人员,示例应用程序在此处。两者共享几乎相同的源代码,并且需要 Adobe Reader 8.1.2 或更高版本。
概述
这里的“混合”一词有双重含义:一是在 Silverlight 宿主应用程序之上渲染 PDF,并在 Silverlight 应用程序布局更新或调整大小时重新定位/调整加载的 PDF 的大小。另一个方面是交换 Silverlight 和 PDF 之间的数据;PDF 可以通知宿主 Silverlight 应用程序它已准备好读取和填充表单数据,而宿主 Silverlight 应用程序需要一个通道来请求 PDF 中填写的全部用户数据,并在客户端隔离存储中持久化/恢复它们。
在 Silverlight 上渲染 PDF 的基本思想是在 Silverlight 应用程序的宿主 HTML 代码中包含一个用于定位的 DIV
元素。DIV
的内容是一个用于调整大小的 IFRAME
,而 IFRAME
的源是另一个 HTML 文件(mqzPDFContainer.htm,我们称之为“加载 HTML”),该文件知道如何通过设置正确的 OBJECT
标签或 EMBED
标签来加载 PDF,具体取决于不同的浏览器。
定位 DIV
在包含 Silverlight 应用程序的宿主 HTML(或 ASP.NET 页面)中创建(在演示项目中,宿主 HTML 文件是 SilverFormsTestPage.html),它具有绝对定位样式、透明背景、无边框,并且最初是隐藏的,内容为空。
当选择了一个特定的 PDF URL 时,将把用于调整大小的 IFRAME
设置为定位 DIV
的内容(innerHTML
)。
为了让定位 DIV
知道其左上角坐标以及 IFRAME
的宽度和高度,我们需要一个 Silverlight 用户控件作为 PDF 的“背面”。IFRAME
的内容(源)将根据此控件的位置和大小进行精确的定位和调整。每当用户调整 Silverlight 应用程序大小导致此“背面”用户控件的位置或大小发生变化时,它将通过 Silverlight HTML Bridge 通知 DIV
和 IFRAME
进行相应的重新定位/调整大小。
当加载 HTML(mqzPDFContainer.htm,即 IFRAME
的源)中的 JavaScript 代码设置了正确的 OBJECT
或 EMBED
标签用于 PDF 文件时,浏览器将接管后续工作。它将加载并实例化 Adobe Reader,然后将 PDF URL 设置为标签的源(OBJECT
元素中的 data
属性,EMBED
标签中的 src
属性)。Reader 加载后,它将开始通过 HTTP 下载指定的 PDF 文件。
请求的 PDF 文件可以位于与 Silverlight 宿主应用程序相同的 Web 服务器上,所有静态文件——对所有用户都相同的模板文件——都将下载到客户端并在浏览器中运行;无需应用程序服务器逻辑。
当指定的 PDF 加载到 Reader 中时,它将启动 PDF 和 Silverlight 之间的数据交换过程。数据交换是双向的,PDF 调用 hostContainer PostMessage
与宿主 HTML 进行交互,而 Silverlight 应用程序的托管代码则调用 Silverlight HTML Bridge 来读取和写入数据。
正如您所见,宿主 HTML 是 Silverlight 和 PDF 之间的中间人。事实上,Silverlight 托管代码永远不会“知道”它正在与 PDF 合作,它只与 HTML Bridge 通信。而且,PDF 永远不会直接与托管代码交互;它所做的只是通过 hostContainer 发布和处理消息,就像周围没有 Silverlight 应用程序一样。
要以视觉方式将 PDF 与 Silverlight 混合,并集成可交换的数据,这实际上是 Silverlight、C#、HTML Bridge、JavaScript、hostContainer 和 PDF 的组合。演示项目是使用 线程安全的 Silverlight Cairngorm 版本构建的。由于细节很多,本文将重点介绍连接 Silverlight 和 PDF 的底层工作;下一篇文章将介绍演示项目中用于整合所有细节的兴趣点。
视觉混合
1. 创建定位 DIV
定位 DIV
通过将以下 HTML 代码插入宿主 HTML 代码(或 ASP.NET 页面标记)来创建。宿主 HTML 文件是演示项目中的 SilverFormsTestPage.html,它包含创建 Silverlight 应用程序的 OBJECT
标签。以下 DIV
将插入到 Silverlight OBJECT 标签之后。
<!--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>
请注意,为 HTML Bridge 设置了 enableHtmlAccess
为 true。此外,DIV
的 ID
(mqzIFrmParent
)至关重要,因为 JavaScript 使用此 ID
获取 DIV
对象的引用。
2. 创建 IFRAME 并进行定位/尺寸调整
所有操作定位 DIV
和相应 IFRAME
属性的代码都封装在一个 JavaScript 文件中: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();
}
};
宿主 HTML SilverFormsTestPage.html 引用了上述 JavaScript 文件。当 Silverlight 托管代码需要设置/更新 PDF(Reader)的位置和尺寸时,它会调用 mqzJsCalls.moveHTMLZone(x,y,w,h)
来重新定位或调整尺寸。当选择了一个新的 PDF URL 时,托管代码将调用 mqzJsCalls.showHTMLZone(true)
使定位 DIV
可见,然后调用 mqzJsCalls.setHTMLZoneSrc(pdfUrl)
将 IFRAME
创建为 DIV
标签的内容(innerHTML
)。我们将在“将 PDF 加载到 IFRAME”部分后面检查生成的 IFRAME
标签中的 SRC
属性。
3. 从 Silverlight 用户控件调用 mqzJsCalls.moveHTMLZone(x,y,w,h)
在演示项目中,左侧是表单导航面板,由 ListBox
实现,并绑定到表单列表数据。右侧由 ContentZone
用户控件填充,该控件充当 PDF 的“背面”(如概述部分所述)。GridSplitter
控件位于导航列表框和内容区域之间,因此用户可以通过拖放来更改两个控件的宽度。此外,用户可以调整浏览器窗口的大小,这会影响 ContentZone
控件的大小。
ContentZone
用户控件的唯一职责是处理 Resize
和 layoutUpdate
Silverlight 事件,然后调用 mqzJsCalls.moveHTMLZone(x,y,w,h)
来正确地定位和调整其尺寸。
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});
}
}
}
一个值得注意的技巧是如何将 ContentZone
用户控件坐标的左上角点(0,0)转换为相对于网页最左上角的坐标。令人惊讶的是,Silverlight 没有 LocalToGlobal
或 GlobalToLocal
这样的 API,我们必须依赖 RootVisual
的 GeneralTransform
对象引用,然后调用 Transform
方法获取全局坐标。
一旦获得左上角坐标以及宽度和高度,我们就可以通过 HtmlPage.Window.Invoke
调用 JavaScript 方法。参数通过对象数组传递给 JavaScript。
4. 将 PDF 加载到 IFRAME
到目前为止,已经完成了加载 PDF 的所有底层工作。由 mqzJsCalls.setHTMLZoneSrc(pdfUrl)
生成的 IFRAME
标签将 pdfUrl
参数设置为 IFRAME
的 SRC
。在演示项目中,pdfUrl
是 mqzPDFContainer.htm 的 URL,PDF 文件通过查询字符串参数指定。例如,当 PDF 文件名为 fed 文件夹下的 ty08_w2.pdf 时,IFRAME
的 pdfUrl
将是:
ClientBin/mqzPDFContainer.htm?fn=ClientBin/fed/ty08_w2.pdf
通过 mqzPDFContainer.htm 文件加载 PDF 的原因是为了能够为每个 PDF 设置 hostContainer
。通过查询字符串传递 PDF 文件名可以使 mqzPDFContainer.htm 仅引用通用的 JavaScript 代码来设置 hostContainer
,而无需表单特定的代码。这是 mqzPDFContainer.htm 的 HTML 代码:
<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>
它相当简单,只引用了两个外部 JavaScript 文件并创建了一个空的 DIV
。两个外部 JavaScript 文件都在演示项目中,mqzQueryStringParser.js 可以更轻松地解析查询字符串并读取指定的 pdfUrl
。我将把细节留给演示项目,并在“将 PDF 加载到 IFRAME”部分重点介绍 mqzLoadExternPDF.js 文件的核心内容。
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;
pdfFileName
变量的值从查询字符串读取。LoadContent
与 HTML 页面的 onload
事件关联,在 HTML 加载到 IRFRAME
时调用 LoadPDFFile(pdfFileName)
。在 LoadPDFFile(pdfFileName)
函数内部,它为 IE、Firefox 2.x(type=application/pdf)和 Safari(type=application/vnd.adobe.pdf)创建 OBJECT
标签,并为 Firefox 3 创建 EMBED
标签。一旦 OBJECT
标签或 EMBED
标签被设置为空 DIV
的 innerHTML
,Web 浏览器就会开始实例化 Adobe Reader 插件,并且插件会加载 data
(在 OBJECT
标签中)或 SRC
(在 EMBED
标签中)属性中指定的 PDF。最终结果是 PDF 被渲染在 Silverlight 之上,精确地位于 ContentZone
控件的位置和大小——视觉上混合。
Silverlight 和 PDF 之间的数据交换
数据集成工作包含四个重要任务:
- PDF 通知宿主它已准备好接收数据。
- 宿主将数据发送到 PDF。
- 宿主通知 PDF 它需要用户数据。
- PDF 格式化用户数据并将其发送回宿主。
让我们逐一来看。
在任何一方可以发送或接收“通知”之前,需要在 PDF 加载到 Adobe Reader 中后立即设置 hostContainer
对象。消息处理程序需要在加载的 HTML 中设置。在托管代码端,需要将可脚本对象暴露给 JavaScript。
1. 设置消息处理程序和 hostContainer
在加载的 HTML 文件(mqzPDFContainer.htm)中,需要设置消息处理程序来与 PDF 文档进行消息的发送/处理。它是一个附加到 OBJECT
/EMBED
对象的 JavaScript 函数。设置工作由引用的 JavaScript 文件 mqzLoadExternPDF.js 完成。在“将 PDF 加载到 IFrame”部分所示的 JavaScript 代码中,“onStartInit()
”被“安排”在 10 毫秒后调用,在该方法内部。它确保在设置 HTML 中的 hostContainer
对象之前,动态插入的 OBJECT
/EMBED
对象已存在。以下代码是 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
”是 PDF 在 Adobe Reader 中加载完成时发出的应用程序定义的自定义消息。“_pdf_FormData
”是另一个来自 PDF 的自定义消息,它通知宿主应用程序其表单数据已准备好(包含在 aMessage
,一个字符串数组中)。
另一方面,hostContainer
对象是 HTML JavaScript 和 PDF 文档之间的桥梁。每个 PDF 文档都需要在 Adobe Reader 中设置一个 hostContainer
对象,然后它就可以从外部 messageHandler
(位于加载的 HTML 中)接收消息(格式为字符串数组)并向其发送消息。以下是设置对象的通用 JavaScript(表示它不是表单特定的,所有 PDF 文件共享同一个 JavaScript 文件)代码:
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()
是如何使用 JavaScript 将整个 PDF XFA 表单数据打包成 XML 格式。请注意 fnfSetFieldsRawValue
函数;当传入的数据不为空时,它调用 xfa.datasets.data.loadXML(formXMLDataStr, 0, 1);
使用 XML 字符串填充 XFA 表单;当数据为空时,它调用 xfa.host.resetData();
重置 XFA 表单。在演示项目中,“重置”按钮最终会调用 xfa.host.resetData();
来清除当前加载的 PDF 中所有用户输入的数据。
2. 从托管代码公开可脚本对象
ScriptableSilverForm
类型具有 ScriptableType
属性,并且旨在将 ScriptableMember
暴露给在宿主 HTML 中运行的 JavaScript。
[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
...
}
上述类型已在应用程序启动时注册(在 App.xaml.cs 文件中)以进行公开:
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 通知宿主它已准备好
如“设置 message handler 和 hostContainer”部分所述,每当 PDF 文件加载到 Adobe Reader 中时,它都会向宿主应用程序发送一个“_pdf_PageOpened
”消息。在“_pdf_PageOpened
”消息处理程序中使用 setTimeout
的方法确保在“GetSavedFormFieldsData()
”方法被“安排”调用后立即返回。当 GetSavedFormFieldsData()
方法执行时,它将调用托管代码中的
方法(详细信息可在下载的演示项目中找到)。ScriptableSilverForm::
GetSavedFormFieldsData()
一旦进入 ScriptableSilverForm::GetSavedFormFieldsData()
,它在启动一个线程池线程以启动“宿主将数据发送到 PDF”的过程后也会立即返回。使用线程池以便立即返回的目的是完成从 PDF 开始,通过 JavaScript 中的 hotContainer
,并最终到达托管代码的控制流。此时,来自 PDF 的消息已到达宿主应用程序的托管代码,是时候由宿主应用程序开始反向行程:将数据发送到 PDF 以填充表单了。
4. 宿主将数据发送到 PDF
在 ScriptableSilverForm
内部,线程池线程会将实际的方法调用分派回 UI 线程执行,因为 Silverlight HTML Bridge 从托管代码调用 JavaScript 方法(HtmlPage.Window.Invoke
)的机制要求在 UI 线程中运行,否则将会失败。
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
是在宿主 HTML 页面(SilverFormsTestPage.html)中定义的 JavaScript 函数。
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()
在 mqzLoadExternPDF.js 文件中实现。
function PopulatePDFForm() {
try {
var objAcrobat = getUsablePDFObj();
objAcrobat.postMessage(savedHostFormData);
}
catch (e) {
alert("PopulatePDFForm Error: name=" + e.name +
" message=" + e.message);
}
setTimeout("pdfIsBridgeBusy = 0;", 100);
}
formData
通过 hostContainer
的 postMessage
方法发送到 PDF。到目前为止,代码完成了反向行程,数据已发送到 PDF,PDF 将填充数据供用户查看或编辑。
5. 宿主通知 PDF 它需要用户数据
同样,当宿主应用程序需要从 PDF 读取用户数据时,SilverFormModel
会调用 JavaScript 函数来通知 PDF 它需要用户数据。
public void BeginReadCurrentFormData()
{
if (null != FormsList)
HtmlPage.Window.Invoke("getFormData");
}
getFormData
是在宿主 HTML 页面(SilverFormsTestPage.html)中定义的 JavaScript 函数。
function getFormData() {
var frmObj = mqzJsCalls.getZoneContent();
if (null == frmObj)
return;
frmObj.ReadPDFFieldsValue();
mqzJsCalls.refreshHTMLZone();
}
ReadPDFFieldsValue()
在 mqzLoadExternPDF.js 文件中实现。
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);
}
它仍然通过 hostContainer
post message 到 PDF。当 PDF 收到消息(“_host_GetAllFields
”)时,它将结束从托管代码开始,通过 JavaScript,并最终到达 PDF 的控制流。然后,它将用户数据打包成 XML 格式(演示项目中的所有 PDF 文件都是 XFA 表单),并将其发送回宿主应用程序。
6. PDF 将用户数据发送给宿主
PDF 通过消息“_pdf_FormData
”发送回 XML 格式的用户数据(请参阅“设置 message handler 和 hostContainer”部分了解详细信息)。JavaScript 消息处理程序最终会通过公开的可脚本对象(在 SilverFormsTestPage.html 文件中定义)调用托管代码。
function pdfFormDataReady(pdfFormDataXML) {
silverCtl.Content.silverForm.OnPDFFieldsDataReady(pdfFormDataXML);
}
同样,托管代码将使用线程池将来自 JavaScript 的调用返回,然后将实际方法分派回 UI 线程执行。
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);
}
}
}
我们现在已经完成了 Silverlight 和 PDF 之间数据交换的所有底层工作。
关于演示项目简介
在演示项目中可以找到一些有趣的地方,例如:
- 使用 Silverlight Cairngorm 在运行时请求表单列表 XML 数据,并将其绑定到导航面板的
ListBox
。 - 当用户选择一个新表单时,将请求当前打开的 PDF 的数据读回宿主应用程序的模型(
SilverFormModel
),然后再打开新的 PDF。 - “保存”按钮还将首先读取当前加载的 PDF 的数据,然后调用包含的
JSONCodec
帮助方法将所有表单数据序列化为 JSON 格式,然后将其持久化到隔离存储中。 - 当应用程序加载时,持久化的 JSON 将被反序列化回表单列表,并且我们有一些有趣的代码来合并潜在的表单列表 XML 数据更改,**在** 用户保存其表单数据之后;
- ...
在单篇文章中涵盖所有细节太多了,我将细节留给可下载的代码...
注意:线程安全的 Silverlight Cairngorm 版本和所有示例 PDF 文件都包含在可下载的源代码中;它需要 Visual Studio 2008 SP1 和 Visual Studio 2008 SP1 的 Silverlight 工具(包含 Silverlight 2 Release Candidate 0)。**在调试之前**,请确保将 SilverFormsWeb 项目设为启动项目,并在 SilverFormsWeb 项目属性对话框的启动操作中将 SilverFormsTestPage.html 设置为“特定页面”。
历史
- 2008.10.01 ---- 首次发布,侧重于混合 PDF 和 Silverlight 视觉效果的底层工作的细节,以及双向数据交换。