I'm building a Blazor server app that has a side navigation component as well as a top navigation component.
I want the side nav's "selected" status to get cleared if the user selects an item from the top nav menu.
In my MainLayout.blazor.cs
I have a variable called ResetSelection
:
protected bool ResetSelection { get; set; } = false;
I'm passing this from the MainLayout.blazor
file into both top and side nav components:
<SideNav AuthUser="@navAuth.User.GetAuthUserModel()" ResetSelection="@ResetSelection" />
<TopNav AuthUser="@Auth.User.GetAuthUserModel()" ResetSelection="@ResetSelection" />
In TopNav.razor.cs
I check if a nav item has been selected, and if it has, I set the variable to true
:
private void itemSelected(MenuEventArgs<CategoryModel> args)
{
// if item selected set the main nav selected item to null
// breakpoint gets hit-- this method gets fired as expected
ResetSelection = true;
}
In the SideNav.razor.cs
component I use an OnParameterSet
to check if the param is true
, and if so I clear the current selected nav item and reset the variable to false:
protected override void OnParametersSet()
{
base.OnParametersSet();
if (ResetSelection == true)
{
// we never get here!
NavMenu.UnselectAll();
ResetSelection = false;
}
}
I don't ever get the OnParametersSet
triggered with a ResetSelection == true
condition-- and I don't understand why. I can't seem to make this interaction work between two child components.
Is the parameter passed in being scoped to the local component when it has its value changed in TopNav.razor.cs
?
You can apply the Blazor Notification Pattern to your problem.
This approach gets away from the fragile spaghetti plumbing inevitable when you try and create more than simple Parameter/Callback relationships between parent/child components.
Create a simple State object and then cascade it. readonly
and IsFixed
prevents RenderTree cascades: renders are driven by events.
State Object:
public class MenuState
{
public event EventHandler<EventArgs>? StateChanged;
public void NotifyStateCahnged(object? sender)
=> this.StateChanged?.Invoke(sender, EventArgs.Empty);
}
Your layout.
<CascadingValue Value="_state" IsFixed>
//... layout markup
</CascadingValue>
@code {
private readonly MenuState _state = new();
}
Then wherever you use it:
@implements IDisposable
<h3>TopMenu</h3>
@code {
[CascadingParameter] private MenuState State { get; set; } = new();
protected override void OnInitialized()
=> this.State.StateChanged += this.OnStateChanged;
protected async Task MenuClicked()
{
// do whatever maybe async
// emulate an async action
await Task.Delay(10);
this.State.NotifyStateCahnged(this);
}
private void OnStateChanged(object? sender, EventArgs e)
{
// only need to render if it wasn't me who triggered the event
if (sender != this)
{
// Do whatever
this.StateHasChanged();
}
}
public void Dispose()
=> this.State.StateChanged -= this.OnStateChanged;
}
An alternative to the cascade is to register MenuState
as a Scoped Service and then inject it where you need it.