This looks like code from a dynamic table.
First, do you really want the scope of your TableService
to be for the lifetime of the SPA and shared by multiple instances of your table?
My guess is no, so you need to scope the your TableState
object to the page holding the table. You can use a cascade to make it available to all the page components.
Here's my TableState
. It controls access to the list to trigger HeadersHaveChanged
events when things change. I've changed the Header
object to a simple readonly struct to make it and the Headers
property immutable.
public readonly record struct Header(string Name);
public class TableState
{
private readonly List<Header> _headers = new();
public IEnumerable<Header> Headers => _headers.AsEnumerable();
public event EventHandler? HeadersHaveChanged;
public void AddHeader(Header header)
{
_headers.Add(header);
this.HeadersHaveChanged?.Invoke(this, EventArgs.Empty);
}
}
My Index
now looks like this:
<CascadingValue Value="_tableState" IsFixed>
<Table>
<Header Name="Id" />
<Header Name="Email" />
</Table>
</CascadingValue>
@code {
private TableState _tableState = new();
}
Table
looks like this. It renders the child content on the first render [which registers the columns] and renders the column code on subsequent renders.
@implements IDisposable
@if (_firstRender)
{
@ChildContent
_firstRender = false;
}
else
{
<table class="table">
<tr>
@foreach (var header in _tableState.Headers)
{
<th>@header.Name</th>
}
</tr>
</table>
}
@code {
[CascadingParameter] private TableState _tableState { get; set; } = default!;
[Parameter] public RenderFragment? ChildContent { get; set; }
private bool _firstRender;
protected override void OnInitialized()
{
ArgumentNullException.ThrowIfNull(_tableState);
_firstRender = true;
_tableState.HeadersHaveChanged += this.OnHeadersChanged;
}
// Do a yield to get the first render
protected async override Task OnInitializedAsync()
=> await Task.Yield();
private void OnHeadersChanged(object? sender, EventArgs e)
=> this.StateHasChanged();
// Isn't absolutely necessary in this casde as the _tableState object is of the same scope
public void Dispose()
=> _tableState.HeadersHaveChanged -= this.OnHeadersChanged;
}
And Header
@code {
[Parameter, EditorRequired] public string Name { get; set; } = "Not Set";
[CascadingParameter] private TableState _tableState { get; set; } = default!;
protected override void OnInitialized()
{
ArgumentNullException.ThrowIfNull(_tableState);
_tableState.AddHeader(new(Name));
}
}