I have a Blazor Server app with many buttons rendered in a for-loop.:
@for (int i = 0; i < _navItems.Count; i )
{
var localI = i;
<div class="col-3 mb-2">
<button @onclick="async () => await SetCurrentAsync(localI)" class="btn btn-sm">
@(i 1)
</button>
</div>
}
However, this approach is not recommended by Microsoft Docs here because the delegates specified in @onclick
are recreated each time the component is rendered:
Blazor's recreation of lambda expression delegates for elements or components in a loop can lead to poor performance.
The solution provided in the docs thereafter(and also in the linked GitHub issue is to create a Button
type with an Action
property that holds the delegate:
@foreach (var button in _buttons)
{
<div class="col-3 mb-2">
<button @key="button.Id" @onclick="button.Action" class="btn btn-sm">
@(i 1)
</button>
</div>
}
@code {
List<Button> _buttons = new();
List<NavItem> _items;
protected override async Task OnInitializedAsync()
{
_items = await GetItemsFromDb();
for(int i = 0; i < _items.Count; i )
{
var localI = i;
_buttons.Add(new Button
{
Id = item.Id,
Action = () => SetCurrent(localI);
});
}
}
class Button
{
public int Id { get; set; }
public Action Action { get; set; }
}
}
Now, the @onclick
references Button.Action
and solves the delegate recreation problem.
It is all fun and games until SetCurrent
is not async.
Action
will have to be changed to Func<Task>
and buttons will have to be added using an async lambda expression:
_buttons.Add(new Button
{
Id = item.Id,
Action = async () => await SetCurrentAsync(localI);
});
And I still have to do:
@onclick="async() => await button.Action"
which would again recreate the delegates. How exactly can I do this for async methods?
CodePudding user response:
Firstly, doing something like this
@onclick="async () => await SetCurrentAsync(localI)"
is unnecessary. You're wrapping a Task within a Task.
@onclick="()=> SetCurrentAsync(localI)"
works exactly the same. The Blazor Component internal event handler (for button clicks,...) wraps whatever action you pass in a Task. At it's simplest it looks like this:
var task = InvokeAsync(EventMethod);
StateHasChanged();
if (!task.IsCompleted)
{
await task;
StateHasChanged();
}
You should always use Func<Task>
to handle both sync Task
and Task
. Using an Action
with an async method is a NoNo - It returns a void
to the Blazor Component event handler.
See the code page below for a working demo. There are three buttons that use the pattern
- First calls a yielding Task method with async Task.
- Second calls a simple Task method.
- Uses an
Action
withasync void
and demonstrates the UI update problem .
The key is that the first two both return a Task
to the Blazor Component event handler so it can handle component rendering correctly.
@page "/"
<h3>Button Actions</h3>
@foreach (var item in _buttonActions)
{
<div class="m-2">
<button class="btn btn-secondary" @onclick="item.Action">@item.Title</button>
</div>
}
<div>
@message
</div>
@code {
private List<ButtonAction> _buttonActions = new List<ButtonAction>();
protected override void OnInitialized()
{
{
var item = new ButtonAction() { Title = "Task" };
item.Action = () => GetValue(item);
_buttonActions.Add(item);
}
{
var item = new ButtonAction() { Title = "Async Task" };
item.Action = () => GetValueAsync(item);
_buttonActions.Add(item);
}
{
var item = new ButtonAction() { Title = "Async Void Task" };
item.MyAction = () => GetValueVoidAsync(item);
_buttonActions.Add(item);
}
}
private string message;
public Task GetValue(ButtonAction item)
{
message = $"Value: {item.Id}";
return Task.CompletedTask;
}
public async Task GetValueAsync(ButtonAction item)
{
await Task.Yield();
message = $"Value: {item.Id}";
}
public async void GetValueVoidAsync(ButtonAction item)
{
await Task.Yield();
message = $"Value: {item.Id}";
}
public class ButtonAction
{
public string Title { get; set; }
public Guid Id { get; } = Guid.NewGuid();
public Func<Task> Action { get; set; }
public Action MyAction { get; set; }
}
}
On performance, I think it really depends on "How many?". I don't use the pattern for edit lists were I may have 25 edit and view buttons. I've never noticed a problem.