VSIX 中自定义命令的异步实现

Hyd*_*rum 5 c# mpf visual-studio vsix async-await

在 VSIX 项目中添加模板自定义命令时,Visual Studio 生成的脚手架代码包括以下常规结构:

\n
    /// <summary>\n    /// Initializes a new instance of the <see cref="GenerateConfigSetterCommand"/> class.\n    /// Adds our command handlers for menu (commands must exist in the command table file)\n    /// </summary>\n    /// <param name="package">Owner package, not null.</param>\n    /// <param name="commandService">Command service to add command to, not null.</param>\n    private GenerateConfigSetterCommand(AsyncPackage package, OleMenuCommandService commandService)\n    {\n        this.package = package ?? throw new ArgumentNullException(nameof(package));\n        commandService = commandService ?? throw new ArgumentNullException(nameof(commandService));\n\n        var menuCommandID = new CommandID(CommandSet, CommandId);\n        var menuItem = new MenuCommand(this.Execute, menuCommandID);\n        commandService.AddCommand(menuItem);\n    }\n\n    /// <summary>\n    /// This function is the callback used to execute the command when the menu item is clicked.\n    /// See the constructor to see how the menu item is associated with this function using\n    /// OleMenuCommandService service and MenuCommand class.\n    /// </summary>\n    /// <param name="sender">Event sender.</param>\n    /// <param name="e">Event args.</param>\n    private void Execute(object sender, EventArgs e)\n    {\n        ThreadHelper.ThrowIfNotOnUIThread();\n        \n        // TODO: Command implementation goes here\n    }\n\n    /// <summary>\n    /// Initializes the singleton instance of the command.\n    /// </summary>\n    /// <param name="package">Owner package, not null.</param>\n    public static async Task InitializeAsync(AsyncPackage package)\n    {\n        // Switch to the main thread - the call to AddCommand in GenerateConfigSetterCommand\'s constructor requires\n        // the UI thread.\n        await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);\n\n        OleMenuCommandService commandService = await package.GetServiceAsync((typeof(IMenuCommandService))) as OleMenuCommandService;\n        Instance = new GenerateConfigSetterCommand(package, commandService);\n    }\n
Run Code Online (Sandbox Code Playgroud)\n

请注意,框架提供的MenuCommand类采用带有签名的标准同步事件处理委托void Execute(object sender, EventArgs e)。另外,从 的存在来看 ThreadHelper.ThrowIfNotOnUIThread(),很明显该Execute方法的主体确实将在 UI 线程上运行,这意味着在我的自定义命令的主体中运行任何阻塞同步操作将是一个坏主意。或者在 Execute() 处理程序的主体中执行任何长时间运行的操作。

\n

因此,我想用来async/await将自定义命令实现中的任何长时间运行的操作与 UI 线程分离,但我不确定如何将其正确地适应 VSIX MPF 框架脚手架。

\n

如果我将 Execute 方法的签名更改为async void Execute(...),VS 会告诉我调用存在问题ThreadHelper.ThrowIfNotOnUIThread():\n“避免在异步或任务返回方法中不在主线程上时抛出异常。而是切换到所需的线程。”

\n

我不确定如何“切换到所需的线程”。这就是方法await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken)中的代码InitializeAsync正在做的事情吗?我应该复制那个吗?

\n

那么异常处理呢?如果我允许同步void Execute()处理程序抛出异常,VS 将捕获它并显示通用错误消息框。但是,如果我将其更改为,async void Execute()则调用 的线程将不会引发未捕获的异常Execute,并且可能会在其他地方导致更严重的问题。这里正确的做法是什么?在正确的上下文中同步访问以重新抛出异常似乎是众所周知的死锁Task.Result的典型示例。我是否应该捕获实现中的所有异常并为无法更优雅地处理的任何内容显示我自己的通用消息框?

\n

编辑以提出更具体的问题

\n

这是一个假的同步自定义命令实现:

\n
internal sealed class GenerateConfigSetterCommand\n{\n    [...snip the rest of the class...]\n\n    /// <summary>\n    /// This function is the callback used to execute the command when the menu item is clicked.\n    /// See the constructor to see how the menu item is associated with this function using\n    /// OleMenuCommandService service and MenuCommand class.\n    /// </summary>\n    /// <param name="sender">Event sender.</param>\n    /// <param name="e">Event args.</param>\n    private void Execute(object sender, EventArgs e)\n    {\n        ThreadHelper.ThrowIfNotOnUIThread();\n\n        // Command implementation goes here\n        WidgetFrobulator.DoIt();\n    }\n}\n\nclass WidgetFrobulator\n{\n    public static void DoIt()\n    {\n        Thread.Sleep(1000);\n        throw new NotImplementedException("Synchronous exception");\n    }\n\n\n    public static async Task DoItAsync()\n    {\n        await Task.Delay(1000);\n        throw new NotImplementedException("Asynchronous exception");\n    }\n}\n
Run Code Online (Sandbox Code Playgroud)\n

当单击自定义命令按钮时,VS 有一些基本的错误处理,显示一个简单的消息框:

\n

同步抛出异常的基本错误消息框

\n

单击“确定”将关闭消息框,VS 继续工作,不受“有问题的”自定义命令的干扰。

\n

现在假设我将自定义命令的执行事件处理程序更改为 na\xc3\xafve 异步实现:

\n
    private async void Execute(object sender, EventArgs e)\n    {\n        // Cargo cult attempt to ensure that the continuation runs on the correct thread, copied from the scaffolding code\'s InitializeAsync() method.\n        await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);\n\n        // Command implementation goes here\n        await WidgetFrobulator.DoItAsync();\n    }\n
Run Code Online (Sandbox Code Playgroud)\n

现在,当我单击命令按钮时,Visual Studio 由于未处理的异常而终止。

\n

我的问题是: \n处理异步 VSIX 自定义命令实现引发的异常的最佳实践方法是什么,这会导致 VS 处理异步代码中未处理的异常的方式与处理同步代码中未处理的异常的方式相同,而不会有死锁的风险主线程?

\n

Ric*_*h N 5

先前接受的答案会生成编译器警告 VSTHRD100“避免 async void 方法”,这表明它可能不完全正确。事实上,Microsoft 线程文档有一条规则:永远不要定义 async void 方法

我认为这里正确的答案是使用 JoinableTaskFactory 的 RunAsync 方法。这看起来如下面的代码所示。 微软的 Andrew Arnott 表示, “这比 async void 更可取,因为异常不会使应用程序崩溃,而且(更具体地说)应用程序不会在异步事件处理程序(可能正在保存文件,例如)。'

有几点需要注意。虽然异常不会使应用程序崩溃,但它们只会被吞噬,因此,例如,如果您想显示消息框,则仍然需要 RunAsync 内的 try..catch 块。而且这段代码是可重入的。我在下面的代码中展示了这一点:如果您快速单击菜单项两次,5 秒后您会看到两个消息框,都声称它们来自第二次调用。

    // Click the menu item twice quickly to show reentrancy
    private int callCounter = 0;
    private void Execute(object sender, EventArgs e)
    {
        ThreadHelper.ThrowIfNotOnUIThread();
        package.JoinableTaskFactory.RunAsync(async () =>
        {
            callCounter++;
            await Task.Delay(5000);
            string message = $"This message is from call number {callCounter}";
            VsShellUtilities.ShowMessageBox(package, message, "", 
                OLEMSGICON.OLEMSGICON_INFO, OLEMSGBUTTON.OLEMSGBUTTON_OK, OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
        });
    }
Run Code Online (Sandbox Code Playgroud)