I have a simple question I can't find a clear answer to.
Currently, since I have a user 'state' model, I use Cascading Parameters to my child components and update them when the state model requires it using something like this:
private async void OnClientStateChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "propertyChanged" && sender is ClientState state)
{
_needsReselect = true;
await InitScreen();
}
}
I would rather use an OnParametersSet
method that gets called when the cascading parameter of my user state is modified so my question is:
Question: Does a change to a Cascading Parameter
fire the OnParametersSet
event? In my testing I can not get it to do so.
How I solved in meantime:
protected override void OnAfterRender(bool firstRender)
{
if (firstRender)
{
if (OperatingSystem.IsBrowser())
{
clientStates!.PropertyChanged -= OnClientStateChanged;
clientStates!.PropertyChanged += OnClientStateChanged;
}
}
}
private async void OnClientStateChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "SelectedViewLevel" && sender is ClientStates state)
{
await InitScreen();
}
}
Important points
A UI event in any ComponentBase
component will trigger a call to StateHasChanged
on the first async yield and at the completion of the handler.
Components have no inbuilt mechanism to detect a change in state. You need to do that manually.
The renderer will call SetParametersAsync
on any child component where it detects a state change in the child's parameters. With objects it can't detect a change, so it will always call SetParametersAsync
.
You must call StateHasChanged
on normal events to force a component render event.
Demo Code
public class CounterState
{
public int Value { get; private set; }
public event EventHandler<int>? ValueChanged;
public void Increment()
{
this.Value++;
this.ValueChanged?.Invoke(this, this.Value);
}
}
CounterComponent
<div class="m-2 p-2 border border-1">
<p role="status">Current count: @this.State?.Value</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@this.ChildContent
</div>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
[CascadingParameter] private CounterState? State { get; set; }
private void IncrementCount()
{
this.State?.Increment();
}
}
BlockComponent
<div class="m-2 p-2 border border-1">
@this.ChildContent
</div>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
}
Counter
@page "/counter"
<PageTitle>Counter</PageTitle>
<h1>Counter</h1>
<p role="status">Current count: @_state.Value</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
<CascadingValue Value="_state" >
<CounterComponent>
<CounterComponent>
<BlockComponent>
<CounterComponent />
</BlockComponent>
</CounterComponent>
</CounterComponent>
</CascadingValue>
@code {
private readonly CounterState _state = new();
private void IncrementCount()
{
_state.Increment();
}
}
Explanation
SetParametersAsync
on any where it detects a parameter state change.As this component is the the owner of the cascade, it also calls SetParametersAsync
on all components that use it.
The cascade stops at the BlockComponent
because there's no state change on that component. The CounterComponent
within the BlockComponent
block doesn't get refreshed.
Example result on clicking the second button down.
One solution is to use an event in the State object and wire up the components that need to refresh to the event.
CounterComponent could look something like this:
@implements IDisposable
<div class="m-2 p-2 border border-1">
<p role="status">Current count: @this.State?.Value</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@this.ChildContent
</div>
@code {
[Parameter] public RenderFragment? ChildContent { get; set; }
[CascadingParameter] private CounterState? State { get; set; }
private Guid _token = Guid.Empty;
protected override void OnInitialized()
{
if (this.State is not null)
this.State.ValueChanged += this.OnStateChanged;
}
private void OnStateChanged(object? sender, Guid token)
{
if (token != _token)
this.StateHasChanged();
}
private void IncrementCount()
{
_token = Guid.NewGuid();
this.State?.Increment(_token);
}
public void Dispose()
{
if (this.State is not null)
this.State.ValueChanged -= this.OnStateChanged;
}
}
And:
public class CounterState
{
public int Value { get; private set; }
public event EventHandler<Guid>? ValueChanged;
public void Increment()
{
this.Value++;
this.ValueChanged?.Invoke(this, Guid.Empty);
}
public void Increment(Guid token)
{
this.Value++;
this.ValueChanged?.Invoke(this, token);
}
}
And Counter something like this. Note that the cascade is now fixed.
<CascadingValue IsFixed Value="_state">
<CounterComponent>
<CounterComponent>
<BlockComponent>
<CounterComponent />
</BlockComponent>
</CounterComponent>
</CounterComponent>
</CascadingValue>
@code {
private readonly CounterState _state = new();
private Guid _token = Guid.Empty;
protected override void OnInitialized()
{
_state.ValueChanged += this.OnStateChanged;
}
private void IncrementCount()
{
_token = Guid.NewGuid();
_state.Increment();
}
private void OnStateChanged(object? sender, Guid token)
{
if (token != _token)
this.StateHasChanged();
}
}