在 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);
}
注意,框架提供的
MenuCommand
类使用带有签名void Execute(object sender, EventArgs e)
的标准同步事件处理委托。此外,由于存在ThreadHelper.ThrowIfNotOnUIThread()
,很明显Execute
方法体将在UI线程上运行,这意味着在自定义命令的方法体中运行任何阻塞同步操作都是不好的。或者说,在那个 Execute() 处理程序的主体中不要执行任何非常长的操作。因此,我想使用
async/await
将自定义命令实现中的任何长时间运行的操作与UI线程解耦,但我不确定如何正确地将其放入VSIX MPF框架脚手架中。如果我将Execute方法的签名更改为
async void Execute(...)
,则VS会告诉我ThreadHelper.ThrowIfNotOnUIThread()
调用存在问题:
!["Avoid throwing when not on the main thread while in an async or Task-returning method. Switch to the thread required instead."](https://istack.dev59.com/5Q1y0.webp)
InitializeAsync
方法中的await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(package.DisposalToken)
代码所做的吗?我应该只是复制它吗?异常处理呢?如果我允许同步的
void Execute()
处理程序引发异常,VS将捕获它并显示通用错误消息框。但是,如果我将其更改为async void Execute()
,则未捕获的异常不会在调用Execute
的线程上引发,并可能在其他地方引起更严重的问题。这里应该做什么正确的事情?通过同步访问Task.Result
以在正确的上下文中重新抛出异常似乎是众所周知的死锁的典型示例。我应该在实现中捕获所有异常,并为任何无法更优雅地处理的内容显示自己的通用消息框吗?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");
}
}
当单击自定义命令按钮时,VS会进行基本的错误处理并显示一个简单的消息框:
![basic error message box for synchronously-thrown exception](https://istack.dev59.com/Xz1Wq.webp)
现在假设我将自定义命令的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();
}
现在,当我点击命令按钮时,由于未处理的异常,Visual Studio会终止。
我的问题是:如何最佳实践地处理异步VSIX自定义命令实现引发的异常,这使得VS将异步代码中的未处理异常与同步代码中的未处理异常视为相同,而又不会冒着主线程死锁的风险?
MenuItemCallback()
,我认为这在当前模板中被Execute()
取代了。 - HydrargyrumMenuItemCallback()
方法(相当于Execute()
)对异步服务进行了fire-and-forget调用。它调用返回Task的服务而没有等待结果,并且没有处理异常。VS可以在不终止的情况下处理异常,但在这种情况下似乎完全未被观察到。我不认为这是最佳实践。 - Hydrargyrum