(翻譯)LearnVSXNow! #10 創建我們第一個工具集-重用程式碼
- 2019 年 10 月 5 日
- 筆記
我們在第6和第7篇創建的Calculate小工具窗還有很多可以改進的地方,所以在這篇文章里,我們不會開發新的功能,而是重構我們的程式碼,封裝出可以重用的類和方法。
VSX背後的對象模型是非常豐富的:有幾百個類和幾千個方法。但我們在開發VS add-in和package的時候,光記住類和方法的名字是不夠的,我們還需要知道相應的GUID以及其他相關的常數。
我覺得在VSX的開發中最難的是開發者必須要把.NET和COM混著用。如果VSX的編程模型(對象模型)更簡潔一點話,對開發人員是非常好的事情。
微軟在interop程式集之上,開發了一些用於託管程式碼的層(其中一個叫做MPF,全稱是Managed Package Framework)。我認為MPF里提供的類和方法是非常棒的,但它們只會涉及到VSX的某些方面,還不夠。
所以在這篇文章里,我會告訴你如何把常用的功能封裝出來,供我們以後開發VSX時使用。我希望你也能夠在開發過程中,逐步創建你自己需要的工具集。
從這篇文章開始,我會創建一個叫做VsxTools的類庫。這一次我僅僅出於演示目的來使用這個類庫,但是既然我們是一起學習VSX的,所以我打算把這個類庫弄成一個真正可用的工具。在這篇文章里我會做如下的重構:
- 改進活動日誌的調用
- 簡化output window的調用
CodePlex上的源碼
當你在看這篇文章的時候,我已經把所有的示例程式碼和文章放到了CodePlex上了(http://www.codeplex.com/LearnVSXNow)。如果下載了最新的源碼,你會看到在PackageStartupSamples目錄下有一個PackageStartupSamples.sln文件。它包含了這系列文章里的所有的例子。我會隨著VS 2008 SDK版本的更新來相應的更新這些例子(當然如果發現了bug的話,我也會更新它們)。
創建VsxTools類庫
我們最好把可重用的程式碼放到一個單獨的類庫里。所以,讓我們創建一個名為VsxTools的C# class library項目,並把它添加到StartupToolsetRefactored項目所在的解決方案中。由於我們需要向這個VsxTools中添加VSX程式碼,所以我們要向這個項目中添加VS SDK interop和MPF程式集引用:
— Microsoft.VisualStudio.OLE.Interop — Microsoft.VisualStudio.Shell.9.0 — Microsoft.VisualStudio.Interop — Microsoft.VisualStudio.Interop.8.0 — Microsoft.VisualStudio.Interop.9.0
接下來,我們可以向這個類庫里添加功能了。
改進活動日誌的調用
如果想往活動日誌里寫日誌的話,我們需要寫差不多半打行數的程式碼,例如:
private void LogCalculation(string firstArg, string secondArg, string operation, string result){ string message = String.Format("Calculation executed: {0} {1} {2} = {3}", firstArg, operation, secondArg, result); IVsActivityLog log = Package.GetGlobalService(typeof(SVsActivityLog)) as IVsActivityLog; if (log == null) return; log.LogEntry( (result == "#Error") ?(UInt32) __ACTIVITYLOG_ENTRYTYPE.ALE_ERROR : (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION, "Calculation", message);}
但是這個方法有很多「噪音」:
- 為了使用這個服務對象,我必須記住IVsActivityLog和SVsActivityLog這兩個名字。
- 我必須在使用它之前判斷它是不是null。
- 我必須知道__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR和__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION這兩個「魔術」常數。雖然從它們的名字上可以猜出它們代表的意思,但是很不直觀,而且很難記住。我甚至還得把它們轉換成System.UInt32類型。
- 在上面這個例子里,由於我們只需要記錄日誌類型、日誌源和日誌消息,所以我們調用了LogEntry方法。但是如果我們想記錄其它資訊,我們還得找另外一個方法才行。
所以必須得想辦法去掉這些「噪音」。如果能用下面這段程式碼豈不是很好?
string message = String.Format("Calculation executed: {0} {1} {2} = {3}", firstArg, operation, secondArg, result);ActivityLog.Write(result == "#Error" ? ActivityLogType.Error : ActivityLogType.Information, "Calculation", message);
在這段程式碼里,我們減少了如下「噪音」:
- 要想使用活動日誌這個service的話,我們只需要記住一個直觀的名字ActivityLog就行了。
- 用一個很直觀的枚舉來代表日誌類型。
- 寫日誌的時候,只需要一個Write方法就夠了。當然這個方法有很多個重載版本,可以覆蓋所有的參數組合。
- 不需要轉換類型,不需要空引用檢查。
另外,這種非常簡單的、帶智慧感知的方式可以提高我們敲程式碼的速度。
定義ActivityLog
改進的ActivityLog模式基於3個類型:
// --- Represents entry types instead of __ACTIVITYLOG_ENTRYTYPE constantspublic enum ActivityLogType{ Information, Warning, Error} // --- Represents an entity holding all log entry propertiespublic sealed class ActivityLogEntry{ ... public ActivityLogType Type { get; set; } public string Source { get; set; } public string Message { get; set; } public Guid? Guid { get; set; } public int? Hr { get; set; } public string Path { get; set; } ...} // --- Provides log services through static Write methodspublic static class ActivityLog{ public static void Write(ActivityLogEntry entry); public static void Write(string source, string message); ... public static void Write(string source, string message, Guid guid, int hr); ... public static void Write(ActivityLogType type, string source, string message); ...}
ActivityLogType枚舉的功能是顯而易見的,所以就不說它了。靜態類ActivityLog通過Write方法供外面調用,這個方法有很多重載版本,可以適應不同的參數組合。如果我們在編程的時候不能確定要記錄日誌的哪些屬性,可以調用接收ActivityLogEntry類型的Write方法的重載版本。在這個方法內部判斷應該調用IVsActivityLog的哪個方法,例如,如果只用到了Hr和Path屬性,我們可以調用LogEntryHrPath方法。
ActivityLog的內部實現
在VsxTools項目里添加一個ActivityLog.cs文件,並在裡面添加上面的三個類型。在ActivityLogEntry類里,我弄了幾個構造函數,每一個負責設置不同的屬性。最主要的「邏輯」是寫在ActivityLog靜態類里的,在這個類里,我添加了一些私有屬性和私有方法:
public static class ActivityLog{ ... private static UInt32 MapLogTypeToAle(ActivityLogType logType) { switch (logType) { case ActivityLogType.Information: return (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_INFORMATION; case ActivityLogType.Warning: return (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_WARNING; default: return (UInt32)__ACTIVITYLOG_ENTRYTYPE.ALE_ERROR; } } private static IVsActivityLog Log { get { return Package.GetGlobalService(typeof (SVsActivityLog)) as IVsActivityLog; } } private static void LogEntry(ActivityLogType type, string source, string message) { IVsActivityLog log = Log; if (log != null) { log.LogEntry(MapLogTypeToAle(type), source, message); } } ...}
這些方法都很簡單,就不解釋它們了。和LogEntry方法一樣,我還添加了IVsActivityLog服務中其他的方法,例如LogEntryGuid。在Write方法里,可以調用這些私有方法:
public static void Write(string source, string message){ Write(ActivityLogType.Information, source, message);} public static void Write(ActivityLogType type, string source, string message){ LogEntry(type, source, message);}
就這些就行了。通過實現這個東西,我們就擁有了一個非常簡單並且容易記住的活動日誌的模型。
在舊程式碼中使用新的ActivityLog模型
現在可以修改CalculationControl.cs文件中的LogCalculation方法了:
private void LogCalculation(string firstArg, string secondArg, string operation, string result){ string message = String.Format("Calculation executed: {0} {1} {2} = {3}", firstArg, operation, secondArg, result); ActivityLog.Write(result == "#Error" ? ActivityLogType.Error : ActivityLogType.Information, "Calculation", message); }
現在,你可以編譯並運行一下StartupToolsetRefactored例子了。別忘了我們曾在第7章講過怎樣查看活動日誌,還有,別忘了在項目屬性的Debug頁簽里加上/log開關,這樣它才能記錄活動日誌。
瞧一瞧output window的後台結構
在這篇文章開始的時候,我說過我要簡化一下output window的使用,所以讓我們開始吧。在第7篇文章中,我們已經用IVsOutputWindow和IVsOutputWindowPane介面向VS的output window寫了日誌了:
private void LogCalculationToOutput(string firstArg, string secondArg, string operation, string result){ string message = String.Format("Calculation executed: {0} {1} {2} = {3} ", firstArg, operation, secondArg, result); IVsOutputWindow outWindow = Package.GetGlobalService(typeof(SVsOutputWindow)) as IVsOutputWindow; Guid generalWindowGuid = VSConstants.GUID_OutWindowGeneralPane; IVsOutputWindowPane windowPane; outWindow.GetPane(ref generalWindowGuid, out windowPane); windowPane.OutputString(message);}
就像活動日誌的調用方式那樣,上面紅色的程式碼也有著類似的「噪音」。但在我們去掉這些噪音之前,讓我們先來瞧一瞧VS output window的結構和與它相關的服務。
Visual Studio只有一個output window,但是它卻可以包含多個pane來隔離多種output。Visual Studio它自己定義了一些output window pane,VSPackage也可以定義他們自己的pane。一個package可以向任何已有的pane中(包括VS IDE定義的和第三方package定義的)輸出消息。下圖展示了VS IDE定義的「常規」pane和一個自定義的「My Debug」pane:


要使用output window,用到兩個簡單的服務:
- SVsOutputWindow服務用來管理(獲取、創建和刪除)output window pane,但它不能用來輸出消息。不過它可以獲取IVsOutputWindowPane或IVsOutputWindowPane2(第二個是第一個的擴展)的實例,這些介面用來把output資訊輸出到相應的window pane中。
- SVsGeneralOutputWindowPane服務用於取得General output window pane對應的IVsOutputWindowPane實例。
這些介面的功能如下:
服務介面 |
功能 |
---|---|
IVsOutputWindow |
這個介面只有3個方法,用來管理output window pane的實例,分別是: CreatePane, DeletePane, GetPane |
IVsOutputWindow2 |
擴展IVsOutputWindow介面,添加了一個新的方法,用於獲取當前在用的pane的ID:GetActivePaneGUID |
IVSOutputWindowPane |
這個介面用於管理對應的pane的內容和可見性。 可以調用Activate 方法顯示一個pane,調用Hide方法來隱藏一個pane。每個pane都有一個名字,可以通過GetName和SetName 方法來獲取名字或設置名字。pane裡面的內容可以通過Clear、OutputString和OutputStringThreadSafe方法來管理。 發送到window pane里的資訊也可以通過調用OutputTaskItemString、 OutputTaskItemStringEx和FlushToTaskList方法來放到任務列表中。 |
IVsOutputWindowPane2 |
擴展IVsOutputWindowPane介面,添加了OutputTaskItemStringEx2方法,可以把output資訊和錯誤列表中的消息關聯起來。 |
總結一下上述表格:用IVsOutputWindow來管理pane,用IVsOutputWindowPane來管理每個pane中的output資訊。
window pane由GUID來標識。在Microsoft.VisualStudio.VSConstants類里,定義了3個VS IDE中常用的pane的GUID:、
Window Pane |
GUID |
---|---|
General |
GUID_OutWindowGeneralPane |
Build |
GUID_BuildOutputWindowPane |
Debug |
GUID_OutWindowDebugPane |
如果一個package創建了一個window pane,必須有它自己的GUID。我們可以用這個GUID來獲取這個pane的引用,就像其他VS IDE內置的pane一樣。但如果這個package沒有公開出這個GUID的話,我們也可以用IVSOutputWindow2的GetActivePaneGUID來得到這個GUID。
負責管理pane的方法
通過SVsOutputWindow得到的IVsOutputWindow介面實例有3個用於管理pane的方法:
public interface IVsOutputWindow{ int GetPane(ref Guid rguidPane, out IVsOutputWindowPane ppPane); int CreatePane(ref Guid rguidPane, string pszPaneName, int fInitVisible, int fClearWithSolution); int DeletePane(ref Guid rguidPane);}
每一個方法的第一個參數都是pane的GUID。這3個方法的名字已經很清楚的告訴我們它們是幹嘛的了。調用CreatePane方法的時候,你需要傳遞3個額外的參數:
- pszPaneName表示pane的初始名字(可以在創建後改變這個名字)
- fInitVisible用於設置pane的初始可見性。如果設成了true(即非0值),這個pane在創建後會立刻顯示。當然,這裡是說這個pane會顯示在output window里,但output window是可以隱藏的。不過你可以通過視圖|輸出(View|Output)菜單來顯示output window。
- fClearWithSolution參數如果設成true的話,pane裡面的內容就會隨著解決方案的關閉而自動清空。
你也許認為,如果我們對VS內置的output pane調用CreatePane和DeletePane的話,VS會報錯。但是不是這樣的,這兩個方法也可以刪除和重新創建原本已經內置的pane。所以在用的時候你必須意識到這一點。
最常用的方法是GetPane,它可以獲取一個IVsOutputWindowPane的實例,從而向相應的pane中寫消息。
把消息發送到pane中
IVsOutputWindowPane介面提供了往pane中寫消息的功能。你可以把文本消息輸出到pane中,也可以輸出到任務列表中,但是在這篇文章中,我僅僅把消息直接輸出到pane中(處理任務列表是以後的文章的主題)。通過調用OutputString或OutputStringThreadSafe這兩個方法,你可以用執行緒安全或執行緒不安全的形式把消息輸出到pane中。什麼時候需要用執行緒安全的方法,什麼時候不需要用,這個要搞清楚。如果你搞不清楚的話,那就用OutputStringThreadSafe吧。
簡化output window的調用
正如你看到的那樣,為了管理output pane並往裡面寫消息,我們需要寫好幾行有噪音的程式碼。現在讓我告訴你一個去掉這些噪音的解決方案。我並不認為這是最好的方案,但這肯定是一個解決方案。如果你有更好的主意,請告訴我。
是什麼方案
由於你們是開發人員,所以沒有什麼比直接看程式碼能夠說的更清楚了。我的解決方案可以通過CalculationControl.cs文件里的這幾行程式碼來描述清楚:
public partial class CalculationControl : UserControl{ ... private void LogCalculationToOutput(string firstArg, string secondArg, string operation, string result) { string message = String.Format("Calculation executed: {0} {1} {2} = {3}", firstArg, operation, secondArg, result); OutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane)); pane.WriteLine(message); } ... [PaneName("My Debug")] [InitiallyVisible(true)] [ThreadSafe(true)] private sealed class MyDebugPane: OutputPaneDefinition {} ...}
紅色的程式碼創建了一個叫做「My Debug」的output window pane ,並且用執行緒安全的形式把消息輸出進去。OutputWindow類的GetPane方法會在需要的時候創建pane。pane以一個簡單的類的形式定義,並標記上一些屬性。所有使輪子轉起來的工作被放到了後台,調用這不用關心。
如果你想往「General」這個pane中寫消息的話,上面的程式碼還可以更短:
OutputWindow.General.WriteLine(message);
這個方案的基礎結構
這個解決方案的基礎是3個類,如下:
類型 |
功能 |
---|---|
OutputPaneDefinition |
可以用這個類來繼承output window pane definition(OWPD)。一個OWPD類型僅僅是一個定義,在它上面可以添加這個pane的特性的attribute。 OutputWindow和OutputWindowPane用這個類的屬性去獲取這些attribute的值。 |
OutputWindow |
這個靜態類負責管理output window pane,就像IVsOutputWindow介面那樣。這個類也提供了靜態屬性,用這些屬性可以直接訪問到VS內置的pane。同時,這個類提供了一個異常處理機制,可以把消息轉發到「Genernal」或「Debug」 pane中,甚至轉發到一個虛擬的pane中(Silent pane)。 |
OutputWindowPane |
這個類負責把消息輸出到它對應的pane中,和IVsOutputWindowPane介面一樣(不過它不支援任務列表的處理)。它提供Write和WriteLine方法,類似System.Console類。 你可以把這個類看成IVsOutputWindowPane的包裝類(Wrapper class)。 |
當用OutputPaneDefinition來定義一個pane時,我們可以把這個pane弄成默認執行緒安全的。這樣的話,就會以執行緒安全的方式當向pane中輸出消息。
定義一個pane類
如果我們需要用VS的標準pane,只需要用OutputWindow類中的General、Debug或Build靜態屬性就行了。
不過如果我們創建VSPackage的話,我們也許需要自己的output window pane。在「傳統」方式下,我們用一個GUID來代表這個pane,但在我的方案下,我用一個繼承自OutputWindowDefinition的類來代表這個pane,這個類上可以添加關於這個pane特性的attribute。在OutputWindowDefinition的默認構造函數里,通過讀取這些attribute來設置屬性值。 下面是這個類的定義:
public abstract class OutputPaneDefinition{ protected OutputPaneDefinition(); public virtual Guid GUID { get; } public string Name { get; } public bool InitiallyVisible { get; } public bool ClearWithSolution { get; } public bool ThreadSafe { get; } public bool IsSilent { get; internal set; }}
我們可以把一個pane定義成安靜的,也就說並沒有物理上的pane,任何輸出到這個pane上的消息都會以安靜的模式處理掉。另外,為了定義一個已經存在的pane(例如VS內置的pane或由第三方package定義的pane),我們可以重寫Guid屬性。
為了演示這些屬性的用法,讓我們看一下OutputWindow類中的「Debug」 pane和Silent pane是怎麼定義的:
public static class OutputWindow{ ... private sealed class DebugPane : OutputPaneDefinition { public override Guid GUID { get { return VSConstants.GUID_OutWindowDebugPane; } } } ... private sealed class SilentPane : OutputPaneDefinition { public SilentPane() { IsSilent = true; } } ...}
OutputWindowDefinition可以識別如下attribute:
public sealed class PaneNameAttribute: StringAttribute {...}public sealed class InitiallyVisibleAttribute: BoolAttribute {...}public sealed class ClearWithSolutionAttribute: BoolAttribute {...}public sealed class ThreadSafeAttribute: BoolAttribute {...}
為了定義一個自己的pane,可以像下面的程式碼那樣創建一個類:
[Guid("6D71C5F7-200C-4322-A264-65C78CF511AA")][PaneName("My Own Pane")][InitiallyVisible(false)][ClearWithSolution(true)][ThreadSafe(true)]private sealed class MyOwnPane: OutputPaneDefinition {}
更詳細的程式碼細節,請參考OutputWindowDefinition.cs文件。
利用OutputWindow管理pane
我參考IVsOutputWindow提供的功能,創建了OutputWindow類,並額外添加了一些小的功能。我聲明了一個OutputPaneHandling屬性,是枚舉類型的,代表當物理上的pane無法取得時,如何處理消息。這個枚舉有如下的枚舉值:
枚舉值 |
含義 |
---|---|
Silent |
不產生任何異常,待輸出的資訊也不發送到任何pane中。 |
ThrowException |
拋出WindowPaneNotFoundException異常。 |
RedirectToGeneral |
輸出資訊轉到General pane中。 |
RedirectToDebug |
輸出資訊轉到Debug pane中。 |
這個類的結構如下:
public static class OutputWindow{ public static OutputPaneHandling OutputPaneHandling { get; set; } public static OutputWindowPane General { get; } public static OutputWindowPane Build { get; } public static OutputWindowPane Debug { get; } public static OutputWindowPane Silent { get; } public static OutputWindowPane CreatePane(Type type); public static OutputWindowPane GetPane(Type type); public static bool DeletePane(Type type); }
CreatePane、GetPane和DeletePane方法接受一個Type類型的參數,這個類型必須繼承自WindowPaneDefinition類。代表內置的pane的類是OutputWindow類的私有嵌套類,你不能用它們的類型作為參數,所以你也不能創建或者刪除它們。CreatePane方法只能夠創建原本不存在的pane,如果這個pane已經創建了,就只會返回它的實例。調用GetPane的時候,如果某個pane不存在,GetPane方法會創建它。
具體細節,可以參考OutputWindow.cs文件。
向pane中寫消息
我在前面提到過,OutputWindowPane類實際上是IVsOutputWindowPane實例的一個包裝。我只不過在設計和實現這個包裝類的時候做了一些小改動。
IVsOutputWindowPane用兩個單獨的方法分別以執行緒安全和不安全的方式寫消息:OutputStringThreadSafe和OutputString。我想隱藏這兩個方法,這樣使用者在用的時候,就不用關心該調用哪一個。在這個類裡面,我加了一個布爾屬性ThreadSafe,由它來決定該調用哪個方法。你還記得吧,WindowPaneDefinition類識別ThreadSafeAttribute,所以當創建了一個pane的實例之後,OutputWindowPane的ThreadSafe屬性值會設置成WindowPaneDefinition的ThreadSafeAttribute指定的初始值。
另外,IVsOutputWindowPane的GetName和SetName方法被封裝成Name屬性。
OutputWindowPane類的成員如下:
public sealed class OutputWindowPane{ internal OutputWindowPane(OutputPaneDefinition paneDef, IVsOutputWindowPane pane); public bool ThreadSafe { get; set; } public string Name { get; set; } public bool IsVirtual { get; } public void Activate(); public void Hide(); public void Clear(); public void Write(string output); public void Write(string format, params object[] parameters); public void Write(IFormatProvider provider, string format, params object[] parameters); public void WriteLine(string output); public void WriteLine(string format, params object[] parameters); public void WriteLine(IFormatProvider provider, string format, params object[] parameters) }
構造函數應該被弄成internal的,這樣OutputWindow類就是OutputWindowPane的工廠類了,使用者沒法自己new一個實例出來。在構造函數中,需要傳入OutputPaneDefinition實例,同時也需要傳入IVsOutputWindowPane的實例。IsVirtual屬性可以用來設置這個pane到底是一個物理上的pane,還是一個虛擬的、安靜的pane。
這個類提供了一些Write和WriteLine方法,用來代替原來的OutputString和OutputStringThreadSafe方法,並模仿System.Console中的聲明方式。
上面這些方法的實現都很簡單,具體你可以參考OutputWindowPane.cs文件。
試用一下這個方案
編譯並運行StartupToolsetRefactored項目,並點擊Calculate按鈕,你會發現消息輸出到了一個叫「My Debug」的output pane中。如果你有時間的話,可以試著對程式碼做些改動(在CalculationControl類的LogCalculationToOutput方法里),並看一下相應的變化:
// --- Original code lines:OutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));pane.WriteLine(message); // --- Change 1: Writing to two panesOutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));pane.WriteLine(message);OutputWindow.General.WriteLine(message); // --- Change 2: Changing the pane nameOutputWindowPane pane = OutputWindow.GetPane(typeof(MyDebugPane));pane.Name = "My Debug (modified)"; pane.WriteLine(message); // --- Change 3: Reflecting to an invalid paneOutputWindowPane pane = OutputWindow.GetPane(typeof(int));pane.WriteLine(message); // --- Change 4: Throwing an exception (VS 2008 will stop!)OutputWindow.OutputPaneHandling = OutputPaneHandling.ThrowException; OutputWindowPane pane = OutputWindow.GetPane(typeof(int));pane.WriteLine(message); // --- Change 5: Silent exception (No output will be shown)OutputWindow.OutputPaneHandling = OutputPaneHandling.Silent;OutputWindowPane pane = OutputWindow.GetPane(typeof(int));pane.WriteLine(message); // --- Change 6: Throwing and handling exceptionOutputWindow.OutputPaneHandling = OutputPaneHandling.ThrowException;try{ OutputWindowPane pane = OutputWindow.GetPane(typeof (int)); pane.WriteLine(message);}catch (WindowPaneNotFoundException ex){ OutputWindow.General.WriteLine(ex.Message);}
總結
在這篇文章里,我們修改了StartupToolsetRefactored項目,以VsxTools的形式提供helper類。這些helper類是託管的類型,減少了由VS 2008 SDK的interop類帶來的「噪音」。我們為活動日誌和output widow pane開發了這種可重用的類。
現在,所有的源程式碼(包括前幾篇文章的例子)和文章可以在CodePlex(http://www.codeplex.com/LearnVSXNow)上找到。
我希望這些helper類能夠對你有用。但是,我寫這篇文章的本意並不是告訴你怎樣去除掉程式碼中的「噪音」,而是希望告訴你:在VS interop類的基礎上創建自己的託管類型是值得的。Microsoft在用MPF來實現這個目的,但依然還有很多地方可以使VSX的開發體驗變得更有趣和更愉快!
當開始這個系列的時候,我還沒有打算創建自己的VSX工具集,但現在我已經決定利用VSX社區的支援來做這些了…