Search code examples
c#parametersblazorblazor-server-side

Passing shared parameter to blazorcomponents


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?


Solution

  • 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.