背景
我在博客园上写博客是使用Windows Live Writer,代码高亮插件是使用Paste from Visual Studio(下文简称VSPaste)。
Windows Live Writer更进一步的资料,可参照【超详细教程】使用Windows Live Writer 2012和Office Word 2013 发布文章到博客园全面总结,下载地址在此处。
VSPaste更进一步的资料,可参照CnBlogs博文排版技巧。由于Windows Live Writer 2012的终止日期是2017年1月10日,并且对应的插件网站也关闭了,所以目前没有官方下载,有需要的可以联系我。
起因
好久没更新过博客了,一是懒,二是没什么值得分享的。恰好手上有了一点可以分享的话题,就开始兴高采烈的写博客了。写着写着,发现了VSPaste复制存在丢失格式的情况,于是就来研究这个问题了。
就丢失格式的情况举一个例子,比如,我复制的是如下代码:
然而,使用VSPaste插入到Windows Live Writer中后,文字全都成了黑色。绿色和蓝色呢?
检查VSPaste是否出错
由于VSPaste已经很久没有更新,所以我的第一反应是查看VSPaste是否出错。为了验证判断,我们不妨建立一个工程进行测试。
查找入口
在建立工程之前,需要先了解Windows Live Writer调用VSPaste的函数入口。在必应上搜索windows live writer plugin develop,发现有一篇名为Developing Plugins for Windows Live Writer的文章,经过了解后发现,插件一定继承自ContentSource或者SmartContentSource。其中ContentSource是直接插入HTML到Windows Live Writer中,而SmartContentSource功能会更丰富一些,比如可以添加后编译。
打开ILSpy,将VSPaste的程序集拖入其中。经过简单查看,发现VSPaste插件的入口类正是继承自SmartContentSource。而且其中做的事情很简单,判断剪贴板中是否存在RTF格式的数据,如果存在,将其转换为HTML。
另外,博客园官方发布的代码着色控件CNBlogs.CodeHighlighter确实如他们所说的,将代码提交至服务器处理。如下图:
填充测试工程
通过的分析,我们可以建立一个简单的测试工程。在分析VSPaste入口时,发现其引用了System.Windows.Forms。所以我们不妨新建一个Windows窗体应用程序来显示转换前的RTF和转换后的HTML。
新建工程后先添加VSPaste的引用,接下来再添加VSPaste所必需的WindowsLive.Writer.Api。这个DLL在哪里呢?由于是Windows Live Writer插件,所以猜测是在Windows Live Writer安装目录下,一查,果然存在这个DLL。可是要是安装目录下不存在该怎么办呢?我比较喜欢用Everything这个软件,可以直接输入文件名称查找,速度又快。但是使用该软件的前提是必须要保证对应的盘是NTFS文件系统。
完成主界面,一个主界面由两个文本框、一个两行的TableLayoutPanel和一个按钮组成,如下:
测试按钮的响应如下:
运行失败及解决方案
我们的测试工程已经完成了,接下来我们运行一下试试。编译成功,运行成功,接下来在VS中复制一段代码,点击运行试试。非常不幸,出现了这个错误:
这个错误我有经验,多出现于P/Invoke场景。也比较好解决,在工程属性的生成标签页中将生成平台改成x86即可。好了,再来尝试,依然报错:
不科学啊,平常都行啊,怎么这次就出问题。再仔细对比一下,还是有区别的,这次是找到的程序集清单定义与程序集引用不匹配。点击查看详细信息,如下图:
仔细阅读FusionLog的信息,发现应该是WindowsLive.Writer.Api的程序集版本不一致。难道是我哪里疏忽了?
打开ILSpy,查看VSPaste的所引用的WindowsLive.Writer.Api。结果如下图:
再在ILSpy中查看我所安装的Windows Live Writer 2012目录下的WindowsLive.Writer.Api的版本信息。结果如下图:
聪明如你,一定已经发现上面的不同了。没错,VSPaste所引用的版本是1.0.0.0,而Windows Live Writer所使用的是1.1.0.0,而且是平台是x86。这也解释了为什么第一次运行时提示试图加载格式不正确的程序。
既然已经知道了问题,那解决起来就简单了。这个时候需要用到程序集重定向,在应用程序配置文件中指定程序集绑定,如下图:
运行结果
在VS中复制最前面那段代码,运行程序,点击测试按钮:
运行结果如上图。RTF格式我了解不是太深入,不过这不影响接下来的操作。新建一个文本文档,将上部文本框中的文本复制到其中,并将其扩展名改为rtf。打开该文件,效果如下图:
从中可以发现,在生成HTML之前,复制出来的RTF已经不正确了。
再次运行结果
在写字板中模拟相应代码,效果如下图:
在RTF中复制代码,运行程序,点击测试按钮:
运行结果如上图。下面的HTML看起来不是很直观,新建一个文本文档,将文本框中的文本复制到其中,并将其扩展名改为html。打开该文件,效果如下图:
从中可以发现,VSPaste并没有出错。
进一步查找原因
从前面的实验可以发现,VSPaste并没有出错,从VS中复制出来的代码已经丢失了RTF格式信息。那么问题究竟会出现在哪里?我以前在VS2015中使用VSPaste都没有问题啊。这时候我有个猜想,如果VS没有出问题,那么很大可能就是哪个插件坑爹了。
查找问题插件
在VS中选择工具—>扩展和更新以打开插件列表,通过二分法来禁用插件以查看问题是否解决(禁用后需要重启VS)。很快,就找到了罪魁祸首,就是下面这货:
查找问题功能
我们找到问题插件后可以就此为止了么?当然可以。但是对于自己来说,总是想打破砂锅问到底。点击上图中右边的详细信息,可以了解到更多的Productivity Power Tools 2015信息。从中可以了解到它的各项功能,也知道了每项功能都可以进行开关。
在VS中选择工具—>选项,并在窗体左边的树状控件中选择Productivity Power Tools。如下图:
在前面了解Productivity Power Tools的功能中,我就已经有怀疑的对象了,就是HTML Copy。尝试将其关闭以查看问题是否解决(禁用后需要重启VS)。经过试验,发现果然是该项功能引发的问题。
还能不能进一步查找问题根源?答案是可以。如果多留意一下Productivity Power Tools 2015的详细信息,就会发现,在该页面右上部分有一个到GitHub的链接。嗯,项目还是微软的,不知为何为出现这种问题。
源码调试
下载代码
从GitHub上下载Productivity Power Tools的代码,由于其是一系列插件的集合,下载后很快就找到了HTML Copy对应的项目。
查找问题代码
代码不是太多,可以采取逐个文件阅读的方法。但是我已经知道症状了,就是复制出来的RTF数据不对,那么不妨查找代码中使用了剪贴板的地方。很快就找到了:
该函数只有一个引用,查看引用,可以找到:
再查看GenerateClipboardData的定义:
再继续查找,可以发现htmlBuilderService和rtfBuilderService都是通过MEF导入的。
在生成html和rtf的代码后面打一个断点,开始调试。在新运行的VS实例中打开工程,复制代码。在断点处查看,可以发现生成的rtf已经丢失了格式信息,而html仍然保留有格式信息。
那么这个rtfBuilderService究竟是何方神圣?在监视窗口查看详细信息:
实现自己的RtfBuilderService
前面已经知道了实现rtfBuilderService的类和所在程序集,在ILSpy中打开该程序集并定位到类:
在工程中新建一个类,类名不能为RtfBuilderService,将ILSpy中的所有代码复制出来放到该类中。将该类的导出类型设为对应的类名,同时在导入IRtfBuilderService的地方改为导入对应的类。如下:
而更改类名的原因是为了避免MEF导入导出失效。如果失效会出现以下情况:
分析RtfBuilderService代码
在我们实现的RtfBuilderService内部代码中查找GenerateRtf方法,发现其使用了如下方法:
先查看GenerateBody方法,发现其主要是通过分析TextRunProperties的属性来生成rtf的:
而文本属性来源于GetClassificationSpans:
调试RtfBuilderService
在GenerateBody方法获取TextRunProperties后面打一个断点,开始调试。在新运行的VS实例中打开工程,复制代码。在断点处查看文本属性的颜色,发现只进入一次断点,且文本前景色为白色,背景色为黑色。
再次复制代码,在监视窗口处查看current的属性信息。其只有的ClassificationType和Span属性。在Span属性上可以看出它的内容仿佛是我们复制的代码,尝试看是否有获取文本的方法,一查,还真有:
对照监视窗口的各项值,可以发现,此时就已经丢失了所有的格式信息。由于我们的ClassificationType为空,所以始终返回的是默认文本属性。
在GetClassificationSpans第一行打一个断点,进行单步调试,可以发现一些信息。调用该方法的cancel参数为空,而GetClassificationSpans返回的列表中无条目,所以总会调用ClassificationType参数为空的NullableClassificationSpan的构造函数。接下来根据调用堆栈一层一层的往上查看,发现在最开始在GenerateClipboardData方法调用GenerateRtf时就已经决定了cancel为空。
这可怎么办,线索又断了,真的是无路可走了么?不,我们还有一条路!这个工程不是有生成HTML的代码么,它不是没丢失格式的嘛,去参考一下呗。
参考生成HTML的代码
经过一番查找,发现了在生成HTML代码中与RtfBuilderService中GetClassificationSpans方法功能类似的代码,连名字都一样。
查看该代码所调用的GetClassificationSpansSync方法:
发现这次调用GetAllClassificationSpans带了一个CancellationToken的参数,而生成WaitContext的IWaitIndicator来源于MEF导入。
怎么样,是不是山重水复疑无路,柳暗花明又一村?
完善RtfBuilderService
依照HTM生成部分依样画葫芦,如下图,红色部分表示新增代码:
经过调试测试,发现生成的RTF已经包含正确的格式信息了。
好了,问题已经解决了,我们到此为止了么?是否还可以做些其它什么?
另一种解决方案
让我们再次回到RtfBuilderService,为什么它会有那么多的重载?
查看实现的接口,发现除了实现IRtfBuilderService外,还实现了IRtfBuilderService2。两个接口的定义对比:
不难发现,IRtfBuilderService2是在IRtfBuilderService每个方法后面加上了一个CancellationToken重载。
所以,我们可以不用新增加类,直接将导入的IRtfBuilderService类型改为IRtfBuilderService2,同时在生成rtf的地方传入CancelToken以调用对应的接口方法。
注意事项
微软建议的在调试插件时需要Productivity Power Tools卸载了。我在研究问题时是卸载了的,但是在写这边博客时没有卸载,貌似也没什么问题。
另外,因为我是先研究的问题,后写的博客,可能有些细节忘记了或者没有写上,有心研究的话可以联系我。
结语
因为事情比较多,断断续续这么久,终于把这篇博客写完了。之所以写这么多,主要是想分享下我解决问题的过程和思路。作为程序员,我个人觉得还是有一些探索精神好一些,很多时候,路不是想象的那么难走。
最后,我给微软提了个issue……