Search code examples
c#blazorcancellationtokensource

How to manage loading state for multiple cancellable async operations?


I've implemented a cancellable search in blazor. To illustrate:

enter image description here

When the user types multiple times, the previous search will be cancelled. This is achieved by using a cancellation token.

private bool _isLoading = false;

private CancellationTokenSource _cancellationTokenSrc;

public Func<string, CancellationToken, Task<IEnumerable<T>>> SearchFuncWithCancel { get; set; }

private void CancelToken()
{
    try
    {
        _cancellationTokenSrc?.Cancel();
    }
    catch
    { }

    _cancellationTokenSrc = new CancellationTokenSource();
}

private async Task OnSearchAsync()
{
    IEnumerable<T> searched_items = Array.Empty<T>();
    
    CancelToken();

    try
    {
        _isLoading = true;
        
        searched_items = (await SearchFuncWithCancel(Text, _cancellationTokenSrc.Token)) ?? Array.Empty<T>();
    }
    catch (Exception e)
    {
        Console.WriteLine("The search function failed to return results: " + e.Message);
    }
    finally
    {
        _isLoading = false;
    }

    //...
}

I am experiencing an issue with the code, where _isLoading is always false for a subsequent search. You can see it in the gif. The loading indicator is displayed the very first time. After that it does not show up anymore.

When a search gets cancelled due to another search the SearchFuncWithCancel method will exit and the finally block will be called. This sets _isLoading to false, while the other search is still awaiting results.

Any ideas how to fix that? My first idea was to use a stack. Whenever search is invoked, we push a true onto the stack. In the finally block we pop from the stack. As long as there is something on the stack, _isLoading must be true. Not sure if that is the best solution for this problem.


Solution

  • It should be perfectly possible for something like this to happen:

    1. Start Searching for "a"
    2. awaiting search "a"
    3. Start searching for "b"
    4. Cancel search "a"
    5. awaiting search "b"
    6. Search operation "a" completes with an operation canceled exception

    The last step would set _isLoading to false, even if the search for "b" is still in progress.

    There are multiple potential ways to solve this:

    1. Replace _isLoading with a counter that is incremented and decremented. You might need to use interlocked operations if multiple threads are used.
    2. let any search operation await the task for the previous operation, this should ensure only one search operation is running at a time.
    3. replace _isLoading with a property that checks the status of the search task. I.e. something like:
    var currentTask = SearchFuncWithCancel(...);
    currentTaskField = currentTask;
    searched_items = await currentTask;
    ...
    public bool IsLoading => !currentTaskField.IsCompleted;
    

    But I'm not sure what the best solution is for this particular case.