2

Have working knowledge of Blazor WebAssembly but not too advanced.

I'm good with EventCallback from child components. What I haven't seen anywhere documented is how to subscribe to EventCallback from pages rendered from Blazors Router.

The use case is i've got a layout page with the placeholder for @Main. I'd like to subscribe to an EventCallback for a certain page within the layout but there's no way to reference that page's EventCallback directly since @Main could be any page and also it has no class type.

Specifically lets say i have several pages that allow a user to make a selection. Further assume they all share the same layout page. See the code below for the layout page code. I've got a cascading parameter to share the selections across all the pages and components. How would I subscribe to the EventCallbacks in the pages that would be inserted into @Main from the layout page?

<main>
    <CascadingValue Value="@selectionIds" Name="selectionIds" >
        @Body 
        <DetailView />
    </CascadingValue>
</main>
1
  • This looks like you are trying to share state and state changes across components. If so then you need to use some form of the Notification Pattern. See this : stackoverflow.com/a/76258228/13065781 and many other answers on the topic - search "Blazor Notification Pattern" on SO or elsewhere. Commented Aug 9, 2023 at 15:10

2 Answers 2

1

Here's a Blazor Notification Pattern implementation that uses a Scoped service to maintain the data. I've used a simple country select to demonstrate the principles. selectionIds doesn't mean a lot out of context.

The first step is to separate the data and state from the component:

public class CountryData
{
    public string? Country { get; private set; }

    public event EventHandler? CountryChanged;

    public void SetCountry(string? value )
    {
        this.Country = value;
        this.CountryChanged?.Invoke( this, EventArgs.Empty );
    }
}

Registered as a Scoped Service

builder.Services.AddScoped<CountryData>();

A CountryDisplay component to dispay the country.

@implements IDisposable
@inject CountryData CountryData

<div class="alert alert-primary m-2">@this.CountryData.Country</div>

@code {

    protected override void OnInitialized()
        => this.CountryData.CountryChanged += OnCountryChanged;

    private void OnCountryChanged(object? sender, EventArgs e)
        => this.StateHasChanged();

    public void Dispose()
        => this.CountryData.CountryChanged -= OnCountryChanged;
}

And a CountrySelector component to modify it:

@inject CountryData CountryData

<div class="bg-light m-2 pt-2 p-4 border border-1">

    <h3>Country Selector</h3>

    <InputSelect class="form-select" @bind-Value:get="this.CountryData.Country" @bind-Value:set="this.CountryData.SetCountry">

        @if (this.CountryData.Country is null)
        {
            <option selected disabled value=""> -- Select a Country -- </option>
        }

        @foreach (var country in CountryProvider.CountryList)
        {
            <option value="@country">@country</option>
        }

    </InputSelect>

</div>

@code {
    // NOTE - @bind-Value:get may give you an error.  It's a SDK issue.  The code will compile.
}

CountryLayout

@inherits LayoutComponentBase

<PageTitle>SO76864163</PageTitle>

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

    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

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

@code {
    private CountryData _countryData = new();
}

And Index:

@page "/"
@layout CountryLayout

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt Title="How is Blazor working for you?" />

<CountrySelector />

And Counter:

@page "/counter"
@layout CountryLayout

<PageTitle>Counter</PageTitle>

<h1>Counter</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

<CountrySelector />

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks Shaun... this was thorough and helpful and i like the approach. Although (while not strictly stated in my post) in the event that there are multiple instances of the component parent / child relationship... I don't want to need to add more logic to a monolithic app wide state manager via a singleton (or scoped singleton in your example). So where possible I prefer components can handle communication directly via the component tree (i.e. parameters & eventcallback) without resorting to further dependencies.
Each approach has it's pros and cons. The fundamental problem is that Blazor has no Form scope to which you can assign your data object. Most of my objects are transient and either cascaded from the top Form component, or through a Provider service which provides a data object instance with a Guid reference which the Form component then cascades. A good example is a ListController which maintains the page, sorting, etc for a List Form and acts as a broker for all the different components that make up a modern grid form. The EditContext is another good example.
Do you have a small mistake? This line public void Dispose() => this.CountryData.CountryChanged += OnCountryChanged; Should it not be: -= instead?
Yes it should, well spotted. Thanks. Updated.
-2

I did considerable more research and for what seems like a common use case this feels far too under documented. The below modifications work.

It wasn't obvious that an EventCallback can be passed just like any other property via CascadingParameter. So you simply need to wrap the @Main route placeholder with an another CascadingParameter for the EventCallback.

<main>
  <CascadingValue Value="@selectionIds" Name="selectionIds" >
    <CascadingValue Value="@SelectionChanged" Name="SelectedChanged" >
      @Body 
      <DetailView />
    </CascadingValue>
  </CascadingValue>
</main>
@code{
   EventCallback<string> SelectionChanged => 
        EventCallback.Factory.Create<string>(this, (ids) => 
           Console.WriteLine($"Selection of {ids} Bubbled Up.")
        );
}

Then in any of the router delivered pages and/or other components:

[CascadingParameter(Name = "SelectionChanged")]
public EventCallback<string> SelectionChanged { get; set; }

private async Task someTrigger(){
   await SelectionChanged.InvokeAsync(selectionIds);
}

2 Comments

This is certainly possible however, passing each EventCallBack as its own CascadingValue is bound to get messy especially if there are multiple EventCallbacks. I think using .NET Events like in this answer would be a better approach.
[Polite] "what seems like a common use case this feels far too under documented." Maybe the reason for that is you are coming at this from the wrong direction. The answer in general is the Blazor Notification pattern. Passing EventCallback's around as cascaded parameters is a terribly convoluted way to solve the problem. Apply SRP, and separate the data and state from the component and things start to become clearer. I've provided an answer that I hope demonstrates the principle.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.