From 1ee8b3283c3cf4262bf926fa243798fd2b809f3a Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Tue, 8 Sep 2026 14:21:44 +0200 Subject: [PATCH] Coalesce the global command re-query A raise walks every bound menu item and toolbar button and re-evaluates its CanExecute, so a caller reporting a burst of state changes paid for that walk once per change. WPF's CommandManager, which this class documents itself as mirroring, coalesces to an idle-priority raise; do the same. The pending flag stays set for the duration of the raise, so an invalidation issued by a CanExecute handler folds into the raise in progress instead of posting another one and never draining. Assisted-by: Claude:claude-opus-5[1m]:Claude Code Assisted-by: OpenCode:openai/gpt-5.5:OpenCode --- ILSpy/Commands/CommandManager.cs | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/ILSpy/Commands/CommandManager.cs b/ILSpy/Commands/CommandManager.cs index fdf8faca9d..1c4d630b5a 100644 --- a/ILSpy/Commands/CommandManager.cs +++ b/ILSpy/Commands/CommandManager.cs @@ -19,6 +19,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Threading; namespace ICSharpCode.ILSpy.Commands { @@ -77,11 +78,33 @@ public static void RemoveRequerySuggested(EventHandler handler) /// public static void InvalidateRequerySuggested() { - var dispatcher = global::Avalonia.Threading.Dispatcher.UIThread; - if (dispatcher.CheckAccess()) + // Coalesced, like the WPF CommandManager this mirrors: a raise walks every bound menu + // item and toolbar button and re-evaluates its CanExecute, so a caller that reports a + // burst of state changes (a multi-row selection, a batch of removed assemblies) would + // otherwise pay for that walk once per change. Posting at Background priority also lets + // the state settle first, so commands re-evaluate once against the final state. + if (Interlocked.Exchange(ref raisePending, 1) != 0) + return; + global::Avalonia.Threading.Dispatcher.UIThread.Post( + RaisePending, global::Avalonia.Threading.DispatcherPriority.Background); + } + + static int raisePending; + + static void RaisePending() + { + // The flag stays set for the duration of the raise, so an InvalidateRequerySuggested + // issued by a CanExecute handler folds into the raise already in progress instead of + // posting another one. Clearing it first would let the two re-post each other forever, + // and the dispatcher would never drain. + try + { Raise(); - else - dispatcher.Post(Raise); + } + finally + { + Interlocked.Exchange(ref raisePending, 0); + } } static void Raise()