Browse Source

Add notification command exception handler

pull/165/head
Ionite 1 year ago
parent
commit
738c8d01d8
No known key found for this signature in database
  1. 76
      StabilityMatrix.Avalonia/Extensions/RelayCommandExtensions.cs
  2. 1
      StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj
  3. 12
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  4. 16
      StabilityMatrix.Avalonia/Views/SettingsPage.axaml

76
StabilityMatrix.Avalonia/Extensions/RelayCommandExtensions.cs

@ -0,0 +1,76 @@
using System;
using System.Diagnostics.CodeAnalysis;
using CommunityToolkit.Mvvm.Input;
using NLog;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Core.Extensions;
namespace StabilityMatrix.Avalonia.Extensions;
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public static class RelayCommandExtensions
{
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
/// <summary>
/// Attach an error handler to the command that will invoke the given action when an exception occurs.
/// </summary>
/// <param name="command">The command to attach the error handler to.</param>
/// <param name="onError">The action to invoke when an exception occurs.</param>
/// <exception cref="ArgumentException">Thrown if the command was not created with the FlowExceptionsToTaskScheduler option enabled.</exception>
public static T WithErrorHandler<T>(this T command, Action<Exception> onError) where T : IAsyncRelayCommand
{
if (command is AsyncRelayCommand relayCommand)
{
// Check that the FlowExceptionsToTaskScheduler flag is set
var options = relayCommand.GetPrivateField<AsyncRelayCommandOptions>("options");
if (!options.HasFlag(AsyncRelayCommandOptions.FlowExceptionsToTaskScheduler))
{
throw new ArgumentException(
"The command must be created with the FlowExceptionsToTaskScheduler option enabled"
);
}
}
command.PropertyChanged += (sender, e) =>
{
if (sender is not IAsyncRelayCommand senderCommand)
{
return;
}
// On ExecutionTask updates, check if there is an exception
if (
e.PropertyName == nameof(AsyncRelayCommand.ExecutionTask)
&& senderCommand.ExecutionTask is { Exception: { } exception }
)
{
onError(exception);
}
};
return command;
}
/// <summary>
/// Attach an error handler to the command that will log the error and show a notification.
/// </summary>
/// <param name="command">The command to attach the error handler to.</param>
/// <param name="notificationService">The notification service to use to show the notification.</param>
/// <param name="logLevel">The log level to use when logging the error. Defaults to LogLevel.Error</param>
/// <exception cref="ArgumentException">Thrown if the command was not created with the FlowExceptionsToTaskScheduler option enabled.</exception>
public static T WithNotificationErrorHandler<T>(
this T command,
INotificationService notificationService,
LogLevel? logLevel = default
) where T : IAsyncRelayCommand
{
logLevel ??= LogLevel.Error;
return command.WithErrorHandler(e =>
{
Logger.Log(logLevel, e, "Error executing command");
notificationService.ShowPersistent("Error", $"[{e.GetType().Name}] {e.Message}");
});
}
}

1
StabilityMatrix.Avalonia/StabilityMatrix.Avalonia.csproj

@ -45,6 +45,7 @@
<PackageReference Include="Polly.Contrib.WaitAndRetry" Version="1.1.1" />
<PackageReference Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageReference Include="Projektanker.Icons.Avalonia.FontAwesome" Version="6.6.0" />
<PackageReference Include="RockLib.Reflection.Optimized" Version="2.0.0" />
<PackageReference Include="Sentry" Version="3.33.1" />
<PackageReference Include="Sentry.NLog" Version="3.33.1" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.55" />

12
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -21,6 +21,7 @@ using FluentAvalonia.UI.Controls;
using NLog;
using SkiaSharp;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Extensions;
using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Services;
@ -112,6 +113,8 @@ public partial class SettingsViewModel : PageViewModelBase
settingsManager.RelayPropertyFor(this,
vm => vm.IsDiscordRichPresenceEnabled,
settings => settings.IsDiscordRichPresenceEnabled);
DebugThrowAsyncExceptionCommand.WithNotificationErrorHandler(notificationService, LogLevel.Warn);
}
partial void OnSelectedThemeChanged(string? value)
@ -387,10 +390,17 @@ public partial class SettingsViewModel : PageViewModelBase
[RelayCommand]
private void DebugThrowException()
{
// Use try-catch to generate traceback information
throw new OperationCanceledException("Example Message");
}
[RelayCommand(FlowExceptionsToTaskScheduler = true)]
private async Task DebugThrowAsyncException()
{
await Task.Yield();
throw new ApplicationException("Example Message");
}
[RelayCommand]
private async Task DebugMakeImageGrid()
{

16
StabilityMatrix.Avalonia/Views/SettingsPage.axaml

@ -226,9 +226,21 @@
<ui:SettingsExpanderItem Content="Exceptions" IconSource="Flag">
<ui:SettingsExpanderItem.Footer>
<Button
<SplitButton
Command="{Binding DebugThrowExceptionCommand}"
Content="Unhandled Exception"/>
Content="Command Exception">
<SplitButton.Flyout>
<ui:FAMenuFlyout>
<ui:MenuFlyoutItem
Text="Async Command Exception"
Command="{Binding DebugThrowAsyncExceptionCommand}"/>
</ui:FAMenuFlyout>
</SplitButton.Flyout>
</SplitButton>
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>

Loading…
Cancel
Save