Blazor: Calculated total value renders one step behind

Viewed 66

In my app, I have the _totalOutput field which is calculated from the list: decimal _totalOutcome => _evaluationList.Sum(e => e.Outcome);

The list contains a class of several properties which I iterate on the page and render as text and one checkbox. When I click on the checkbox then the value of Score, Weight, and Outcome (last column) changes appropriately. But "Total Outcome" renders one step behind. Calculation steps:

  1. if checked (unchecked) Score = 100 (0 is unchecked)
  2. Outcome = Score * (Weight / totalWeight)
  3. Total outcome summarized

I added force refresh button on the page for test purposes (StateHasChanged). After checking boxes if I click on the button then totalOutcome renders correctly.

How can I show an actual totalOutcome on the page?

enter image description here

Index.razor

@page "/"


<div style="margin-bottom:16px">Total Outcome: @_totalOutcome</div>

<button style="margin-bottom:16px" onclick="@(() => StateHasChanged())">Force refresh (StateHasChanged)</button>


@foreach (var item in _evaluationList)
{
    <div style="display:flex">
        <div style="margin-right: 32px">
            @item.Id
        </div>
        <div style="margin-right: 32px">
            @item.Name
        </div>
        <div style="margin-right: 32px">
            <input type="checkbox" @bind-value="@item.Enabled">
        </div>
        <div style="margin-right: 32px; width: 40px">
            @{
                item.Score = item.Enabled ? 100 : 0;
                <div>@item.Score</div>
            }
        </div>
        <div style="margin-right: 32px; width: 40px">
            @item.Weight
        </div>
        <div style="margin-right: 32px; width: 40px">
            @{
                var totalWeight = _evaluationList.Sum(e => e.Weight);
                item.Outcome = Math.Round(item.Score * (item.Weight / totalWeight), 2);
                <div>@item.Outcome</div>
            }
        </div>
    </div>
}


@code {
    List<Evaluation> _evaluationList = new List<Evaluation>()
    {
        new Evaluation { Id=1, Name="Item 1", Enabled=false, Score=0, Weight=10 },
        new Evaluation { Id=2, Name="Item 2", Enabled=false, Score=0, Weight=20 },
        new Evaluation { Id=3, Name="Item 3", Enabled=false, Score=0, Weight=30 },
        new Evaluation { Id=4, Name="Item 4", Enabled=false, Score=0, Weight=40 },
        new Evaluation { Id=5, Name="Item 5", Enabled=false, Score=0, Weight=50 },
        new Evaluation { Id=6, Name="Item 6", Enabled=false, Score=0, Weight=60 },
        new Evaluation { Id=7, Name="Item 7", Enabled=false, Score=0, Weight=70 },
        new Evaluation { Id=8, Name="Item 8", Enabled=false, Score=0, Weight=80 },
    };

    decimal _totalOutcome => _evaluationList.Sum(e => e.Outcome);

    public class Evaluation
    {
        public int Id { get; set; }
        public string? Name { get; set; }
        public bool Enabled { get; set; }
        public decimal Score { get; set; }
        public decimal Weight { get; set; }
        public decimal Outcome { get; set; }
    }
}
2 Answers

A quick fix : move this line

<div style="margin-bottom:16px">Total Outcome: @_totalOutcome</div> 

below the @foreach() loop.


You do a lot of calculation inside the markup section. That is generally not a good idea. And it really goes wrong when you modify data during the render.

Your current code caluclates and displays _totalOutcome first. Then it executes the foreach() where it changes the underlying data for _totalOutcome.

You need an event for the Enabled checkbox to compute item.Score and item.Outcome. That will ensure stable data during the rendering, just display those properties like any other field.

Roughly:

<input type="checkbox" @onchange="args => EnableChange(args, item)">

void EnableChange(ChangeEventArgs args, Evaluation item)
{
    item.Enabled = (bool)args.Value!;
    item.Score = item.Enabled ? 100 : 0;
    var totalWeight = _evaluationList.Sum(e => e.Weight);                
    item.Outcome = Math.Round(item.Score * (item.Weight / totalWeight), 2);
}

While Henk's answer above fixes the issue, There are better ways to managing data in the UI, and doing calculations in the render cycle is bad.

Here's a different approach.

First a view class that manages the data the UI uses and registered as a DI injected service:

public class EvaluationViewService
{
    public List<Evaluation> EvaluationList = new List<Evaluation>();

    public EvaluationViewService()
    {
        this.EvaluationList = new List<Evaluation>()
        {
            new Evaluation { Id=1, Name="Item 1", Enabled=false, Weight=10 },
            new Evaluation { Id=2, Name="Item 2", Enabled=false, Weight=20 },
            new Evaluation { Id=3, Name="Item 3", Enabled=false, Weight=30 },
            new Evaluation { Id=4, Name="Item 4", Enabled=false, Weight=40 },
            new Evaluation { Id=5, Name="Item 5", Enabled=false, Weight=50},
            new Evaluation { Id=6, Name="Item 6", Enabled=false, Weight=60 },
            new Evaluation { Id=7, Name="Item 7", Enabled=false, Weight=70 },
            new Evaluation { Id=8, Name="Item 8", Enabled=false, Weight=80 },
        };
    }

    public decimal TotalOutcome => EvaluationList.Sum(e => e.Outcome);
    public decimal TotalWeight => EvaluationList.Sum(e => e.Weight);

    public void Update()
    {
        var totalWeight = this.TotalWeight;
        foreach( var evaluation in EvaluationList)
        {
            evaluation.Score = evaluation.Enabled ? 100 : 0;
            evaluation.Outcome = Math.Round(evaluation.Score * (evaluation.Weight /totalWeight), 2);
        }
    }
}

In Program:

builder.Services.AddTransient<EvaluationViewService>();

And then your UI component looks like this. I've manually wired up the checkboxes instead of doing the @bind.

@page "/"
@inject EvaluationViewService Service

<div style="margin-bottom:16px">Total Outcome: @Service.TotalOutcome</div>

@foreach (var item in Service.EvaluationList)
{
    <div style="display:flex">
        <div style="margin-right: 32px">
            @item.Id
        </div>
        <div style="margin-right: 32px">
            @item.Name
        </div>
        <div style="margin-right: 32px">
            <input type="checkbox" value="@item.Enabled" @onchange="(e) => OnCheckChanged(item, e)">
        </div>
        <div style="margin-right: 32px; width: 40px">
                <div>@item.Score</div>
        </div>
        <div style="margin-right: 32px; width: 40px">
            @item.Weight
        </div>
        <div style="margin-right: 32px; width: 40px">
                <div>@item.Outcome</div>
        </div>
    </div>
}

@code {
    private void OnCheckChanged(Evaluation item, ChangeEventArgs e)
    {
        if( bool.TryParse(e.Value?.ToString(), out bool value))
        {
            item.Enabled = value;
            Service.Update();
        }
    }
}
Related