Blazor/razor onclick event with index parameter - c#

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);
}
}

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>
}

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.

Related

How to "return" multiple times with for loop?

Hopefully this post gives more clarity as to what I am trying to achieve.
Objective: I want to spawn 20 apples(that have an attached button) from a list at runtime. When the apples are clicked they will spawn a popup with information pertaining to the apple that was clicked.
What I'm doing currently: I am using a for loop to run through the list to spawn the apples. I currently have the following code:
public class AppleInventory : MonoBehaviour
{
[SerializeField] private ApplesScript applPrefab;
[SerializeField] private Transform applParent;
public ApplesScript CreateApples()
{
var appl = Instantiate(applPrefab, applParent);
for (int i = 0; i < apples.Count; i++)
{
appl = Instantiate(applPrefab, applParent);
appl.InitAppleVisualization(apples[i].GetAppleSprite());
appl.AssignAppleButtonCallback(() => CreateApplePopUpInfo(i));
appl.transform.position = new Vector2(apples[i].x, apples[i].y);
}
return appl;
}
}
The Problem: The problem is that when I use the for loop and click on the button,it returns the following error: ArgumentOutOfRangeException: Index was out of range. Must be non-negative and less than the size of the collection. The popup information also does not update.
Code without for loop: The code works to spawn one apple when I remove the for loop and set the int i = to a specific number, like below. It will give the correct popup info for any number that "i" is set to. This lets me know that it is not the rest of the code that is the issue. This leads me to believe it is the "return" line along with the for loop that is the issue. It seems I may need to "return" for each iteration but I am unsure of how to go about doing this.
public ApplesScript CreateApples()
{
int i = 7;
var appl = Instantiate(applPrefab, applParent);
appl.InitAppleVisualization(apples[i].GetAppleSprite());
appl.AssignAppleButtonCallback(() => CreateApplePopUpInfo(i));
appl.transform.position = new Vector2(apples[i].x, apples[i].y);
return appl;
}
Thank you,
-
UPDATE
The fix was so simple. I just ended up creating a new method specifically for the for loop and it worked the way I wanted. My code now looks like this:
public void StarterOfApplesCreation()
{
for (int i = 0; i < apples.Count; i++)
{
CreateApples(i);
}
}
public void CreateApples(int i)
{
var appl = Instantiate(applPrefab, applParent);
appl.InitAppleVisualization(apples[i].GetAppleSprite());
appl.AssignAppleButtonCallback(() => CreateApplePopUpInfo(i));
appl.transform.position = new Vector2(apples[i].x, apples[i].y);
}
You have two options. The conventional option is to create all the items first and then return them all in some sort of list, e.g.
public static void Main()
{
foreach (var thing in GetThings(5))
{
Console.WriteLine(thing.Number);
}
Console.ReadLine();
}
public static Thing[] GetThings(int count)
{
var things = new Thing[count];
for (var i = 0; i < count; i++)
{
things[i] = new Thing { Number = i };
}
return things;
}
The more modern option is to use an iterator. It actually will return one item at a time. It has the limitation that you have to use the items there and then - you won't have random access like you would an array or the like - but it also has advantages, e.g.
public static void Main()
{
foreach (var thing in GetThings(5))
{
Console.WriteLine(thing.Number);
}
Console.ReadLine();
}
public static IEnumerable<Thing> GetThings(int count)
{
for (var i = 0; i < count; i++)
{
var thing = new Thing { Number = i };
yield return thing;
}
}
The result of an iterator will usually be used as the source for a foreach loop or a LINQ query. Note that you can always call ToArray or ToList on the result of an iterator if you do want random access in specific situations, but you still have the advantages of an iterator elsewhere. For instance, let's say that your method produces 1000 items and you want to find the first one that matches some condition. Using my first example, you would have to create all 1000 items every time, even if the first one was a match. Using an iterator, because the items are processed as they are created, you can abort the process as soon as you find a match, meaning that you won't unnecessarily create the remaining items.
Note that my examples use the following class:
public class Thing
{
public int Number { get; set; }
}
You can copy and paste the code into a Console app that doesn't use top-level statements. The bones of the code will still work with top-level statements, but you'll need to make a few other modifications.
Store each separate "appl" that gets instantiated in an Array, ie appls[i]=appl
Do this within the for loop.
If you think about it, by putting the line "return appl;" outside the for loop, you are only storing that last game object, not all of them. Thats why creating an array of gameobjects and assigning them within the loop may work for you.

How to add dynamic checkboxes in a Blazor-server app that use a list of booleans?

To clarify my question more I'll first show you add a checkbox using the blazor component library I'm using:
<MudCheckBox #bind-Checked="#Basic_CheckBox1"></MudCheckBox>
#code{
public bool Basic_CheckBox1 { get; set; } = false;
}
the code above creates a checkbox and then toggles the boolean Basic_CheckBox1 to true or false based on if the checkbox is checked or not.
here is my code
#for (int i = 0; i < checkboxNames.Count; i++)
{
<MudCheckBox #bind-Checked="#checkboxBools[i]" Label="#checkboxNames[i]"></MudCheckBox>
}
this code works until I check or uncheck my created checkboxes, then I get the index out of range error, how can I fix this? I need to use a list of booleans because I don't know how many checkboxes I will need until runtime, if there is any other solution please let me know.
Think of it like the bind isn't done there and then in the loop, but later. By the time the bind comes to be done, the value of i is off the end of the array Length, having been incremented repeatedly. Instead, you can save the current value of your loop variable i into another variable, then use that other variable in your bind:
#for (int i = 0; i < checkboxNames.Length; i++)
{
var x = i;
<MudCheckBox #bind-Checked="#checkboxBools[x]" Label="#checkboxNames[i]"></MudCheckBox>
}
checkBoxBools should be an array/list that is the same length (initalized to have the same number of elements as) checkBoxNames.. For example:
string[] checkboxNames = new string[]{"a","b","c"};
bool[] checkboxBools = new bool[3];
Or, if something else is providing a variable number of names:
checkboxNames = context.People.Select(p => p.Name).ToArray();
checkboxBools = new bool[checkBoxNames.Length];
https://try.mudblazor.com/snippet/GamQEFuFIwdRbzEx

MVC Razor For loop binding to ViewModel problem

I am seeing a confusing issue with my viewmodel posting back to my controller and I am confused as of why it is not working. Though I have an idea as of why it may not be working which I explained near the bottom.
Basically I use a for loop to bind my model to HTML in the razor view
#for (int i = 0; i < Model.CheckBoxTag.Count; i++)
{
#if (Model.CheckBoxTag[i].TagTypeName == "test")
{
....
}
}
When I submit the form, the test CheckBoxTag objects are sent to my controller as expected.
However, when I do the same further down the html page only using the Escalation tags:-
#for (int i = 0; i < Model.CheckBoxTag.Count; i++)
{
#if (Model.CheckBoxTag[i].TagTypeName == "test1")
{
...
}
}
The test1 CheckBoxTag objects are not sent back to the controller. (The count is still 3, whereas it should be 6)
The fact it's the same code I am unsure how to tackle it.
My theory: I believe it is not posting back to my controller because the test for loop are the 1st elements in the collection, therefore it always goes into the IF. Whereas the test1 objects are near the bottom of the collection so therefore the IF is skipped quite a few times in the loop.
Is that correct? If not, what could be the issue?
Thanks
As stated in the comments section, the indexers must be consecutive.
Therefore in the for loops I put
#for (int i = 0; i < Model.CheckBoxTag.Count; i++)
{
#if (Model.CheckBoxTag[i].TagTypeName == "test1")
{
// New!
<input type="hidden" name="CheckBoxTag.Index" value="#i" />
...
}
}
So now the indexers "[i]" are now being incremented on every loop

Loop list in jquery and use the looped values

In my last question I was having problems looping through a list with jQuery. Then we figured this out and it worked perfectly:
public List<Sale> AllSales { get; set; }
for (var i = 0; i < <%= AllSales.Count %>; i++) {
}
I now need to use the values inside the loop so I thought it would be as simple as this :
for (var i = 0; i < <%= AllSales.Count %>; i++) {
var date = <%= AllSales[i].Date %>;
alert(date);
}
When I first tried this, it said "The name 'i' does not exist in the current context
", so I just put 0 instead of i instead of AllSales[0]. Then nothing happens.
What am I missing?
You have javascript loop which you want to iterate on server side list this is not possible. You can use ajax to send data to client side. This is a nice article for using jQuery ajax with csharp.
Assigning the values of your list separated with comma to some hidden field and accessing that hidden field in javascript could be a possible solution. But if you want to use more attributes of your list object then it would be very messy solution. Using ajax is best option.

How do I display the number of loops in a foreach loop in an HTML page

I'm currently trying to count the number of search results returned in my ASP.NET MVC view to display how many results the search gave in return.
I've tried counting the number of loops of the foreach that displays the search results.
I've also tried counting the number of items in the Model object returned with the view:
<% Html.Display(Model.Count().ToString());%>
it never really posts anything on my site.
Anybody got an idea how to solve this issue?
The only way to know how many iterations a foreach loop has taken is to include a counter yourself:
int count = 0;
foreach (var thing in things) {
count++;
// Do something useful
}
// count is now the number of iterations
To display in a .aspx view then use:
<%= count %>
or in Razor:
#count
If this isn't working for you, then some other factor is at play. Can you show a short working example where it doesn't work?
Maybe
<span><% Model.Count() %></span>
#foreach (var item in Model.PageInfo.Products.Select((x, i) => new { Data = x, Index = i }))

Categories