撰寫一個擴充套件將 ASP.NET MVC5 專案轉換成 .NET Core 3.1 的專案
圖片取自:https://stackoverflow.com/questions/5513708/vs-extensibility-architecture-package-api-visual-studio-library
前言
記得,在去年的 Taiwan .NET Conf 2019 的分享裡,我分享了一場『該準備從 .NET Framework 4.x 遷移到 .NET Core 3.0 的嗎?』因為分享的當下 .NET Core 3.1 還未推出,而在分享前,我除了思考當前企業所面臨的挑戰外,心裡也迸出一個想法就是,我何不撰寫一個能夠將 ASP.NET MVC 5 的專案直接轉換為 .NET Core 3 專案的擴充套件呢?因為一直以來,我都有在涉獵 VS Extensibility & VS Package 等開發,自身也有撰寫一些 VS 擴充套件,像是 MyORM Extensions ,也有推出 C# Project Templates 線上課程『團隊開發系列-設計符合團隊的範本精靈 (Project Template)』等等,也有上架在 VS Marketplace 上,於是在這場 Session 開始前兩個禮拜,便著手開始撰寫開發這個套件。
下方為當時的 Slides:
架構規劃
但因為時間非常有限,而我又必須先做出一個雛形,不過我希望在 VS 上的操作模式是:【我可以任意在任何一個 ASP.NET MVC 5 的專案點滑鼠右鍵隨即有個選單 "Convert MVC5 to .NET Core 3"】的 MenuItem
因此,我需要單鍵就可將 ASP.NET MVC 5 的專案轉換成 .NET Core 3 的專案類型。
關於 VS Pacakges
要做到這件事,不單單是像 MyORM 透過 IWizard 介面與 Project Templates 就可以實現,VS Extensibility SDK 中可開發的擴充套件提供了下面幾種類型:
- Analyzer
- Project Templates
- Item Templates
- ToolbarControl
- VsPackages
若以部署來源來說,應該還有 Assembly 類型,而這些是可以緩和開發的,比如說,我可以開發一個 VS Toolbar Control 包含自定義視窗的 Project Templates 但是如果需要在 VS 增加按鈕或是選單透過 .vsct 描述檔案來定義,它是一個 XML 檔案,不過不管是 ToolbarControl 或是 MenuItem 若該事件處裡需與 VS 之間互動就得使用 VsPackages 的專案了。
程式撰寫
二話不說開始進行套件的開發,這邊 VS 2019 與 VS 2017 的操作順序稍有些不同,這邊我暫時以 VS 2019 為主。
- 建立 VSIX Project
- 建立 ConvertMvc5ToCore3CommandPackage.vsct
- 建立 ConvertMvc5ToCore3Command.cs 命令實作檔案
- 撰寫 ConvertMvc5ToCore3Command.InitializeAsync() 方法
- 決定 Execute 要做的事情
這邊得先解釋當你在 VS 點了自定義的按鈕或選單,VS 會呼叫 Execute 方法,該方法實作在 AsyncPackage 類別中,你可以將所有程式碼撰寫在 InitialzeAsync 裡,但我的習慣是會拆另外一個檔案,避免 VsPackage.cs 內容過於複雜難以管理。
初次建立的 VsPackage 的程式碼內容如下:
using System;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.VisualStudio;
using Microsoft.VisualStudio.OLE.Interop;
using Microsoft.VisualStudio.Shell;
using Microsoft.VisualStudio.Shell.Interop;
using Microsoft.Win32;
using Task = System.Threading.Tasks.Task;
namespace ConvertMVC5ToCore3
{
/// <summary>
/// This is the class that implements the package exposed by this assembly.
/// </summary>
/// <remarks>
/// <para>
/// The minimum requirement for a class to be considered a valid package for Visual Studio
/// is to implement the IVsPackage interface and register itself with the shell.
/// This package uses the helper classes defined inside the Managed Package Framework (MPF)
/// to do it: it derives from the Package class that provides the implementation of the
/// IVsPackage interface and uses the registration attributes defined in the framework to
/// register itself and its components with the shell. These attributes tell the pkgdef creation
/// utility what data to put into .pkgdef file.
/// </para>
/// <para>
/// To get loaded into VS, the package must be referred by <Asset Type="Microsoft.VisualStudio.VsPackage" ...> in .vsixmanifest file.
/// </para>
/// </remarks>
[PackageRegistration(UseManagedResourcesOnly = true, AllowsBackgroundLoading = true)]
[InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] // Info on this package for Help/About
[ProvideMenuResource("Menus.ctmenu", 1)]
[Guid(ConvertMvc5ToCore3CommandPackage.PackageGuidString)]
[SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")]
[ProvideAutoLoad(VSConstants.UICONTEXT.SolutionExists_string, PackageAutoLoadFlags.BackgroundLoad)]
public sealed class ConvertMvc5ToCore3CommandPackage : AsyncPackage
{
/// <summary>
/// ConvertMvc5ToCore3CommandPackage GUID string.
/// </summary>
public const string PackageGuidString = "a4ff042d-6f8c-45f7-ae4b-47a5f9114161";
/// <summary>
/// Initializes a new instance of the <see cref="ConvertMvc5ToCore3CommandPackage"/> class.
/// </summary>
public ConvertMvc5ToCore3CommandPackage()
{
// Inside this method you can place any initialization code that does not require
// any Visual Studio service because at this point the package object is created but
// not sited yet inside Visual Studio environment. The place to do all the other
// initialization is the Initialize method.
}
#region Package Members
/// <summary>
/// Initialization of the package; this method is called right after the package is sited, so this is the place
/// where you can put all the initialization code that rely on services provided by VisualStudio.
/// </summary>
/// <param name="cancellationToken">A cancellation token to monitor for initialization cancellation, which can occur when VS is shutting down.</param>
/// <param name="progress">A provider for progress updates.</param>
/// <returns>A task representing the async work of package initialization, or an already completed task if there is none. Do not return null from this method.</returns>
protected override async Task InitializeAsync(CancellationToken cancellationToken, IProgress<ServiceProgressData> progress)
{
// When initialized asynchronously, the current thread may be a background thread at this point.
// Do any initialization that requires the UI thread after switching to the UI thread.
await this.JoinableTaskFactory.SwitchToMainThreadAsync(cancellationToken);
}
#endregion
}
}
開發 VsPackages 最難處理的就是 .vsct (Vsct, Visual Studio Command Table) VS IDE 命令表描述檔案了,因為 VS 自身也不支援直接新增或編輯這個檔案,你只可以用 vsct.exe 從 .cto 檔案來建立 .vsct 檔,或透過 Perl 腳本來建立,我通常直接複製官方 ConvertCTCToVSCT.pl Perl 語言來建立,有時比較懶就直接拿上一次的範本直接拿來修改,前面我提到它就是個 XML 檔案,一般不是太複雜的套件 OzCode, Web Essentials 等套件,以目前 Convert ASP.NET MVC 5 to .NET Core 3 這個套件來說,我只需要一個 <Command> 與 <Symbols> 標籤即可!
比較重要的是,VS 的選單與按鈕都是 CommandID,它代表一個命令,這個命令會使用一個 Guid,來對應 CommandID,因為不管是 Button 或 ToolBar 都需要建立 Symbols。
接著 Commands 的 Package 名稱要與 GuidSymbols 相同,程式碼部分使用前面的,Guid 來建立 CommandID 物件,因為在建立 CommandID 物件時,需要這個 Guid,並且將 CommandID 放入 OleMenuCommand 裡,因為 VS 的選單都是 OleMenuCommand。
private ConvertMvc5ToCore3Command(AsyncPackage package, OleMenuCommandService commandService)
{
this.package = package ?? throw new ArgumentNullException(nameof(package));
commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));
var menuCommandID = new CommandID(CommandSet, CommandId);
var menuItem = new OleMenuCommand(this.Execute, menuCommandID);
menuItem.BeforeQueryStatus += MenuItem_BeforeQueryStatus;
commandService.AddCommand(menuItem);
}
如上方程式碼,Execute 方法便是在這裡指定,當然從程式中可以看的出來方法名稱不一定要取名為 Execute,MenuItem_BeforeQueryStatus 事件則是當按下去的專案不是 Web 類型專案時,可以 Disable 掉目前的 Menu Item,CommandService 是 VS IDE 提供的服務,將自定義的 OleMenuCommand 新增到 VS 選單裡面。
ServiceCommand 其實 OleMenuCommandService 它是在 AsyncPackage.InnitalizeAsync() 方法內透過傳入的 AyncPackage 的 GetServiceAsync 建立的。
OleMenuCommandService commandService = await package.GetServiceAsync(typeof(IMenuCommandService)) as OleMenuCommandService;
前面 ConvrtMvc5ToCore3Command 的程式碼中,我們在產生一個 OleMenuCommand 時,我們不是傳入 Execute() 方法的參考,Execute() 方法的程式碼如下:
private async void Execute(object sender, EventArgs e)
{
await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();
string title = "Convert ASP.NET MVC5 to ASP.NET Core 3";
string message = "您想要將目前的 ASP.NET MVC5 的專案 Convert 成 .NET Core 3 類型的專案嗎?";
var result = VsShellUtilities.ShowMessageBox(this.package, message, title, OLEMSGICON.OLEMSGICON_INFO, OLEMSGBUTTON.OLEMSGBUTTON_OKCANCEL, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
if (result == 1)
{
try
{
Project activeProject = GetSelectedProject();
await ConvertProjectAsync(activeProject);
}
catch (Exception ex)
{
VsShellUtilities.ShowMessageBox(this.package, ex.Message, title, OLEMSGICON.OLEMSGICON_INFO, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
}
}
}
這段程式碼就是當你按滑鼠右鍵點選『Convert ASP.NET MVC5 to ASP.NET Core 3』時,所執行的內容,大家容易誤會的是 VsShellUtilities 類別了,不是在 .NET 裡顯示 MessageBox 都是 System.Windows.Forms/WPF 當然理論上是沒錯,單須交由 IDE 去執行 MessageBox。
第一行的 await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); 是 AsyncPackages 提供的非同步方法,前面提到從 VS 2017 開始支援擴充套件的延遲載入,所以套件也會支援非同步 Execute,所以當非同步執行時,若不希望影響到 IDE 的 Main Thread 導致整個 IDE 停住可呼叫這個方法,好比是 System.Windows.Forms 裡的 DoEvents();
所以如果你有撰寫過 VS 2015 之前的 Extensibility SDK 擴充套件的話部分寫法會有些不同,不過要全部解說完畢可能要下一篇文章了,哈哈。
待續...
Github 原始程式碼:https://github.com/wugelis/ConvertMVC5ToCore31
關於 Gelis:
資深 .NET 技術顧問
FB 社團 (軟體開發之路):
https://www.facebook.com/groups/361804473860062/
FB 粉絲團 (Gelis 的程式設計訓練營):
https://www.facebook.com/gelis.dev.learning/
我講授過的課程 SlideShare:
https://www.slideshare.net/GelisWu
以下是我經營的項目與內容:
(1). 企業內訓課程
(2). 專業顧問
企業內訓課程:
1. .NET Core 3.1 從入門到進階
先前實體課程連結
2. 跨平台的 Web API Framework 框架設計
先前實體課程連結
3. 決戰 OOAD 系列課程 - 使用 UML
先前實體課程連結
4. 單元測試 UnitTest 與 Moq 物件實務課程
先前實體課程連結
5. 快速開發系列 - C# Project Templates 範本設計
留言
張貼留言