From d77434292184601bd0c80892b558824d69842a5e Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 27 Nov 2025 15:51:35 +0800 Subject: [PATCH 1/8] Avoid duplicate query calls on programmatic text updates Refactored `MainWindow.xaml.cs` and `MainViewModel.cs` to prevent redundant query calls when updating the query text programmatically. - Added `_ignoreTextChange` flag in `MainWindow.xaml.cs` to skip `TextChanged` event handling during programmatic updates. - Introduced `SetQueryTextBoxText` method to safely update the query text box without triggering events. - Updated `QueryTextBox_TextChanged1` to respect `_ignoreTextChange`. - Modified `MainViewModel.cs` to directly update `_queryText` and use `SetQueryTextBoxText` for text box synchronization. - Improved logic for clearing query text programmatically. - Added comments to clarify the intent behind these changes. --- Flow.Launcher/MainWindow.xaml.cs | 11 ++++++++++ Flow.Launcher/ViewModel/MainViewModel.cs | 27 +++++++++++++++++++++--- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 06b2dda9eed..6294c935a56 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -76,6 +76,9 @@ public partial class MainWindow : IDisposable private const double DefaultRightMargin = 66; //* this value from base.xaml private bool _isClockPanelAnimating = false; + // Search Delay + private bool _ignoreTextChange = false; + // IDisposable private bool _disposed = false; @@ -1421,11 +1424,19 @@ private void SetupResizeMode() private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) { + if (_ignoreTextChange) return; var textBox = (TextBox)sender; _viewModel.QueryText = textBox.Text; _viewModel.Query(_settings.SearchQueryResultsWithDelay); } + public void SetQueryTextBoxText(string text) + { + _ignoreTextChange = true; + QueryTextBox.Text = text; + _ignoreTextChange = false; + } + #endregion #region Dialog Jump diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index c35f96e6200..ace8fd67636 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -759,7 +759,14 @@ public void ChangeQueryText(string queryText, bool isReQuery = false) if (QueryText != queryText) { // Change query text first - QueryText = queryText; + // We use private field and manually set QueryTextBox instead of QueryText setter + // so that QueryTextBox_TextChanged1 will not be invoked to avoid duplicated Query calls + _queryText = queryText; + if (Application.Current?.MainWindow is MainWindow mainWindow) + { + mainWindow.SetQueryTextBoxText(queryText); + } + // When we are changing query from codes, we should not delay the query Query(false, isReQuery: false); @@ -791,7 +798,14 @@ private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false if (QueryText != queryText) { // Change query text first - QueryText = queryText; + // We use private field and manually set QueryTextBox instead of QueryText setter + // so that QueryTextBox_TextChanged1 will not be invoked to avoid duplicated Query calls + _queryText = queryText; + if (Application.Current?.MainWindow is MainWindow mainWindow) + { + mainWindow.SetQueryTextBoxText(queryText); + } + // When we are changing query from codes, we should not delay the query await QueryAsync(false, isReQuery: false); @@ -870,11 +884,18 @@ private ResultsViewModel SelectedResults } _queryTextBeforeLeaveResults = QueryText; + // We use private field and manually set QueryTextBox instead of QueryText setter + // so that QueryTextBox_TextChanged1 will not be invoked to avoid duplicated Query calls + _queryText = string.Empty; + if (Application.Current?.MainWindow is MainWindow mainWindow) + { + mainWindow.SetQueryTextBoxText(string.Empty); + } + // Because of Fody's optimization // setter won't be called when property value is not changed. // so we need manually call Query() // http://stackoverflow.com/posts/25895769/revisions - QueryText = string.Empty; // When we are changing query because selected results are changed to history or context menu, // we should not delay the query Query(false); From cda1b00bbafc55b5802d629b41568f8f5ed9f1dc Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 28 Nov 2025 10:59:32 +0800 Subject: [PATCH 2/8] Refactor QueryText clearing logic in MainViewModel Simplified the process of clearing the `QueryText` property by replacing manual updates to the `_queryText` private field and `QueryTextBox` control with the use of the `QueryText` property setter. This change avoids triggering duplicate query calls while maintaining the intended behavior. Updated comments to reflect the new approach. --- Flow.Launcher/ViewModel/MainViewModel.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index ace8fd67636..28ca981767f 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -884,13 +884,8 @@ private ResultsViewModel SelectedResults } _queryTextBeforeLeaveResults = QueryText; - // We use private field and manually set QueryTextBox instead of QueryText setter - // so that QueryTextBox_TextChanged1 will not be invoked to avoid duplicated Query calls - _queryText = string.Empty; - if (Application.Current?.MainWindow is MainWindow mainWindow) - { - mainWindow.SetQueryTextBoxText(string.Empty); - } + // We can use QueryText setter because the Query as follows does not requery + QueryText = string.Empty; // Because of Fody's optimization // setter won't be called when property value is not changed. From 73c264b8a09306822171e3459525d8687f6cd748 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Fri, 28 Nov 2025 17:23:45 +0800 Subject: [PATCH 3/8] Refactor query progress tracking for concurrency Replaced the single `_progressQuery` field with a thread-safe `ConcurrentDictionary` to enable tracking multiple queries concurrently. Each query is now associated with a unique `Guid` (`updateGuid`) for better isolation and management. Updated progress bar logic to use `_progressQueryDict`, ensuring that progress visibility is tied to the correct query session. Entries in `_progressQueryDict` are properly cleaned up when queries complete or are canceled. These changes improve thread safety, prevent race conditions, and enhance support for concurrent query handling. --- Flow.Launcher/ViewModel/MainViewModel.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 28ca981767f..455fd5e7461 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Globalization; @@ -37,7 +38,7 @@ public partial class MainViewModel : BaseModel, ISavable, IDisposable, IResultUp private Query _lastQuery; private bool _previousIsHomeQuery; - private Query _progressQuery; // Used for QueryResultAsync + private readonly ConcurrentDictionary _progressQueryDict = new(); // Used for QueryResultAsync private Query _updateQuery; // Used for ResultsUpdated private string _queryTextBeforeLeaveResults; private string _ignoredQueryText; // Used to ignore query text change when switching between context menu and query results @@ -1420,6 +1421,9 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b return; } + // Create a Guid for this update session so that we can filter out in progress checking + var updateGuid = Guid.NewGuid(); + try { _updateSource?.Dispose(); @@ -1431,7 +1435,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b ProgressBarVisibility = Visibility.Hidden; - _progressQuery = query; + _progressQueryDict.TryAdd(updateGuid, query); _updateQuery = query; // Switch to ThreadPool thread @@ -1486,7 +1490,8 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b _ = Task.Delay(200, currentCancellationToken).ContinueWith(_ => { // start the progress bar if query takes more than 200 ms and this is the current running query and it didn't finish yet - if (_progressQuery != null && _progressQuery.OriginalQuery == query.OriginalQuery) + if (_progressQueryDict.TryGetValue(updateGuid, out var progressQuery) && + progressQuery.OriginalQuery == query.OriginalQuery) { ProgressBarVisibility = Visibility.Visible; } @@ -1542,7 +1547,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b // this should happen once after all queries are done so progress bar should continue // until the end of all querying - _progressQuery = null; + _progressQueryDict.Remove(updateGuid, out _); if (!currentCancellationToken.IsCancellationRequested) { @@ -1553,7 +1558,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b finally { // this make sures progress query is null when this query is canceled - _progressQuery = null; + _progressQueryDict.Remove(updateGuid, out _); } // Local function From c48a7c856368bbf9ed25ff61a57e562611071ba7 Mon Sep 17 00:00:00 2001 From: Jack Ye Date: Fri, 28 Nov 2025 17:32:15 +0800 Subject: [PATCH 4/8] Improve code comments Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 455fd5e7461..868c5555a2b 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -1557,7 +1557,7 @@ private async Task QueryResultsAsync(bool searchDelay, bool isReQuery = false, b } finally { - // this make sures progress query is null when this query is canceled + // this ensures the query is removed from the progress tracking dictionary when this query is canceled or completes _progressQueryDict.Remove(updateGuid, out _); } From 556fc613d483ee31e3435811730398ba31bc65fb Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Wed, 3 Dec 2025 19:11:35 +0800 Subject: [PATCH 5/8] Fix potential null reference in QueryTextBox_KeyUp Added a null-conditional operator (`?.`) to the `UpdateSource()` method call on the `BindingExpression` object `be` to prevent a possible `NullReferenceException` if `be` is null. This change improves code robustness and ensures safer execution. --- Flow.Launcher/MainWindow.xaml.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index 6294c935a56..b207cddc8c9 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -1359,7 +1359,7 @@ private void QueryTextBox_KeyUp(object sender, KeyEventArgs e) if (_viewModel.QueryText != QueryTextBox.Text) { BindingExpression be = QueryTextBox.GetBindingExpression(TextBox.TextProperty); - be.UpdateSource(); + be?.UpdateSource(); } } From 417dd35cc6347eb5f316c4a2212ba80b3796e1cb Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 25 Dec 2025 21:05:18 +0800 Subject: [PATCH 6/8] Refactor query text update to remove event bypass logic Removed _ignoreTextChange flag and SetQueryTextBoxText method, allowing programmatic query text updates to trigger normal event flow and query logic. Simplified MainViewModel to set QueryText directly and invoke queries, ensuring consistent and streamlined query handling. Also removed unnecessary null check in QueryTextBox_KeyUp. --- Flow.Launcher/MainWindow.xaml.cs | 13 +------------ Flow.Launcher/ViewModel/MainViewModel.cs | 20 +++----------------- 2 files changed, 4 insertions(+), 29 deletions(-) diff --git a/Flow.Launcher/MainWindow.xaml.cs b/Flow.Launcher/MainWindow.xaml.cs index b207cddc8c9..06b2dda9eed 100644 --- a/Flow.Launcher/MainWindow.xaml.cs +++ b/Flow.Launcher/MainWindow.xaml.cs @@ -76,9 +76,6 @@ public partial class MainWindow : IDisposable private const double DefaultRightMargin = 66; //* this value from base.xaml private bool _isClockPanelAnimating = false; - // Search Delay - private bool _ignoreTextChange = false; - // IDisposable private bool _disposed = false; @@ -1359,7 +1356,7 @@ private void QueryTextBox_KeyUp(object sender, KeyEventArgs e) if (_viewModel.QueryText != QueryTextBox.Text) { BindingExpression be = QueryTextBox.GetBindingExpression(TextBox.TextProperty); - be?.UpdateSource(); + be.UpdateSource(); } } @@ -1424,19 +1421,11 @@ private void SetupResizeMode() private void QueryTextBox_TextChanged1(object sender, TextChangedEventArgs e) { - if (_ignoreTextChange) return; var textBox = (TextBox)sender; _viewModel.QueryText = textBox.Text; _viewModel.Query(_settings.SearchQueryResultsWithDelay); } - public void SetQueryTextBoxText(string text) - { - _ignoreTextChange = true; - QueryTextBox.Text = text; - _ignoreTextChange = false; - } - #endregion #region Dialog Jump diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 868c5555a2b..1415660f1b5 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -760,13 +760,7 @@ public void ChangeQueryText(string queryText, bool isReQuery = false) if (QueryText != queryText) { // Change query text first - // We use private field and manually set QueryTextBox instead of QueryText setter - // so that QueryTextBox_TextChanged1 will not be invoked to avoid duplicated Query calls - _queryText = queryText; - if (Application.Current?.MainWindow is MainWindow mainWindow) - { - mainWindow.SetQueryTextBoxText(queryText); - } + QueryText = queryText; // When we are changing query from codes, we should not delay the query Query(false, isReQuery: false); @@ -799,13 +793,7 @@ private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false if (QueryText != queryText) { // Change query text first - // We use private field and manually set QueryTextBox instead of QueryText setter - // so that QueryTextBox_TextChanged1 will not be invoked to avoid duplicated Query calls - _queryText = queryText; - if (Application.Current?.MainWindow is MainWindow mainWindow) - { - mainWindow.SetQueryTextBoxText(queryText); - } + QueryText = queryText; // When we are changing query from codes, we should not delay the query await QueryAsync(false, isReQuery: false); @@ -885,15 +873,13 @@ private ResultsViewModel SelectedResults } _queryTextBeforeLeaveResults = QueryText; - // We can use QueryText setter because the Query as follows does not requery - QueryText = string.Empty; - // Because of Fody's optimization // setter won't be called when property value is not changed. // so we need manually call Query() // http://stackoverflow.com/posts/25895769/revisions // When we are changing query because selected results are changed to history or context menu, // we should not delay the query + QueryText = string.Empty; Query(false); if (HistorySelected()) From bdd42da92607197b439ac141ab2f128bd69398c6 Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 25 Dec 2025 21:06:47 +0800 Subject: [PATCH 7/8] Revert unnecessary changes --- Flow.Launcher/ViewModel/MainViewModel.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 1415660f1b5..10dc73f2f77 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -761,7 +761,6 @@ public void ChangeQueryText(string queryText, bool isReQuery = false) { // Change query text first QueryText = queryText; - // When we are changing query from codes, we should not delay the query Query(false, isReQuery: false); @@ -794,7 +793,6 @@ private async Task ChangeQueryTextAsync(string queryText, bool isReQuery = false { // Change query text first QueryText = queryText; - // When we are changing query from codes, we should not delay the query await QueryAsync(false, isReQuery: false); @@ -876,10 +874,10 @@ private ResultsViewModel SelectedResults // Because of Fody's optimization // setter won't be called when property value is not changed. // so we need manually call Query() + QueryText = string.Empty; // http://stackoverflow.com/posts/25895769/revisions // When we are changing query because selected results are changed to history or context menu, // we should not delay the query - QueryText = string.Empty; Query(false); if (HistorySelected()) From 9333913f3e99e3c47d5fc4d6cb21ab1adc56028b Mon Sep 17 00:00:00 2001 From: Jack251970 <1160210343@qq.com> Date: Thu, 25 Dec 2025 21:07:24 +0800 Subject: [PATCH 8/8] Revert url change --- Flow.Launcher/ViewModel/MainViewModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher/ViewModel/MainViewModel.cs b/Flow.Launcher/ViewModel/MainViewModel.cs index 10dc73f2f77..4a0850dbc14 100644 --- a/Flow.Launcher/ViewModel/MainViewModel.cs +++ b/Flow.Launcher/ViewModel/MainViewModel.cs @@ -874,8 +874,8 @@ private ResultsViewModel SelectedResults // Because of Fody's optimization // setter won't be called when property value is not changed. // so we need manually call Query() - QueryText = string.Empty; // http://stackoverflow.com/posts/25895769/revisions + QueryText = string.Empty; // When we are changing query because selected results are changed to history or context menu, // we should not delay the query Query(false);