[Question] Global exception handling & IAsyncRelayCommand #175
-
Forgive me if I'm overlooking something, but I can't seem to figure out how to catch exceptions thrown from a AsyncRelayCommand handler in my global exception handlers. Is this even possible? With RelayCommand it works fine, but with AsyncRelayCommand I can only see the exception in the Visual Studio output window. MainWindow.xaml
MainWindowViewModel.cs
App.xaml.cs
When I was using Caliburn.Micro framework a few years back, I had a similar issue when I switched from
Is there something similar that I have to do with MVVM Toolkit? Thank you very much! 🙏 EDIT: I'm using WPF |
Beta Was this translation helpful? Give feedback.
Replies: 16 comments 6 replies
-
Hello ShahinDohan, thank you for your interest in Windows Community Toolkit! I have automatically added a "needs triage" label to help get things started. Our team will analyze and investigate the issue, and escalate it to the relevant team if possible.. Other community members may also answer the question and provide feedback 🙌 |
Beta Was this translation helpful? Give feedback.
-
Any ideas @Sergio0694? |
Beta Was this translation helpful? Give feedback.
-
Hi @ShahinDohan, the reason why faulted async commands are not bubbling up is because we're explicitly ignoring exceptions there to avoid crashing the entire application in case a commands fails to execute but the exception isn't handled. This method is called internally by AsyncRelayCommand command = ...; // Assume we have a command here
command.PropertyChanged += (s, e) =>
{
if (e.PropertyName == nameof(AsyncRelayCommand.ExecutionTask) &&
((AsyncRelayCommand)s).ExecutionTask is Task task &&
task.Exception is AggregateException exception)
{
// Log your exception here
}
}; As in, the Let me know what you think! cc. @michael-hawker |
Beta Was this translation helpful? Give feedback.
-
@Sergio0694 Thank you Sergio for the explanation! I tried your solution and it worked, but to be honest it's quite ugly to have that for every async command 😄 I don't know how useful others would find this, but to me it sounds like a very important feature to have. I would never expect uncaught exceptions to be swallowed. The user would never know something went wrong unless we explicitly catch exceptions in the command handler and show a message box or at least log the error, and even then developers could easily forget to do that. I would definitely appreciate a dedicated event for this if possible 🙏 Thank you! |
Beta Was this translation helpful? Give feedback.
-
Happy to help! 🙂
Just to be clear, I was referring to an instance event on the command type(s), not a global static one. Alternatively, just hypothetically speaking, would you rather have commands throw exceptions directly? |
Beta Was this translation helpful? Give feedback.
-
Oh, sorry I thought it would be a global static one. I don't think an instance event would be too helpful since I can just catch the exception in my async command handler, but I'd like the exception to propagate if not handled.
Well since the regular RelayCommand does exactly that, I expected the async one to work the same way. Maybe there's a technical reason why that's not a good idea, but yes I would personally prefer that it just threw the exceptions directly. I think it's the developers responsibility to prevent the application from crashing by handling the exception in the global exception handlers, like what I'm doing as well. My 2 cents 🙂 |
Beta Was this translation helpful? Give feedback.
-
Yeah, that's a fair point. The reason why Eg. you might prefer a single static event, whereas others might prefer individual instance events for every command. |
Beta Was this translation helpful? Give feedback.
-
Yeah I figured there was some technical reason for all of this. Unfortunately I don't know what would work for everyone, perhaps others can share their opinions on the matter, but personally I think it's nice to have some way to globally catch these exceptions so they don't just silently disappear. Caliburn.Micro's Coroutine solution was quite nice, and it always showed useful stack traces as far as I remember. Perhaps something inspired by that could be done in MVVM Toolkit? For now I'll just have to remember to add a try-catch in every AsyncRelayCommand handler, and use my awesome MVVM DialogService to show the error in the UI and log it. Curious to hear what others think! |
Beta Was this translation helpful? Give feedback.
-
@Sergio0694 Thanks for the explanation, I found it quite informative. |
Beta Was this translation helpful? Give feedback.
-
I ended up creating a Factory interface in the Mvvm Layer /// <summary>
/// Factory for creating <see cref="AsyncRelayCommand"/>s with additional processing.
/// </summary>
public interface IAsyncRelayCommandFactory
{
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<Task> execute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand Create(Func<Task> execute, Func<bool> canExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="cancelableExecute">The cancelable execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute, Predicate<T?> canExecute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="execute">The execution logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute);
/// <summary>
/// Create a reference to AsyncRelayCommand.
/// </summary>
/// <typeparam name="T">The type of the command parameter.</typeparam>
/// <param name="execute">The execution logic.</param>
/// <param name="canExecute">The execution status logic.</param>
/// <returns>AsyncRelayCommand.</returns>
AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute, Predicate<T?> canExecute);
} and its platform specific implementation in the application layer. This one is for a WPF app, but some parts could be different for Blazor: public class AsyncRelayCommandFactory : IAsyncRelayCommandFactory
{
private readonly ILogger<AsyncRelayCommandFactory> logger;
private readonly IDialogService dialogService;
public AsyncRelayCommandFactory(ILogger<AsyncRelayCommandFactory> logger, IDialogService dialogService)
{
this.logger = logger;
this.dialogService = dialogService;
}
public AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute)
=> Register(new AsyncRelayCommand<T>(execute));
public AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute)
=> Register(new AsyncRelayCommand<T>(cancelableExecute));
public AsyncRelayCommand<T> Create<T>(Func<T?, Task> execute, Predicate<T?> canExecute)
=> Register(new AsyncRelayCommand<T>(execute, canExecute));
public AsyncRelayCommand<T> Create<T>(Func<T?, CancellationToken, Task> cancelableExecute, Predicate<T?> canExecute)
=> Register(new AsyncRelayCommand<T>(cancelableExecute, canExecute));
public AsyncRelayCommand Create(Func<Task> execute)
=> Register(new AsyncRelayCommand(execute));
public AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute)
=> Register(new AsyncRelayCommand(cancelableExecute));
public AsyncRelayCommand Create(Func<Task> execute, Func<bool> canExecute)
=> Register(new AsyncRelayCommand(execute, canExecute));
public AsyncRelayCommand Create(Func<CancellationToken, Task> cancelableExecute, Func<bool> canExecute)
=> Register(new AsyncRelayCommand(cancelableExecute, canExecute));
private AsyncRelayCommand Register(AsyncRelayCommand command)
{
RegisterError(command);
return command;
}
private AsyncRelayCommand<T> Register<T>(AsyncRelayCommand<T> command)
{
RegisterError(command);
return command;
}
private void RegisterError(IAsyncRelayCommand command) => command.PropertyChanged += (s, e) =>
{
if (s is null)
{
return;
}
if (e.PropertyName == nameof(AsyncRelayCommand.ExecutionTask) &&
((IAsyncRelayCommand)s).ExecutionTask is Task task &&
task.Exception is AggregateException exception)
{
logger.LogCritical(exception, "Exception");
var errorVm = new ErrorDialogViewModel(exception, additionalArgs);
dialogService.ShowDialog(this, errorVm);
}
};
} The interface would be injected into the caller class and used like this: private readonly IAsyncRelayCommandFactory asyncRelayCommandFactory;
public MyViewModel(IAsyncRelayCommandFactory asyncRelayCommandFactory)
{
this.asyncRelayCommandFactory = asyncRelayCommandFactory;
}
...
MyCommandNumber156 = asyncRelayCommandFactory.Create<MyClass>(async arg => await SomeMethodAsync(arg, param1)); |
Beta Was this translation helpful? Give feedback.
-
As @virzak pointed out, this can really be a problem if a developer forgets to catch the exception, because then it just disappears and the user is left wondering what happened, and the developer has no logs to look at. I really like the way Caliburn.Micro handles this with a custom global event handler:
As I mentioned before I had a similar issue like this in Caliburn.Micro, but they provided a way to catch the exceptions globally using the Could we have something similar in MVVM Toolkit? I think this really needs to be addressed as it's quite a big "gotcha" with potentially huge consequences. Preferably the exception would be thrown, because developers can forget to subscribe to the custom event handler as well, but if there are technical limitations then the custom event handler solution seems to be the next best thing. @Sergio0694 Thoughts? Thanks! |
Beta Was this translation helpful? Give feedback.
-
Hello. I just started using Windows Comunity Toolkit MVVM and I don't really know what would be the best approach to handle exceptions inside async commands. I also expect the exception to just be thrown. Any idea? I tried to extend to |
Beta Was this translation helpful? Give feedback.
-
@Sergio0694, looks like there is no way to use [ICommand] source generator without swallowing the exception at this point. Are you considering a global error handling mechanism? |
Beta Was this translation helpful? Give feedback.
-
Implementing an error handling mechanism is ideal but I think more problematic is the way the Task version is being called, which has different implications (exception handling, unobserved exceptions, etc.). I noticed that in the implementation of the
Isn't this (very) problematic? |
Beta Was this translation helpful? Give feedback.
-
I would like to know if there is any progress so far or a more appropriate way to deal with it? |
Beta Was this translation helpful? Give feedback.
-
Given the first post mentions |
Beta Was this translation helpful? Give feedback.
Given the first post mentions
TaskScheduler.UnobservedException
, I'll mention that as of #113, this is now resolved, and all exceptions being thrown from async commands will correctly bubble up to that global event if not handled before.