void vs Task .. state not updating on the page

101 Views Asked by At

I have two custom components on a page; a Month picker and a Grid. Both components work great on their own. The Grid is displaying items from a an API, I can request items between a date range by supplying a start and end date. I have implemented a Month Picker module which will allow me to navigate all items by Month. The MonthPicker picker implements an Event Callback which will perform the request each time it changes.

Below is what I have, which current works.

Index.razor:

<MonthPicker @ref="MonthPicker1" DefaultMonth=07 DefaultYear=2021 OnMonthPickerChange="OnMonthPickerChange"></MonthPicker>
@if (listResponse is null || transactionClasses is null)
{
    Loading...
} 
else
{
    <Grid Items="listResponse.Transactions" @ref="MyGrid" PageSize=40>
        <GridBody Context="row">
            <GridCell>@row.Item.Id</GridCell>
            <GridCell>@row.Item.Description</GridCell>
        </GridBody>
    </Grid>
}

Index.razor.cs:

namespace Accounting.Web.Pages.Transactions
{
    public partial class List : ComponentBase
    {
        [Inject]
        private IHttpService HttpService { get; set; }

        private Request.ListDto listRequest { get; set; } = new();

        [Parameter]
        public ResponseContext.ListDto listResponse { get; set; } = default!;

        private MonthPicker MonthPicker1 { get; set; } = default!;
        private Grid<ResponseContext.ItemDto> MyGrid { get; set; } = default!;


        protected async override Task OnInitializedAsync()
        {
            listRequest.DateStart = DateTime.Parse("2021-07-01", new CultureInfo("en-GB", true));
            listRequest.DateEnd = DateTime.Parse("2021-07-30", new CultureInfo("en-GB", true));
            listResponse = await HttpService.Post<ResponseContext.ListDto>("http://localhost:4000/transactions/list", listRequest, true);
        }


        public async Task OnMonthPickerChange()
        {
            listResponse = null;
            listRequest.DateStart = DateTime.Parse("2021-07-01", new CultureInfo("en-GB", true));
            listRequest.DateEnd = DateTime.Parse("2021-07-30", new CultureInfo("en-GB", true));
            listResponse = await HttpService.Post<ResponseContext.ListDto>("http://localhost:4000/transactions/list", listRequest, true);
            System.Console.WriteLine(MonthPicker1.Month + " : " + MonthPicker1.Year);
        }
    }
}

However for the event callback method OnMonthPickerChange, initially I had the following:

public async void OnMonthPickerChange()

In other words, I was using async void instead of Task. Using void resulted in the listResponse remaining empty (for rendering) in the template and therefore the list would be displayed as empty when using the MonthPicker. It was only by luck I decided to try Task instead of void which resolved the issue, but I don't have any idea why one works why the other doesn't.

I was hoping someone could shed light on this mysetery for me.

1

There are 1 best solutions below

0
MrC aka Shaun Curtis On

This is the actual code that handles all UI events in ComponentBase. Your handler is callback.

If you don't provide a Task then task is null and there's nothing to wait on. That will work as long as your handler is synchronous.

However, as soon as your handler yields, HandleEventAsync runs to completion before your handler completes. The render has already happened before you update the data. Everything is one step behind,

Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object? arg)
{
    var task = callback.InvokeAsync(arg);
    var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
        task.Status != TaskStatus.Canceled;

    // After each event, we synchronously re-render (unless !ShouldRender())
    // This just saves the developer the trouble of putting "StateHasChanged();"
    // at the end of every event callback.
    StateHasChanged();

    return shouldAwaitTask ?
        CallStateHasChangedOnAsyncCompletion(task) :
        Task.CompletedTask;
}

// Only called if the original task yields and is therefore still running
private async Task CallStateHasChangedOnAsyncCompletion(Task task)
{
    try
    {
        await task;
    }
    catch // avoiding exception filters for AOT runtime support
    {
        // Ignore exceptions from task cancellations, but don't bother issuing a state change.
        if (task.IsCanceled)
        {
            return;
        }
        throw;
    }
    StateHasChanged();
}