Blazor - object displayed in a table not updating after changing a value

Viewed 38

I am developing a Server-Side Blazor app in which a user inputs excel files with data. The app goes over each excel file, checks if it is valid and creates output files (based on excel file content) for user to download.

There is a table that displays IEnumerable<IExcelFile>, a list of objects with following parameters: FileName, FileType, Package (not for display). I would like to call a method that iterates over each IExcelFile and take some actions for each excel (check if valid, process). When the method is running I would like FileType of each IExcelFile to indicate, on which step of the process a particular file is. e.g. When method starts all IExcelFiles have a "Queued" FileType, when one of IExcelFiles is processed it's FileType changes to "Processing", when an exception is thrown for one excel file it's FileType changes to "Error" and so on.

I am able to change those parameters just before and after running this method but not when the method is running. Below is the code:

Table:

<table class="table table-hover">
    <thead>
        <tr>
            <th>FileName</th>
            <th>FileType</th>
            <th style="width: 10px;"></th>
        </tr>
    </thead>
    <tbody>
        @if (excelFiles != null && excelFiles.Count() > 0)
        {
            foreach (var excelFile in excelFiles)
            {
                <tr>
                    <td>@excelFile.FileName</td>
                    <td>@excelFile.FileType.ToString()</td>
                    <td>
                        @if (!IsLoading)
                        {
                            <button class="btn btn-close" @onclick="() => DeleteExcelFile(excelFile)"></button>
                        }
                    </td>
                </tr>
            }
        }
        else
        {
            <tr>
                <td colspan="3">
                    No files provided
                </td>
            </tr>
        }
    </tbody>
</table>

Processing excel file starts when the user clicks a "Start" button:

@if (!IsLoading)
{
    <div class="mb-3">
    <label for="formFileMultiple" class="form-label">Select excel files from your drive</label>
    @for (int i = 0; i < numberOfInputFiles; i++)
    {
        <InputFile @key="i" OnChange="UploadExcelFiles" multiple style="@GetInputFileStyle(i)" class="form-control" type="file" id="formFileMultiple"></InputFile>
    }
    </div>

    @if (IsDownloadAvailable)
    {
        <button type="button" class="btn btn-success" @onclick="DownloadDubCards">Download</button>
        <button type="button" class="btn btn-primary" @onclick="ClearExcelFileList">Clear</button> 
    }
    else
    {
        <button type="button" class="btn btn-primary" @onclick="GenerateDubCards">Start</button>
        <button type="button" class="btn btn-primary" @onclick="ClearExcelFileList">Clear</button> 
    }
       
}
else
{
    <div class="spinner-border text-primary" role="status" style="left: 50%; position: absolute; width: 3rem; height: 3rem;">
        <span class="visually-hidden">Loading...</span>
    </div>
}

And the method that is processing the excel files:

private void GenerateDubCards()
{
    foreach (var excelFile in excelFiles)
    {
        excelFile.FileType = IExcelFile.Type.Queued;
    }
    foreach (var excelFile in excelFiles)
    {
        try
        {
            excelFile.FileType = IExcelFile.Type.Processing;
            List<IDubCardSet> tempDubCardSets = dubCardGenerator.CalculateDubCardSetsFromExcelFile(excelFile);
            dubCardSets = dubCardGenerator.AddDubCardSets(tempDubCardSets);
            foreach (var dubCardSet in dubCardSets.Where(dcs => dcs.DubCards.Count == 0))
            {
                dubCardGenerator.CreateDubCards(dubCardSet);
            }
            excelFile.FileType = IExcelFile.Type.Completed;
        }
        catch (Exception ex)
        {
            excelFile.FileType = IExcelFile.Type.Error;
            AppLogger.GetInstance().Info(ex.Message);
            Modal.Open("Something went wrong!", ex.Message);
        }
    }
    IsDownloadAvailable = true;
}

So basically after this method runs I just see all excel files have a FileType "Complete", but when the method runs I see no other values in between (even when debugging step by step).

Additional context that I hope is irrelevant for this issue:

  1. This is a child component but no parameters are passed from the parent to child nor are any parameters from child shared with or dependent on parent.
  2. This components has other methods that use StateHasChanged() like deleting or adding list elements that work properly.

I tried changing method from private void to async Task, call StateHasChanged() anywhere but with no success. I tried pretty much any related solutions on Stack Overflow, but it didn't work. Any help would be much appreciated, Thanks.

1 Answers

I've simplified your code into a single page which I believe demonstrates what you're trying to achieve. It demonstrates the principles of using async coding and using StateHasChanged to drive re-renders. The key bit is that the backend process that gets the data has to be a yielding async process for this to work properly. The Renderer needs thread time to update the UI. Blocking processes stop that happening.

@page "/"
<h3>Async File Processing</h3>
<div class=m-2>
    <button class="btn btn-primary" @onclick=GenerateDubCards>Process</button>
</div>

@foreach(var file in excelFiles)
{
    <div class="m-2 p-2 @GetCss(file)">@file.Name : @file.State</div>
}

@code {
    private List<ExcelFile> excelFiles = new() { new ExcelFile { Name="UK"}, new ExcelFile { Name="Spain"}, new ExcelFile { Name="Portugal"}, new ExcelFile { Name="Australia"}};

    private string GetCss(ExcelFile file)
    => file.State switch
    {
        FileState.Complete => "bg-success text-white",
        FileState.Processing => "bg-warning text-white",
        FileState.Error => "bg-danger text-white",
        _ => "bg-primary text-white"
    };

    private async  Task GenerateDubCards()
    {
        List<Task> tasks = new List<Task>();
        foreach(var file in excelFiles)
        {
            tasks.Add(ProcessAFile(file));
        }
        await Task.WhenAll(tasks.ToArray());
    }

    private async Task ProcessAFile(ExcelFile file)
    {
        file.State = FileState.Processing;
        StateHasChanged();
        // emulate the file work with a variable length Task Delay
        await Task.Delay(Random.Shared.Next(5000));
        file.State = FileState.Complete;
        if (file.Name.StartsWith("S"))
            file.State = FileState.Error;

        StateHasChanged();
    }

    public class ExcelFile
    {
        public string Name { get; set; } = string.Empty;
        public FileState State { get; set; } = FileState.None;
    }

    public enum FileState
    {
        None,
        Processing,
        Complete,
        Error
    }
}

enter image description here

Related