Refactor UI thread invocation with DispatcherHelper

Introduce DispatcherHelper for safe UI thread access and replace direct Dispatcher.Invoke calls across the codebase. This centralizes thread invocation logic, reduces boilerplate, and improves maintainability. Some methods are refactored for clarity and UI thread safety.
This commit is contained in:
Jack251970 2026-03-10 11:48:55 +08:00
parent 916b4ff091
commit 036713c669
16 changed files with 177 additions and 91 deletions

View file

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure.Storage;
using Flow.Launcher.Plugin;
@ -99,14 +100,14 @@ namespace Flow.Launcher.Core.Plugin
{
case TextBox textBox:
var text = value as string ?? string.Empty;
textBox.Dispatcher.Invoke(() => textBox.Text = text);
DispatcherHelper.Invoke(() => textBox.Text = text);
break;
case PasswordBox passwordBox:
var password = value as string ?? string.Empty;
passwordBox.Dispatcher.Invoke(() => passwordBox.Password = password);
DispatcherHelper.Invoke(() => passwordBox.Password = password);
break;
case ComboBox comboBox:
comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value);
DispatcherHelper.Invoke(() => comboBox.SelectedItem = value);
break;
case CheckBox checkBox:
var isChecked = value is bool boolValue
@ -114,7 +115,7 @@ namespace Flow.Launcher.Core.Plugin
// If can parse the default value to bool, use it, otherwise use false
: value is string stringValue && bool.TryParse(stringValue, out var boolValueFromString)
&& boolValueFromString;
checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = isChecked);
DispatcherHelper.Invoke(() => checkBox.IsChecked = isChecked);
break;
}
}

View file

@ -0,0 +1,101 @@
using System;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Threading;
namespace Flow.Launcher.Core.Resource;
#pragma warning disable VSTHRD001 // Avoid legacy thread switching APIs
public static class DispatcherHelper
{
public static void Invoke(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
{
Invoke(Application.Current?.Dispatcher, action, priority);
}
public static T Invoke<T>(Func<T> func, DispatcherPriority priority = DispatcherPriority.Normal)
{
return Invoke(Application.Current?.Dispatcher, func, priority);
}
public static void Invoke(Dispatcher dispatcher, Action action, DispatcherPriority priority = DispatcherPriority.Normal)
{
if (dispatcher == null) return;
if (dispatcher.CheckAccess())
{
action();
}
else
{
dispatcher.Invoke(action, priority);
}
}
public static T Invoke<T>(Dispatcher dispatcher, Func<T> func, DispatcherPriority priority = DispatcherPriority.Normal)
{
if (dispatcher == null) return default;
if (dispatcher.CheckAccess())
{
return func();
}
else
{
return dispatcher.Invoke(func, priority);
}
}
public static async Task InvokeAsync(Action action, DispatcherPriority priority = DispatcherPriority.Normal)
{
await InvokeAsync(Application.Current?.Dispatcher, action, priority);
}
public static async Task<T> InvokeAsync<T>(Func<T> func, DispatcherPriority priority = DispatcherPriority.Normal)
{
return await InvokeAsync(Application.Current?.Dispatcher, func, priority);
}
public static async Task InvokeAsync(Func<Task> func, DispatcherPriority priority = DispatcherPriority.Normal)
{
await InvokeAsync(Application.Current?.Dispatcher, func, priority);
}
public static async Task InvokeAsync(Dispatcher dispatcher, Action action, DispatcherPriority priority = DispatcherPriority.Normal)
{
if (dispatcher == null) return;
if (dispatcher.CheckAccess())
{
action();
}
else
{
await dispatcher.InvokeAsync(action, priority);
}
}
public static async Task<T> InvokeAsync<T>(Dispatcher dispatcher, Func<T> func, DispatcherPriority priority = DispatcherPriority.Normal)
{
if (dispatcher == null) return default;
if (dispatcher.CheckAccess())
{
return func();
}
else
{
return await dispatcher.InvokeAsync(func, priority);
}
}
public static async Task InvokeAsync(Dispatcher dispatcher, Func<Task> func, DispatcherPriority priority = DispatcherPriority.Normal)
{
if (dispatcher == null) return;
if (dispatcher.CheckAccess())
{
await func();
}
else
{
await dispatcher.InvokeAsync(func, priority);
}
}
}

View file

@ -596,7 +596,7 @@ namespace Flow.Launcher.Core.Resource
/// </summary>
public async Task RefreshFrameAsync()
{
await Application.Current.Dispatcher.InvokeAsync(() =>
await DispatcherHelper.InvokeAsync(() =>
{
// Get the actual backdrop type and drop shadow effect settings
var (backdropType, useDropShadowEffect) = GetActualValue();
@ -623,7 +623,7 @@ namespace Flow.Launcher.Core.Resource
/// </summary>
public async Task SetBlurForWindowAsync()
{
await Application.Current.Dispatcher.InvokeAsync(() =>
await DispatcherHelper.InvokeAsync(() =>
{
// Get the actual backdrop type and drop shadow effect settings
var (backdropType, _) = GetActualValue();

View file

@ -334,7 +334,7 @@ namespace Flow.Launcher
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
{
Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
pluginUpdateWindow.ShowDialog();
@ -345,7 +345,7 @@ namespace Flow.Launcher
// check updates on startup
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
{
Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
pluginUpdateWindow.ShowDialog();
@ -444,7 +444,7 @@ namespace Flow.Launcher
{
// Dispose needs to be called on the main Windows thread,
// since some resources owned by the thread need to be disposed.
_mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose);
DispatcherHelper.Invoke(_mainWindow?.Dispatcher, _mainWindow.Dispose);
_mainVM?.Dispose();
DialogJump.Dispose();
_internationalization.Dispose();

View file

@ -3,6 +3,7 @@ using System.IO.Pipes;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using Flow.Launcher.Core.Resource;
// http://blogs.microsoft.co.il/arik/2010/05/28/wpf-single-instance-application/
// modified to allow single instace restart
@ -100,7 +101,9 @@ namespace Flow.Launcher.Helper
await pipeServer.WaitForConnectionAsync();
// Do an asynchronous call to ActivateFirstInstance function
Application.Current?.Dispatcher.Invoke(ActivateFirstInstance);
#pragma warning disable VSTHRD103 // Call async methods when in an async method
DispatcherHelper.Invoke(ActivateFirstInstance);
#pragma warning restore VSTHRD103 // Call async methods when in an async method
// Disconect client
pipeServer.Disconnect();

View file

@ -3,9 +3,9 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Microsoft.Win32;
@ -22,11 +22,11 @@ public static class WallpaperPathRetrieval
public static Brush GetWallpaperBrush()
{
// Invoke the method on the UI thread
if (!Application.Current.Dispatcher.CheckAccess())
{
return Application.Current.Dispatcher.Invoke(GetWallpaperBrush);
}
return DispatcherHelper.Invoke(GetWallpaperBrush1);
}
private static Brush GetWallpaperBrush1()
{
try
{
var wallpaperPath = Win32Helper.GetWallpaperPath();

View file

@ -153,7 +153,7 @@ namespace Flow.Launcher
Localize.appUpdateButtonContent(),
() =>
{
Application.Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
var releaseNotesWindow = new ReleaseNotesWindow();
releaseNotesWindow.Show();
@ -225,7 +225,7 @@ namespace Flow.Launcher
{
case nameof(MainViewModel.MainWindowVisibilityStatus):
{
Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
if (_viewModel.MainWindowVisibilityStatus)
{
@ -269,7 +269,7 @@ namespace Flow.Launcher
{
// QueryTextBox seems to be update with a DispatcherPriority as low as ContextIdle.
// To ensure QueryTextBox is up to date with QueryText from the View, we need to Dispatch with such a priority
Dispatcher.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length);
DispatcherHelper.Invoke(() => QueryTextBox.CaretIndex = QueryTextBox.Text.Length);
_viewModel.QueryTextCursorMovedToEnd = false;
}
break;
@ -542,7 +542,7 @@ namespace Flow.Launcher
// Switch to Normal state
WindowState = WindowState.Normal;
Application.Current?.Dispatcher.Invoke(new Action(() =>
DispatcherHelper.Invoke(() =>
{
double normalWidth = Width;
double normalHeight = Height;
@ -555,7 +555,7 @@ namespace Flow.Launcher
{
DragMove();
}
}), DispatcherPriority.ApplicationIdle);
}, DispatcherPriority.ApplicationIdle);
}
else
{
@ -726,19 +726,8 @@ namespace Flow.Launcher
{
Win32Helper.RegisterSleepModeListener(() =>
{
if (Application.Current == null)
{
return;
}
// We must run InitSoundEffects on UI thread because MediaPlayer is a DispatcherObject
if (!Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(InitSoundEffects);
return;
}
InitSoundEffects();
DispatcherHelper.Invoke(InitSoundEffects);
});
}
catch (Exception e)

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
namespace Flow.Launcher
@ -29,11 +30,16 @@ namespace Flow.Launcher
MessageBoxImage icon = MessageBoxImage.None,
MessageBoxResult defaultResult = MessageBoxResult.OK)
{
if (!Application.Current.Dispatcher.CheckAccess())
{
return Application.Current.Dispatcher.Invoke(() => Show(messageBoxText, caption, button, icon, defaultResult));
}
return DispatcherHelper.Invoke(() => Show1(messageBoxText, caption, button, icon, defaultResult));
}
private static MessageBoxResult Show1(
string messageBoxText,
string caption = "",
MessageBoxButton button = MessageBoxButton.OK,
MessageBoxImage icon = MessageBoxImage.None,
MessageBoxResult defaultResult = MessageBoxResult.OK)
{
try
{
msgBox = new MessageBoxEx(button);

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Animation;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.SharedModels;
@ -80,12 +81,12 @@ namespace Flow.Launcher
Show();
await Dispatcher.InvokeAsync(async () =>
await DispatcherHelper.InvokeAsync(async () =>
{
if (!closing)
{
closing = true;
await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin);
fadeOutStoryboard.Begin();
}
});
}

View file

@ -3,6 +3,7 @@ using System.IO;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media.Animation;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Plugin.SharedModels;
@ -82,12 +83,12 @@ namespace Flow.Launcher
Show();
await Dispatcher.InvokeAsync(async () =>
await DispatcherHelper.InvokeAsync(async () =>
{
if (!closing)
{
closing = true;
await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin);
fadeOutStoryboard.Begin();
}
});
}

View file

@ -1,7 +1,7 @@
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Windows;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;
using Microsoft.Toolkit.Uwp.Notifications;
@ -41,7 +41,7 @@ namespace Flow.Launcher
public static void Show(string title, string subTitle, string iconPath = null)
{
Application.Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
ShowInternal(title, subTitle, iconPath);
});
@ -91,7 +91,7 @@ namespace Flow.Launcher
public static void ShowWithButton(string title, string buttonText, Action buttonAction, string subTitle, string iconPath = null)
{
Application.Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
ShowInternalWithButton(title, buttonText, buttonAction, subTitle, iconPath);
});

View file

@ -2,6 +2,7 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Flow.Launcher.Core.Resource;
namespace Flow.Launcher
{
@ -22,19 +23,7 @@ namespace Flow.Launcher
ProgressBoxEx progressBox = null;
try
{
if (!Application.Current.Dispatcher.CheckAccess())
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
progressBox = new ProgressBoxEx(cancelProgress)
{
Title = caption
};
progressBox.TitleTextBlock.Text = caption;
progressBox.Show();
});
}
else
await DispatcherHelper.InvokeAsync(() =>
{
progressBox = new ProgressBoxEx(cancelProgress)
{
@ -42,7 +31,7 @@ namespace Flow.Launcher
};
progressBox.TitleTextBlock.Text = caption;
progressBox.Show();
}
});
await reportProgressAsync(progressBox.ReportProgress).ConfigureAwait(false);
}
@ -54,28 +43,20 @@ namespace Flow.Launcher
}
finally
{
if (!Application.Current.Dispatcher.CheckAccess())
{
await Application.Current.Dispatcher.InvokeAsync(() =>
{
progressBox?.Close();
});
}
else
await DispatcherHelper.InvokeAsync(() =>
{
progressBox?.Close();
}
});
}
}
private void ReportProgress(double progress)
{
if (!Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(() => ReportProgress(progress));
return;
}
DispatcherHelper.Invoke(() => ReportProgress1(progress));
}
private void ReportProgress1(double progress)
{
if (progress < 0)
{
ProgressBar.Value = 0;

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Specialized;
@ -141,7 +141,7 @@ namespace Flow.Launcher
public void OpenSettingDialog()
{
Application.Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
SettingWindow sw = SingletonWindowOpener.Open<SettingWindow>();
});

View file

@ -9,6 +9,7 @@ using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure.Http;
using iNKORE.UI.WPF.Modern;
@ -28,7 +29,7 @@ namespace Flow.Launcher
private void ThemeManager_ActualApplicationThemeChanged(ThemeManager sender, object args)
{
Application.Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
if (ThemeManager.Current.ActualApplicationTheme == ApplicationTheme.Light)
{
@ -124,7 +125,7 @@ namespace Flow.Launcher
{
var output = await GetReleaseNotesMarkdownAsync().ConfigureAwait(false);
Application.Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
RefreshProgressRing.Visibility = Visibility.Collapsed;
if (string.IsNullOrEmpty(output))

View file

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using System.Windows;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Plugin;
using Flow.Launcher.ViewModel;
@ -115,7 +116,7 @@ public partial class SettingsPanePluginStoreViewModel : BaseModel
{
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
{
Application.Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
pluginUpdateWindow.ShowDialog();

View file

@ -16,6 +16,7 @@ using System.Windows.Threading;
using CommunityToolkit.Mvvm.DependencyInjection;
using CommunityToolkit.Mvvm.Input;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Helper;
using Flow.Launcher.Infrastructure;
using Flow.Launcher.Infrastructure.DialogJump;
@ -763,12 +764,11 @@ namespace Flow.Launcher.ViewModel
public void ChangeQueryText(string queryText, bool isReQuery = false)
{
// Must check access so that we will not block the UI thread which causes window visibility issue
if (!Application.Current.Dispatcher.CheckAccess())
{
Application.Current.Dispatcher.Invoke(() => ChangeQueryText(queryText, isReQuery));
return;
}
DispatcherHelper.Invoke(() => ChangeQueryText1(queryText, isReQuery));
}
private void ChangeQueryText1(string queryText, bool isReQuery = false)
{
if (QueryText != queryText)
{
// Change query text first
@ -795,12 +795,11 @@ namespace Flow.Launcher.ViewModel
private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false)
{
// Must check access so that we will not block the UI thread which causes window visibility issue
if (!Application.Current.Dispatcher.CheckAccess())
{
await Application.Current.Dispatcher.InvokeAsync(() => ChangeQueryTextAsync(queryText, isReQuery));
return;
}
await DispatcherHelper.InvokeAsync(() => ChangeQueryText1Async(queryText, isReQuery));
}
private async Task ChangeQueryText1Async(string queryText, bool isReQuery = false)
{
if (QueryText != queryText)
{
// Change query text first
@ -1951,10 +1950,12 @@ namespace Flow.Launcher.ViewModel
if (dialogWindowHandleChanged)
{
// Only update the position
Application.Current?.Dispatcher.Invoke(() =>
#pragma warning disable VSTHRD103 // Call async methods when in an async method
DispatcherHelper.Invoke(() =>
{
(Application.Current?.MainWindow as MainWindow)?.UpdatePosition();
});
#pragma warning restore VSTHRD103 // Call async methods when in an async method
_ = ResetWindowAsync();
}
@ -2049,7 +2050,7 @@ namespace Flow.Launcher.ViewModel
if (_previousMainWindowVisibilityStatus)
{
// Only update the position
Application.Current?.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
(Application.Current?.MainWindow as MainWindow)?.UpdatePosition();
});
@ -2112,7 +2113,7 @@ namespace Flow.Launcher.ViewModel
if (App.LoadingOrExiting) return;
// When application is exiting, the Application.Current will be null
Application.Current?.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
// When application is exiting, the Application.Current will be null
if (Application.Current?.MainWindow is MainWindow mainWindow)
@ -2194,7 +2195,7 @@ namespace Flow.Launcher.ViewModel
}
// When application is exiting, the Application.Current will be null
Application.Current?.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
// When application is exiting, the Application.Current will be null
if (Application.Current?.MainWindow is MainWindow mainWindow)
@ -2328,7 +2329,7 @@ namespace Flow.Launcher.ViewModel
public void FocusQueryTextBox()
{
// When application is exiting, the Application.Current will be null
Application.Current?.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
// When application is exiting, the Application.Current will be null
if (Application.Current?.MainWindow is MainWindow window)