-
Notifications
You must be signed in to change notification settings - Fork 10.4k
Blazor : @for loop increment the counter in-loop #16809
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
@YordanYanakiev thanks for contacting us. I'm not sure what you are trying to say. Could you provide a minimalistical repro project that reproduces the issue, what your expectation is and the current behavior you are observing? |
The expected results should be - the parameter of each of the functions to be always the same inside the loop, but each function receive somehow increased value of the counter (c) in the very same iteration. |
@YordanYanakiev thanks for the repro. I believe that this is an unfortunate consequence of how we create the child content. Here is a look at the compiled razor code for the loop. I believe the issue is that the integer gets captured into the closure defined for the child content and as such it is always 10. This is most unfortunate, as its definitely counter-intuitive. I'm not sure if there is something we can do about it. for( int c = 0; c < 10; c++ )
{
__builder.AddContent(17, " ");
__builder.OpenElement(18, "li");
__builder.AddAttribute(19, "class", "nav-item px-3");
__builder.AddMarkupContent(20, "\r\n ");
__builder.OpenComponent<Microsoft.AspNetCore.Components.Routing.NavLink>(21);
__builder.AddAttribute(22, "class", "nav-link");
__builder.AddAttribute(23, "href", first( c )
);
__builder.AddAttribute(24, "Match", Microsoft.AspNetCore.Components.CompilerServices.RuntimeHelpers.TypeCheck<Microsoft.AspNetCore.Components.Routing.NavLinkMatch>(
NavLinkMatch.All
));
__builder.AddAttribute(25, "ChildContent", (Microsoft.AspNetCore.Components.RenderFragment)((__builder2) => {
__builder2.AddMarkupContent(26, "\r\n <span class=\"oi oi-home\" aria-hidden=\"true\"></span>");
__builder2.AddContent(27, second(c));
__builder2.AddMarkupContent(28, "\r\n ");
}
));
__builder.CloseComponent();
__builder.AddMarkupContent(29, "\r\n ");
__builder.CloseElement();
__builder.AddMarkupContent(30, "\r\n");
} We'll discuss this within the team and see if we can do something about this or if there is an alternative way to achieve the same thing. |
It's actually even worse.
And call it in the very same body of the loop - the result in it will be MAYBE 0. |
I don't see how the result might be 0. In any case this seems to be an issue with NavLink specifically, as there is a pattern to support this (the same way Router does). The problem here is that NavLink defines the content as a RenderFragment and that we should probably have made NavLink something like NavLink with the content being RenderFragment so that T can be passed in as context inside the loop. We can't fix that for 3.1 I believe, but we might be able to do something in 5.0. In general the answer for this type of situation is to use templated components as described in https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.0#templated-components |
As more levels of DOM objects is adding seems like the bug getting worse. |
A trivial way to workaround this is to put the scope inside a separate component and render the contents there. For example <div class="top-row pl-4 navbar navbar-dark">
<a class="navbar-brand" href="">BlazorApp10</a>
<button class="navbar-toggler" @onclick="ToggleNavMenu">
<span class="navbar-toggler-icon"></span>
</button>
</div>
<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
<ul class="nav flex-column">
@for( int c = 0; c < 1; c++ )
{
<LinkItem Index="c" />
}
</ul>
</div>
@code {
private bool collapseNavMenu = true;
private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;
private void ToggleNavMenu()
{
collapseNavMenu = !collapseNavMenu;
}
} <li class="nav-item px-3">
<NavLink class="nav-link" href="@first()" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span>@second()
</NavLink>
</li>
@code{
[Parameter] public int Index { get; set; }
private string first()
{
return Index.ToString();
}
private string second()
{
return Index.ToString();
}
} |
Yeah. I've actually consider this, but if the language construction offer a clear (on a first sign) approach usually peoples will go for it. This is my point. |
Ok, so we've discussed this within the team and apparently this is a C# behavior for which there is a trivial alternative, which is to use @foreach(var c in Enumerable.Range(0,10))
{
<li class="nav-item px-3">
<NavLink class="nav-link" href="@(first( c ))" Match="NavLinkMatch.All" >
<span class="oi oi-home" aria-hidden="true"></span>@(second( c ))
</NavLink>
</li>
}` Also, capturing the variable inside of the loop also produces the desired effects. See @for( int c = 0; c < 10; c++ )
{
var current = c;
<li class="nav-item px-3">
<NavLink class="nav-link" href="@(first( current ))" Match="NavLinkMatch.All" >
<span class="oi oi-home" aria-hidden="true"></span>@(second( current ))
</NavLink>
</li>
} Given this, It is unlikely that we do anything here at the Razor level (as this is simply c#) and the recommendation is to use foreach or capture the loop variable in a local inside the loop scope as defined above. I've filed an issue to track whether we add an analyzer to inform users when they run into this case. #16815 |
I have considered it, yet it is giant issue I believe, and probably it have to be fixed or just the normal loops with counters removed as a feature for the syntax, although many components with enumerated names or so will be depending on such feature. |
@YordanYanakiev change your code like this: @for (int c = 0; c < 1; c++)
{
int local_c = c; // <== Add this, and then use local variable
<li class="nav-item px-3">
<NavLink class="nav-link" href="@(first( c ))" Match="NavLinkMatch.All">
<span class="oi oi-home" aria-hidden="true"></span>@(second(local_c))
</NavLink>
</li>
} Notice that you have to use local variable only in places where it will be captured into the closure. Of course you can use it also in other places, but it is not necessary. Similar problems where reported here #15633 and here #16140. It is documented also at the end of this section: I have to agree with @javiercn that in case of components with child content it is not obvious that in final code we will have closure. |
It looks that I was to slow with my answer and Javier was faster. |
I would actually suggest the internal compiler to create a local variable inside the normal loop with counter and use it instead of the counter, when it is used in razor statement. This will solve the problem above. |
We wouldn't declare the variable locally, Blazor transpiles to C# in the closest way possible and doesn't have insight into the C# code that you write in the page while its compiling to it, and even if it had, we wouldn't alter the user code in that way. |
Describe the bug
While looping with @for the counter is been autoincrementing for each hit after the first usage inside a funtion.
To Reproduce
Got Exceptions? Include both the message and the stack trace
-->
Further technical details
dotnet --info
The text was updated successfully, but these errors were encountered: