撰寫一個擴充套件將 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 為主。

  1. 建立 VSIX Project
  2. 建立 ConvertMvc5ToCore3CommandPackage.vsct
  3. 建立 ConvertMvc5ToCore3Command.cs 命令實作檔案
  4. 撰寫 ConvertMvc5ToCore3Command.InitializeAsync() 方法
  5. 決定 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 &lt;Asset Type="Microsoft.VisualStudio.VsPackage" ...&gt; 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 範本設計

留言

這個網誌中的熱門文章

軟體工程師 - 成長的 10 個階段

常見的程式碼壞味道(Code Smell or Bad Smell)

什麼是 gRPC ?