Consider the scope of your state tracking object and your component/page. Ideally, they should match.
Your static class doesn't. It's shared by all users and all instances of the page.
My original answer, which you quoted here, doesn't either. It's something I've been working on for a while. DotNetCore doesn't provide us with a DI container we can scope to the component/page. OwningComponentBase
recognises the problem and attempts to solve it, but isn't fit for purpose.
Consider the following Counter page implementation.
A Counter State object:
public class CounterState : IDisposable
{
// This is here simply to demonstrate Dependancy Injection
private NavigationManager _navigationManager;
public int Counter { get; private set; }
public event EventHandler<CounterChangedEventArgs>? CounterChanged;
public CounterState(NavigationManager navigationManager)
=> _navigationManager = navigationManager;
public void IncrementCounter(object? sender)
{
this.Counter++;
this.CounterChanged?.Invoke(sender, CounterChangedEventArgs.Create(this.Counter));
}
// implementated to demonstrate dealing with an IDisposable State object
public void Dispose() { }
}
And a custom EventArgs
public class CounterChangedEventArgs : EventArgs
{
public int CounterValue { get; set; }
public static CounterChangedEventArgs Create(int value)
=> new CounterChangedEventArgs { CounterValue = value };
}
A simple Counter display component:
@implements IDisposable
<div class="bg-dark text-white m-2 p-2">
<pre>Value : @_counterState?.Counter </pre>
</div>
@code {
[CascadingParameter] private CounterState _counterState { get; set; } = default!;
protected override void OnInitialized()
{
ArgumentNullException.ThrowIfNull(_counterState);
_counterState.CounterChanged += this.OnCounterChanged;
}
private void OnCounterChanged(object? sender, CounterChangedEventArgs e)
=> this.InvokeAsync(StateHasChanged);
public void Dispose()
=> _counterState.CounterChanged -= this.OnCounterChanged;
}
And an Incrementer component:
<div class="m-2 p-2">
<button class="btn btn-primary" @onclick=this.OnIncrementCounter>Increment</button>
</div>
@code {
[CascadingParameter] private CounterState? _counterState { get; set; }
protected override void OnInitialized()
=> ArgumentNullException.ThrowIfNull(_counterState);
private Task OnIncrementCounter()
{
_counterState?.IncrementCounter(this);
return Task.CompletedTask;
}
}
And finally the revised Counter
page:
@page "/counter"
@implements IDisposable
@inject IServiceProvider serviceProvider
<PageTitle>Counter</PageTitle>
<h1>Counter Page</h1>
<CascadingValue Value="_counterState">
<CounterViewer />
<CounterViewer />
<CounterViewer />
<CounterViewer />
<CounterViewer />
<div class="row">
<div class="col-2">
<CounterButton />
</div>
<div class="col-2">
<CounterButton />
</div>
<div class="col-2">
<CounterButton />
</div>
<div class="col-2">
<CounterButton />
</div>
</div>
</CascadingValue>
@code {
private CounterState? _counterState;
protected override void OnInitialized()
{
// demonstrates creating an object instance in the context of thw Service Container
// this will inject any defined dependencies (in our case the Scoped NavigationManager)
_counterState = ActivatorUtilities.CreateInstance<CounterState>(serviceProvider);
ArgumentNullException.ThrowIfNull(_counterState);
}
// This will get called by the Renderer when the page/component goes out of scope
public void Dispose()
=> _counterState?.Dispose();
}
This implementation:
- Creates a object to track state and raise events to notify state changes.
- The object has the same scope as the page.
- The page cascades the state object.
- Sub components capture the cascaded state and call methods to update the state and/or register event handlers to react to state changes.
- The page creates the state object in the DI container context, so any required DI services get injected.
The important feature of this pattern is matching the scope of the state object to it's owning component. Using DI: Scoped
is too broad and Transient
too narrow [each component gets a new instance]. If you cascade a Transient obtained service that implements IDisposable
, you create a memory leak. Creating the instance using ActivatorUtilities
solves the DI issues. However you are responsible for implementing disposal.
There's an article here that covers the subject in more detail - https://www.codeproject.com/Articles/5352916/Matching-Services-with-the-Blazor-Component-Scope.