From 6aeb97b0f78a5b3d61ff42d933cf8e8ce36c58ea Mon Sep 17 00:00:00 2001 From: Romain Ferraton <16419423+rferraton@users.noreply.github.com> Date: Wed, 18 Mar 2026 12:29:14 +0100 Subject: [PATCH 1/2] Fix issue #105 (#106) --- .../Controls/QuerySessionControl.axaml.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs index 76f6534..23ee6f6 100644 --- a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs +++ b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs @@ -319,10 +319,13 @@ private string GetTextFromCursor() return text[batchStart..batchEnd].Trim(); } + private void SetStatus(string text, bool autoClear = true) { _statusClearCts?.Cancel(); _statusClearCts?.Dispose(); + _statusClearCts = null; // ← prevent stale reference to a disposed CTS + StatusText.Text = text; if (autoClear && !string.IsNullOrEmpty(text)) @@ -331,12 +334,18 @@ private void SetStatus(string text, bool autoClear = true) var token = _statusClearCts.Token; _ = Task.Delay(3000, token).ContinueWith(_ => { - Avalonia.Threading.Dispatcher.UIThread.Post(() => StatusText.Text = ""); + Avalonia.Threading.Dispatcher.UIThread.Post(() => + { + StatusText.Text = ""; + _statusClearCts = null; // ← also clear after natural expiry + }); }, TaskContinuationOptions.OnlyOnRanToCompletion); + } } - private async void Connect_Click(object? sender, RoutedEventArgs e) + + private async void Connect_Click(object? sender, RoutedEventArgs e) { await ShowConnectionDialogAsync(); } From ae08ed5cb740f46442653b17b00d3499a5f03ff5 Mon Sep 17 00:00:00 2001 From: Erik Darling <2136037+erikdarlingdata@users.noreply.github.com> Date: Wed, 18 Mar 2026 06:30:06 -0500 Subject: [PATCH 2/2] Fix ObjectDisposedException crash in SetStatus (#105) (#107) The CTS disposal added in #97 could race with the 3-second Task.Delay continuation. If SetStatus was called again after the old CTS was disposed but before the continuation ran, Cancel() on the disposed CTS threw ObjectDisposedException, crashing the app. Fix: null out the field before Cancel/Dispose so the next call never touches the disposed instance. Closes #105 Co-authored-by: Claude Opus 4.6 (1M context) --- .../Controls/QuerySessionControl.axaml.cs | 23 ++++++++----------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs index 23ee6f6..fc29d39 100644 --- a/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs +++ b/src/PlanViewer.App/Controls/QuerySessionControl.axaml.cs @@ -322,30 +322,25 @@ private string GetTextFromCursor() private void SetStatus(string text, bool autoClear = true) { - _statusClearCts?.Cancel(); - _statusClearCts?.Dispose(); - _statusClearCts = null; // ← prevent stale reference to a disposed CTS + var old = _statusClearCts; + _statusClearCts = null; + old?.Cancel(); + old?.Dispose(); StatusText.Text = text; if (autoClear && !string.IsNullOrEmpty(text)) { - _statusClearCts = new CancellationTokenSource(); - var token = _statusClearCts.Token; - _ = Task.Delay(3000, token).ContinueWith(_ => + var cts = new CancellationTokenSource(); + _statusClearCts = cts; + _ = Task.Delay(3000, cts.Token).ContinueWith(_ => { - Avalonia.Threading.Dispatcher.UIThread.Post(() => - { - StatusText.Text = ""; - _statusClearCts = null; // ← also clear after natural expiry - }); + Avalonia.Threading.Dispatcher.UIThread.Post(() => StatusText.Text = ""); }, TaskContinuationOptions.OnlyOnRanToCompletion); - } } - - private async void Connect_Click(object? sender, RoutedEventArgs e) + private async void Connect_Click(object? sender, RoutedEventArgs e) { await ShowConnectionDialogAsync(); }