Button.onClick().AddListener() acting weirdly when looping in for loop

Hello there,
well my title isn’t quit explicit but here’s my issue.
I’m having an inventory system where I need to instantiate a button for every item (in that case, readable notes).

Here’s the function that update the panel containing notes

private void UpdateNotePanel()
    {
        GameObject noteButtonPrefab = Resources.Load<GameObject>("Prefabs/NoteButton");
        if(noteButtonPrefab == null)
        {
            Debug.LogError("Button prefab missing");
            return;
        }

        foreach(Note n in player.Inventory.GetAllNotes())
        {
            //If the button isn't already present in the notelayout
            if(noteLayout.transform.FindChild(n.textFileName + "_but") == null)
            {
                GameObject instance = Instantiate<GameObject>(noteButtonPrefab);
                instance.name = n.textFileName + "_but";
                instance.transform.SetParent(noteLayout.transform);
                instance.transform.localScale = new Vector3(1, 1, 1);
                instance.GetComponentInChildren<Button>().GetComponentInChildren<Text>().text = n.textFileName;
                Debug.Log("setting button " + n.textFileName + "_but ");

                //This line is acting weird.
                instance.GetComponentInChildren<Button>().onClick.AddListener(() => { NoteReader.noteReader.Read(n, 1); });
            }
        }
    }

for a reason I don’t understand the parameters I pass to noteReader.Read() are, for all button I instantiate, the last one in the foreach loop.
(I’m not quit sure everybody understand what I mean but that’s kind of tricky to explain…)
Instead of passing the current Note for each current instantiated button, it passes the last one to all of them.

What is going on here ? I am missing something ?
Thanks in advance.

Well, It was easy to come up with a solution…

Apparently using a “classic” for loop instead of an foreach loop solved de problem.
But no idea why…

Someone have an explanation ? I’d like to understand :frowning:

Lamda expressions such as the ones used to add listeners to UI buttons have strange behavior when put into loops like this. I’m not sure why it would just work by changing foreach to a for loop, as I had thought the problem was something else.

I know from my own experience that when using Lamdas in for-loops, you have to save a copy of the indexer to a local variable in the loop in order for it to work right. So for example:

for(int i = 0; i < Notes.Length; ++i)
{
     //Assuming that returns an array (if it's a list, then .Count)
     int num = i;
     if(noteLayout.transfomr.FindChild(Notes[i] + "_but") == null)
     {
        GameObject instance = Instantiate<GameObject>(noteButtonPrefab);
        instance.name = Notes[i].textFileName + "_but"; //Using "i" anywhere else is fine, just not on the Lambda
        instance.transform.SetParent(noteLayout.transform);
        instance.transform.localScale = new Vector3(1, 1, 1);
        instance.GetComponentInChildren<Button>().GetComponentInChildren<Text>().text = Notes[i].textFileName;
        Debug.Log("setting button " + Notes[i].textFileName + "_but ");

        instance.GetComponentInChildren<Button>().onClick.AddListener(() => { NoteReader.noteReader.Read(Notes[num], 1); }); //Passing the local variable instead of the indexer works, using "i" will always return the last one
     }
}

It’s a little more difficult since I don’t know what all your references do or what type GetAllNotes returns, but the issue is pretty much the same.

2 Likes

Well, saving a reference of the current value with the indexer to a local variable seems to work aswell as it is what I did.
But indeed, that’s kind of weird.

In any cases, thanks for your explanation.

Check out the “Closing over the loop variable” in my sig for an explanation of exactly why this is happening.

This behaviour should change once Unity upgrades to a modern .Net version.