Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
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;

Expand Down Expand Up @@ -99,22 +100,22 @@
{
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
? boolValue
// 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;
}
}
Expand Down Expand Up @@ -422,14 +423,14 @@
}
case "hyperlink":
{
var hyperlink = new Hyperlink

Check warning on line 426 in Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`hyperlink` is not a recognized word. (unrecognized-spelling)
{
ToolTip = attributes.Description,
NavigateUri = attributes.url
};

hyperlink.Inlines.Add(attributes.urlLabel);

Check warning on line 432 in Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`hyperlink` is not a recognized word. (unrecognized-spelling)
hyperlink.RequestNavigate += (sender, e) =>

Check warning on line 433 in Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`hyperlink` is not a recognized word. (unrecognized-spelling)
{
API.OpenUrl(e.Uri);
e.Handled = true;
Expand All @@ -443,7 +444,7 @@
TextAlignment = TextAlignment.Left,
TextWrapping = TextWrapping.Wrap
};
textBlock.Inlines.Add(hyperlink);

Check warning on line 447 in Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`hyperlink` is not a recognized word. (unrecognized-spelling)

contentControl = textBlock;

Expand Down
102 changes: 102 additions & 0 deletions Flow.Launcher.Core/Resource/DispatcherHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
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
{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai are there any logic changes in this PR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jjw24, based on a detailed review of the AI-generated summaries and the diffs, the PR is predominantly a refactoring with no intentional logic changes. However, there are a few subtle behavioral differences worth being aware of:

  1. Msg.xaml.cs & MsgWithButton.xaml.cs — Nested dispatch removal:
    The old code had a nested await Dispatcher.InvokeAsync(...) wrapping fadeOutStoryboard.Begin() inside an outer dispatched block. The new code removes that inner dispatch and calls fadeOutStoryboard.Begin() directly inside the DispatcherHelper.InvokeAsync delegate. Since the outer delegate is already guaranteed to run on the UI thread, this is functionally equivalent — but it is a structural change.

  2. DispatcherHelper fast-path ignores DispatcherPriority:
    In both Invoke and InvokeAsync, when CheckAccess() is true (already on the UI thread), the delegate is executed directly without going through the dispatcher — meaning the DispatcherPriority parameter is silently ignored in the fast path. Based on a prior interaction, this is an intentional design choice. Call sites that previously relied on priority scheduling (e.g., DispatcherPriority.Render in Theme.cs, DispatcherPriority.ApplicationIdle in MainWindow.xaml.cs) will have the priority honored only when called off the UI thread.

  3. ProgressBoxEx.xaml.cs — CheckAccess branching removed:
    Explicit CheckAccess() + conditional Invoke logic was replaced with DispatcherHelper.Invoke(...), which handles CheckAccess internally. Functionally equivalent.

  4. App.xaml.cs disposal — null handling:
    Changed from _mainWindow?.Dispatcher.Invoke(_mainWindow.Dispose) (null-conditional, skips entirely if null) to DispatcherHelper.Invoke(_mainWindow?.Dispatcher, _mainWindow.Dispose) (passes a potentially-null dispatcher, which DispatcherHelper early-returns on). Functionally equivalent.

In summary: No meaningful logic changes, but items 1 and 2 are the most notable structural differences to validate if you want extra confidence.


🧠 Learnings used
Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 4335
File: Flow.Launcher.Core/Resource/DispatcherHelper.cs:63-85
Timestamp: 2026-03-10T04:07:09.482Z
Learning: In Flow.Launcher.Core/Resource/DispatcherHelper.cs, the InvokeAsync overloads use a fast path via CheckAccess(): if already on the UI thread, execute the action/func directly without dispatcher.InvokeAsync, even if a DispatcherPriority is provided. When reviewing this file or related code, assume this is an intentional design choice by the maintainer. Verify that tests or code paths confirm the UI-thread fast-path is employed, and ensure there is no unnecessary dispatching when CheckAccess() is true. Consider adding/maintaining tests that cover InvokeAsync on the UI thread with various priorities, and document that the priority parameter is ignored in the fast-path.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3854
File: Flow.Launcher/App.xaml.cs:252-275
Timestamp: 2025-10-16T10:48:30.573Z
Learning: In Flow Launcher's App.xaml.cs, the plugin initialization block (lines 252-275) that includes PreStartPluginExecutablePathUpdate, PluginManager.LoadPlugins, PluginManager.InitializePluginsAsync, _mainVM.QueryResults(), and API.SaveAppAllSettings() does not require additional Task.Run wrappers or Dispatcher.InvokeAsync calls according to maintainer Jack251970, as the threading model is already safe as designed.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3854
File: Flow.Launcher/App.xaml.cs:246-262
Timestamp: 2025-07-21T09:19:49.684Z
Learning: In Flow Launcher's App.xaml.cs, the asynchronous plugin initialization task (containing AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate, PluginManager.LoadPlugins, PluginManager.InitializePluginsAsync, and AutoPluginUpdates) does not require additional try-catch error handling according to maintainer Jack251970, as these operations are designed to handle exceptions internally.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3672
File: Flow.Launcher/MainWindow.xaml.cs:318-318
Timestamp: 2025-06-08T14:12:12.842Z
Learning: In Flow.Launcher, the App.NotifyIcon static property is initialized in the App class before MainWindow creation, so null checks are not needed when accessing App.NotifyIcon in MainWindow lifecycle methods.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3672
File: Flow.Launcher/MainWindow.xaml.cs:244-247
Timestamp: 2025-06-08T14:12:21.348Z
Learning: In Flow.Launcher, App.NotifyIcon is created before MainWindow creation, so null checks for App.NotifyIcon are not necessary when accessing it from MainWindow code.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3593
File: Flow.Launcher/HotkeyControlDialog.xaml:6-6
Timestamp: 2025-09-05T11:56:27.267Z
Learning: In Flow.Launcher's migration to iNKORE.UI.WPF.Modern UI framework, dialog resource keys like PopuBGColor, PopupButtonAreaBGColor, PopupButtonAreaBorderColor, and PopupTextColor are provided by the iNKORE.UI.WPF.Modern framework itself, not defined locally in the codebase theme files.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 4319
File: Flow.Launcher.Core/Resource/Theme.cs:676-682
Timestamp: 2026-03-08T15:54:13.490Z
Learning: In WPF-based C# code reviews, note that Border.BackgroundProperty, Control.BackgroundProperty, and Panel.BackgroundProperty share the same DependencyProperty instance. Because Border and Control add ownership to Panel.BackgroundProperty, comparing a Setter's Property against Control.BackgroundProperty will match setters registered for Border.BackgroundProperty as well. When reviewing code that relies on setter-property identity for BackgroundProperty, assume reference equality across these types and verify behavior accordingly.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. No need to call dispatcher again.
  2. If it is already within the UI thread, the priority cannot be changed.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai For item 4, previously, some callsites used control.Dispatcher (which would exist if the control exists) or would throw (making the bug obvious). With the helper, does it mean you can end up with missing UI updates that are harder to diagnose

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Look at the full DispatcherHelper implementation to confirm null handling
cat Flow.Launcher.Core/Resource/DispatcherHelper.cs

Length of output: 3087


@jjw24, that's a valid concern. Here's the breakdown:

For the specific App.xaml.cs case (item 4):
The old code was already _mainWindow?.Dispatcher.Invoke(...) (null-conditional), so both old and new paths silently skip when _mainWindow is null. No behavioral change there specifically.

The broader risk you're pointing at:
The real exposure is the many other call sites that changed from a non-null-conditional pattern:

// Old — throws NullReferenceException if Application.Current is null (bug is obvious)
Application.Current.Dispatcher.Invoke(someAction);

// New — silently no-ops if Application.Current?.Dispatcher resolves to null (bug is hidden)
DispatcherHelper.Invoke(someAction);

Confirmed from the implementation — the no-dispatcher overloads delegate to Application.Current?.Dispatcher, and the dispatcher overloads short-circuit with if (dispatcher == null) return; / return default. There's no logging, no assertion, no exception on null.

This means:

Scenario Old behavior New behavior
Application.Current is null at shutdown Throws (noisy) Silently skips (safe)
Application.Current is unexpectedly null due to a bug Throws (obvious) Silently skips (hidden)
Func<T> overload, dispatcher is null Throws Returns default(T) — could propagate a wrong value upstream

In practice, Application.Current being null outside of shutdown is rare in a WPF app, so the risk is mostly theoretical. The silent-skip behavior is actually preferable during shutdown (e.g., avoiding crashes in Dispose). However, if you'd like stronger diagnostics, a Debug.Assert or a conditional log could be added inside DispatcherHelper for null-dispatcher scenarios without affecting release behavior:

public static void Invoke(Dispatcher dispatcher, Action action, DispatcherPriority priority = DispatcherPriority.Normal)
{
    if (dispatcher == null)
    {
        Debug.Assert(false, "DispatcherHelper.Invoke called with null dispatcher");
        return;
    }
    // ...
}

This would preserve silent-skip in release builds while making it diagnosable in debug sessions.


🧠 Learnings used
Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 4335
File: Flow.Launcher.Core/Resource/DispatcherHelper.cs:63-85
Timestamp: 2026-03-10T04:07:09.482Z
Learning: In Flow.Launcher.Core/Resource/DispatcherHelper.cs, the InvokeAsync overloads use a fast path via CheckAccess(): if already on the UI thread, execute the action/func directly without dispatcher.InvokeAsync, even if a DispatcherPriority is provided. When reviewing this file or related code, assume this is an intentional design choice by the maintainer. Verify that tests or code paths confirm the UI-thread fast-path is employed, and ensure there is no unnecessary dispatching when CheckAccess() is true. Consider adding/maintaining tests that cover InvokeAsync on the UI thread with various priorities, and document that the priority parameter is ignored in the fast-path.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3854
File: Flow.Launcher/App.xaml.cs:252-275
Timestamp: 2025-10-16T10:48:30.573Z
Learning: In Flow Launcher's App.xaml.cs, the plugin initialization block (lines 252-275) that includes PreStartPluginExecutablePathUpdate, PluginManager.LoadPlugins, PluginManager.InitializePluginsAsync, _mainVM.QueryResults(), and API.SaveAppAllSettings() does not require additional Task.Run wrappers or Dispatcher.InvokeAsync calls according to maintainer Jack251970, as the threading model is already safe as designed.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3854
File: Flow.Launcher/App.xaml.cs:246-262
Timestamp: 2025-07-21T09:19:49.684Z
Learning: In Flow Launcher's App.xaml.cs, the asynchronous plugin initialization task (containing AbstractPluginEnvironment.PreStartPluginExecutablePathUpdate, PluginManager.LoadPlugins, PluginManager.InitializePluginsAsync, and AutoPluginUpdates) does not require additional try-catch error handling according to maintainer Jack251970, as these operations are designed to handle exceptions internally.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3672
File: Flow.Launcher/MainWindow.xaml.cs:318-318
Timestamp: 2025-06-08T14:12:12.842Z
Learning: In Flow.Launcher, the App.NotifyIcon static property is initialized in the App class before MainWindow creation, so null checks are not needed when accessing App.NotifyIcon in MainWindow lifecycle methods.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3672
File: Flow.Launcher/MainWindow.xaml.cs:244-247
Timestamp: 2025-06-08T14:12:21.348Z
Learning: In Flow.Launcher, App.NotifyIcon is created before MainWindow creation, so null checks for App.NotifyIcon are not necessary when accessing it from MainWindow code.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3593
File: Flow.Launcher/HotkeyControlDialog.xaml:6-6
Timestamp: 2025-09-05T11:56:27.267Z
Learning: In Flow.Launcher's migration to iNKORE.UI.WPF.Modern UI framework, dialog resource keys like PopuBGColor, PopupButtonAreaBGColor, PopupButtonAreaBorderColor, and PopupTextColor are provided by the iNKORE.UI.WPF.Modern framework itself, not defined locally in the codebase theme files.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 4319
File: Flow.Launcher.Core/Resource/Theme.cs:676-682
Timestamp: 2026-03-08T15:54:13.490Z
Learning: In WPF-based C# code reviews, note that Border.BackgroundProperty, Control.BackgroundProperty, and Panel.BackgroundProperty share the same DependencyProperty instance. Because Border and Control add ownership to Panel.BackgroundProperty, comparing a Setter's Property against Control.BackgroundProperty will match setters registered for Border.BackgroundProperty as well. When reviewing code that relies on setter-property identity for BackgroundProperty, assume reference equality across these types and verify behavior accordingly.

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coderabbitai give your opinion on the below three points and address these questions:

  1. Potentially changes which dispatcher is used- many replacements now use Application.Current.Dispatcher via the helper, instead of a specific DispatcherObject.Dispatcher (like textBox.Dispatcher). In most WPF apps those are the same UI dispatcher, but if any UI object is associated with a different dispatcher, would this change behaviour? How rare would this type of scenario be?

  2. Async semantics may change timing and ordering- I feel because Func is now fully awaited when invoked through the helper, some flows could become more sequential (callers now wait for completion where previously they may have effectively “fire-and-forget”-ed part of the work). Can this expose deadlocks or responsiveness issues if any UI-thread-dispatched async work blocks or takes longer than expected?

  3. Harder to see “this is definitely UI-thread” at the callsite- centralization improves consistency, but it can obscure that some operations are happening on the UI thread (and should remain small/fast). Would reviewers now have to trust/inspect the helper?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jjw24, great questions — here's my assessment of each point:


1. Potentially changing which dispatcher is used

In practice, this is almost always a non-issue. In a standard single-threaded WPF application, every UIElement.Dispatcher property returns the same underlying Dispatcher instance as Application.Current.Dispatcher — they are literally the same object reference, since all UI elements are created on (and owned by) the same thread.

The scenario where a specific DispatcherObject.Dispatcher differs from Application.Current.Dispatcher requires UI objects to be explicitly created on a separate thread with its own Dispatcher loop (e.g., a splash screen or a secondary window on a dedicated UI thread). This pattern is extremely rare, advanced, and nothing in Flow Launcher's codebase suggests it is used.

Additionally, the helper already provides the escape hatch: where a specific dispatcher is needed, the overload Invoke(Dispatcher dispatcher, ...) is available and is already used — e.g., in App.xaml.cs:

// Specific dispatcher still passed explicitly
DispatcherHelper.Invoke(_mainWindow?.Dispatcher, _mainWindow.Dispose)

Risk level: Low. The only real risk would be if a future contributor adds a secondary-thread UI element and uses DispatcherHelper.Invoke(action) (no explicit dispatcher) — but that would be a misuse of the helper in a rare scenario, not a defect in the refactoring itself.


2. Async semantics changing timing and ordering

This is the most substantive concern. Two sub-cases to consider:

a) Func<Task> now fully awaited:
Previously, patterns like dispatcher.InvokeAsync(async () => { ... }) would return a DispatcherOperation<Task> — the outer operation completes when the delegate starts, not when the inner Task completes. Callers who didn't await that inner task effectively fire-and-forgot it. The helper now double-awaits, so callers wait for full completion. This makes flows more sequential and deterministic, which is generally safer — but could expose latency if any dispatched async block takes longer than expected.

b) Deadlock risk:
With InvokeAsync (non-blocking dispatch), deadlock risk is low. The bigger concern would be with Invoke (synchronous) — if called from a background thread with a delegate that itself tries to await something back on the UI thread, that would deadlock. However, the Invoke overloads accept Action/Func<T> (not Func<Task>), so async-in-sync is not directly exposed through the helper's API surface.

Concrete example to watch: In Msg.xaml.cs and MsgWithButton.xaml.cs, the nested await Dispatcher.InvokeAsync(...) around fadeOutStoryboard.Begin() was removed — Begin() is now called directly inside the outer DispatcherHelper.InvokeAsync delegate. This is correct (already on the UI thread), but it's worth confirming that the storyboard's Completed event or any continuation still fires as expected under all paths.

Risk level: Medium. The double-await change is a real semantic shift worth explicitly documenting in the helper's XML doc comments for InvokeAsync(Func<Task>), so future callers understand the contract.


3. Harder to see "this is definitely UI-thread" at the callsite

This is a real but manageable readability trade-off. Previously, Application.Current.Dispatcher.Invoke(...) made it visually obvious that UI-thread dispatch was happening. With the helper, DispatcherHelper.Invoke(...) still signals a dispatcher is being used, but the mechanism is one level of abstraction away.

The main reviewer burden becomes: you now need to trust that DispatcherHelper.Invoke always dispatches to the UI thread rather than seeing it spelled out. A few mitigations worth considering:

  • XML doc comments on the helper methods clearly stating "executes on the UI thread via Application.Current.Dispatcher" would help future reviewers.
  • The fast-path via CheckAccess() (as noted in a prior learning) is intentional and correct — but this too is now hidden inside the helper, so reviewers inspecting a callsite won't immediately know whether a dispatch actually occurs.
  • The #pragma warning disable VSTHRD001 suppression at the file level covers all methods — this is fine, but it means static analysis won't flag misuse.

Recommendation: Consider adding a /// <summary> doc comment to each public method in DispatcherHelper explaining the dispatcher source, priority semantics, and the fast-path behavior. This would reduce the "trust the helper" burden for reviewers significantly.


Summary table:

Concern Risk Mitigation
Wrong dispatcher used Low Explicit-dispatcher overloads exist; single-thread WPF norm
Async timing/ordering change Medium Func<Task> double-await is a real semantic change; add XML docs
Reduced callsite clarity Low–Medium Add XML doc comments to the helper

🧠 Learnings used
Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 4335
File: Flow.Launcher.Core/Resource/DispatcherHelper.cs:63-85
Timestamp: 2026-03-10T04:07:09.482Z
Learning: In Flow.Launcher.Core/Resource/DispatcherHelper.cs, the InvokeAsync overloads use a fast path via CheckAccess(): if already on the UI thread, execute the action/func directly without dispatcher.InvokeAsync, even if a DispatcherPriority is provided. When reviewing this file or related code, assume this is an intentional design choice by the maintainer. Verify that tests or code paths confirm the UI-thread fast-path is employed, and ensure there is no unnecessary dispatching when CheckAccess() is true. Consider adding/maintaining tests that cover InvokeAsync on the UI thread with various priorities, and document that the priority parameter is ignored in the fast-path.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 3854
File: Flow.Launcher/App.xaml.cs:252-275
Timestamp: 2025-10-16T10:48:30.573Z
Learning: In Flow Launcher's App.xaml.cs, the plugin initialization block (lines 252-275) that includes PreStartPluginExecutablePathUpdate, PluginManager.LoadPlugins, PluginManager.InitializePluginsAsync, _mainVM.QueryResults(), and API.SaveAppAllSettings() does not require additional Task.Run wrappers or Dispatcher.InvokeAsync calls according to maintainer Jack251970, as the threading model is already safe as designed.

Learnt from: Jack251970
Repo: Flow-Launcher/Flow.Launcher PR: 4319
File: Flow.Launcher.Core/Resource/Theme.cs:676-682
Timestamp: 2026-03-08T15:54:13.490Z
Learning: In WPF-based C# code reviews, note that Border.BackgroundProperty, Control.BackgroundProperty, and Panel.BackgroundProperty share the same DependencyProperty instance. Because Border and Control add ownership to Panel.BackgroundProperty, comparing a Setter's Property against Control.BackgroundProperty will match setters registered for Border.BackgroundProperty as well. When reviewing code that relies on setter-property identity for BackgroundProperty, assume reference equality across these types and verify behavior accordingly.

{
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
{
var task = await dispatcher.InvokeAsync(func, priority);
await task;
}
}
}
4 changes: 2 additions & 2 deletions Flow.Launcher.Core/Resource/Theme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ private void SetResizeBoarderThickness(Thickness? effectMargin)
/// </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();
Expand All @@ -619,7 +619,7 @@ await Application.Current.Dispatcher.InvokeAsync(() =>
/// </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();
Expand Down
6 changes: 3 additions & 3 deletions Flow.Launcher/App.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ private static void AutoPluginUpdates()
var timer = new PeriodicTimer(TimeSpan.FromHours(5));
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
{
Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
pluginUpdateWindow.ShowDialog();
Expand All @@ -345,7 +345,7 @@ await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
// check updates on startup
await PluginInstaller.CheckForPluginUpdatesAsync((plugins) =>
{
Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
var pluginUpdateWindow = new PluginUpdateWindow(plugins);
pluginUpdateWindow.ShowDialog();
Expand Down Expand Up @@ -444,7 +444,7 @@ protected virtual void Dispose(bool disposing)
{
// 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();
Expand Down
5 changes: 4 additions & 1 deletion Flow.Launcher/Helper/SingleInstance.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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
Expand All @@ -24,7 +25,7 @@
/// running as Administrator, can activate it with command line arguments.
/// For most apps, this will not be much of an issue.
/// </remarks>
public static class SingleInstance<TApplication> where TApplication : Application, ISingleInstanceApp

Check warning on line 28 in Flow.Launcher/Helper/SingleInstance.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`TApplication` is not a recognized word. (unrecognized-spelling)
{
#region Private Fields

Expand Down Expand Up @@ -100,7 +101,9 @@
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();
Expand Down Expand Up @@ -135,7 +138,7 @@
return;
}

((TApplication)Application.Current).OnSecondAppStarted();

Check warning on line 141 in Flow.Launcher/Helper/SingleInstance.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`TApplication` is not a recognized word. (unrecognized-spelling)
}

#endregion
Expand Down
10 changes: 5 additions & 5 deletions Flow.Launcher/Helper/WallpaperPathRetrieval.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
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;

Expand All @@ -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(GetWallpaperBrushCore);
}

private static Brush GetWallpaperBrushCore()
{
try
{
var wallpaperPath = Win32Helper.GetWallpaperPath();
Expand Down
23 changes: 6 additions & 17 deletions Flow.Launcher/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@
using Flow.Launcher.Plugin.SharedCommands;
using Flow.Launcher.Plugin.SharedModels;
using Flow.Launcher.ViewModel;
using iNKORE.UI.WPF.Modern;

Check warning on line 28 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`NKORE` is not a recognized word. (unrecognized-spelling)
using iNKORE.UI.WPF.Modern.Controls;

Check warning on line 29 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`NKORE` is not a recognized word. (unrecognized-spelling)
using DataObject = System.Windows.DataObject;
using Key = System.Windows.Input.Key;
using MouseButtons = System.Windows.Forms.MouseButtons;
Expand Down Expand Up @@ -116,7 +116,7 @@
{
var handle = Win32Helper.GetWindowHandle(this, true);
_hwndSource = HwndSource.FromHwnd(handle);
_hwndSource.AddHook(WndProc);

Check warning on line 119 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
Win32Helper.HideFromAltTab(this);
Win32Helper.DisableControlBox(this);
}
Expand Down Expand Up @@ -153,7 +153,7 @@
Localize.appUpdateButtonContent(),
() =>
{
Application.Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
var releaseNotesWindow = new ReleaseNotesWindow();
releaseNotesWindow.Show();
Expand Down Expand Up @@ -225,7 +225,7 @@
{
case nameof(MainViewModel.MainWindowVisibilityStatus):
{
Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
if (_viewModel.MainWindowVisibilityStatus)
{
Expand Down Expand Up @@ -269,7 +269,7 @@
{
// 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;
Expand Down Expand Up @@ -376,7 +376,7 @@
{
try
{
_hwndSource.RemoveHook(WndProc);

Check warning on line 379 in Flow.Launcher/MainWindow.xaml.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Wnd` is not a recognized word. (unrecognized-spelling)
}
catch (Exception)
{
Expand Down Expand Up @@ -542,7 +542,7 @@
// Switch to Normal state
WindowState = WindowState.Normal;

Application.Current?.Dispatcher.Invoke(new Action(() =>
DispatcherHelper.Invoke(() =>
{
double normalWidth = Width;
double normalHeight = Height;
Expand All @@ -555,7 +555,7 @@
{
DragMove();
}
}), DispatcherPriority.ApplicationIdle);
}, DispatcherPriority.ApplicationIdle);
}
else
{
Expand Down Expand Up @@ -726,19 +726,8 @@
{
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)
Expand Down
14 changes: 10 additions & 4 deletions Flow.Launcher/MessageBoxEx.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using Flow.Launcher.Core.Resource;
using Flow.Launcher.Infrastructure;

namespace Flow.Launcher
Expand All @@ -29,11 +30,16 @@ public static MessageBoxResult Show(
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(() => ShowCore(messageBoxText, caption, button, icon, defaultResult));
}

private static MessageBoxResult ShowCore(
string messageBoxText,
string caption = "",
MessageBoxButton button = MessageBoxButton.OK,
MessageBoxImage icon = MessageBoxImage.None,
MessageBoxResult defaultResult = MessageBoxResult.OK)
{
try
{
msgBox = new MessageBoxEx(button);
Expand Down
5 changes: 3 additions & 2 deletions Flow.Launcher/Msg.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;

Expand Down Expand Up @@ -80,12 +81,12 @@ public async void Show(string title, string subTitle, string iconPath)

Show();

await Dispatcher.InvokeAsync(async () =>
await DispatcherHelper.InvokeAsync(async () =>
{
if (!closing)
{
closing = true;
await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin);
fadeOutStoryboard.Begin();
}
});
}
Expand Down
5 changes: 3 additions & 2 deletions Flow.Launcher/MsgWithButton.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
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;

Expand Down Expand Up @@ -82,12 +83,12 @@ public async void Show(string title, string buttonText, Action buttonAction, str

Show();

await Dispatcher.InvokeAsync(async () =>
await DispatcherHelper.InvokeAsync(async () =>
{
if (!closing)
{
closing = true;
await Dispatcher.InvokeAsync(fadeOutStoryboard.Begin);
fadeOutStoryboard.Begin();
}
});
}
Expand Down
6 changes: 3 additions & 3 deletions Flow.Launcher/Notification.cs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -41,7 +41,7 @@ internal static void Uninstall()

public static void Show(string title, string subTitle, string iconPath = null)
{
Application.Current.Dispatcher.Invoke(() =>
DispatcherHelper.Invoke(() =>
{
ShowInternal(title, subTitle, iconPath);
});
Expand Down Expand Up @@ -91,7 +91,7 @@ private static void LegacyShow(string title, string subTitle, string iconPath)

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);
});
Expand Down
Loading
Loading