I have a blazor component (aka razor component, aka MyPage.razor file) with a @page directive. I'll call the thing/object "page" from here on.
It needs to await a Task (HTTP request simulated with Task.Delay) inside its OnParametersSetAsync()
.
When the page is left (user navigates somehwere else), awaited Tasks must be cancelled, so that there is no ping-ping (with data access) when thew new page is loaded and the old pages's Task finally finishes delayed. This is the reason for Dipose()
.
Since the Framework calls OnParametersSetAsync()
rather than my own code, I'm not sure if I should let the OperationCanceledException
simply bubble up (at finally probably be ignore afaik as the master of async said) - or if I should catch it and return
gracefully from OnParametersSetAsync()
.
Is Blazor handeling cancellation from LifeCycle methods properly or is this the recommended way? Sadly the docu is very sparse. The example offers an button event handler, but IDK if that also counts for LifeCycle methods. But at least it seems it doesnt hurt the event handler (LongRunningWork), that it is not catched in user code.
I have testes both scenarios, and it seems either way, both work seemingly...
What I've noticed, is that even if the async Task OnParametersSetAsync()
completes but another page is already active the the Task belongs to an already disosed page, no children LifeCycle methods are called anymore. The big question here is, is it "only" the C# user code in the remaining body of OnParametersSetAsync()
that is executed delayed after the page has already been disposed - or will the successful completion of OnParametersSetAsync()
trigger some other framework methods/events, even if the page has already been disposed, resulting in highly unpredictable behaviour? I'd also like to know that answer.
In any case, even if this would not cause problems, cancellation might still be important, so that at the end of the user code in OnParametersSetAsync()
does not do any operations (e.g. on some data in some injected service or sth like that) that shouldn't be done anymore after disposing. So what's the right way?
Edit: Stephen said:
Ideally, you want to observe all your Task exceptions.
which is not possible, since OnParametersSetAsync()
is called from the framework not from the user code, so I can't observe it inside the caller!
// MyPage.razor
<Child SomePara=@SomePara></Child>
//@code{
// ...
//private CancellationTokenSource cts = new();
//object SomePara = new();
// catch it?
protected override async Task OnParametersSetAsync()
{
Debug.WriteLine($"OnParametersSetAsync Start");
// sync stuff...
// async stuff:
try
{
await Task.Delay(5000, cts.Token);
await UnknownExternalTaskIWantToCancelAsync(cts.Token);
}
catch (Exception)
{
return; //??
throw; //??
}
//when cancel is requested, stop here, this component is being disposed and should do as little as possible, especially nothing async and should't inform children to render
//when cancel is requested, while above Tasks are awaited, all below code MUST NOT run
// ??
//cts.Token.ThrowIfCancellationRequested();
Debug.WriteLine($"OnParametersSetAsync End");
// stuff I don't want do be done after cancelled
}
// let it bubble up?
protected override async Task OnParametersSetAsync()
{
Debug.WriteLine($"OnParametersSetAsync Start");
// sync stuff...
// async stuff:
await Task.Delay(5000, cts.Token);
await UnknownExternalTaskIWantToCancelAsync(cts.Token);
//when cancel is requested, stop here, this Razor component is being disposed and should do as little as possible, especially nothing async and should't inform children to render
//when cancel is requested, while above Tasks are awaited, all below code MUST NOT run
// ??
//cts.Token.ThrowIfCancellationRequested();
Debug.WriteLine($"OnParametersSetAsync End");
// stuff I don't want do be done after cancelled
}
public void Dispose()
{
Debug.WriteLine($"Disposing");
cts.Cancel();
cts.Dispose();
}
async Task UnknownExternalTaskIWantToCancelAsync(CancellationToken cts)
{
//This could be a HTTP call or some other Task.
Debug.WriteLine($" . . . . . START.");
await Task.Delay(10000, cts);
Debug.WriteLine($" . . . . . FINISHED.");
}
//}
One could imagine also a pretty hacky idea, but thats prolly bad:
// Hacky option ?????
bool isCancelled = false;
protected override async Task OnParametersSetAsync()
{
Debug.WriteLine($"OnParametersSetAsync Start");
// sync stuff...
// async stuff:
await Task.Delay(5000);
await UnknownExternalTaskIWantToCancelAsync(cts.Token);
//when cancel is requested, stop here, this Razor component is being disposed and should do as little as possible, especially nothing async and should't inform children to render
if (isCancelled)
{
return;
}
Debug.WriteLine($"OnParametersSetAsync End");
// stuff I don't want do be done after cancelled
}
public void Dispose()
{
Debug.WriteLine($"Disposing");
isCancelled = true ;
}
Update:
In the link I provided above, the official docs DON'T CATCH the exception in LongRunningWork
!! Which leads to a Console msg:
Exception thrown: 'System.Threading.Tasks.TaskCanceledException' in System.Private.CoreLib.dll A task was canceled.
Question: Is it ok to do it exactly like that with LifeCycle methods, specifically OnParametersSetAsync()
without wrapping a try-catch block around the Task to catch the OperationCanceledException
which is legit way a CTS cancels the Task. As the nature of an exception, it bubbles up until catched. OperationCanceledException
are special and won't crash the App, but is it ok to let it bubble up in a Blazor LifeCycle method?? LongRunningWork
is NOT a Blazor LifeCycle method, but only a event handler for a button click event. Is it ok to treat a LifeCycle methods the same way in this regard??
Update:
I've read several posts about Task cancellation aswell as the official docs, but none answers the specific case for the Blazor LifeCycle methods, like OnParametersSetAsync
.
Links:
Regarding asynchronous Task, why is a Wait() required to catch OperationCanceledException?
How to properly cancel Task and catch OperationCanceledException?
How to cancel a Task using CancellationToken?
Elegantly handle task cancellation
How to Correctly Cancel a TPL Task with Continuation
Cancelling a Task is throwing an exception
Please try to refer to make code examples of "catch it?" or "let it bubble up?" in your answers, thanks very much.