Reputation: 883
I have an UWP App that has an PDFReader in it. The PDFReader has a search window. After entering a word in the search textbox you are able to start the search. Currently it is so that if you start the search you have to wait 3 seconds till you see a message how often the word occurs.
Now I want to fill those 3 seconds with a "Searching..." message.
One solution I had was this:
private async void DoSearch(...)
{
//...
Task<string> searchingText = setSearchMsg();
infoBox.Text = await searchingText;
//...
}
private async Task<string> setSearchMsg()
{
infoBox.Text = "Searching...";
await Task.Delay(1);
return "";
}
But this just does not look right. One thing I noticed is when I'm looking for a word and the word can be found like 3000 times in 80 pages the infoBox
skips the "Searching..." message and again fills the infoBox with emptiness.
To fix this I could change the Task.Delay
to like Task.Delay(100)
which cannot be the right solution I assume.
Upvotes: 0
Views: 433
Reputation: 52240
Sounds like your search logic is blocking the main thread and doesn't contain any await
statements and therefore never yields control. This means that the message loop that handles repainting the label is not having a chance to run consistently, so you get inconsistent display results.
I suggest moving the search logic into its own method and calling it asynchronously on the thread pool. Keep the display logic in your existing method but use Task.Run
to execute a separate method that performs the actual search.
private async void DoSearch(...)
{
infoBox.Text = "Searching....";
await Task.Run( () => DoActualSearch() );
infoBox.Text = "";
}
Upvotes: 2
Reputation: 117057
You should use Microsoft's Reactive Framework (aka Rx) - NuGet System.Reactive.Windows.Threading
and add using System.Reactive; using System.Reactive.Linq;
- then you can do something amazing.
I mocked up a bit of code similar to yours. I started with a TextBox
and TextBlock
and a dummy method for performing a search:
private async Task<string> PerformSearchAsync(string text)
{
await Task.Delay(TimeSpan.FromSeconds(2.0));
return "Hello " + text;
}
Now, in the constructor, after this.InitializeComponent();
I created the following observable that responds to all of the TextChanged
events on my textBox1
:
IObservable<EventPattern<TextChangedEventArgs>> textChanges =
Observable
.FromEventPattern<TextChangedEventHandler, TextChangedEventArgs>(
h => textBox1.TextChanged += h,
h => textBox1.TextChanged -= h);
Then I wrote a query to respond to the text changes:
IObservable<string> query =
textChanges
.Select(x =>
Observable
.FromAsync(() => PerformSearchAsync(textBox1.Text))
.StartWith("Searching..."))
.Switch();
This is essentially waiting for each text change and initiating an async call to PerformSearchAsync
- which will return the search results as a string - and it also immediately returns the string "Searching..."
. The .Switch()
is used to make sure only the latest call to PerformSearchAsync
actually returns a result.
Now I can simply observe the query:
_subscription =
query
.ObserveOnDispatcher()
.Subscribe(x => textBlock1.Text = x);
The variable _subscription
is defined as private IDisposable _subscription = null;
. It is used so that I can call _subscription.Dispose()
to safely close down the subscription.
It behaves the way you want.
My whole code looks like this:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;
namespace App5
{
/// <summary>
/// An empty page that can be used on its own or navigated to within a Frame.
/// </summary>
public sealed partial class MainPage : Page
{
private IDisposable _subscription = null;
public MainPage()
{
this.InitializeComponent();
IObservable<EventPattern<TextChangedEventArgs>> textChanges =
Observable
.FromEventPattern<TextChangedEventHandler, TextChangedEventArgs>(
h => textBox1.TextChanged += h,
h => textBox1.TextChanged -= h);
IObservable<string> query =
textChanges
.Select(x =>
Observable
.FromAsync(() => PerformSearchAsync(textBox1.Text))
.StartWith("Searching..."))
.Switch();
_subscription =
query
.ObserveOnDispatcher()
.Subscribe(x => textBlock1.Text = x);
}
private async Task<string> PerformSearchAsync(string text)
{
await Task.Delay(TimeSpan.FromSeconds(2.0));
return "Hello " + text;
}
}
}
You should be able to adapt this to work with your code.
Upvotes: 5
Reputation: 2104
This code snippet should work. The magic is change your Text, before awaiting.
If search performed too quickly and text changes also quickly, you can add a small delay say 100ms for every search, because for user no matter to wait 3 seconds or 3.1 seconds.
private async void DoSearch(...)
{
//...
infoBox.Text="Searching..."
var result= await SearchTextAsync();
//await Task.Delay(100ms); //add this if your search can perform too quickly to avoid flickering effect
infoBox.Text = result;// or clean
//...
}
Upvotes: 1
Reputation: 14700
Setting the "Searching..." message isn't the async part of your code - it's instantaneous. It's the search that actually takes time, right? But what you're doing is setting the message... waiting... and immediately clearing the message. You need to set the message, do the slow operation, then clear the message.
It should be something like this:
private async void DoSearch(...)
{
// Before starting the heavy-duty async operation.
infoBox.Text = "Searching...";
// Do the search. Infobox should display "Searching..." throughout.
await pdfReader.SearchAndHighlightWords(searchString);
// Now we're back from the slow async operation. Clear the infobox.
infoBox.Text = "";
}
Upvotes: 1