Blazor: Access parameter from layout

How can I access a page's route parameter from the layout?

I have a page that accepts a route parameter like the following:

@page /my-page/{Slug}

I am needing to access the value of Slug when rendering markup in the shared layout.

I have tried implementing OnParametersSet in the layout file like the following, but the value is not set. It's only assigned at the page level.

@inherits LayoutComponentBase

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <div class="top-row px-4">
            @this.Slug   <<<<------ display the parameter
    </div>

    <div class="content px-4">
        @Body
    </div>
</div>

@code
{
    [Parameter]
    public string Slug { get; set; }

    protected override void OnParametersSet()
    {
        // Slug is always null :-/
    }
}

Solution 1:

You can make the RouteData available to all your components via CascadingParameters. In your App.razor, do this:

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <CascadingValue Value="@routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MyLayout)" />
        </CascadingValue>
    </Found>
</Router>

Then, in all components where you need the RouteData, simply add:

@code
{
    [CascadingParameter]
    RouteData RouteData { get; set; }
}

The value will be automatically populated.

Solution 2:

After some muddling around, I have come up with the following solution. It can probably all be done in the .razor files, but I implemented some of the mess in the "code-behind" files to hide what seems to be a kludge.

In the Layout instance, if you override OnParametersSet, and drill into Body.Target you'll find the RouteData, containing the route parameter(s). You can then propagate these value(s) to the child components in the layout.

Page with a "Slug" parameter we want to make available to the Layout

@page "/mypage/{Slug}"

Layout .razor file

@inherits Components.MyLayoutBase

<div class="sidebar">
    <!-- Pass the Slug property to the NavMenu's Parameter -->
    <MyNavMenu Slug="@Slug" />
</div>

<div class="main">
    <div class="top-row px-4"></div>    
    <div class="content px-4">
        @Body
    </div>
</div>

Layout code behind

public class MyLayoutBase : LayoutComponentBase
{
    public string Slug { get; set; }

    protected override void OnParametersSet()
    {
        // pull out the "Slug" parameter from the route data
        object slug = null;
        if ((this.Body.Target as RouteView)?.RouteData.RouteValues?.TryGetValue("Slug", out slug) == true)
        {
            Slug = slug?.ToString();
        }
    }
}

Navigation Menu

<div class="top-row pl-4 navbar navbar-dark">
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="@($"/some-page/{Slug}/foo")">
                <span class="oi oi-home" aria-hidden="true"></span> Foo
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="@($"/some-page/{Slug}/bar")">
                <span class="oi oi-home" aria-hidden="true"></span> Bar
            </NavLink>
        </li>
    </ul>
</div>

@code {

    [Parameter]
    public string Slug { get; set; }

    bool collapseNavMenu = true;

    string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    string setupUrl = string.Empty;

    void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }
}