2

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?

gunr2171
  • 16,104
  • 25
  • 61
  • 88
Amal K
  • 4,359
  • 2
  • 22
  • 44
  • 2
    I don't know much Blazor but why would you need to do `async() => await button.Action`? You already do this when assigning that `Action`. That is - why not just do `button.Action`? – Evk Nov 08 '21 at 16:04
  • 1
    I'd even say there is no reason to change anything. `Action = () => SetCurrentAsync(localI)`, no need to await anything here, your `Action` is `Func` and that already returns `Task`. And same with click handler, it supports handlers which return task, so just do `onclick="button.Action" – Evk Nov 08 '21 at 16:28
  • 1
    @Evk see [this post](https://stackoverflow.com/questions/55497072/execute-async-method-on-button-click-in-blazor), it's already a known pattern – gunr2171 Nov 08 '21 at 17:23
  • @Evk Thanks. I wasn't completely sure if the on click handler would internally await the method and I didn't want to end up not awaiting the task. But it appears that it does, in fact, internally await and doesn't need an additional one. – Amal K Nov 08 '21 at 18:10
  • @gunr2171 It seems like the await is not required anymore in newer versions of Blazor. See [this post](https://stackoverflow.com/a/60488286/11455105). – Amal K Nov 08 '21 at 18:15
  • 1
    That's actually really good to know. This was something driving me crazy as well. – gunr2171 Nov 08 '21 at 18:16
  • 1
    It went away a long time ago. It seriously irks me every time I still see in MS Docs. You see it in MS Docs, it must be the right way! – MrC aka Shaun Curtis Nov 08 '21 at 18:38
  • Even if click was not internally awaited - your approach wouldn't change anything. You just wrap one task inside another task, which is not (internally) awaited, and the result is the same (exceptions will not propagate). – Evk Nov 08 '21 at 20:22

1 Answers1

2

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

  1. First calls a yielding Task method with async Task.
  2. Second calls a simple Task method.
  3. Uses an Action with async 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.

MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
  • Thanks. I was concerned about performance because in my case, sometimes you could end up with little more than a hundred buttons. (It's a question navigator for a quiz) – Amal K Nov 08 '21 at 18:19