Blazor Server System.ObjectDisposedException issues when updating pages

Viewed 587

Added the process to update the data every second in Blazor Server.

This works fine, but when I press refresh page (F5) in my browser I get the following error:

System.ObjectDisposedException: 'Cannot process pending renders after the renderer has been disposed.
ObjectDisposed_ObjectName_Name'

The target code is here

@code {
  private List<Models.Recipe> recipes { get; set; }
  private List<Models.NowOrder> nowOrders { get; set; }
  private List<Models.PlanOrder> planOrders { get; set; }
  System.Threading.Timer _timer;

  protected override void OnInitialized()
  {
    using (var dbContext = DbFactory.CreateDbContext())
    {
        this.recipes = dbContext.Recipes.ToList();
        this.planOrders = dbContext.PlanOrders.ToList();
        this.nowOrders = dbContext.NowOrders.ToList();
    }

    _timer = new System.Threading.Timer(async (_) =>
    {
        Time = DateTime.Now.ToString();
        databaseValue = await TimerProcessGetValue();
        await InvokeAsync(StateHasChanged);    
    }, null, 0, 1000);
  }

  public void Dispose() 
  {
    _timer?.Dispose();
  }

  public async Task<int?> TimerProcessGetValue()
  {
      int? timerProcessValue;
      using (var dbContext = DbFactory.CreateDbContext())
      {
         timerProcessValue = (await dbContext.TestTable.SingleAsync(x => x.Id == 1)).TestValue;
      }
      return timerProcessValue;
  }
}

When refreshing the page "await InvokeAsync(StateHasChanged);"

If you comment out the following part, you will not get an error even if you press the F5 key, so I think that my handling of asynchronous processing is wrong, but I am troubled because I can not find the same case even if I search for the error I am.

What do you think is the cause of this? Thank you for your cooperation.

_timer = new System.Threading.Timer(async (_) =>
{
    Time = DateTime.Now.ToString();
    databaseValue = await TimerProcessGetValue();
    await InvokeAsync(StateHasChanged);    
}, null, 0, 1000);

Change to verification code

        _timer = new System.Threading.Timer(async (_) =>
    {
        Time = DateTime.Now.ToString();
        //databaseValue = await TimerProcessGetValue();
        await Task.Delay(500); //Added verification code.
        await InvokeAsync(StateHasChanged);    
    }, null, 0, 1000);
  }

Append.(10/10/2021)

Version used: net core 5.0.7.

Browser used: Edge.

I'm not using any other browser.

My environment is limited and I can only use Edge ...

Below are the three codes we have verified.

①This is an example using Dispose (WaitHandle).

This did not improve the error.

public void Dispose()
{
    using(System.Threading.WaitHandle waitHandle = new System.Threading.ManualResetEvent(false))
    {
        if (_timer.Dispose(waitHandle))
        {
            const int millisecondsTimeout = 500;
            if (!waitHandle.WaitOne(millisecondsTimeout))
            {
                System.Diagnostics.Debug.WriteLine("Dispose Test");
            }
        }
    }
}

②This is an example using System.Timers.Timer.

the reload using the F5 key was successful.

private int currentCount = 0;
private Timer timer2 = new(1000);

    protected override void OnInitialized()
{
    timer2.Elapsed += (sender, eventArgs) => OnTimerCallback();
    timer2.Start();
}

private void OnTimerCallback()
{
    _ = InvokeAsync(() =>
    {
        currentCount++;
        Time = DateTime.Now.ToString();
        databaseValue = TimerProcessGetValue().Result;
        StateHasChanged();
    });
}

public void Dispose() => timer2.Dispose();

③Added error handling.

Even with this method, I succeeded in reloading with the F5 key without any error.

    try
    {
        _timer = new System.Threading.Timer(async (_) =>
        {
            Time = DateTime.Now.ToString();
            databaseValue = await TimerProcessGetValue();
            await InvokeAsync(StateHasChanged);
        }, null, 0, 1000);
    }
    catch
    {
        // empty on purpose
    }
2 Answers

https://docs.microsoft.com/en-us/dotnet/api/system.threading.timer?view=net-5.0

When a timer is no longer needed, use the Dispose method to free the resources held by the timer. Note that callbacks can occur after the Dispose() method overload has been called, because the timer queues callbacks for execution by thread pool threads. You can use the Dispose(WaitHandle) method overload to wait until all callbacks have completed.

This is why await InvokeAsync(StateHasChanged); is being called after the component is disposed.

To expand on @Brianparker's answer:

replace @implements IDisposable with @implements IAsyncDisposable

and then replace the Dispose() method with

public ValueTask DisposeAsync()
{
    return _timer?.DisposeAsync() ?? ValueTask.CompletedTask;
}

Update

but I get the same error

I can't reproduce your error but I suppose that F5 is rather brutal.

You could try putting a try/catch aroud the StateHasChanged call:

try
{
  await InvokeAsync(StateHasChanged);    
}
catch
{  // empty on purpose
}
Related