Blazor race condition "OnAfterRenderAsync" "DisposeAsync"
Asked Answered
D

0

2

Google search on this topic only gives 4 results, one mention from MudBlazor

I have the following @code in the bottom of a .razor file:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
  CancellationTokenSource cts = new();
  var token = cts.Token;
  _CTSList.Add(cts);
  try
  {
    if (cts.IsCancellationRequested)
    {
      cts.Token.ThrowIfCancellationRequested();
    }
    if (_elementRef.Id is not null)
    {
      await JS.InvokeVoidAsync("MyJsFunction", cts.Token);
    }
  }
  catch {
  }
  finally
  {
    _CTSList.Remove(cts);
    cts.Dispose();
  }
}

public async ValueTask DisposeAsync()
{
  foreach (var cts in _CTSList)
  {
    cts.Cancel();
    Debug.WriteLine("a cts has been canceled ");
  }
  await JS.InvokeVoidAsync("MyCleanUpFunction");
}

Basically, there is some JS interop that should get executed at certain events, and it seems it's the Blazor way, that JS interop has to wait until OnAfterRenderAsync in most cases, especially when ElementReferences are used.

Ofc when the user navigates away from the page, that JS should no longer be invoked, and ElementReferences not more referenced (will cause errors). Also I want to run some cleanup JS code, like setting some vars to null in JS when they are no longer needed (when navigating away).

I tried CancelationTokens, but even they seem to have a problem, and are probably even unnecessary. I'd like to have general, solid pattern for this.

The main problem however is that there is concurrency between OnAfterRenderAsync and DisposeAsync. Or call it a race condition between those async methods.

While testing, I actually managed, that during the short foreach loop in DisposeAsync that OnAfterRenderAsync was deleting items from the List, causing an access violation.

In my mind this is very counter intuitive. How can OnAfterRenderAsync still run, when the razor component is already disposing??? Obviously we would not want that ??? I', now thinking, if maybe a lock around both method bodies would help, or maybe another pattern is necessary for what I want.

Background: In JS I have an ResizeObserver that needs disposing (I guess, cuz its observed Element gets removed from the DOM when navigating away), and should ofc NOT be created again in an OnAfterRenderAsync AFTER DisposeAsync has run. DisposeAsync MUST run last (not simultaneously).

So would a lock help? What else can be done? Why is this so horribly complicated, is there a better way to do JS stuff like this?

•MudPopover: Fixed leaks caused by concurrency between OnAfterRenderAsync and DisposeAsync #3963

Dufrene answered 25/8, 2022 at 13:53 Comment(7)
Manage your js with a service injected at higher level in a hierarchy sense. If its at the MainLayout level or higher your app has effectively been disposed so does it matter?Endmost
It looks like the library you referenced used locks to address this. github.com/MudBlazor/MudBlazor/pull/3963/filesAssuan
The documentation hints to this issue here: learn.microsoft.com/en-us/aspnet/core/blazor/components/…. They are suggesting using a null check and "nulling out" the variable at the end of the dispose method.Assuan
This is exactly what I've also done now in the mean time. I dropped the approach of using a CTS and use a lock and a bool. Maybe when I've got some more time, I'll update the code in the question. Antoher idea was, converting the ValueTask into a Task that can be passed and then awaited in both methods.Dufrene
How did you eventually solve this, do you have code you could show? Like you stated above, this problem is very counter intuitive.Ultimate
See here for related issue and solutions.Ultimate
AFAIK at this point in time, I think OnAfterRender is not called after DisposeAsync, so a bool flag inside DisposeAsync can be used and should be checked each async-step inside OnAfterRender, or as I mention use a lock. So that once DisposeAsync has run, nothing OnAfterRender exists immediately and does no more work, or maybe even tries disposing itself. Atm the issue is only a problem if inside OnAfterRender, after a valid flag check, there is artificially Task.Delay, so that there is time for DisposeAsync to squat in. The above steps, prevent that. Let me know, if there is more problems.Dufrene

© 2022 - 2024 — McMap. All rights reserved.