13

I have the below code but the index parameter that is passed when I click the <tr> element is always 9.

That is becuase I have 9 rows in the table that is passed to the component as data. So looks like the index is always the value of variable 'i' which was last set... in this case the value of i after the last row in foreach loop is 9 so i am getting the index parameter as 9 on clicking all the rows in the table...

What is the issue in my code which is not setting the i value for each row onclick.

    <table border="1">

        @for(int i=0;i< ListData.DataView.Table.Rows.Count; i++)
        {
            <tr @onclick="(() => RowSelect(i))">
                @foreach (ModelColumn col in ListData.ListColumns)
                {
                    <td>@ListData.DataView.Table.Rows[i][col.Name]</td>
                }
            </tr>
        }

    </table>
@code {
 
    private async Task  RowSelect(int rowIndex)
    {
        await ListRowSelected.InvokeAsync(rowIndex);
    }
}
tomRedox
  • 28,092
  • 24
  • 117
  • 154
Codegeek
  • 173
  • 1
  • 7

2 Answers2

17

General

Actually your problem is about lambda that captures local variable. See the following simulation with a console application for the sake of simplicity.

class Program
{
    static void Main(string[] args)
    {
        Action[] acts = new Action[3];

        for (int i = 0; i < 3; i++)
            acts[i] = (() => Job(i));

        foreach (var act in acts) act?.Invoke();
    }

    static void Job(int i) => Console.WriteLine(i);
}

It will output 3, 3, 3 thrice rather than 0, 1, 2.

Blazor

Quoted from the official documentation about EventCallback:

<h2>@message</h2>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <button class="btn btn-primary"
            @onclick="@(e => UpdateHeading(e, buttonNumber))">
        Button #@i
    </button>
}

@code {
    private string message = "Select a button to learn its position.";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        message = $"You selected Button #{buttonNumber} at " +
            $"mouse position: {e.ClientX} X {e.ClientY}.";
    }
}

Do not use a loop variable directly in a lambda expression, such as i in the preceding for loop example. Otherwise, the same variable is used by all lambda expressions, which results in use of the same value in all lambdas. Always capture the variable's value in a local variable and then use it. In the preceding example, the loop variable i is assigned to buttonNumber.

So you need to make a local copy for i as follows.

@for(int i=0;i< ListData.DataView.Table.Rows.Count; i++)
{
   int buffer=i;
    <tr @onclick="(() => RowSelect(buffer))">
        @foreach (ModelColumn col in ListData.ListColumns)
        {
            <td>@ListData.DataView.Table.Rows[i][col.Name]</td>
        }
    </tr>
}
Second Person Shooter
  • 14,188
  • 21
  • 90
  • 165
4

This happens because the value of i isn't rendered to the page (as it would have been in MVC/Razor Pages), it's just evaluated when you trigger the event. You won't trigger the event until the page has rendered, and so by that point the loop will have completed, so the value for i will always be the value at the end of the loop.

There are a couple of ways to deal with this, either use a foreach loop instead if that's suitable (which is what most of the Blazor documentation examples do), or declare a local variable inside the loop:

@for(int i=0;i< ListData.DataView.Table.Rows.Count; i++)
{
   int local_i=i;

    // Now use local_i instead of i in the code inside the for loop
}

There's a good discussion of this in the Blazor Docs repo here, which explains the problem as follows:

Problem is typically seen in event handlers and binding expressions. We should explain that in for loop we have only one iteration variable and in foreach we have a new variable for every iteration. We should explain that HTML content is rendered when for / foreach loop is executed, but event handlers are called later. Here is an example code to demonstrate one wrong and two good solutions.

This is all particularly confusing if you're coming to Blazor from an MVC/Razor Page background, where using for is the normal behaviour. The difference is that in MVC the value of i is actually written to the html on the page, and so would be different for each row of the table in your example.

As per the issue linked above and @Fat Man No Neck's answer, the root cause of this is down to differences in the behaviour of for and foreach loops with lambda expressions. It's not a Blazor bug, it's just how C# works.

tomRedox
  • 28,092
  • 24
  • 117
  • 154