Home > Blockchain >  Avoid event delegate recreation for async methods
Avoid event delegate recreation for async methods

Time:11-09

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

  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.

  • Related