peeyush singh
peeyush singh

Reputation: 1407

How to handle busy indicator in WPF when using async methods on re-entrant calls?

In WPF controls (e.g. grid), we can usually set a boolean property to show a control is busy loading data and in the UI this will lead to a "Loading..." indicator.

When using async methods we just have to ensure that we turn IsBusy = "true" before we call the method and IsBusy="false" after the await.

However in case where I can call the grid load method multiple times when the first call completes it will turn the busy indicator off even when the second call is in progress.

Any way to resolve this? I can set a global counter for storing the number of request and set the status of indicator based on the count of this global variable, but its a dirty approach and will not scale if I have multiple asyn events in my code.

Example scenario

In the image below, I can search name of students and for each name my service would get back details (marks, etc.) and display it in the second grid.

I want to show a busy indicator while the second grid is waiting for data (otherwise the user might not know if the program is doing anything).

On entering the name the following method is called:

Imagine GetStudentResults takes 5 seconds (for every call). I enter first name on 0 second, then at 3 seconds I enter another name. Now at 5 seconds the first call returns and it turns off the busy indicator, while the second name details are not retrieved. This is what I want to avoid.

private async void SearchName(string name)
{
    ResultDisplayGrid.IsBusy = true;
    await GetStudentResults();
    ResultDisplayGrid.IsBusy = false;
}

enter image description here enter image description here

Upvotes: 0

Views: 2191

Answers (3)

Aaron
Aaron

Reputation: 185

Having thought about this since the latest comment, it is going to require a more complex solution involving proper task management and this starts going outside of my comfort-zone when assisting others.

The quickest and simplest method in my opinion would be to prevent user interaction with the text box or GUI once the search has started, therefore preventing additional searches before the previous one has completed. This of course would mean that users would need to wait for each search to complete before the next one can start.

My next approach would be to store the GetStudentResults Task and use a CancellationToken. For example SearchName might become:

private CancellationTokenSource ctsSearch;
private Task tSearch;

private async void SearchName(string name)
{
    if(ctsSearch != null)
    {
        ctsSearch.Cancel();

        if(tSearch != null)
            await tSearch;
    }

    ctsSearch = new CancellationTokenSource();

    ResultDisplayGrid.IsBusy = true;
    tSearch = GetStudentResults(ctsSearch.Token);
    await tSearch;
    ResultDisplayGrid.IsBusy = false;

}

In the above code, we are cancelling the previous task before we attempt to run GetStudentResults again. In your GetStudentResults method you will need to find places that you can insert:

if(token.IsCancellationRequested)
    return Task.FromResult(false); //Replace this return type with whatever suits your GetStudentResults return type.

My GetStudentResults method was:

private Task<bool> GetStudentResults(CancellationToken token)
{
    for(int i = 0; i < 10000; i++)
    {
        if (token.IsCancellationRequested)
            return Task.FromResult(false);

        Console.WriteLine(i);
    }
    return Task.FromResult(true);
}

Somebody might have some other ideas, but to me these are the simplest approaches.

Upvotes: 1

Janne Matikainen
Janne Matikainen

Reputation: 5121

You will need to use the CancellationTokenSource to get a token that you can track if the task has been cancelled by re-entry.

private CancellationTokenSource tokenSource;

public async void Search(string name)
{
    this.tokenSource?.Cancel();
    this.tokenSource = new CancellationTokenSource();
    var token = this.tokenSource.Token;

    this.IsBusy = true;
    try
    {
        // await for the result from your async method (non void)
        var result = await this.GetStudentResults(name, token);

        // If it was cancelled by re-entry, just return
        if (token.IsCancellationRequested)
        {
            return;
        }

        // If not cancelled then stop busy state
        this.IsBusy = false;
        Console.WriteLine($"{name} {result}");
    }
    catch (TaskCanceledException ex)
    {
        // Canceling the task will throw TaskCanceledException so handle it
        Trace.WriteLine(ex.Message);
    }
}

Also your GetStudentResults should take the token into account and stop what ever background processing it is doing if the token.IsCancellationRequested is set to true.

Upvotes: 0

Mac
Mac

Reputation: 957

Try wrapping your async call inside the try-finally block, when everything's done, it wall call the finally to set the IsBusy flag to false.

  private async void SearchName(string name)
    {
        ResultDisplayGrid.IsBusy = true;

            try{
                await GetStudentResults();
            }
            finally{
                ResultDisplayGrid.IsBusy = false;
            }
    }

Upvotes: 1

Related Questions