Reputation: 579
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
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:
select
is bound to a property (let's call it MyItem
) using the @bind
attribute, e.g. @bind="MyItem"
.EventCallback<T> MyItemChanged
must be created, where T
is the type of MyItem
, a string
in this case.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
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