記錄了從查找Windows Live Writer上VSPasste插件丟失RTF格式信息問題的原因,到最終解決問題的整個經歷。 ...
背景
我在博客園上寫博客是使用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……