2

When consuming a component in another razor component, there is usually a need to call async methods triggered by an EventCallback property, onClick for instance. Is there any particular way to call it to assure asynchronicity?

Let's assume the following example:

@* CASE 1*@
<MyButton OnClick=DoSomething>Click Me</MyButton>

@* CASE 2*@
<MyButton OnClick=@(async () => await DoSomething())>Click Me</MyButton>

@code {
    private async Task DoSomething(){ ... }
}

After compiling, is there any distinction between using a delegate or calling the function directly? Should we prefer one or the other?

This question doesn't derive from an error or code running improperly but only from the fact that I get no feedback from Visual Studio and probably for a good reason but I would like to know if I'm writing improper code either way.

Choice Remark: Hard to pick a single answer for all provide valid points to the discussion, further reading is encouraged. It seems the confusion stems from a misunderstanding of the delegate's role in the call stack, in that way I think that Shaun's answer shows the most succinct and explicit example of that.

Hugo
  • 89
  • 1
  • 6
  • Does this answer your question? [Execute async method on button click in blazor](https://stackoverflow.com/questions/55497072/execute-async-method-on-button-click-in-blazor) – kltft Mar 17 '23 at 19:03
  • 1
    @kltft Thanks for sharing, I've missed it. It's similar but in the end looks like there are people supporting both sides and the uncertainty still remains. I guess we should use the most appropriate case for the occasion. – Hugo Mar 17 '23 at 19:13

3 Answers3

2

Lambda expressions are fully supported and in fact, in some cases they may be required. Consider you are doing a @foreach(item in items) to pass the item to the function you'd likely need to use a lambda expression:

@(async () => await DoSomething(item))

Here is Microsoft documentation supporting lambas:

https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-7.0

weeksdev
  • 4,265
  • 21
  • 36
  • Indeed, I've used both cases in similar scenarios but I'm still not sure if using a Lambda is required to ensure the method runs async, even when it doesn't need arguments passed in. – Hugo Mar 17 '23 at 19:07
  • 2
    @Hugo check this [example](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/event-handling?view=aspnetcore-7.0#focus-an-element). No lambda used. There are plenty other examples in Blazor documentation with the same approach. – Dimitris Maragkos Mar 17 '23 at 19:19
  • 1
    This code is wasteful. It wraps a Task within a Task. just calling `DoSomething(item)` achieves the same result. – MrC aka Shaun Curtis Mar 17 '23 at 21:08
  • @MrCakaShaunCurtis you will get error if you try to do that: cannot convert Task to EventCallback. Its basically if you are in foreach and need to pass "current" item you're gonna get in this situation. – weeksdev Jun 01 '23 at 21:00
  • Apologies, I was too succinct. What I meant was `OnClick="() => DoSomething(item)"`. You don't need the extra Task wrapper. – MrC aka Shaun Curtis Jun 01 '23 at 21:22
1

In this specific case, the first is better.

This code block is wrapping a Task within a Task. It wasteful on resources: each Task is a state machine object.

OnClick=@(async () => await DoSomething())>

However, what actually happens depends on MyButton.

Here's a demo version. I've added two button event handlers, with comments on how each executes.

<button @attributes=this.AdditionalAttributes @onclick=this.OnButtonClickAsync></button>

@code {
    [Parameter] public EventCallback<MouseEventArgs> OnClick { get; set; }
    [Parameter] public RenderFragment? ChildContent { get; set; }
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object>? AdditionalAttributes { get; set; }

    private async Task OnButtonClickAsync(MouseEventArgs e)
    {
        // you are awaiting the delegate in the parent component
        // if it yields you'll await it's completion
        await this.OnClick.InvokeAsync(e);
        // Code here will only execute after the parent delegate has completed, awaits and all
        // any exceptions will bubble up to here 
    }

    private void OnButtonClick(MouseEventArgs e)
    {
        // This is a Fire and Forget call
        this.OnClick.InvokeAsync(e);
        // code here will execute as soon as the parent delegate yields
        // exceptions won't bubble up to here
    }
}

More generally, it's a matter of personal preference.

I like clean markup, so I code like this.

<button disabled="@buttonCss"></button>

@code {
    private bool _isDisabled;
    private string buttonCss => _isDisabled ? "btn btn-danger" : "btn btn-success";
}

Others like succinct [inline] code.

<button disabled="@(_isDisabled ? "btn btn-danger": "btn btn-success")"></button>

@code {
    private bool _isDisabled;
}
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31
1

The following snippet taken straight out of Microsoft docs:

In the following example, UpdateHeading:

  • Is called asynchronously when the button is selected.
  • Waits two seconds before updating the heading.

Pages/EventHandlerExample2.razor:

@page "/event-handler-example-2"

<h1>@currentHeading</h1>

<p>
    <label>
        New title
        <input @bind="newHeading" />
    </label>
    <button @onclick="UpdateHeading">
        Update heading
    </button>
</p>

@code {
    private string currentHeading = "Initial heading";
    private string? newHeading;

    private async Task UpdateHeading()
    {
        await Task.Delay(2000);

        currentHeading = $"{newHeading}!!!";
    }
}

As you can see lambda is not used (@onclick="UpdateHeading") but still it is mentioned that UpdateHeading will be called asynchronously.

Also when passing parameters, wrapping in async await is not required. The following will work correctly:

@page "/event-handler-example-2"

<h1>@currentHeading</h1>

<p>
    <label>
        New title
        <input @bind="newHeading" />
    </label>
    <button @onclick="() => UpdateHeading(2000)">
        Update heading
    </button>
</p>

@code {
    private string currentHeading = "Initial heading";
    private string? newHeading;

    private async Task UpdateHeading(int delay)
    {
        await Task.Delay(delay);

        currentHeading = $"{newHeading}!!!";
    }
}

A lambda with async/await would be useful if instead of declaring UpdateHeading, you wanted to use anonymous method:

@page "/event-handler-example-2"

<h1>@currentHeading</h1>

<p>
    <label>
        New title
        <input @bind="newHeading" />
    </label>
    <button @onclick="@(async () => { await Task.Delay(2000); currentHeading = $"{newHeading}!!!"; })">
        Update heading
    </button>
</p>

@code {
    private string currentHeading = "Initial heading";
    private string? newHeading;
}
Dimitris Maragkos
  • 8,932
  • 2
  • 8
  • 26