简体   繁体   English

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

[英]Async implementation of Custom Commands in VSIX

When you add a template custom command in a VSIX project, the scaffolding code that Visual Studio generates includes the following general structure:在 VSIX 项目中添加模板自定义命令时,Visual Studio 生成的基架代码包括以下一般结构:

    /// <summary>
    /// Initializes a new instance of the <see cref="GenerateConfigSetterCommand"/> class.
    /// Adds our command handlers for menu (commands must exist in the command table file)
    /// </summary>
    /// <param name="package">Owner package, not null.</param>
    /// <param name="commandService">Command service to add command to, not null.</param>
    private GenerateConfigSetterCommand(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 MenuCommand(this.Execute, menuCommandID);
        commandService.AddCommand(menuItem);
    }

    /// <summary>
    /// This function is the callback used to execute the command when the menu item is clicked.
    /// See the constructor to see how the menu item is associated with this function using
    /// OleMenuCommandService service and MenuCommand class.
    /// </summary>
    /// <param name="sender">Event sender.</param>
    /// <param name="e">Event args.</param>
    private void Execute(object sender, EventArgs e)
    {
        ThreadHelper.ThrowIfNotOnUIThread();
        
        // TODO: Command implementation goes here
    }

    /// <summary>
    /// Initializes the singleton instance of the command.
    /// </summary>
    /// <param name="package">Owner package, not null.</param>
    public static async Task InitializeAsync(AsyncPackage package)
    {
        // Switch to the main thread - the call to AddCommand in GenerateConfigSetterCommand's constructor requires
        // the UI thread.
        await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);

        OleMenuCommandService commandService = await package.GetServiceAsync((typeof(IMenuCommandService))) as OleMenuCommandService;
        Instance = new GenerateConfigSetterCommand(package, commandService);
    }

Note that the framework-provided MenuCommand class takes a standard synchronous event-handling delegate with the signature void Execute(object sender, EventArgs e) .请注意,框架提供的MenuCommand class 采用带有签名void Execute(object sender, EventArgs e)的标准同步事件处理委托。 Also, judging by the presence of ThreadHelper.ThrowIfNotOnUIThread() , it seems pretty clear that the body of the Execute method will indeed be running on the UI thread, which means it would be a bad idea to have any blocking synchronous operations running in the body of my custom command.此外,从ThreadHelper.ThrowIfNotOnUIThread()的存在来看,很明显Execute方法的主体确实会在 UI 线程上运行,这意味着在我的自定义命令的主体。 Or do anything very long running in the body of that Execute() handler.或者在该 Execute() 处理程序的主体中执行任何长时间运行的操作。

So I'd like to use async/await to decouple any long-running operations in my custom command implementation from the UI thread, but I'm not sure how to correctly fit that into the VSIX MPF framework scaffolding.因此,我想使用async/await将自定义命令实现中的任何长时间运行的操作与 UI 线程分离,但我不确定如何正确地将其放入 VSIX MPF 框架脚手架中。

If I change the signature of the Execute method to async void Execute(...) , VS tells me that there's a problem with the ThreadHelper.ThrowIfNotOnUIThread() call:如果我将 Execute 方法的签名更改为async void Execute(...) ,VS 会告诉我ThreadHelper.ThrowIfNotOnUIThread()调用存在问题: “避免在异步或任务返回方法中不在主线程上时抛出。切换到所需的线程。”

I'm not sure how to "switch to the thread required instead".我不确定如何“切换到所需的线程”。 Is that what the await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken) code in the InitializeAsync method is doing?这是InitializeAsync方法中的await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken)代码在做什么吗? Should I just copy that?我应该复制那个吗?

What about exception handling?异常处理呢? If I allow the synchronous void Execute() handler to throw an exception, VS will catch it and show a generic error messagebox.如果我允许同步void Execute()处理程序抛出异常,VS 将捕获它并显示一个通用错误消息框。 But if I change it to async void Execute() then uncaught exceptions won't be raised on the thread which invoked the Execute , and may cause a more serious problem elsewhere.但是,如果我将其更改为async void Execute()不会在调用Execute的线程上引发未捕获的异常,并且可能会在其他地方导致更严重的问题。 What's the correct thing to do here?在这里做什么是正确的? Synchronously accessing Task.Result to rethrow exceptions in the correct context seems like a canonical example of the well-known deadlock .同步访问Task.Result以在正确的上下文中重新抛出异常似乎是众所周知的 deadlock的典型示例。 Should I just catch all exceptions in my implementation and display my own generic message boxes for anything which can't be handled more gracefully?我是否应该在我的实现中捕获所有异常并为无法更优雅地处理的任何内容显示我自己的通用消息框?

EDIT to ask more specific question编辑以提出更具体的问题

Here's a fake synchronous custom command implementation:这是一个假的同步自定义命令实现:

internal sealed class GenerateConfigSetterCommand
{
    [...snip the rest of the class...]

    /// <summary>
    /// This function is the callback used to execute the command when the menu item is clicked.
    /// See the constructor to see how the menu item is associated with this function using
    /// OleMenuCommandService service and MenuCommand class.
    /// </summary>
    /// <param name="sender">Event sender.</param>
    /// <param name="e">Event args.</param>
    private void Execute(object sender, EventArgs e)
    {
        ThreadHelper.ThrowIfNotOnUIThread();

        // Command implementation goes here
        WidgetFrobulator.DoIt();
    }
}

class WidgetFrobulator
{
    public static void DoIt()
    {
        Thread.Sleep(1000);
        throw new NotImplementedException("Synchronous exception");
    }


    public static async Task DoItAsync()
    {
        await Task.Delay(1000);
        throw new NotImplementedException("Asynchronous exception");
    }
}

When the custom command button is clicked, VS has some basic error handling which shows a simple message box:单击自定义命令按钮时,VS 有一些基本的错误处理,显示一个简单的消息框:

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

Clicking Ok dismisses the message box and VS continues working, undisturbed by the "buggy" custom command.单击 Ok 关闭消息框,VS 继续工作,不受“错误”自定义命令的干扰。

Now let's say I change the custom command's Execute event handler to a naïve async implementation:现在假设我将自定义命令的 Execute 事件处理程序更改为天真的异步实现:

    private async void Execute(object sender, EventArgs e)
    {
        // Cargo cult attempt to ensure that the continuation runs on the correct thread, copied from the scaffolding code's InitializeAsync() method.
        await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken);

        // Command implementation goes here
        await WidgetFrobulator.DoItAsync();
    }

Now, when I click the command button, Visual Studio terminates, due to the unhandled exception.现在,当我单击命令按钮时,由于未处理的异常,Visual Studio 终止。

My question is: What is the best practice way to handle exceptions arising from an async VSIX Custom Command implementation, which leads to VS treating unhandled exceptions in async code the same way it treats unhandled exceptions in synchronous code, without risking a deadlock of the main thread?我的问题是:处理由异步 VSIX 自定义命令实现引起的异常的最佳实践方法是什么,这导致 VS 在异步代码中处理未处理的异常的方式与在同步代码中处理未处理的异常的方式相同,而不会冒主死锁的风险线?

The previously-accepted answer generates a compiler warning, VSTHRD100 'Avoid async void methods', which is some indication that it may not be fully correct.先前接受的答案会生成一个编译器警告 VSTHRD100 'Avoid async void methods',这表明它可能不完全正确。 In fact the Microsoft threading documentation has a rule to never define async void methods .事实上, 微软线程文档有一条规则,永远不要定义异步 void 方法

I think the correct answer here is to use the JoinableTaskFactory's RunAsync method.我认为这里的正确答案是使用 JoinableTaskFactory 的 RunAsync 方法。 This would look as in the code below.这将如下面的代码所示。 Andrew Arnott of Microsoft says 'This is preferable [to async void] both because exceptions won't crash the app and (more particularly) the app won't close in the middle of an async event handler (that might be saving a file, for example).' 微软的 Andrew Arnott 说: “这比 async void 更可取,因为异常不会使应用程序崩溃,而且(更具体地说)应用程序不会在异步事件处理程序(可能正在保存文件,例如)。'

There are a couple of points to note.有几点需要注意。 Although exceptions won't crash the app they just get swallowed, so if you want to display a message box, for example, you'll still need a try..catch block inside the RunAsync.尽管异常不会使应用程序崩溃,但它们只是被吞没,因此,例如,如果您想显示一个消息框,您仍然需要在 RunAsync 中使用 try..catch 块。 Also this code is reentrant.此代码也是可重入的。 I've shown this in the code below: if you click the menu item twice quickly, after 5 seconds you get two messageboxes both claiming they came from the second call.我在下面的代码中显示了这一点:如果您快速单击菜单项两次,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);
        });
    }

The documentation that describes the correct usage of the ThreadHelper.JoinableTaskFactory APIs is here .描述正确使用ThreadHelper.JoinableTaskFactory API 的文档在这里

In the end, I did the following:最后,我做了以下事情:

private async void Execute(object sender, EventArgs e)
{
    try
    {
         await CommandBody();
    }
    catch (Exception ex)
    {
        // Generic last-chance MessageBox display 
        // to ensure the async exception can't kill Visual Studio.
        // Note that software for end-users (as opposed to internal tools)
        // should usually log these details instead of displaying them directly to the user.
        await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync();

        VsShellUtilities.ShowMessageBox(
            this._package,
            ex.ToString(),
            "Command failed",
            OLEMSGICON.OLEMSGICON_CRITICAL,
            OLEMSGBUTTON.OLEMSGBUTTON_OK,
            OLEMSGDEFBUTTON.OLEMSGDEFBUTTON_FIRST);
    }
}

private async Task CommandBody()
{
    // Actual implementation logic in here
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM