Nothing is worse than waiting around for an unresponsive UI, am I right? With frontend frameworks, there’s a real possibility that we’ll leave our using waiting for responses that never return. Blazor isn’t any different in this regard and can cause the same frustrations in our users.
This post will show how we can set reasonable timeouts for dependency calls and give our users back control and cancel an existing asynchronous operation.
CancellationTokenSource and Timeouts
It should go without saying, but all software has a failure in its future, and no operation is guaranteed to succeed. When we perform calls over the network, we should understand what’s a good time to wait for a successful response from our dependency, and forever is never a good answer.
In a Blazor application, we’ll be calling several asynchronous operations, and we need to account for slow or unresponsive services. Luckily, most (good) async
APIs allow us to pass in a CancellationToken
, which will enable us to cancel the call maybe. I say maybe because not all asynchronous calls are cancellable, but we still may be able to abort waiting for a response.
Let’s look at how we can create an instance of CancellationToken
tied to a timeout of 3 seconds
. In our example, we’ll be calling a weather forecast API, which may timeout from time to time. Inside our Blazor component’s @code
block, we’ll find our GetForecast
method.
private async void GetForecast(EditContext editContext)
{
var source = new CancellationTokenSource(TimeSpan.FromSeconds(3));
Message = null;
try
{
_loading = true;
_forecast = await _client.Forecast
.GetForecastAsync(new ForecastRequestEntity()
.WithPostalCode(Input.Value)
.WithDays(3), source.Token
);
}
catch (Exception e)
{
Message = source.IsCancellationRequested
? "Request to API timed out"
: e.Message;
_logger.LogError(e, "couldn't retrieve forecast");
}
finally
{
_loading = null;
// poke blazor to reset
// in case an error has occurred
StateHasChanged();
}
}
Let’s walk through the code:
- We create a
CancellationTokenSource
and pass in aTimeSpan
instance of3 seconds
. - When calling our client’s
GetForecastAsync
method, we pass our CancellationToken usingsource.Token
. - If the request times out, our code will throw an
OperationCanceledException
which we can handle in the code after ourasync
call.
Note, the phrase CancellationToken
is misleading. It only keeps a Task
from starting but does not cancel an already started Task. The returning response from the Task may cause your UI to update if it does succeed. To keep the UI from updating after we’ve canceled, we need to check the IsCancellationRequest
property on the source
.
var result = await _client.Forecast
.GetForecastAsync(new ForecastRequestEntity()
.WithPostalCode(Input.Value)
.WithDays(3), source.Token
);
if (source?.IsCancellationRequested == false) {
_forecast = result;
}
We can even give users control of canceling the Task
by setting the CancellationTokenSource
to a member of our Blazor component.
<button class="btn btn-primary" type="submit" @onclick="() => source?.Cancel()">
<span class="spinner-grow spinner-grow-sm" role="status" aria-hidden="true"></span>
Loading...
</button>
Let’s change our method to reflect the change.
private async void GetForecast(EditContext editContext)
{
// set the member, not a local variable
source = new CancellationTokenSource(TimeSpan.FromSeconds(3));
Message = null;
try
{
_loading = true;
var result = await _client.Forecast
.GetForecastAsync(new ForecastRequestEntity()
.WithPostalCode(Input.Value)
.WithDays(3), source.Token
);
if (source?.IsCancellationRequested == false) {
_forecast = result;
}
}
catch (Exception e)
{
Message = source?.IsCancellationRequested == true
? "Request to API timed out"
: e.Message;
_logger.LogError(e, "couldn't retrieve forecast");
}
finally
{
_loading = null;
source = null;
// poke blazor to reset
// in case an error has occurred
StateHasChanged();
}
}
Conclusion
We saw two ways where we can give users back a responsive Blazor UI. First, we can set timeouts with CancellationTokenSource
, and secondly, we can manually cancel by invoking the Cancel
method. It is important to note, Tasks may not stop executing when canceled, which means it’s essential for us to determine whether we want to set our variables by checking the IsCancellationRequested
property.
To see this example in its entirety, check out the GitHub Repository with a working sample. You’ll need access to an API token, but its free and easy to sign up for one.
I hope you enjoyed this post, and I wish you success on your Blazor journey.