Changing Name of Method Call at Runtime?

Hey all.
I have an array of button names whose number is determined by whatever I put in in the inspector window. So if there are 8 names, I want 8 buttons, and I can easily add or delete a button and name it right in the inspector.
I’m trying to set up a loop to find those buttons in the UI (haven’t gotten around to creating buttons at runtime yet, I just created 8 manually in the UI Toolkit) and assign listeners to them without having to hard-code the number of buttons in the code. The problem is that I don’t know how to set up a separate method for each button to do something on-click. Here’s what I have so far:

int numArrayElements = buttonNames.Length;
        buttonsArray = new Button[numArrayElements]; // Initializes Array with number of elements equal to length of list of names from editor.

        root = GetComponent<UIDocument>().rootVisualElement;

        // Finding UI buttons and setting up listeners.
        for (int i = 0; i < numArrayElements; i++)
        {
            string number = (i+1).ToString(); //Converting i + 1 into string to add to get "button1", "button2", etc.
            buttonsArray[i] = root.Q<Button>("button:" + number); // Finds "button1", "button2", etc in the UI.
            buttonsArray[i].clicked += ButtonIPressed; // Right here is where I get stuck.
       // Trying to set up listeners and I can't change the I in ButtonIPressed method call
       // to get "Button1Pressed" or "Button2Pressed".
        }

You should not have several methods like that. Instead you can create a single method that takes an argument that lets you identify the button. You can subscribe a closure instead that captures a local variable in order to identify the button. You have to be careful with closures in loops as for example the loop variable “i” only exists once. So capturing this variable means all callbacks use the same value ( which is numArrayElements after the loop has finished). You have to use a local variable inside the loop, so each iteration a new one is used.

Since you have your buttons in an array, just passing the index may be enough here

void ButtonPressed(int aIndex)
{
    Debug.Log("button #" + aIndex + "has been pressed");
}

// [ ... ]

        for (int i = 0; i < numArrayElements; i++)
        {
            // [ ... ]
            int index = i; // required for the closure that follows
            buttonsArray[i].clicked += ()=> ButtonPressed(index);
        }
1 Like

Hi Bunny,
Thanks for your post. I’ll implement this code asap and see how it works. I’ll also have to read more on what a ‘closure’ is, as this term is new to me.

Can you elaborate on this? Is some reference to the variable itself being used instead of the number that’s stored in it? And what is ‘capturing’?

Thank you!

If you just did it normally like this

for (int i = 0; i < numArrayElements; i++)
    buttonsArray[i].clicked += ()=> ButtonPressed(i); // <== gets the final value of i when it is called

The value ButtonPressed would not be the same as it originally was when it is called. This is because it is called with a ‘reference’ to i, so when you call it, it will use whatever was last stored into i when you call it, which is way after the loop is finished. That’s why you have to make a local version of the variable.

2 Likes

Well, closures are a topic on their own. The wikipedia article may be a bit overkill. The important part is essentially about the “capturing”. There are two ways to look at it, from a purely functional point of view and from the technical point of view.

The functional point of view is simply: A closure is an anonymous inline declared method that has access to local variables which are normally not accessable from inside the method. So the method simply can “hold onto” the local variables. It’s important to keep in mind that the code in the method is not execute right now but it has access to the variable that was “captured” when it is executed. So it does not read the “value” of the variable when you create the closure but actually accesses the variable itself. So if the variable is changed afterwards, it would contain the new current value of that variable.

Technically the compiler actually creates a seperate class that will hold the variable in order to share it and the anonymous method would be a member method of that internal class. It’s a mess you really don’t want to look at and luckily you almost never see it as the compiler takes care of it. Most C# compilers name those classes with a variation of “AnonStorey”, just in case you see them in a stack trace one day or the other.

3 Likes

Maybe I should add that the ()=>Statement syntax is a lambda expression (=> is the lambda operator) which provides a shorthand for a normal anonymous delegate which would look like this:

delegate()
{
    Statement;
}

Almost nobody uses the usual anonymous method syntax as lambda expressions are shorter, especially for small inline code.

2 Likes

Thanks you two. Okay. So in my case, my loop, with a closure instead of the original code, would create a new variable incremented by +1 every time it loops, which would be referenced by the created function, so that there are actually 8 different functions and 8 different variables sigh 8 different values. That sound right?

1 Like

Well, almost :slight_smile:
There is only one method, which is part of a compiler generated class. Each iteration you would create a new instance of that class and each of those classes would have their variable set to the current value of “i”. In addition each button gets its own delegate (method / function reference) to the same method but on different objects.

Though your view would also work as a mental image, conceptionally.

Some more info

This may go too far, but just in case you don’t know, methods of a class (so the actual executable code) only exist once and all methods belong to the class and not really to an instance of the class. So called “instance methods” are often thought of being part of the object instance itself, but in reality the code for the method only exists once. Instance methods simply have an additional implicit first argument which is the object instance they are called on. That’s the this reference inside a method.

A delegate simply holds the information of the method (MethodInfo) as well as the actual object instance. For static methods the object instance is null. For any other delegate, this instance reference is what is passed as first argument to the method.

A simple example

public class MyClass
{
    public int myVar = 5;
    public void Test(string arg)
    {
        Debug.Log("Test::" + arg + " myVar = " + myVar);
    }
}

var obj1 = new MyClass();
obj1.Test("FooBar"); // prints "Test::FooBar myVar = 5"

This is just a small class with an instance method called Test. As you can see it takes a single string argument. The body of the method just logs this message to the console. Since it’s an instance method it has access to the myVar variable. As I said under the hood an instance method essentially has an implicit first argument and the method would actually look like this

    public static void Test(MyClass this, string arg)
    {
        Debug.Log("Test::" + arg + " myVar = " + this.myVar);
    }

The call of our Test method is essentially

MyClass.Test(obj1, "FooBar");

So multiple instances of MyClass really use the same method, but the method has the object instance reference as an additional parameter.

A delegate to a method stores two things, the (essentially static) method reference as well as a “target” reference to an object which is passed as the first “context” argument this.

As I said a closure is a compiler generated class for your anonymous method. Anonymous just means it does not have an accessible name in any context of the language. Under the hood it actually has a name and has to be defined somewhere. Let me give you a very simplified example what happens when you create a closure and capture a local variable.

System.Action del;

void MyMethod()
{
    int myLocalVar = 42;
    del = () => Debug.Log("Value: " + myLocalVar++);
}

As you can see “myLocalVar” is a local variable that “usually” only exists inside the method and would usually be allocated on the stack. When the method is finished, the stack is cleaned up and the variable does not exist anymore. However when you create a closure like we did, the compiler generates a class that encapsulates that variable as well as the closure method we defined here. So the code would actually look more like this:

System.Action del;

private internal class MyMethodClosure
{
    public int myLocalVar;
    public void AnonMethod1()
    {
        Debug.Log("Value: " + myLocalVar++);
    }
}

void MyMethod()
{
    MyMethodClosure inst = new MyMethodClosure() { myLocalVar = 42 };
    del = inst.AnonMethod1;
}

Note that this is very simplified pseudo code but it may help to understand what the compiler actually does for you. One important thing you should notice: When you call MyMethod, we create a new context / closure object that is used by the anonymous method we store in the delegate “del”. Invoking that delegate several times would first print 42, then 43, then 44 and so on. The method has captured that variable (which internally became a class member variable).

Calling MyMethod again would create a new closure instance and would overwrite the old one. So that old “inst” would be up for garbage collection since it is no longer referenced from anywhere.

The issue with for loops is, when you capture the for loop variable, the system would only create one context instance which would be used in each iteration. However if you declare a local variable inside the for loop body, it would be a new variable each iteration and you get a new context instance for each iteration. So each button gets its own delegate which use different context instances.

I’m sure I went way to far with this post, so I put it in a spoiler ^^. As I already mentioned in 99% of the cases you don’t really need to know how any of this works under the hood. You just need to be aware of the way how closures captures variables.

2 Likes

@Bunny83 Thank you!!

@Bunny83 maybe you don’t need to know how this works under the hood but it is great to read, thanks!

1 Like