0

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

peeyush singh
  • 1,337
  • 1
  • 12
  • 23
  • Can you share your code? – Md. Abdul Alim Sep 07 '18 at 04:17
  • 1
    May I ask, why do you have to load the grid multiple times? you can just update your observable collection whenever you need to update the grid source. – Mac Sep 07 '18 at 04:51
  • @Mac, the problem is not to load data multiple time but to notify the user that data in grid is being refreshed. I have added an example scenario, hope that explains my problem better – peeyush singh Sep 07 '18 at 05:27
  • @peeyushsingh Why won't the counter approach you mentioned scale? – pere57 Sep 07 '18 at 09:46
  • @pere57 Currently I am doing this for one grid/control in my UI so it is still feasible and I will probably end using this approach. However if the number of controls which require similar handling increase then it means an increasing no. of global variable. I will need to handle those global variable (shared state) properly in scenario where due to asyn/await my code can end up taking multiple path. Imagine if the controls can affect each other. Figuring which control is loaded and which needs to be refreshed again would be hard. Cleaning up the UI/service calls is what I am planning for now. – peeyush singh Sep 07 '18 at 09:55

3 Answers3

1

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;
            }
    }
Mac
  • 949
  • 8
  • 12
  • The actual code is wrapped in try/finally. Code give above in question is for illustration only. Do not understand how putting it in finally helps here, as the code works same with ot without finally (as long as we ignore anything about exceptions). – peeyush singh Sep 07 '18 at 06:24
  • Statement wrapped inside the finally block is always executed before the flow exits the method that's why your flag is guaranteed to be ran after your preceeding calls, there maybe some raise conditions in your code, or maybe you're having problems returning to the ui thread as i think your GetStudent... async call is updating the ui, in which case you will need to use the dipatcher, see this thread https://stackoverflow.com/questions/11625208/accessing-ui-main-thread-safely-in-wpf – Mac Sep 07 '18 at 07:51
1

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.

Aaron
  • 167
  • 1
  • 10
  • think of scenario's like a comparision website, where you keep typing names of products and they keep getting added. So disabling controls so as not to let user interact or cancelling the current search and launching a new one would both not be feasible. – peeyush singh Sep 07 '18 at 06:31
  • Check the logic on 2nd call when there is the ctsSearch, you will end up awaiting search twice. – Janne Matikainen Sep 07 '18 at 07:06
  • @JanneMatikainen I'll have to test that properly then. I'm 99% certain that first call to `await tSearch` should only last until the cancellation check is hit and only occur if `ctsSearch` is not null and `tSearch` is not null. The second `await tSearch` matches the original code provided. – Aaron Sep 07 '18 at 07:24
0

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.

Janne Matikainen
  • 5,061
  • 15
  • 21