Skip to content

Commit 4e7109f

Browse files
committed
fix(Avalonia): catch exceptions on ui to prevent crashes
1 parent 5e60e5e commit 4e7109f

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

SnapX.Avalonia/App.axaml.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Avalonia;
55
using Avalonia.Controls;
66
using Avalonia.Controls.ApplicationLifetimes;
7+
using Avalonia.Controls.Primitives;
78
using Avalonia.Input;
89
using Avalonia.Input.Platform;
910
using Avalonia.Layout;
@@ -12,9 +13,11 @@
1213
using Avalonia.Media.Imaging;
1314
using Avalonia.Platform;
1415
using Avalonia.Styling;
16+
using Avalonia.Threading;
1517
using CommunityToolkit.Mvvm.DependencyInjection;
1618
using CommunityToolkit.Mvvm.Input;
1719
using CommunityToolkit.Mvvm.Messaging;
20+
using FluentAvalonia.UI.Controls;
1821
using FluentAvalonia.UI.Windowing;
1922
using Microsoft.Extensions.DependencyInjection;
2023
using Serilog;
@@ -376,6 +379,56 @@ private async void HandleClipboardCopyEvent(NeedClipboardCopyEvent @event)
376379
}
377380
}
378381

382+
private async void HandleErrorMessageEvent(ErrorMessageEvent @event)
383+
{
384+
await Dispatcher.UIThread.InvokeAsync(async () =>
385+
{
386+
try
387+
{
388+
var textBlock = new SelectableTextBlock
389+
{
390+
Text = @event.FullError
391+
? @event.Exception.ToString()
392+
: @event.Exception.Message,
393+
TextWrapping = TextWrapping.Wrap,
394+
MaxWidth = 600,
395+
};
396+
397+
var scrollViewer = new ScrollViewer
398+
{
399+
Content = textBlock,
400+
MaxHeight = 400,
401+
HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
402+
};
403+
404+
var dialog = new ContentDialog
405+
{
406+
Title = $"Error in {@event.Context}",
407+
Content = scrollViewer,
408+
CloseButtonText = "Close",
409+
DefaultButton = ContentDialogButton.Close,
410+
PrimaryButtonText = @event.FullError ? "Copy" : null,
411+
};
412+
TaskHelpers.PlayNotificationSoundAsync(NotificationSound.Error);
413+
var result = await dialog.ShowAsync();
414+
415+
if (result == ContentDialogResult.Primary)
416+
{
417+
var topLevel = TopLevel.GetTopLevel(
418+
MyMainWindow is not null ? MyMainWindow : dialog
419+
);
420+
await topLevel?.Clipboard?.SetTextAsync(@event.Exception.ToString());
421+
}
422+
}
423+
catch (Exception ex)
424+
{
425+
// Fallback to console if the UI is in a state where dialogs can't open
426+
DebugHelper.Logger?.Error("Critical: Could not open FluentAvalonia ContentDialog.");
427+
DebugHelper.Logger?.Error(ex.ToString());
428+
}
429+
});
430+
}
431+
379432
private static async Task<IClipboard> GetClipboardAsync()
380433
{
381434
if (Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
@@ -440,6 +493,19 @@ private static async Task<IClipboard> GetOrCreateClipboardWindowAsync()
440493

441494
public override void OnFrameworkInitializationCompleted()
442495
{
496+
// Crashes must be contained, AT ALL COSTS!
497+
Dispatcher.UIThread.UnhandledException += (s, e) =>
498+
{
499+
e.Handled = true;
500+
var ex = e.Exception;
501+
502+
ex.ShowError(true, "UI Dispatcher Critical Error");
503+
};
504+
TaskScheduler.UnobservedTaskException += (s, e) =>
505+
{
506+
e.SetObserved();
507+
e.Exception.ShowError(true, "Unobserved Task Exception");
508+
};
443509
var locator = new ViewLocator();
444510
DataTemplates.Add(locator);
445511
var services = new ServiceCollection();

SnapX.Core/Events.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public class NeedFileOpenerEvent
1515
public TaskSettings TaskSettings { get; set; }
1616
}
1717

18+
public record ErrorMessageEvent(Exception Exception, string Context, bool FullError);
19+
1820
public class NeedMainWindowHandle
1921
{
2022
// The subscriber will fill this property

0 commit comments

Comments
 (0)