ASV封装
2008-03-06
从http://www.kevlindev.com/dom/asv_sharp/index.htm下载并翻译
I.
介绍
II.
ASV3Sharp
的实现
- System.Type 类
- 调用方法
- 取得属性值
- 设置属性值
- ASVObject
- ASV3Sharp
III.
安装
- DLL动态库
- 在窗口工具箱中添加ASV控件
- ActiveX封装动态库
IV.
使用
- 调用方法和设置属性
- 事件处理器
- 从C#调用ECMAScript函数
- 从ECMAScript 调用C#方法
V.
已知问题
VI.
修改记录
- 版本 1.2
- 版本1.1
- 版本1.0
VII.
下载
- 库文件
- 源代码
- 演示程序
介绍
Adobe
已经以ActiveX组件的方式实现了其SVG查看器. 这就给Windows开发人员提供了在自己的应用程序中内置SVG查看器的良机. 本文介绍如何在C#应用程序中使用SVG控件.
不幸的是, 最新的Adobe查看器(aka. ASV), 3.0版本, 仅仅提供了SVGWindow接口. 如果需要操作实际的SVG DOM本身, 你需要使用C#的反射功能. Stefan Goessner在其站点上对此有详尽的介绍,并提供了代码. 顺便提一下这种方法能够兼容最终的3.0版本. 实际上本文介绍的库也将使用这种方法. 同时, Jim Longson实现了一个令人印象深刻的例子(需要Yahoo SVG开发人员组的成员身份下载), 该示例对我有很大帮助. 关于内置SVG控件的更多资料可以在SVG Wiki中找到.
我曾经编写了相当多与ASV相关的ECMAScript代码, 并习惯使用ECMAScript提供的SVG DOM API. 因此对C#中缺省不提供这些API感到很失望. 幸运的是, 之前我在一个IDL解释器上花费过一些时间, 尽管这个解释器还没有完善, 但已经足够用于生成有用的SVG库. 通过该解释器, 我生成了一系列封装类, 可以使C#开发人员在SVG控件中使用SVG DOM API. 本文接下来讨论这些类是如何实现的, 以及在自己的C#项目中如何使用这些类.
声明: 我刚刚开始使用C#和.NET开发. 因此对该库很可能有更好的实现方法. 如果你发现了某些错误, 或者有些建议, 请马上与我联系以便对代码进行改进.
ASV3Sharp
的实现
本章讨论ASV3Sharp库的底层,读者会对如何实现SVG DOM API调用有一个更好的了解. 这些知识对如何绕过ASV3Sharp调用很有帮助.
根据Adobe最新的支持文档中描述的, ASV控件提供了SVGWindow接口. 该接口对控件中显示的SVG文档提供了足够的控制方法. 同时, 你可以使用postURL, getURL等方法. svgDocument是DOM开发人员使用最多的接口之一, 但是, 如前所述, 该接口并没有提供给开发人员. 幸运的是, 反射机制可以让我们调用其方法, 前提是我们必须知道方法名称以及需要提供给方法的参数.
System.Type
类
如果仔细阅读了Visual Studio .NET文档, 会发现一个叫做System.Type的类. 通过该类可以在运行时调用方法, 取得或设置属性. (注意 通过该类可以完成更多的工作, 不过这里仅仅介绍我们感兴趣的部分) 以字符串的形式提供方法和属性名, 并提供所需的参数, 这种机制被称为反射. 我们仅仅使用一个叫做InvokeMember方法, 就能够完成调用方法, 取得或设置属性的操作. 通过一个BindingFlags类型的参数能够指定我们想要执行的哪一种操作.
调用方法
现在来看一下调用方法的例子. 我们假设SVG控件已经被创建, 初始化, 分配给ASV3Control并显示了一个SVG文档.
AxSVGACTIVEXLib.AxSVGCtl svgWindow = ASV3Control.getWindow();
object svgDocument = svgWindow.getDocument();
Type type = svgDocument.GetType();
object svgRoot = type.InvokeMember(
"getRootElement",
BindingFlags.InvokeMethod,
null,
svgDocument,
null);
第一我们注意到对通过该控件取得的所有对象都使用最一般的类型, object. 第二我们要取得与对象相关的Type对象, 在本例中为SVGDocument. 通过Type对象中的InvokeMember方法可以调用控制对象中的方法, 或取得/设置控制对象中的属性. InvokeMember参数中第一个为控制对象的方法或属性名称, 第二个参数决定对控制对象进行何种操作. 对我们而言第三个参数永远设为null. 第四个参数为控制对象, 最后是一个对象数组, 以传递需要的参数.
本例中调用svgDocument对象的getRootElement()方法, 并返回当前控件显示的SVG文档中最上层的SVG元素.
取得属性值
下一个例子演示如何通过访问属性取得SVG根节点元素.
AxSVGACTIVEXLib.AxSVGCtl svgWindow = ASV3Control.getWindow();
object svgDocument = svgWindow.getDocument();
Type type = svgDocument.GetType();
object svgRoot = type.InvokeMember(
"rootElement",
BindingFlags.GetProperty,
null,
svgDocument,
null);
几乎与上一个例子一模一样, 唯一的区别是BindingFlag类型改为GetProperty以便指定取得属性. 返回的还是当前控件显示的SVG文档中最上层的SVG元素.
设置属性值
下一个例子演示如何设置当前控件显示的SVG文档的currentScale属性.
type = svgRoot.GetType();
type.InvokeMember(
"currentScale",
BindingFlags.SetProperty,
null,
svgRoot,
object[] {2});
这次我们使用BindingFlag的类型为SetProperty以指定设置属性. 最后一个参数说明了如何在调用方法或设置属性时传递参数. 该参数必须为对象数组, 因此即使只有一个参数也需要创建一个数组进行传递.
ASVObject
对InvokeMember的调用相当简单, 但对我来说在文字上太繁复了. 因此我创建了个新类ASVObject以简化方法调用以及属性取得或设置.下面是使用ASVObject类后改写的以上示例代码.
AxSVGACTIVEXLib.AxSVGCtl svgWindow = ASV3Control.getWindow();
ASVObject svgDocument = new ASVObject(svgWindow.getDocument());
ASVObject svgRoot = new ASVObject(svgDocument.GetProperty("rootElement"));
svgRoot.SetProperty("currentScale", 2);
因为要将控件返回的对象全部封装为ASVObject, 因此编程人员需要增加一些开销. 但是, 一旦使用ASVObject, 代码变得更加简单明了. 你可以使用InvokeMethod(方法名, 参数1, 参数2, …)的方式调用方法, 可以通过GetProperty(属性名)以及SetProperty(属性名, 属性值)来取得或设置属性.
在调用的方法需要多个参数时, 可以简单的将参数列在方法名后, 下面是一个例子:
svgRoot.InvokeMethod(
"insertBefore", someSVGNode, svgRoot.GetProperty("firstChild"));
ASVObject
将在调用Type对象的InvokeMember方法前把所有参数封装到一个对象数组中.
ASV3Sharp
既便ASVObject简化了代码, 但它并不能防止传递错误参数. 例如, 它无法阻止你给Application对象设置currentScale属性. 当然, 这个属性是无效的, ASV将(或者应该)产生错误, 但更好的办法是在编译时发现这些错误, 而不是在运行时. 这就是为什么要引入ASV3Sharp库.
我以C#接口和C#类两种方式重建了SVG IDL接口层次. 所有的C#类从ASVObject派生. IDL接口相关的所有的方法和属性被包括在C#类中. 请注意由于IDL接口的多重继承特点, 所以我在每个类中包含了所有父类的方法和属性. 这很可能并不是设计SVG类层次的最理想方式, 但由于我们仅仅需要封装ASV, 因此我认为这些重复不会带来任何问题.
现在看一下如何通过ASV3Sharp实现上面的缩放示例.
using KevLinDev.ASV3;
ASVWindow window = new ASVWindow( ASV3Control );
SVGDocument svgDocument = window.svgDocument;
SVGSVGElement svgRoot = svgDocument.rootElement;
svgRoot.currentScale = 2;
我们在一开始包含了ASV3名字空间, 以便缩短类型引用.在将控件封装在一个ASVWindow对象中后, 我们就可以象直接使用ASV API一样进行操作了.
最终, ASV3Sharp会调用Type对象的InvokeMember方法, 但对开发人员隐藏了其细节实现. 这就给开发人员可以直接使用SVG DOM API的错觉. 封装类可以引发编译器的在编译时的类型检查, 从而避免了潜在的运行时错误. 还有, 大多数的返回值被类型映射为有效的类型(与返回object类型形成鲜明对比).
安装
在开始使用ASV3Sharp库之前, 需要对Visual Studio项目进行一些设置.
动态库
首先, 需要在项目中包含ASV3Sharp.dll. 在项目解决方案资源管理器中右键引用, 从右键菜单中选择添加引用, 单击浏览.., 选择ASV3Sharp.dll, 最后单击完成. 现在应当在引用目录中看到ASV3Sharp.
在Windows Form 工具箱中添加ASV控件
如果从未在C#项目中使用过ASV控件, 则需要在Windows Forms工具箱中进行添加. 在设计模式下打开一个窗口, 在Windows Forms工具箱的任何地方单击右键, 选择 “选择项”, 在COM组件中选择SVG Document, 单击完成. 现在应在工具箱中看到一个名为SVG Document的新栏目. 如果在COM组件中没有找到SVG Document, 则需要安装Adobe SVG Viewer 3.0.
ActiveX
封装动态库
ASV3Sharp
库使用一系列ActiveX封装动态库, 以便C#与ASV3进行交互. Visual Studio会在向窗口添加ASV控件时自动生成这些封装动态库. 你可以在不需要时删除ASV控件, 封装动态库将保留在引用目录中.
对使用命令行创建C#项目的情况, 需要通过以下命令手工生成ASV ActiveX封装动态库.
aximp "C:/WINNT/system32/Adobe/SVG Viewer 3.0/NPSVG3.dll"
该命令在命令执行目录生成两个封装动态库, AxInterop.SVGACTIVEXLib.dll和Interop.SVGACTIVEXLib.dll. 确保在命令行生成你的项目时包含这两个动态库, 以及ASV3Sharp.dll.
好了, 我们现在可以开始使用ASV3Sharp动态库了.
使用
方法调用和属性
在前面章节中通过示例讨论过这些问题, 所以在此简单的提一下.如果一切顺利, 你可以通过SVG IDL(以及所有相关IDL)查找到某个方法调用或属性, 然后象使用其它C#方法和属性一样进行操作. 下面例子中创建ASVWindow对象, 然后定位到SVG根元素, 并更改其缩放属性值.
using KevLinDev.ASV3;
ASVWindow window = new ASVWindow( ASV3Control );
SVGDocument svgDocument = window.svgDocument;
SVGSVGElement svgRoot = svgDocument.rootElement;
svgRoot.currentScale = 2;
事件处理器
在我看来, 这才是有趣事情的开始. 这就是使用C#对象作为事件处理器是可行的. 例如, 下面的代码中先找到一个ID为"clickMe"的元素, 并为其设置一个事件处理器(该处理器为当前C#对象). 在clickMe元素上单击鼠标则会触发对象的handleEvent方法. 结果是给用户显示一个消息框.
using KevLinDev.ASV3;
public void setEventHandler() {
ASVWindow window = new ASVWindow( ASV3Control );
SVGDocument svgDocument = window.svgDocument;
SVGRectElement rect = new SVGRectElement(
svgDocument.getElementById("clickMe").Object);
rect.addEventListener("mousedown", this, false);
}
public void handleEvent(object evt) {
MouseEvent e = new MouseEvent(evt);
MessageBox.Show( "clicked at (" + e.clientX + "," + e.clientY + ")" );
}
该例子也暴露了库的局限性和以及一个奇特的特点.首先注意rect变量的定义.可以看到在SVGRectElement 的构造函数中调用了getElementById(), 这里的风险在于, 根据IDL定义 getElementById()返回一个元素对象, 但addEventListener并没有在元素接口中定义. 我们只好假定返回的是一个矩形对象, 从而创建了一个新的SVGRectElement对象, 当然我们的假设很可能是错误的.
ASVObject
类中隐藏的细节是一个Object类型的属性. 该属性为ASV控制返回对象的引用. 所有的ASVObject派生类的构造函数都需要该对象, 这就是我们去取得它的原因.
很大程度上, 这里最有可能是我缺乏C#知识的一个表现. 最开始我创建了一个单独的构造函数,并将ASVObject作为一个参数. 不幸的是, 只有原始的构造函数被调用(使用object作为其参数), 我不知道如何解决这个问题. 此外, 我觉得用类型转换会比构造函数更好, 但想不出一个简便的方法, 去定义显式类型处理器, 而且能够避免所有可能的类型组合的组合爆炸. 还是那句话, 我当前的知识有限.
下一个问题是不可见的. 它与addEventListener的调用有关. 其中第二个参数期望一个实现了IEventListener C#接口的对象. 好了, 为了处理ASVObject隐藏的奥秘, 所有的接口从IASVObject接口. 所有的IASVObject派生类必须定义一个Object属性. 因为在addEventListener使用了当前对象(this), 我们必须保证,1) 当前对象包含并实现了IeventListener 2) 定义一个Object属性(因为IEventListener从IASVObject派生). 在这种情况下, Object属性简单的返回其自身. (接口中定义了属性, 这点比较奇怪 –译者)
最后一个奇怪的地方是handleEvent. 在理想情况下, handleEvent应当接收一个Event对象, 或者Event对象的子类. 然而ASV3Sharp事件对象是一个封装对象, 在ASV中发送给handleEvent一个真正的事件对象, 所以我们不得不通过该对象创建一个封装对象. 一旦得到了封装对象, 我们就可以使用标准的API调用了.(就象例子中的clientX 和 clientY)
提示: 使用C#对象作为事件处理器可以使用ASV脚本引擎或者Microsoft脚本引擎.
从C#调用ECMAScript 函数
从C#调用ECMAScript函数使用到ASVObject的助理方法. 在ECMAScript中的最顶层的方法实际上是SVGWindow对象的属性. 下面的代码调用了SVG文档中的sayHello()函数, 并将"Billy"字符串作为唯一的参数传递. 正象前面提到的, 在函数调用时需要多少参数就可以传递多少参数.
using KevLinDev.ASV3;
ASVWindow svgWindow = new ASVWindow( ASV3Control );
string result = (string) svgWindow.InvokeMethod("sayHello", "Billy");
MessageBox.Show("C#: result = " + result);
"result"
变量将保存ECMAScript sayHello()函数的返回值. 提醒一下, InvokeMethod使用了Type类的方法, 因此返回的是一个一般对象. 在例子中, 我们假设sayHello()函数返回一个字符串, 因此将返回值强制类型转换到一个字符串.
提示: 使用ASV脚本引擎无法在SVG文档中实现这个功能.
从ECMAScript调用C#函数
尽管还不完善, 但还是能够实现从ECMAScript调用C#方法. 在C#中一切都是对象, 如果希望调用一个具体的方法, 则需要得到一个对象的实例. 因此要ECMAScript能够访问到希望调用的对象, 必须在SVG window对象中设置一个属性指向该对象.
using KevLinDev.ASV3;
ASVWindow svgWindow = new ASVWindow( ASV3Control );
svgWindow.SetProperty("csDocument", this);
这就是要点. 为了实现这个功能, 需要在SVG文档ECMAScript代码中定义一个名为csDocument的属性. (在实际演示程序中名为csharp –译者). 同时,我没能发现在C#中创建新ECMAScript对象属性的方法.
为了调用C#对象方法, 例如sayHello(), 我们需要如下的ECMAScript代码
var csDocument;
function callCSharp() {
if ( window.csDocument != null )
csDocument.sayHello("Johnny");
}
我在此又一次遇到知识匮乏的问题. 我能够成功的从ECMAScript中调用C#中Form对象的方法, 然而我无法调用任何ASVObject派生对象的方法. 调用ASVObject派生对象的方法会得到 “自动化没有实现” 的错误, 如果能够解决这个问题, 则可以扩展ASV功能, 以达到ECMAScript中几乎透明的程度.
提示: 使用ASV脚本引擎无法在SVG文档中实现这个功能.
检查返回值是否为Null
由于所有的返回值都被封装在ASVObject中, 因此, 为了检查返回值是否为null, 必须检查其Object属性. 例如, 下面的代码对指定节点的所有子节点进行遍历, 在对子节点进行遍历之前, while语句将检查子节点是否为null.
node = parent.firstChild;
while ( node.Object != null ) {
// do something with the current node
node = node.nextSibling;
}
已知问题
- 无法在使用ASVObject构造对象时触发接受ASVObject的构造函数. 很明显,在使用基于ASVObject构造函数前就调用了使用Object为参数的构造函数.
- 我希望使用强制类型转换的手段完成从一般类型到特定类型的转换. 如果能够实现该转换, 同时能避免创建各种类型转换组合, 则问题1也就不存在了.
- 如果方法能够返回特定类型,而不是现在的一般类型就好了. 例如, getElementById()返回一个元素, 我希望它能够返回一个SVGRectElement或者其它与之相匹配的特定元素.
- 无法从C#中为ECMAScript对象中增加新的属性.
- 事件处理器必须创建自己的基于ASVObject的对象, 这点无法避免, 原因时ASV必须向事件处理器提供对象.
- 我无法从调用ECMAScript任何ASVObject派生对象的方法. 调用ASVObject派生对象的方法会得到 “自动化没有实现” 的错误. 对我来说奇怪的是:1) 可以调用Form的方法, 2)可以使用任何对象作为事件处理器. 从错误消息看来这是与COM相关的问题. 似乎需要启动些什么以完成这种类型的交互, 但看起来它已经启动, 因为我已经能够调用一些方法. 对此如果有什么建议将不胜感激.
- 我无法使用ASV脚本引擎实现从C#调用ECMAScript方法, 反之也不行. 但由于能够实现使用ASV脚本引擎通过事件处理器触发C#方法, 这个功能似乎应该可以实现.
我确信随着该库的使用会有更多的问题得到暴露. 因此如果你遇到什么问题请告知我, 我会将其加入上面的问题列表(当然更好的是解决问题). 正如上面所说, 我正在学习C#, 因此如果你发现什么非正规的地方, 或者有些什么建议, 请给我发邮件.
修改记录
版本 1.2
- SVG控件返回的对象全部封装为ASVObject. 当SVG控件返回null 时引发了ASVObject构造函数异常. 现在正在测试ASVObject 构造函数中对null的处理. 顺便说一下, 你必须检查返回对象的Object 属性是否为null, 例如if (svgNode.firstChild.Object == null) { ... }.
版本1.1
- IDL中定义的无符号返回类型无法强制类型转换回正确的无符号类型. 似乎所有IDL无符号类型在ASVSharp中都是有符号类型. COM应该支持无符号类型, 所以我认为问题可能出在SVG控件上, 但并不能确定.
版本1.0
- 发布动态库, 源代码和演示程序
下载
下载动态库(ASV3Sharp, AxInterop.SVGACTIVEXLib.dll 和 Interop.SVGACTIVEXLib.dll)
下载ASV3Sharp源代码
下载一个简单的浏览器. 支持XSLT并演示本文中提到的相关技术.
Copyright 2000-2003, Kevin Lindsey