Persistent listeners for Button.onClick are removed at runtime, if buttons are instances of prefab

In my menu scene I want to attach a listener to the onClick event of all buttons, so every button plays a sound when clicked.

This is too much work to do manually, so I wrote a ButtonSoundManager component and attached it to the Canvas. It contains a public void PlaySound() method that plays the sound.

In the same component I wrote this method to attach the listener to all buttons via an editor menu item:

#if UNITY_EDITOR
[MenuItem(@"Menu Sounds/Attach or Fix menu sounds on this scene")]
public static void AttachMenuSound()
{
    // Find the ButtonSoundManager instance, since we are in a static method
    ButtonSoundManager manager = GameObject.Find("Canvas").GetComponent<ButtonSoundManager>();

    Button[] allButtons = manager.GetComponentsInChildren<Button>(true);
    foreach (Button b in allButtons)
    {
        // Register click listeners
        UnityEngine.Events.UnityAction action = manager.PlaySound;

        // RemovePersistentListener will remove all matching listeners, even if there
        // are multiple of them.
        UnityEditor.Events.UnityEventTools.RemovePersistentListener(
            b.onClick, action);
        UnityEditor.Events.UnityEventTools.AddPersistentListener(
            b.onClick, action);
    }
}
#endif

In the editor this works exactly as expected. After clicking this new menu item, in the inspector I do see the listener attached to the onClick event of all buttons.

However, as soon as I enter play mode, these listeners are immediately removed. When I exit play mode the listeners are still gone, as if they never existed. If I enter play mode and then click the menu item, I can attach the listeners at runtime and they work.

What is going on? Why are the listeners removed at runtime? I have near zero experience writing editor code so I may be making a stupid mistake.

Also I am on Unity 5.4.1f1 and am not able to update due to company policies.


UPDATE The buttons in my scene are instances of a prefab, which does not have event listeners. If I do “Break Prefab Instance”, the listener is no longer removed at runtime. However I want to keep my instances as instances.


UPDATE 2 Adding a call to Undo.RecordObject before removing/adding listeners seems to fix the problem for most buttons. However for the remaining buttons, my code either duplicates the last listener (there are other listeners on the buttons), or adds an empty listener, instead of adding manager.PlaySound.


UPDATE 3 I still have no idea why, but for those buttons where my code doesn’t work, I manually added the listener and ran the code again, and now it works perfectly. The answer to the initial question is Undo.RecordObject. Since the question is still in moderation, I cannot post an answer, so I am leaving it here.

After hours of trying and searching, I find a workaround. It is a workaround because it is said clearly in the doc that the api is not recommended to be used after 5.3.

Call EditorUtility.SetDirty(b) after modification.

Undo.RecordObject seems not work on button event.

I have the same problem works for other buttons in scene but doesn’t work for these ones weird…