使用C++和XML建立智能文档

原文标题:《Create Word and Excel Smart Documents with C++ and XML》

原文作者:Mike Kelly

翻译:陶刚

原文链接:http://msdn.microsoft.com/msdnmag/issues/03/12/SmartDocuments/default.aspx

代码下载: http://download.microsoft.com/download/a/5/f/a5f7e731-af88-476e-9d57-02b44945ed3a/SmartDocuments.exe

关键字:智能文档(Smart Document)XML大纲定义(XSD)接口(interface)

摘要:Office 2003中最“酷”的部分之一是称为“智能文档(Smart Document)”的可编程能力特性,它允许开发者使用可编程的内容和操作来扩充Word和Excel文档。在典型情况下,演示Office的可编程能力的示例使用的都是Visual Basic或Visual Basic .NET的。在本文中,作者使用C++为Excel开发了一个智能文档。他描述了新的ISmartDocument接口并演示了如何使用该接口管理一个类似Excel电子表格的简单事务列表。

智能文档是微软Word 2003和Excel 2003中新的可编程特性。开发者可以建立用可编程代码片段增强了的Word或Excel智能文档,以辅助用户建立和修改它们。重复使用模版内容、为如何格式化或完成文档实现业务规则、通过正式批准的过程路由(routing)文档等等过程,Office智能文档都可以简化。

多年以来利用ActiveX和Visual Basic for Applications (VBA)在自定义的解决方案中使用微软Office应用程序是可行的。但是它们有一些限制,特别是在布署、用户界面和安全性方面。智能文档把Office XP引入的流行的“任务面板”接口和自定义解决方案之间紧密集成,从而解决了这些问题。有了智能文档,你就能用XML定义自定义事务面板内容,它能把帮助、指令和可编程控件自动地与用户在文档中的位置同步。自定义事务面板控件的代码可以使用多种语言开发:Visual Basic 6.0、Visual Basic .NET、Visual C++或Visual C#。

当用户简单地打开来自可信服务器的文档或电子邮件中的附件的时候,智能文档和相关的组件就会被安装。它们可以自动从可信的服务器上更新自身,极大的简化了更新的布署。智能文档遵循Office安全性设置,要求它们下载自可信服务器并有可信源的签名。你可以为用户提供一个控件以决定是否打开不符合执行安全性标准智能文档

尽管目前有很多好的跟踪进度表的解决方案,但是我还是把这种进度表作为存储在Excel中的数据库,因为我希望你简单地安装Office 2003并下载本文的代码后,不需要安装其它的任何附加产品就能使用这个解决方案。

在电子表格中每个包含有用信息的进度表数据项都有一行记录:谁在做这个事务、估计完成这个事务需要的时间总计、这个事务是否已经完成。我假定在项目小组,开发者每周更新这个进度表以反映上一周的工作情况。此外,由于我希望知道小组正在进行什么事务,我将要求他们确定下一周将做什么事务。这也是对进度估计什么时候进行修改的时间。我建立的智能文档解决方案能把所有这些进度改变包装为良好格式化的Word状态文档——这恰好是经理们喜欢的。作为附带的优点,我将给状态文档使用Word的新的XML特性。实际上,我的Excel智能文档解决方案将生成一个XML状态摘要,我将在Word中把这个摘要与XSLT结合起来以得到良好格式化的文档。

智能文档的概念

在Office 2003智能文档解决方案中有五个主要的部分。首先,要有Word文档或Excel模版。它是用户打开以执行业务功能的智能文档——在例子中为每个软件项目生成了一个良好格式化的每周状态报告。请注意,在Office 2003中,只有Word和Excel支持智能文档。

其次,要有XML大纲定义(XSD)。Word文档或Excel电子表格都使用来自XML大纲的元素作记号,用于覆盖全部或部分文档或工作簿上面的XML数据模型。例如,在项目进度工作簿中,有一个项目名称(Project Name)单元和一个开发者姓名(Developer Name)单元。它们分别与XML大纲中的ProjectName和DeveloperName元素对应。“做标记”就是工作簿开发者如何把这些特定单元与XML大纲元素关联。

第三,你要有操作处理程序(action handler)DLL,可以在Visual Basic、Visual Basic .NET、Visual C++或Visual C#中开发它。接着把它被布署到Web或网络服务器上。这个DLL实现了ISmartDocument OLE接口。

第四,要有说明解决方案安装指令的XML扩展包清单文件。它提供了解决方案DLL的服务器位置和解决方案的COM CLSID。同时它还记载了其它一些与解决方案关联的文件和解决方案的ID。解决方案的ID唯一地标识了该智能文档解决方案。尽管你可以把任何唯一的标识符字符串作为解决方案的ID,但是最好使用生成的GUID。

最后,要有你希望安装到本地计算机的解决方案需要的其它文件(例如GIF或JPEG、HTML或文档片段)。这些都必须在清单文件中指定,当打开智能文档的时候被Office自动地安装或更新。这样它们才能被智能文档解决方案的代码使用。

我要阐明一下示例中每个文档的角色,智能文档是附带了XML大纲和自定义逻辑的文档——在示例中,它是Excel电子表格。我生成Word文档这个事件是偶然的,我也能轻易的把信息放入数据库中或生成一个PowerPoint演示文稿。

你可以考虑一下XML元素,它们把电子表格或文档分解为逻辑结构的组件,就像钩子一样,智能文档解决方案能够在文档的不同部分中附加控件以辅助用户工作的。这些控件的行为是在运行的时候由你的操作处理程序DLL定义的。这些控件实际上变成了一个完善的内容敏感的(context-sensitive)帮助系统,当用户移动进入文档或电子表格的特定部分(与XML元素对应)的时候,它们显示在新文档的操作事务面板中。因为智能文档控件不仅仅是帮助文本,所以你的内容敏感帮助实际上作为用户的助手,帮助用户完成使用该文档的业务事务系统。

建立智能文档

生成智能文档解决方案的涉及到的大多数工作包括建立一个Word或Excel文档,并把XML元素与它关联。实际上,你一般可以把已有的Word或Excel文档作为建立智能文档的基础。因为多数业务文档已经有了一定的结构,当你定义XML元素的时候仅仅需要对它进行形式化(formalize)和命名。

从开发者的角度看,该文档或电子表格是解决方案的基本UI。当用户编辑的时候,智能文档操作处理程序DLL定义了出现在该文档旁边的附加的UI。在可能的情况下,在建立智能文档的时候使用Word和Excel的下层能力,而不是在智能文档DLL中编写自定义代码是明智的。我的Excel进度表工作薄中的公式就演示了这一点。我希望在上一周的星期一得到状态报告,但是人们通常在星期一或星期四添加状态报告;在这以后是本周的星期一了。我不是编写一段C++代码实现这种操作,而是编写了Excel公式来压缩这个计算过程并把它放在工作表的一个单元中:

     
     =IF(WEEKDAY(TODAY(),3)<2,
    TODAY()-(7+WEEKDAY(TODAY(),3)),
    TODAY()-WEEKDAY(TODAY(),3))


我简单地使用一个自己编写的从Excel工作薄中得到值的函数从该工作表单元中载入计算过的日期值。

开始

现在我们从建立一个Excel电子表格以跟踪软件进度表开始。注意,我假定你已经下载了示例代码,它包含了必要的支持文件。

启动微软Excel 2003并打开Project Schedule (Original).xls。这个电子表格与大多数Excel电子表格一样,已经有了一个结构。现在使用XSD文档(例子中是ScheduleSmartDocument.xsd)把一个XML大纲与这个结构关联。在Excel中,点击“数据”菜单下的“XML”并选择“XML源”。当“XML源”事务面板出现的时候,点击“工作薄映射”按钮并添加ScheduleSmartDocument.xsd文件。接着点击“确定”关闭“浏览”对话框,再次点击“确定”关闭“XML映射”对话框。图1显示了添加大纲后的Excel“XML源”事务面板。



图1.XML源



为了建立映射,你必须把元素从XML大纲中拖到工作薄上。对于类似Project Name和Developer的元素,这是很简单的事情,只需要把该XML元素名称从“XML源”事务面板上拖动到包含相关数据的工作薄单元上(例如,把ProjectName拖动到A2上)。对于分层显示的重复的进度表数据项,可以通过选择数据项把该组拖动到标题行(A5)上。你可以通过选择“XML源”事务面板中特定的元素(例如Description)来确认映射关系;工作薄中相应的部分应该高亮度显示。把结果工作薄保存为Project Schedule (Mapped).xls。在示例中,我将建立单个电子表格,或者你可以把进度表保存为Excel工作薄模版。这样的话,你就简化了用户建立该文档的新副本的过程,每个新的副本都将成为一个Excel智能文档。

ISmartDocument接口教程

下一步是建立智能文档操作DLL并安装它。在你编写智能文档操作处理程序DLL(它实现了ISmartDocument接口方法)之前,最好先了解一下这个接口。

当用户在文档中移动的时候,你的操作处理程序DLL将建立并管理一组出现在文档操作事务面板中的控件,允许你基于文档中的位置提供自定义的用户界面。映射到文档的XML大纲元素定义了用户在文档中的位置。XML元素应该指定给用户显示哪些控件。图2显示了你可以建立的控件类型。你将使用操作处理程序DLL中实现的ISmartDocument接口的方法建立这些控件。当这些控件被激活的时候(例如进入了文本框或者点击了某个按钮),Office通过ISmartDocument接口方法调用你的自定义操作处理程序DLL代码。

图2.智能文档控件类型



我把IsmartDocument接口的方法分为几类:配置方法,它为Office描述了建立在文档操作事务面板中的控件(图3);控件绘制时(draw-time)方法,它们作为控件调用并绘制在事务面板上(图4);修改通知方法,Office把它们作为文档操作事务面板控件调用,由用户维护(图5)。

图3. ISmartDocument接口配置方法



图4. ISmartDocument接口控件绘制方法





图5. ISmartDocument接口通知方法



注意,对于大多数DLL,你不会使用所有的控件类型或实现所有的IsmartDocument方法。例如,因为我的事务面板中没有ActiveX或列表框控件,在例子中就只有这些方法的样板实现。甚至于根本不编写任何操作处理程序DLL也可能利用一些简单的智能文档特性(例如提供超级链接或内容敏感性帮助文本)。作为代替的是你只需要在关联的Word或Excel智能文档中简单地提供一个XML文件,该文件描述了你希望与多种XML元素关联的操作。

图6摘录了ISmartDocument方法的参数。

图6. ISmartDocument通用方法参数



当Office载入你的操作DLL的时候,它首先调用图3所示的配置方法。Office首先调用SmartDocInitialize(允许你执行具体的一次性的初始化)。接着它将通过调用你的get_SmartDocXmlTypeCount实现查询文档中定义的拥有关联控件的XML元素的数量(文档中的XML元素没有关联控件的情况也是可以的)。在例子中,文档操作面板是空的,因此你可能希望为这些元素定义一些标准的帮助内容。你一旦告诉Office你对多少XML文档元素有兴趣后,它将查询每个元素的名称,这些名称是通过调用多个get_SmartDocXmlTypeName获得的。你必须返回XML元素的合格的名称(例如schema#element)。每个元素都有说明,当用户定位到这个元素中的时候,说明会出现在文档操作面板的标题中。这是通过调用get_SmartDocXmlTypeCaption返回的。对get_SmartDocXmlTypeName和 get_SmartDocXmlTypeCaption的调用是成对出现的,每个XML元素执行一次。下一步,Office希望知道每个元素的显示的事务面板中的控件信息。这些信息包括:

·get_ControlCount——给定的某个元素的控件数量。

·get_ControlID——允许你根据某个元素的控件索引(从1开始)给每个控件指定一个唯一的ID号。

·get_ControlNameFromID——在给定控件ID后返回控件的(非固定的)名称。这个名称没有显示,但是用于引用Word或Excel对象模型中的控件。

·get_ControlTypeFromID——给定控件ID后,返回控件的类型(从图2中)。

当用户在文档中移动的时候,Office用适当的控件更新文档操作事务面板,它接着在事务面板中绘制控件的时候调用控件绘制方法(图4所示)。首先它调用get_ControlCaptionFromID,允许你为控件提供一个说明。注意说明不是在配置的时候提供的,而是在显示的时候提供的,因此可以动态地修改它。Office不会绘制有空白的或NULL说明字符串的控件。这可能用于动态地隐藏和显示控件。为了隐藏一个控件,你只需要简单地为该控件从get_ControlCaptionFromID返回一个一个NULL或空字符串。

接着Office为每个控件调用该控件类型特定的填充方法(例如PopulateHelpContent)。你使用这些方法为控件提供内容,例如图像的位置或超级链接的文本。注意与控件关联的文档元素的XML和Text内容被传递到所有的填充方法。同时也要注意,C_TYPE_LABEL和C_TYPE_SEPARATOR类型的控件没有关联的填充方法。在所有的控件绘制了并且事务面板完成了以后,Office将调用OnPaneUpdateComplete。

当用户与事务面板中的控件交互操作的时候,Office调用图5显示的列表中的某个通知方法。标签和分隔符没有关联的通知方法。对于其它控件,通知方法表明要么状态改变了,要么被点击了,这都可能调用一个操作(例如超级链接或图像)。

注意,通知太多比太少更容易造成Office出错。例如,在Excel中的单元选择发生改变的时候(即使XML元素仍然没有变化),Office重新绘制文档操作工作面板并调用你的方法,这样在事务面板上你才拥有真正的动态内容。例如,你能够通过Excel对象模型查询被选择的单元的值并根据这个值更新控件。在示例代码中我利用了这个优点来更新事务面板中被选择的事务的信息,而不需要在文档操作DLL中操作Excel VBA事件。

属性包

大多数控件绘制方法都提供了ISmartDocProperties参数。这是一个名称/值对的属性组(继承自IPropertyBag),提供了控件上的附加信息。这是你指定控件的大小、字体、对齐方式等的地方。图7提供了应用于所有控件的关键的名称。属性值,包括整形属性的值,都是作为BSTR提供给的ISmartDocProperties::Write的。例如,PopulateTextboxContent下述代码行会把文本框控件的宽度设置为50个像素:

      
      Props->Write(CComBSTR("W"), CComBSTR("50"));


图7. 所用控件的ISmartDocProperties



拥有关联标签的控件(C_TYPE_TEXTBOX、C_TYPE_CHECKBOX)都有图8所示的附加属性以描述该控件的标签属性。注意这些只能影响该控件的标签,不会影响绘制控件的字体。最后,还有一些其它的属性只能应用于特定类型的控件。这些属性显示在图9中。

图8.控件标签的IsmartDocProperties



图9. 其它的ISmartDocProperties名称/值对



建立智能文档操作DLL

我决定在Visual C++中使用活动模版库(ATL)建立智能文档操作DLL。ATL是个很好的选择,因为它的覆盖区域比MFC小一些,但是它仍然提供了操作智能文档需要的大量方便的COM功能。但是,如果你更熟悉MFC,你也可以选择它。

从MSDN Online上下载Office 2003智能文档SDK之后,我启动Visual Studio .NET并建立了一个新的ATL类型的项目,接着使用Add | Class给项目添加一个ATL简单对象。这就是示例代码中的CScheduleSmartDoc。从该对象的类环境菜单(使用View | Class打开类资源管理器)中,我使用Add | Implement Interface让这个类实现ISmartDocument接口。如果在开发计算机上没有安装Office 2003,你必须把有Office 2003的计算机上的类型库复制到你的计算机上。你一般可以在C:/Program Files/Common Files/Microsoft Shared/Smart Tag/mstag.tlb中找到它,但是你要确保得到的是1.2版本(Office 2003版本)。接着你应该把我的示例代码和接口描述作为向导,实现你所需要的方法。

安装智能文档解决方案

Office使用一个XML文件(叫作XML扩展包列表文件)描述你的智能文档解决方案,它包含了更新和配置该解决方案的目录。典型情况下这个文件叫作manifest.xml。我在示例代码中添加了一个框架manifest.xml(图10所示),并用红色高亮度显示了你应该修改的东西。

·解决方案的ID。你应该使用GuidGen为智能文档解决方案生成一个唯一的GUID。这是强制性的,所有的解决方案都需要有一个唯一的ID,并且虽然没有严格的限制为GUID,但是这是确保唯一性的最好的方法。

·updateFrequency,它是Office应该多久(按分钟记)检查服务器的清单更新一次。更新都是由XML扩展包清单中指定的每个文件的版本标志控制的。如果版本不同,并且更新事件超期了,Office将把对应的新版本的文件从服务器下载到本地计算机上。

·智能文档操作DLL的文件路径。这可以是URL或网络路径(如果DLL布署在网络上)或与清单文件位置关联的路径或文件名称。

·智能文档操作DLL的CLSID。因为它实现了IsmartDocument COM接口,它就拥有一个Office用于载入它的COM CLSID。你可以在生成的用于实现ISmartDocument接口的ATL类的头文件中找到它。它是coclass(例子中的CScheduleSmartDoc)的uuid属性。

图10. XML清单文件

      
      <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<manifest xmlns="http://schemas.microsoft.com/office/xmlexpansionpacks/2003">
    <version>1.1</version>
    <updateFrequency>20160</updateFrequency>
    <uri>ScheduleSmartDocument</uri>
    <solution>
        <solutionID>{15960625-1612-46AB-877C-BBCB59503FCE}</solutionID>
        <type>smartDocument</type>
        <alias lcid="*">Simple Schedule Smart Document</alias>
        <targetApplication>Excel.Application.11</targetApplication>
        <file>
            <type>solutionActionHandler</type>
            <version>1.0</version>
            <filePath>ScheduleSmartDocument.dll</filePath>
            <CLSID>{0A9D54DE-12F8-4711-AF10-7D4ACA1D1924}</CLSID>
            <regsvr32/>
        </file>
        <file>
            <type>Other</type>
            <version>1.0</version>
            <filePath>ScheduleGraphic.GIF</filePath>
        </file> 
    </solution>
    <solution>
        <solutionID>{443E624C-AE85-4b20-8522-1CFD5B4E7CBC}</solutionID> 
        <alias>Schedule Smart Document XSL Transform</alias> 
        <type>Transform</type> 
        <context>http://schemas.microsoft.com/office/word/2003/wordml
            </context> 
        <file>
            <type>primaryTransform</type> 
            <version>1.0</version> 
            <filePath>ScheduleReport.xsl</filePath> 
        </file>
    </solution> 
</manifest>



如果你还需要随着解决方案安装一些附加的文件(例如GIF或包含附加文本的HTML文件),你可以把它们列举在 元素的下面,使用 元素分隔开。我只有出现在事务面板中的一张附加的GIF。

另一件阻碍了我几个小时的事情是清单中的 元素的名称必须与你用于映射文档元素的XML大纲的名称相同。在例子中是ScheduleSmartDocument。

一旦你的智能文档与XML大纲一起标识了,你就能附加一个XML扩展包,把操作处理程序DLL与该文档关联。在Excel中,通过Data | XML | XML Expansion Packs打开对话框。你使用这个对话框先添加少量的扩展包(执行解决方案的manifest.xml文件),接着给文档附加一个扩展包。这个过程与用XML标记文档相似,只有智能文档开发者需要这样做——用户不需要这个对话框。换句话说,你使用Add-Ins 对话框(Tools | Templates and Add-Ins)的“XML扩展包”选项卡给文档附加了一个扩展包。

当用户打开拥有关联扩展包的文档的时候,Office将自动安装或更新他们的本地计算机上的扩展包文件。典型情况下扩展包安装在C:/Documents and Settings/All Users/Application Data/Microsoft/Schemas目录中,但是在未来的Office版本中可能改变这种情况。如果用户没有系统管理员权限,扩展包就安装在相应的每个用户的应用程序数据目录中(例如C:/Documents and Settings/Mike/Local Settings/Application Data/Microsoft/Schemas)。该XML扩展包中的用 true标记的文件不会被下载并安装在用户的计算机上,而是每次使用时从服务器位置上直接打开。

如果你已经在本地计算机上开发了一个解决方案,你会发现在把它复制到布署服务器之后,Office仍然在你的开发计算机上使用的路径中查找清单文件。我也碰到了这个问题,最后发现Office把解决方案的路径加密存储在称为Solution URL的自定义文档属性中了。你把这个文件布署到服务器之后,需要把这个属性改变为服务器路径或使用“XML扩展包”对话框从布署服务器上附加该扩展包(这样将重新设置该属性的值)。接着你可以把拥有解决方案文件的服务器路径Excel工作薄保存回服务器。

为了作为用户测试这种情形,你应该关闭工作薄,选择Data | XML | XML Expansion Packs(Word中在Tools | Templates and Add-Ins下面),并从本地计算机上删除ScheduleSmartDocument扩展包文件。这使得你的开发计算机看起来像一台正常的用户计算机,即不知道扩展包的任何信息。下一步,从服务器上打开电子表格。这时候会出现一个对话框,询问你是否希望下载XML扩展包,你应该点击“是”。Office将下载扩展包清单中列举的文件并把它们安装在本地计算机上。

XML扩展包的特性比我们的简单的解决方案需要的特性多一些。实际上它们可以用于从服务器到客户端分发任何类型的文件,甚至于可以被链接成“清单集合”,这会引起Word或Excel引用几个链接的清单,就好像它们是一个大的清单一样。

访问智能文档内容

为了能起到作用,你的智能文档操作DLL一般需要访问(并且可能修改)下层的Office文档。在例子中,它是Excel项目进程表电子表格。为了这个目的,ISmartDocument的几个方法为你下层文档提供了一个IDispatch COM接口指针。在Office上编写COM的人知道,Idispatch提供了进入Office对象模型的通道。在Visual Basic中,使用Idispatch和类型库是相当自动化的,但是在C++中稍微复杂一些。

起先我准备使用#import指令,它允许Visual C++为类型库中的所有接口生成ATL智能指针包装。但是要让它正确的编译需要做大量的工作,它常常提示有东西出错了。果真,我找到了知识库文章“Office Application Remains in Memory After Program Finishes”,它描述了在Office类型库中使用#import指令所遇到的知名的问题以及相应的建议。

最后,我决定建立自己的类CexcelWorkbook来包装需要的Excel方法。这个类继承自ATL模版CcomDispatchDriver,这使它相对容易通过IDispatch调用Excel对象模型上的方法。使用CcomDispatchDriver的方法GetIDOfName,你可以得到一个给定的属性或方法(例如,Excel对象模型中的ActiveSheet的范围)的DISPID。你一旦有了DISPID(为了效率更高,我在类中对它进行了缓冲处理),就可以使用某个其它的CcomDispatch方法(例如GetProperty或InvokeN,此处的N是参数的数量)访问对象模型中的属性和方法。注意Exce Range值是作为VBA属性(而不是方法)暴露的,但是它也需要一个参数(范围地址)。因为CcomDispatchDriver没有提供为GetProperty调用传递的参数的途径,我在自定义类(CExcelWorkbook)中实现了一个新的方法(GetProperty1),它用于处理这种情况。

调试智能文档DLL

一切都在预料之中,在我尝试建立智能文档操作DLL的时候也没有出现异常。我试图在Excel中附加一个XML大纲的时候,收到了一个错误信息“XML扩展包逻辑丢失或无效”。为了调试这个错误,我首先使用ListDLLs(http://www.sysinternals.com上的一个方便的工具)确定Excel是否载入了我的库,这样清单才能看起来足够好,它可以让Excel找到该DLL的。我退出Excel,接着修改了项目的Debugging属性(右键点击解决方案并选择“属性”)告诉Visual Studio使用Excel作为该DLL的EXE容器。我浏览Excel.exe并选择它作为Command值。

接着,我按下Ctrl-B打开“New Breakpoints”对话框,在Function字段中输入DllMain,当出现“disambiguate symbol”窗口(显然有两个重载的DllMain函数)的时候选择了它们两个,通过这些操作在DllMain中设置了一个断点。我的目的是当Excel第一次调用该DLL的时候得到控制权。接着我按下F5,Excel启动了。Visual Studio警告没有Excel符号,但是我早就知道了。我打开智能文档,使用Data | XML | XML Expansion Packs试图再次附加XML大纲。Visual Studio同ATL生成的DllMain中的断点一起出现了。

这个时候我的目标是确定DLL中是否有方法、哪些方法被调用了。我在自己的IsmartDocument接口实现中的所有方法上设置了断点,以确定它们其中的哪些被调用了。实际上,有几个方法被调用了,并且通过逐步运行我找到了一个普通的索引问题,传递到get_SmartDocXmlTypeName的控件索引是从1开始的,但是C++代码把它处理为从0开始的,因此对最后一个元素的调用返回了E_INVALIDARG。后来我给所有的接口方法的入口点添加了ATLTRACE2宏,使自己更容易知道正在调用什么、什么时候调用。

改变通知

对于示例解决方案,我需要知道用户什么时候选择了电子表格的数据项区域中的某行,这意味着他希望编辑该事务的相关信息。接着我从当前行中抓取信息并填充事务面板,允许他输入本周的工作和下一周的计划工作的相关信息。当他改变了事务面板中的某些东西的时候,我将使用Excel对象模型把新的信息复制回原工作表行。但是我如何知道什么时候选择了新的行?有一种比较复杂的解决方案,即使用Excel事件(在Visual Basic中容易,但是在C++中不是太容易),但是我发现这是没有必要的。在Excel中无论选择什么时候发生了改变,都会调用IsmartDocument接口方法,因此无论什么事情,你仅仅需要改变通知。通过更新每次改变后的进度表事务内部视图,我能够忽略行选择的变化。

安全性

因为宏病毒和其它的脚本技术的恶意使用变得很普遍,Office在两个途径做了修改:通过提高默认的安全性设置,防止运行大多数没有签名的、潜在的恶意代码;通过增加安全性设置的数量,为即使没有数字签名的解决方案的运行提供了更多的管理权限。这考虑了现实情况:很多Office解决方案(嵌入Word或Excel文档中的宏)都已经广泛的布署在大型组织中,要让它们完全安全将花费一定时间。如果新版本Office突然要求所有的解决方案必须有数组签名(理想的解决方案),这将给很多组织带来布署方面的障碍。但是,Office安全性设置的增加也使安全性更加复杂,在本文中我没有谈到这个主题。

应用于宏和插件程序的相同的Office安全性设置也可应用于智能文档。这些设置包括:

·用户宏的安全性设置(高、中或低,在Tools | Macro | Security中设置;默认的是高)。

·智能文档是否从可信(trusted)位置载入:可信的文件系统目录(例如每个用户的或工作组模版目录)、公司局域网上的Web服务器或可信的Internet站点。

·智能文档组件是否是数字签名的,如果是,发行人是否在Tools | Macro | Security | Trusted Publishers设置为可信发行人。

·是否在Tools | Macro | Security | Trusted Publishers中选择了“相信所有安装了的插件程序和模版”。

默认情况下,Tools | Macro | Security中的安全性层次被设置高,Office阻止任何没有数字签名的插件程序DLL(包含智能文档操作DLL)的载入。上面列举的安全性设置的其它组合可以允许智能文档被载入(可能给用户显示一个安全性提示),但是保证组织中的安全性的最好途径仍然是使用从VeriSign或GTE CyberTrust得到的数字证书对所有的已布署的解决方案(包括智能文档组件)进行数字签名。微软2003 智能文档SDK中的XMLSign.exe工具可以用于对XML智能文档清单文件进行数字签名。

此外,如果某个Office智能文档解决方案是从Web服务器上运行的,微软Internet Explorer和Office安全性设置都会影响解决方案是否能运行。如果服务器上的XML扩展包清单文件既不在Internet Explorer可信站点中,也不在局域网区域,就不会试图检索它,也不会给用户提示把该站点添加到Internet Explorer可信列表站点列表中。如果XML扩展包清单文件位于可信的服务器或局域网上,清单是否被载入依赖于它是否签名了。如果它是签名了的,并允许运行,它仍然受到用户Office安全性设置的约束。

在智能文档解决方案开发过程中,你可以通过编辑注册表的下述键下面的REG_DWORD值“DisableManifestSecurityCheck”激活或禁止XML扩展包清单文件安全性检查:

      
      HKEY_LOCAL_MACHINE/Software/Microsoft/Office/Common/Smart Tag


如果它的值为1将禁止XML扩展包清单文件安全性检查,如果值为0就激活了它。当你试图引用某个XML扩展包清单文件的时候,如果这个注册表设置是1(禁止安全性检查),将出现一个对话框警告用户禁止安全性是危险的。它同时提供一个“确定”按钮,这个按钮允许你立即重新激活XML扩展包安全性检查。你应该仅仅在开发解决方案的时候把开发计算机的注册表的这个值设置为1(这个时候每次编译每个组件后进行数字签名可能不方便),但是要确保在签名后的组件的最后测试中激活它。在用户计算机上禁止XML扩展包清单文件安全性检查是非常不可取的。

尽管本文讨论了在C++中智能文档操作DLL的建立,但是用Visual Basic .NET或C#建立受控操作DLL也是可行的。在那种情况下,将应用.NET框架组件安全性模型,但是这超出了本文讨论的范围。

生成状态报告

我的示例依据Excel对象模型使用Idispatch访问来自进度表智能文档工作表的信息以生成状态报告。我也可以选择开发一些C++代码来驱动Word对象模型建立状态文档,但是我选择了另一种途径,它也利用了Word 2003中的新XML特性的优点。我把状态报告生成为XML文件,并使用XSLT在Word中打开它以提供良好的格式化。这种方法利用了使用XSLT很容易把几个XML文件(可能是整个小组的状态报告)合并成一个大的报告的优点,如果它们作为Word文档保存就很难合并了。

在花费一段时间研究如何实现XML保存后,我决定定义一个类来管理将显示在状态报告上的数据项列表。当开发者更新进度表中的信息的时候,智能文档操作DLL中的代码就会建立并更新这个事务列表,这与文档操作工作面板中放置的控件对应。最后,开发者点击工作面板中的控件生成XML状态报告。每个类负责使用MSXML DOM正确地保持存储在XML中的信息。处理这种情行的处理方法是CScheduleTaskList和CScheduleTask。我用于格式化它的Word XSL样式表是ScheduleReport.xsl,它在示例代码中提供了。当你安装智能文档解决方案的时候,这个XLST文件就会被安装好,并让Word知道它,这样当你在Word中打开XML状态报告的时候,它将自动应用这个样式表。你也可以使用Word中的XML数据视图事务面板来选择这个样式表。

在我生成的XML中有一个指令值得提起:

      
      <?mso-application progid="Word.Document"?>


这条指令告诉Windows资源管理器这个XML文件的默认的应用程序是Word。你也可能注意到结果是这个文件的图标改变了。

让这个Word样式表精确地显示状态报告需要花一段时间,因此此处我没有提供详细信息。但是,我要警告你,如果你查看我提供的样式表,你会对显示的这个Word XML大纲的复杂程度感到惊奇。其中的很多是样板代码,很多都是在Word中建立示例状态文档(我希望通过这种方式设置样式和字体)并把该文档保存为XML格式的时候获得的。我把生成的XML中的大部分复制到自己的XSLT样式表中。但是,我不能确定这种办法生成的XML是在Word中生成良好格式化的状态报告所需要的最小的XML.

我可以根据自己的经验为开发XSLT提供一些建议。要记住XML是一种非常精确的、大小写敏感的语言。在这方面它与C++没有不同,但是如果出错了,你接收到的诊断信息可能是某种类型的“不能应用样式表”信息,而不是编译器提供的详细描述。如果你对某些已经在运行的东西做大范围的修改,应该逐步增加并做好版本控制,这样才能回滚到以前的修改去。我遇到了一个问题,即样式表引用了ScheduleSmartDocument名字空间,但是由于操作DLL中出了错误,在生成的XML状态报告中该名字空间的名称变成了ScheduleSmartDoc。结果当我在XML状态报告上应用该XSLT的时候,Word简单地显示了一个空文档,因为样式表中使用的XPATH与XML中的不匹配。

让Word自动地应用正确的样式表需要花点力气。最后,我让安装智能文档操作DLL的清单也安装该样式表。为了让Word知道这个样式表,清单中的解决方案条目必须含有context属性,它引用该Word XML名字空间(http://schemas.microsoft.com/office/word/2003/wordml)。

最后一步是把智能文档URI(示例中的是ScheduleSmartDocument)作为清单、样式表和生成的XML状态报告文档的XML名字空间。在例子中,我把ScheduleSmartDocument作为名字空间的名称。你应该根据组织中的唯一的URI来使用名字空间。

反向操作,从Word文档生成XML看起来是个好的办法。但是,幅面和切割结果的Word输出的时候要非常小心。例如,我试图减小列表部分的复杂性(它描述了文档中使用的格式列表),因为我的状态报告只需要一个简单的、一层的符号列表。在一个小时以后,我放弃了,留下大量的未改动的部分,虽然我确信其中的很多是我不需要的。

如果你在Visual Studio中打开一个生成好的Word XML文件,你会发现它对于Visual Studio .NET大纲查看器来说太复杂了。因此,我建议当XML打开的时候,首先点击“数据视图”,强制Visual Studio分解该XML,接着返回“XML视图”查看良好格式化的版本(它在Notepad中打开的时候,实际上是不可阅读的)。

我的最后一条建议是在调整样式表的时候可以使用一个非常强大的工具。MSXSL.EXE命令行工具允许你给XML文件应用样式表并查看生成的XML输出。你可以从链接http://msdn.microsoft.com/library/en-us/dnxml/html/msxsl.asp处下载它。

结论

我首先是在C++中开始开发操作DLL的,这是因为目前Office编码的太多示例都是用Visual Basic编写的。但是,使用C++执行一些甚至于很简单的事务(例如引用Excel中的一个单元值)也比使用Visual Basic明显复杂多了。图11显示了与Visual Basic中ActiveSheet.Range("A1").Value语句等同的C++代码(注意这个方法的示例代码有不同的版本,因为我用设置值方法把它重新编写为共享代码)。我提供了智能文档处理程序希望对Excel对象模型执行的少量操作的C++代码,(注意这个方法的示例代码有不同的版本,因为我用设置值方法把它重新编写为共享代码)。我提供了智能文档处理程序希望对Excel对象模型执行的少量操作的C++代码,但是我编写的示例根本没有因为使用C++而受益。我鼓励读者认真的考虑使用C++与Visual Basic之间的代价。

      
      C++中的ActiveSheet.Range("A1").Value
HRESULT CExcelWorkbook::HrCellValue(
    int iRow,
    int iColumn,
    CComVariant *pValue)
{
USES_CONVERSION;
HRESULT        hr;

    ATLASSERT(p);        // IDispatch必须在调用方法前设置
    if (pValue == NULL)
        return E_INVALIDARG;

    pValue->Clear();
    
    AssureDispidRange();

    TCHAR        tzRangeReference[20];

    ATLASSERT(iColumn<26);     // 支持的不能多于26个,

    // 为"CurrentRow"处理特定的值
    if (iRow == iCurrentRow && FAILED(hr=GetSelectedRow(iRow)))
        return hr;

    wsprintf(tzRangeReference, "%c%d", 'A'+(iColumn-1), iRow);

    CComVariant    varRangeName(T2COLE(tzRangeReference));
    CComVariant    varRange;

    if (SUCCEEDED(hr=GetProperty1(m_dispidRange, &varRangeName, 
                  &varRange)))
    {
        ATLASSERT(varRange.vt == VT_DISPATCH);
        ATLASSERT(varRange.pdispVal);
        LPDISPATCH            lpRange = varRange.pdispVal;

        CComDispatchDriver    dispRange(lpRange);

        AssureDispidRangeValue(dispRange);

        hr = dispRange.GetProperty(m_dispidRangeValue, pValue);
    }

    return hr;
}


Office 2003中的另一个选择是C#,它是有吸引力的,但是请你确保自己清楚了解非受控代码(Office)调用受控代码(用C#或Visual Basic .NET编写的操作处理程序部件)的性能问题。有可能使用Visual Basic是最好的办法。

智能文档为开发者提供了非常强大的用户界面范例。你可以在利用微软Office应用程序的所有功能的时候,同时提供无缝地集成到Office事务面板用户界面中的自定义UI和行为。我认为智能文档将很快在大型组织中广泛使用,以简化业务过程,如同目前使用的带有宏的电子表格一样。



图1.XML源



为了建立映射,你必须把元素从XML大纲中拖到工作薄上。对于类似Project Name和Developer的元素,这是很简单的事情,只需要把该XML元素名称从“XML源”事务面板上拖动到包含相关数据的工作薄单元上(例如,把ProjectName拖动到A2上)。对于分层显示的重复的进度表数据项,可以通过选择数据项把该组拖动到标题行(A5)上。你可以通过选择“XML源”事务面板中特定的元素(例如Description)来确认映射关系;工作薄中相应的部分应该高亮度显示。把结果工作薄保存为Project Schedule (Mapped).xls。在示例中,我将建立单个电子表格,或者你可以把进度表保存为Excel工作薄模版。这样的话,你就简化了用户建立该文档的新副本的过程,每个新的副本都将成为一个Excel智能文档。

ISmartDocument接口教程

下一步是建立智能文档操作DLL并安装它。在你编写智能文档操作处理程序DLL(它实现了ISmartDocument接口方法)之前,最好先了解一下这个接口。

当用户在文档中移动的时候,你的操作处理程序DLL将建立并管理一组出现在文档操作事务面板中的控件,允许你基于文档中的位置提供自定义的用户界面。映射到文档的XML大纲元素定义了用户在文档中的位置。XML元素应该指定给用户显示哪些控件。图2显示了你可以建立的控件类型。你将使用操作处理程序DLL中实现的ISmartDocument接口的方法建立这些控件。当这些控件被激活的时候(例如进入了文本框或者点击了某个按钮),Office通过ISmartDocument接口方法调用你的自定义操作处理程序DLL代码。

图2.智能文档控件类型



我把IsmartDocument接口的方法分为几类:配置方法,它为Office描述了建立在文档操作事务面板中的控件(图3);控件绘制时(draw-time)方法,它们作为控件调用并绘制在事务面板上(图4);修改通知方法,Office把它们作为文档操作事务面板控件调用,由用户维护(图5)。

图3. ISmartDocument接口配置方法



图4. ISmartDocument接口控件绘制方法





图5. ISmartDocument接口通知方法



注意,对于大多数DLL,你不会使用所有的控件类型或实现所有的IsmartDocument方法。例如,因为我的事务面板中没有ActiveX或列表框控件,在例子中就只有这些方法的样板实现。甚至于根本不编写任何操作处理程序DLL也可能利用一些简单的智能文档特性(例如提供超级链接或内容敏感性帮助文本)。作为代替的是你只需要在关联的Word或Excel智能文档中简单地提供一个XML文件,该文件描述了你希望与多种XML元素关联的操作。

图6摘录了ISmartDocument方法的参数。

图6. ISmartDocument通用方法参数



当Office载入你的操作DLL的时候,它首先调用图3所示的配置方法。Office首先调用SmartDocInitialize(允许你执行具体的一次性的初始化)。接着它将通过调用你的get_SmartDocXmlTypeCount实现查询文档中定义的拥有关联控件的XML元素的数量(文档中的XML元素没有关联控件的情况也是可以的)。在例子中,文档操作面板是空的,因此你可能希望为这些元素定义一些标准的帮助内容。你一旦告诉Office你对多少XML文档元素有兴趣后,它将查询每个元素的名称,这些名称是通过调用多个get_SmartDocXmlTypeName获得的。你必须返回XML元素的合格的名称(例如schema#element)。每个元素都有说明,当用户定位到这个元素中的时候,说明会出现在文档操作面板的标题中。这是通过调用get_SmartDocXmlTypeCaption返回的。对get_SmartDocXmlTypeName和 get_SmartDocXmlTypeCaption的调用是成对出现的,每个XML元素执行一次。下一步,Office希望知道每个元素的显示的事务面板中的控件信息。这些信息包括:

·get_ControlCount——给定的某个元素的控件数量。

·get_ControlID——允许你根据某个元素的控件索引(从1开始)给每个控件指定一个唯一的ID号。

·get_ControlNameFromID——在给定控件ID后返回控件的(非固定的)名称。这个名称没有显示,但是用于引用Word或Excel对象模型中的控件。

·get_ControlTypeFromID——给定控件ID后,返回控件的类型(从图2中)。

当用户在文档中移动的时候,Office用适当的控件更新文档操作事务面板,它接着在事务面板中绘制控件的时候调用控件绘制方法(图4所示)。首先它调用get_ControlCaptionFromID,允许你为控件提供一个说明。注意说明不是在配置的时候提供的,而是在显示的时候提供的,因此可以动态地修改它。Office不会绘制有空白的或NULL说明字符串的控件。这可能用于动态地隐藏和显示控件。为了隐藏一个控件,你只需要简单地为该控件从get_ControlCaptionFromID返回一个一个NULL或空字符串。

接着Office为每个控件调用该控件类型特定的填充方法(例如PopulateHelpContent)。你使用这些方法为控件提供内容,例如图像的位置或超级链接的文本。注意与控件关联的文档元素的XML和Text内容被传递到所有的填充方法。同时也要注意,C_TYPE_LABEL和C_TYPE_SEPARATOR类型的控件没有关联的填充方法。在所有的控件绘制了并且事务面板完成了以后,Office将调用OnPaneUpdateComplete。

当用户与事务面板中的控件交互操作的时候,Office调用图5显示的列表中的某个通知方法。标签和分隔符没有关联的通知方法。对于其它控件,通知方法表明要么状态改变了,要么被点击了,这都可能调用一个操作(例如超级链接或图像)。

注意,通知太多比太少更容易造成Office出错。例如,在Excel中的单元选择发生改变的时候(即使XML元素仍然没有变化),Office重新绘制文档操作工作面板并调用你的方法,这样在事务面板上你才拥有真正的动态内容。例如,你能够通过Excel对象模型查询被选择的单元的值并根据这个值更新控件。在示例代码中我利用了这个优点来更新事务面板中被选择的事务的信息,而不需要在文档操作DLL中操作Excel VBA事件。

属性包

大多数控件绘制方法都提供了ISmartDocProperties参数。这是一个名称/值对的属性组(继承自IPropertyBag),提供了控件上的附加信息。这是你指定控件的大小、字体、对齐方式等的地方。图7提供了应用于所有控件的关键的名称。属性值,包括整形属性的值,都是作为BSTR提供给的ISmartDocProperties::Write的。例如,PopulateTextboxContent下述代码行会把文本框控件的宽度设置为50个像素:

     
     Props->Write(CComBSTR("W"), CComBSTR("50"));


图7. 所用控件的ISmartDocProperties



拥有关联标签的控件(C_TYPE_TEXTBOX、C_TYPE_CHECKBOX)都有图8所示的附加属性以描述该控件的标签属性。注意这些只能影响该控件的标签,不会影响绘制控件的字体。最后,还有一些其它的属性只能应用于特定类型的控件。这些属性显示在图9中。

图8.控件标签的IsmartDocProperties



图9. 其它的ISmartDocProperties名称/值对



建立智能文档操作DLL

我决定在Visual C++中使用活动模版库(ATL)建立智能文档操作DLL。ATL是个很好的选择,因为它的覆盖区域比MFC小一些,但是它仍然提供了操作智能文档需要的大量方便的COM功能。但是,如果你更熟悉MFC,你也可以选择它。

从MSDN Online上下载Office 2003智能文档SDK之后,我启动Visual Studio .NET并建立了一个新的ATL类型的项目,接着使用Add | Class给项目添加一个ATL简单对象。这就是示例代码中的CScheduleSmartDoc。从该对象的类环境菜单(使用View | Class打开类资源管理器)中,我使用Add | Implement Interface让这个类实现ISmartDocument接口。如果在开发计算机上没有安装Office 2003,你必须把有Office 2003的计算机上的类型库复制到你的计算机上。你一般可以在C:/Program Files/Common Files/Microsoft Shared/Smart Tag/mstag.tlb中找到它,但是你要确保得到的是1.2版本(Office 2003版本)。接着你应该把我的示例代码和接口描述作为向导,实现你所需要的方法。

安装智能文档解决方案

Office使用一个XML文件(叫作XML扩展包列表文件)描述你的智能文档解决方案,它包含了更新和配置该解决方案的目录。典型情况下这个文件叫作manifest.xml。我在示例代码中添加了一个框架manifest.xml(图10所示),并用红色高亮度显示了你应该修改的东西。

·解决方案的ID。你应该使用GuidGen为智能文档解决方案生成一个唯一的GUID。这是强制性的,所有的解决方案都需要有一个唯一的ID,并且虽然没有严格的限制为GUID,但是这是确保唯一性的最好的方法。

·updateFrequency,它是Office应该多久(按分钟记)检查服务器的清单更新一次。更新都是由XML扩展包清单中指定的每个文件的版本标志控制的。如果版本不同,并且更新事件超期了,Office将把对应的新版本的文件从服务器下载到本地计算机上。

·智能文档操作DLL的文件路径。这可以是URL或网络路径(如果DLL布署在网络上)或与清单文件位置关联的路径或文件名称。

·智能文档操作DLL的CLSID。因为它实现了IsmartDocument COM接口,它就拥有一个Office用于载入它的COM CLSID。你可以在生成的用于实现ISmartDocument接口的ATL类的头文件中找到它。它是coclass(例子中的CScheduleSmartDoc)的uuid属性。

图10. XML清单文件

     
     <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<manifest xmlns="http://schemas.microsoft.com/office/xmlexpansionpacks/2003">
    <version>1.1</version>
    <updateFrequency>20160</updateFrequency>
    <uri>ScheduleSmartDocument</uri>
    <solution>
        <solutionID>{15960625-1612-46AB-877C-BBCB59503FCE}</solutionID>
        <type>smartDocument</type>
        <alias lcid="*">Simple Schedule Smart Document</alias>
        <targetApplication>Excel.Application.11</targetApplication>
        <file>
            <type>solutionActionHandler</type>
            <version>1.0</version>
            <filePath>ScheduleSmartDocument.dll</filePath>
            <CLSID>{0A9D54DE-12F8-4711-AF10-7D4ACA1D1924}</CLSID>
            <regsvr32/>
        </file>
        <file>
            <type>Other</type>
            <version>1.0</version>
            <filePath>ScheduleGraphic.GIF</filePath>
        </file> 
    </solution>
    <solution>
        <solutionID>{443E624C-AE85-4b20-8522-1CFD5B4E7CBC}</solutionID> 
        <alias>Schedule Smart Document XSL Transform</alias> 
        <type>Transform</type> 
        <context>http://schemas.microsoft.com/office/word/2003/wordml
            </context> 
        <file>
            <type>primaryTransform</type> 
            <version>1.0</version> 
            <filePath>ScheduleReport.xsl</filePath> 
        </file>
    </solution> 
</manifest>



如果你还需要随着解决方案安装一些附加的文件(例如GIF或包含附加文本的HTML文件),你可以把它们列举在 元素的下面,使用 元素分隔开。我只有出现在事务面板中的一张附加的GIF。

另一件阻碍了我几个小时的事情是清单中的 元素的名称必须与你用于映射文档元素的XML大纲的名称相同。在例子中是ScheduleSmartDocument。

一旦你的智能文档与XML大纲一起标识了,你就能附加一个XML扩展包,把操作处理程序DLL与该文档关联。在Excel中,通过Data | XML | XML Expansion Packs打开对话框。你使用这个对话框先添加少量的扩展包(执行解决方案的manifest.xml文件),接着给文档附加一个扩展包。这个过程与用XML标记文档相似,只有智能文档开发者需要这样做——用户不需要这个对话框。换句话说,你使用Add-Ins 对话框(Tools | Templates and Add-Ins)的“XML扩展包”选项卡给文档附加了一个扩展包。

当用户打开拥有关联扩展包的文档的时候,Office将自动安装或更新他们的本地计算机上的扩展包文件。典型情况下扩展包安装在C:/Documents and Settings/All Users/Application Data/Microsoft/Schemas目录中,但是在未来的Office版本中可能改变这种情况。如果用户没有系统管理员权限,扩展包就安装在相应的每个用户的应用程序数据目录中(例如C:/Documents and Settings/Mike/Local Settings/Application Data/Microsoft/Schemas)。该XML扩展包中的用 true标记的文件不会被下载并安装在用户的计算机上,而是每次使用时从服务器位置上直接打开。

如果你已经在本地计算机上开发了一个解决方案,你会发现在把它复制到布署服务器之后,Office仍然在你的开发计算机上使用的路径中查找清单文件。我也碰到了这个问题,最后发现Office把解决方案的路径加密存储在称为Solution URL的自定义文档属性中了。你把这个文件布署到服务器之后,需要把这个属性改变为服务器路径或使用“XML扩展包”对话框从布署服务器上附加该扩展包(这样将重新设置该属性的值)。接着你可以把拥有解决方案文件的服务器路径Excel工作薄保存回服务器。

为了作为用户测试这种情形,你应该关闭工作薄,选择Data | XML | XML Expansion Packs(Word中在Tools | Templates and Add-Ins下面),并从本地计算机上删除ScheduleSmartDocument扩展包文件。这使得你的开发计算机看起来像一台正常的用户计算机,即不知道扩展包的任何信息。下一步,从服务器上打开电子表格。这时候会出现一个对话框,询问你是否希望下载XML扩展包,你应该点击“是”。Office将下载扩展包清单中列举的文件并把它们安装在本地计算机上。

XML扩展包的特性比我们的简单的解决方案需要的特性多一些。实际上它们可以用于从服务器到客户端分发任何类型的文件,甚至于可以被链接成“清单集合”,这会引起Word或Excel引用几个链接的清单,就好像它们是一个大的清单一样。

访问智能文档内容

为了能起到作用,你的智能文档操作DLL一般需要访问(并且可能修改)下层的Office文档。在例子中,它是Excel项目进程表电子表格。为了这个目的,ISmartDocument的几个方法为你下层文档提供了一个IDispatch COM接口指针。在Office上编写COM的人知道,Idispatch提供了进入Office对象模型的通道。在Visual Basic中,使用Idispatch和类型库是相当自动化的,但是在C++中稍微复杂一些。

起先我准备使用#import指令,它允许Visual C++为类型库中的所有接口生成ATL智能指针包装。但是要让它正确的编译需要做大量的工作,它常常提示有东西出错了。果真,我找到了知识库文章“Office Application Remains in Memory After Program Finishes”,它描述了在Office类型库中使用#import指令所遇到的知名的问题以及相应的建议。

最后,我决定建立自己的类CexcelWorkbook来包装需要的Excel方法。这个类继承自ATL模版CcomDispatchDriver,这使它相对容易通过IDispatch调用Excel对象模型上的方法。使用CcomDispatchDriver的方法GetIDOfName,你可以得到一个给定的属性或方法(例如,Excel对象模型中的ActiveSheet的范围)的DISPID。你一旦有了DISPID(为了效率更高,我在类中对它进行了缓冲处理),就可以使用某个其它的CcomDispatch方法(例如GetProperty或InvokeN,此处的N是参数的数量)访问对象模型中的属性和方法。注意Exce Range值是作为VBA属性(而不是方法)暴露的,但是它也需要一个参数(范围地址)。因为CcomDispatchDriver没有提供为GetProperty调用传递的参数的途径,我在自定义类(CExcelWorkbook)中实现了一个新的方法(GetProperty1),它用于处理这种情况。

调试智能文档DLL

一切都在预料之中,在我尝试建立智能文档操作DLL的时候也没有出现异常。我试图在Excel中附加一个XML大纲的时候,收到了一个错误信息“XML扩展包逻辑丢失或无效”。为了调试这个错误,我首先使用ListDLLs(http://www.sysinternals.com上的一个方便的工具)确定Excel是否载入了我的库,这样清单才能看起来足够好,它可以让Excel找到该DLL的。我退出Excel,接着修改了项目的Debugging属性(右键点击解决方案并选择“属性”)告诉Visual Studio使用Excel作为该DLL的EXE容器。我浏览Excel.exe并选择它作为Command值。

接着,我按下Ctrl-B打开“New Breakpoints”对话框,在Function字段中输入DllMain,当出现“disambiguate symbol”窗口(显然有两个重载的DllMain函数)的时候选择了它们两个,通过这些操作在DllMain中设置了一个断点。我的目的是当Excel第一次调用该DLL的时候得到控制权。接着我按下F5,Excel启动了。Visual Studio警告没有Excel符号,但是我早就知道了。我打开智能文档,使用Data | XML | XML Expansion Packs试图再次附加XML大纲。Visual Studio同ATL生成的DllMain中的断点一起出现了。

这个时候我的目标是确定DLL中是否有方法、哪些方法被调用了。我在自己的IsmartDocument接口实现中的所有方法上设置了断点,以确定它们其中的哪些被调用了。实际上,有几个方法被调用了,并且通过逐步运行我找到了一个普通的索引问题,传递到get_SmartDocXmlTypeName的控件索引是从1开始的,但是C++代码把它处理为从0开始的,因此对最后一个元素的调用返回了E_INVALIDARG。后来我给所有的接口方法的入口点添加了ATLTRACE2宏,使自己更容易知道正在调用什么、什么时候调用。

改变通知

对于示例解决方案,我需要知道用户什么时候选择了电子表格的数据项区域中的某行,这意味着他希望编辑该事务的相关信息。接着我从当前行中抓取信息并填充事务面板,允许他输入本周的工作和下一周的计划工作的相关信息。当他改变了事务面板中的某些东西的时候,我将使用Excel对象模型把新的信息复制回原工作表行。但是我如何知道什么时候选择了新的行?有一种比较复杂的解决方案,即使用Excel事件(在Visual Basic中容易,但是在C++中不是太容易),但是我发现这是没有必要的。在Excel中无论选择什么时候发生了改变,都会调用IsmartDocument接口方法,因此无论什么事情,你仅仅需要改变通知。通过更新每次改变后的进度表事务内部视图,我能够忽略行选择的变化。

安全性

因为宏病毒和其它的脚本技术的恶意使用变得很普遍,Office在两个途径做了修改:通过提高默认的安全性设置,防止运行大多数没有签名的、潜在的恶意代码;通过增加安全性设置的数量,为即使没有数字签名的解决方案的运行提供了更多的管理权限。这考虑了现实情况:很多Office解决方案(嵌入Word或Excel文档中的宏)都已经广泛的布署在大型组织中,要让它们完全安全将花费一定时间。如果新版本Office突然要求所有的解决方案必须有数组签名(理想的解决方案),这将给很多组织带来布署方面的障碍。但是,Office安全性设置的增加也使安全性更加复杂,在本文中我没有谈到这个主题。

应用于宏和插件程序的相同的Office安全性设置也可应用于智能文档。这些设置包括:

·用户宏的安全性设置(高、中或低,在Tools | Macro | Security中设置;默认的是高)。

·智能文档是否从可信(trusted)位置载入:可信的文件系统目录(例如每个用户的或工作组模版目录)、公司局域网上的Web服务器或可信的Internet站点。

·智能文档组件是否是数字签名的,如果是,发行人是否在Tools | Macro | Security | Trusted Publishers设置为可信发行人。

·是否在Tools | Macro | Security | Trusted Publishers中选择了“相信所有安装了的插件程序和模版”。

默认情况下,Tools | Macro | Security中的安全性层次被设置高,Office阻止任何没有数字签名的插件程序DLL(包含智能文档操作DLL)的载入。上面列举的安全性设置的其它组合可以允许智能文档被载入(可能给用户显示一个安全性提示),但是保证组织中的安全性的最好途径仍然是使用从VeriSign或GTE CyberTrust得到的数字证书对所有的已布署的解决方案(包括智能文档组件)进行数字签名。微软2003 智能文档SDK中的XMLSign.exe工具可以用于对XML智能文档清单文件进行数字签名。

此外,如果某个Office智能文档解决方案是从Web服务器上运行的,微软Internet Explorer和Office安全性设置都会影响解决方案是否能运行。如果服务器上的XML扩展包清单文件既不在Internet Explorer可信站点中,也不在局域网区域,就不会试图检索它,也不会给用户提示把该站点添加到Internet Explorer可信列表站点列表中。如果XML扩展包清单文件位于可信的服务器或局域网上,清单是否被载入依赖于它是否签名了。如果它是签名了的,并允许运行,它仍然受到用户Office安全性设置的约束。

在智能文档解决方案开发过程中,你可以通过编辑注册表的下述键下面的REG_DWORD值“DisableManifestSecurityCheck”激活或禁止XML扩展包清单文件安全性检查:

     
     HKEY_LOCAL_MACHINE/Software/Microsoft/Office/Common/Smart Tag


如果它的值为1将禁止XML扩展包清单文件安全性检查,如果值为0就激活了它。当你试图引用某个XML扩展包清单文件的时候,如果这个注册表设置是1(禁止安全性检查),将出现一个对话框警告用户禁止安全性是危险的。它同时提供一个“确定”按钮,这个按钮允许你立即重新激活XML扩展包安全性检查。你应该仅仅在开发解决方案的时候把开发计算机的注册表的这个值设置为1(这个时候每次编译每个组件后进行数字签名可能不方便),但是要确保在签名后的组件的最后测试中激活它。在用户计算机上禁止XML扩展包清单文件安全性检查是非常不可取的。

尽管本文讨论了在C++中智能文档操作DLL的建立,但是用Visual Basic .NET或C#建立受控操作DLL也是可行的。在那种情况下,将应用.NET框架组件安全性模型,但是这超出了本文讨论的范围。

生成状态报告

我的示例依据Excel对象模型使用Idispatch访问来自进度表智能文档工作表的信息以生成状态报告。我也可以选择开发一些C++代码来驱动Word对象模型建立状态文档,但是我选择了另一种途径,它也利用了Word 2003中的新XML特性的优点。我把状态报告生成为XML文件,并使用XSLT在Word中打开它以提供良好的格式化。这种方法利用了使用XSLT很容易把几个XML文件(可能是整个小组的状态报告)合并成一个大的报告的优点,如果它们作为Word文档保存就很难合并了。

在花费一段时间研究如何实现XML保存后,我决定定义一个类来管理将显示在状态报告上的数据项列表。当开发者更新进度表中的信息的时候,智能文档操作DLL中的代码就会建立并更新这个事务列表,这与文档操作工作面板中放置的控件对应。最后,开发者点击工作面板中的控件生成XML状态报告。每个类负责使用MSXML DOM正确地保持存储在XML中的信息。处理这种情行的处理方法是CScheduleTaskList和CScheduleTask。我用于格式化它的Word XSL样式表是ScheduleReport.xsl,它在示例代码中提供了。当你安装智能文档解决方案的时候,这个XLST文件就会被安装好,并让Word知道它,这样当你在Word中打开XML状态报告的时候,它将自动应用这个样式表。你也可以使用Word中的XML数据视图事务面板来选择这个样式表。

在我生成的XML中有一个指令值得提起:

     
     <?mso-application progid="Word.Document"?>


这条指令告诉Windows资源管理器这个XML文件的默认的应用程序是Word。你也可能注意到结果是这个文件的图标改变了。

让这个Word样式表精确地显示状态报告需要花一段时间,因此此处我没有提供详细信息。但是,我要警告你,如果你查看我提供的样式表,你会对显示的这个Word XML大纲的复杂程度感到惊奇。其中的很多是样板代码,很多都是在Word中建立示例状态文档(我希望通过这种方式设置样式和字体)并把该文档保存为XML格式的时候获得的。我把生成的XML中的大部分复制到自己的XSLT样式表中。但是,我不能确定这种办法生成的XML是在Word中生成良好格式化的状态报告所需要的最小的XML.

我可以根据自己的经验为开发XSLT提供一些建议。要记住XML是一种非常精确的、大小写敏感的语言。在这方面它与C++没有不同,但是如果出错了,你接收到的诊断信息可能是某种类型的“不能应用样式表”信息,而不是编译器提供的详细描述。如果你对某些已经在运行的东西做大范围的修改,应该逐步增加并做好版本控制,这样才能回滚到以前的修改去。我遇到了一个问题,即样式表引用了ScheduleSmartDocument名字空间,但是由于操作DLL中出了错误,在生成的XML状态报告中该名字空间的名称变成了ScheduleSmartDoc。结果当我在XML状态报告上应用该XSLT的时候,Word简单地显示了一个空文档,因为样式表中使用的XPATH与XML中的不匹配。

让Word自动地应用正确的样式表需要花点力气。最后,我让安装智能文档操作DLL的清单也安装该样式表。为了让Word知道这个样式表,清单中的解决方案条目必须含有context属性,它引用该Word XML名字空间(http://schemas.microsoft.com/office/word/2003/wordml)。

最后一步是把智能文档URI(示例中的是ScheduleSmartDocument)作为清单、样式表和生成的XML状态报告文档的XML名字空间。在例子中,我把ScheduleSmartDocument作为名字空间的名称。你应该根据组织中的唯一的URI来使用名字空间。

反向操作,从Word文档生成XML看起来是个好的办法。但是,幅面和切割结果的Word输出的时候要非常小心。例如,我试图减小列表部分的复杂性(它描述了文档中使用的格式列表),因为我的状态报告只需要一个简单的、一层的符号列表。在一个小时以后,我放弃了,留下大量的未改动的部分,虽然我确信其中的很多是我不需要的。

如果你在Visual Studio中打开一个生成好的Word XML文件,你会发现它对于Visual Studio .NET大纲查看器来说太复杂了。因此,我建议当XML打开的时候,首先点击“数据视图”,强制Visual Studio分解该XML,接着返回“XML视图”查看良好格式化的版本(它在Notepad中打开的时候,实际上是不可阅读的)。

我的最后一条建议是在调整样式表的时候可以使用一个非常强大的工具。MSXSL.EXE命令行工具允许你给XML文件应用样式表并查看生成的XML输出。你可以从链接http://msdn.microsoft.com/library/en-us/dnxml/html/msxsl.asp处下载它。

结论

我首先是在C++中开始开发操作DLL的,这是因为目前Office编码的太多示例都是用Visual Basic编写的。但是,使用C++执行一些甚至于很简单的事务(例如引用Excel中的一个单元值)也比使用Visual Basic明显复杂多了。图11显示了与Visual Basic中ActiveSheet.Range("A1").Value语句等同的C++代码(注意这个方法的示例代码有不同的版本,因为我用设置值方法把它重新编写为共享代码)。我提供了智能文档处理程序希望对Excel对象模型执行的少量操作的C++代码,(注意这个方法的示例代码有不同的版本,因为我用设置值方法把它重新编写为共享代码)。我提供了智能文档处理程序希望对Excel对象模型执行的少量操作的C++代码,但是我编写的示例根本没有因为使用C++而受益。我鼓励读者认真的考虑使用C++与Visual Basic之间的代价。

     
     C++中的ActiveSheet.Range("A1").Value
HRESULT CExcelWorkbook::HrCellValue(
    int iRow,
    int iColumn,
    CComVariant *pValue)
{
USES_CONVERSION;
HRESULT        hr;

    ATLASSERT(p);        // IDispatch必须在调用方法前设置
    if (pValue == NULL)
        return E_INVALIDARG;

    pValue->Clear();
    
    AssureDispidRange();

    TCHAR        tzRangeReference[20];

    ATLASSERT(iColumn<26);     // 支持的不能多于26个,

    // 为"CurrentRow"处理特定的值
    if (iRow == iCurrentRow && FAILED(hr=GetSelectedRow(iRow)))
        return hr;

    wsprintf(tzRangeReference, "%c%d", 'A'+(iColumn-1), iRow);

    CComVariant    varRangeName(T2COLE(tzRangeReference));
    CComVariant    varRange;

    if (SUCCEEDED(hr=GetProperty1(m_dispidRange, &varRangeName, 
                  &varRange)))
    {
        ATLASSERT(varRange.vt == VT_DISPATCH);
        ATLASSERT(varRange.pdispVal);
        LPDISPATCH            lpRange = varRange.pdispVal;

        CComDispatchDriver    dispRange(lpRange);

        AssureDispidRangeValue(dispRange);

        hr = dispRange.GetProperty(m_dispidRangeValue, pValue);
    }

    return hr;
}


Office 2003中的另一个选择是C#,它是有吸引力的,但是请你确保自己清楚了解非受控代码(Office)调用受控代码(用C#或Visual Basic .NET编写的操作处理程序部件)的性能问题。有可能使用Visual Basic是最好的办法。

智能文档为开发者提供了非常强大的用户界面范例。你可以在利用微软Office应用程序的所有功能的时候,同时提供无缝地集成到Office事务面板用户界面中的自定义UI和行为。我认为智能文档将很快在大型组织中广泛使用,以简化业务过程,如同目前使用的带有宏的电子表格一样。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值