Search Unity

How to create buttons from code that calls a method with the i variable from the for-loop?

Discussion in 'UI Toolkit' started by KristofferH, Oct 4, 2021.

  1. KristofferH

    KristofferH

    Joined:
    Oct 27, 2012
    Posts:
    52
    I'm trying to generate a bunch of buttons in my UI based on the number on objects, I do this with a for-loop and register at method call to each button that uses the i variable from the for-loop as a parameter for the method. This works fine and as expected when using IMGUI in the inspector, like this:

    (Note: these code examples are simplified for clarity)

    Code (CSharp):
    1. for (int i = 0; i < stages.Count; i++)
    2. {
    3.    if (GUILayout.Button(triggerEvent.name))
    4.       mgn.PrepareStage(i);
    5. }
    However when I try to do this with UIElements it does not work because all buttons calls the method using the last i value, i.e. i is 10 for all eleven buttons as if I just had typed: mgn.PrepareStage(10);

    Code (CSharp):
    1. for (int i = 0; i < stages.Count; i++)
    2. {
    3.    Button prepareButton = stageElement.Q<Button>("prepare-button");
    4.    prepareButton.text = triggerEvent.name;
    5.    prepareButton.RegisterCallback<ClickEvent>((evt) =>
    6.    {
    7.       mgn.PrepareStage(i);
    8.    });
    9. }
    How should I do this to get it to work?

    Unity 2020.3.16f1 (built-in version av UIElements)
     
  2. jonathanma_unity

    jonathanma_unity

    Unity Technologies

    Joined:
    Jan 7, 2019
    Posts:
    229
    Hi KristofferH,

    The reason i is always 10 in your case is because the lambda passed to RegisterCallback capture the variable i which means that all callbacks reference the same variable. This is not specific to UI Toolkit but more of a general C# behavior. What you can do instead is pass the variable when calling RegisterCallback.

    Here's an example:
    Code (CSharp):
    1. int i = 0;
    2. stageElement.Query("prepare-button").ForEach(element =>
    3. {
    4.     element.RegisterCallback<ClickEvent, int>((evt, index) =>
    5.     {
    6.         Debug.Log($"Click {index}");
    7.     }, i);
    8.     ++i;
    9. });
     
  3. KristofferH

    KristofferH

    Joined:
    Oct 27, 2012
    Posts:
    52
    @jonathanma_unity Yes, that worked! :)
    So before the variable i was passed as a reference so all buttons were reading from the same memory space, but by doing it like this instead the value is passed by value instead, so that they each get their own local copy?
     
  4. jonathanma_unity

    jonathanma_unity

    Unity Technologies

    Joined:
    Jan 7, 2019
    Posts:
    229
    Yes you can see it that way.
     
    KristofferH likes this.
  5. MaxRoetzler

    MaxRoetzler

    Joined:
    Jan 3, 2010
    Posts:
    136
    @jonathanma_unity how would you go about the same issue, but with the RegisterValueChangedCallback?

    I'm creating ObjectFields in a loop and need to know the index of the ObjectField that was changed. I can't use IndexOf(obj.previousValue) since some elements reference the same object. Lambda doesn't work in a loop, and the event doesn't contain any information about the 'sender' object.
     
  6. jonathanma_unity

    jonathanma_unity

    Unity Technologies

    Joined:
    Jan 7, 2019
    Posts:
    229
    Hi, RegisterValueChangedCallback doesn't take any extra argument, but this is simply a wrapper around RegisterCallback<ChangeEvent<T>> that allow less typing. If you need an extra argument you need to use the full RegisterCallback syntax.

    For example with an IntField:
    Code (CSharp):
    1. for (int i = 0; i < 10; i++)
    2. {
    3.     field.RegisterCallback<ChangeEvent<int>, int>(Callback, i);
    4. }
    5.  
    6. void Callback(ChangeEvent<int> evt, int index)
    7. { ... }
     
    Last edited: Aug 30, 2022
    Onigiri likes this.
  7. MaxRoetzler

    MaxRoetzler

    Joined:
    Jan 3, 2010
    Posts:
    136
    I thought I already tried this, but maybe I got something wrong. I'll have another look again, thanks for the response.