Kaine
Kaine

Reputation: 579

Blazor component <select> changed event not firing

I'm new to Blazor and can't seem to figure out why my component event handler doesn't seem to fire. I'm using .NET 8 and the sample template configured to:

In the server project, in the 'Components/Pages' folder, I have a component called DropDownFilter.razor with the following code:

DropDownFilter.razor

@rendermode InteractiveServer

<div>
    <span>
        <select @bind="Item">
            @foreach (var item in Items)
            {
                <option value="@item">@item</option>
            }
        </select>
    </span>
</div>

@code {

    [Parameter]
    public List<string> Items { get; set; } = [];

    [Parameter]
    public EventCallback<List<string>> ItemsChanged { get; set; }

    [Parameter]
    public string Item { get; set; } = string.Empty;

    [Parameter]
    public EventCallback<string> ItemChanged { get; set; }
}

I also have a component SearchFilters.razor:

SearchFilters.razor

@rendermode InteractiveServer

<h3>Search Filters</h3>

<section>
    <div>
        <DropDownFilter ItemChanged="MakeChanged" @bind-Items="Makes" />
    </div>
</section>

@code {

    [Parameter]
    public string Make { get; set; } = string.Empty;

    private List<string> Makes { get; set; } = ["Audi", "BMW", "Volvo"];

    private void MakeChanged(string make)
    {
        // This method is never called.
        this.Make = make;
    }

}

Then, my Home.razor looks like this:

Home.razor

@page "/"
@rendermode InteractiveServer

<PageTitle>Home</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SearchFilters/>

The problem I have is that the MakeChanged() method in the SearchFilters.razor component never seems to fire. I've tried it with both string and ChangedEventArgs, but neither seem to work.

My understanding is that the @bind expects an implied and corresponding EventCallback<> ...Changed() handler, e.g. @bind="MyItem" and the implied handler EventCallback<> MyItemChanged(). I guess that I could try wiring these up explicitly, but I was hoping to take advantage of the new features of Blazor.

Clearly my solution is going to be more complex, with multiple DropDownFilter components whose contents will be determined based on the choice made in the previous one, and their values being drawn from an API, but I've simplifying my test down to this example.

There's obviously something I'm missing, and would be grateful for a pointer in the right direction.

Thanks,

Kaine

Upvotes: 3

Views: 907

Answers (2)

Kaine
Kaine

Reputation: 579

With help from "@MrC aka Shaun Curtis" putting me on the right track, the solution was:

DropDownFilter.razor

<div>
    <span>
        <select @bind="Item" @bind:after="OnItemChanged">
            @foreach (var item in Items)
            {
                <option value="@item">@item</option>
            }
        </select>
    </span>
</div>

@code {

    [Parameter]
    public List<string> Items { get; set; } = [];

    [Parameter]
    public EventCallback<List<string>> ItemsChanged { get; set; }

    [Parameter]
    public string Item { get; set; } = string.Empty;

    [Parameter]
    public EventCallback<string> ItemChanged { get; set; }

    private async Task OnItemChanged()
    {
        await this.ItemChanged.InvokeAsync(this.Item);
    }

}

It seems that, while requiring a callback event property matching the property name used in the @bind attribute (e.g. if the bind property is MyItem then the callback event property must be called MyItemChanged), the callback event property isn't fired automatically when the bound property value is changed. Instead, the @bind:after attribute can be used to call another event handler, which itself can then be used to invoke the callback event property after the bound value has changed.

So, in the chain of events:

  1. The select is bound to a property (let's call it MyItem) using the @bind attribute, e.g. @bind="MyItem".
  2. A corresponding matching callback event property called EventCallback<T> MyItemChanged must be created, where T is the type of MyItem, a string in this case.
  3. In order to get the corresponding EventCallback<T> MyItemChanged to fire , it has to be invoked manually. The trigger for this can be called using the @bind:after attribute, which in this case is mapped to a new event handler method I have called OnItemChanged() (which strictly speaking should probably called OnMyItemChanged() to fit in with what's being explained here, but could quite easily have been called anything I liked).

I hope that's clear, reading it back, my explanation seems a little confusing, but hopefully the example speaks for itself.

Upvotes: 0

MrC aka Shaun Curtis
MrC aka Shaun Curtis

Reputation: 30167

You aren't binding correctly in DropDownFilter. In your code you are just changing the value of Item. You need to set up the two way binding separately.

<div>
    <span>
        <select @bind:get="Item" @bind:set="this.OnItemChanged">
            @foreach (var item in Items)
            {
                <option value="@item">@item</option>
            }
        </select>
    </span>
</div>

@code {
    [Parameter] public List<string> Items { get; set; } = [];
    [Parameter] public string Item { get; set; } = string.Empty;
    [Parameter] public EventCallback<string> ItemChanged { get; set; }

    private Task OnItemChanged(string value)
    {
        return this.ItemChanged.InvokeAsync(value);
    }
}

You can then

<DropDownFilter ItemsChanged="Makees" @bind-Item="Make" />

If you want to flow the Make down to the parent you need to do the same with the get and set as I've sown above.

Also note: You should not be setting the render mode on individual components. A component such as DropDownFilter should be rendermode agnostic: you can use it anywhere. Either set it at the page/form level or the global level. See the note in https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-8.0#apply-a-render-mode-to-a-component-definition

Upvotes: 0

Related Questions