Search Unity

Question List Undo-Redo Issue in a Custom Editor Window

Discussion in 'Editor & General Support' started by Serega4567, Jan 29, 2023.

  1. Serega4567

    Serega4567

    Joined:
    Jan 11, 2017
    Posts:
    23
    Hello, dear Unity users!

    As I see, many developers have problems with lists, and I've also encountered a rather strange case when writing a custom editor window.

    In these two GIFs, I check undo-redo functionality by adding elements to the list and removing them from it with the buttons. While it happens once per click, everything looks fine:



    Now I remove several elements at once by holding the Delete key:


    In this case, deleting the elements only took one line in the Undo History window. When the operation is cancelled, the returned list elements become null, and an error appears in the console:
    Code (CSharp):
    1. The serialized array of [SerializeReference] objects is missing entry for Refid [a long number here]
    However, these elements do not disappear without a trace. If I go through the Undo History from the bottom up, everything gets fixed.

    I would be deeply grateful if someone could explain to me what is wrong here. The code I use is at the end of the post.

    Thank you!

    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. using Random = UnityEngine.Random;
    4.  
    5. namespace UndoRedoListIssue
    6. {
    7.     [Serializable]
    8.     public class ListElement
    9.     {
    10.         [SerializeField, Range(0f, 1f)]
    11.         private float value;
    12.         [SerializeField]
    13.         private Color color;
    14.  
    15.         public ListElement()
    16.         {
    17.             value = Random.value;
    18.             color = Color.HSVToRGB(Random.value, 1f, 1f);
    19.         }
    20.     }
    21. }
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEngine;
    3.  
    4. namespace UndoRedoListIssue
    5. {
    6.     // This class contains fields that:
    7.     // 1) should support undo-redo functionality
    8.     // 2) should be able to be saved & loaded as an asset.
    9.     // In this example, it's just a list.
    10.  
    11.     public class Data : ScriptableObject
    12.     {
    13.         [SerializeReference]
    14.         private List<ListElement> list;
    15.  
    16.         public static Data CreateInstance(string name, HideFlags hideFlags)
    17.         {
    18.             var data = ScriptableObject.CreateInstance<Data>();
    19.             data.name = name;
    20.             data.hideFlags = hideFlags;
    21.             data.list = new List<ListElement>();
    22.             return data;
    23.         }
    24.     }
    25. }
    Code (CSharp):
    1. using System.Collections;
    2. using UnityEditor;
    3. using UnityEditorInternal;
    4. using UnityEngine;
    5.  
    6. namespace UndoRedoListIssue
    7. {
    8.     // The main class – an editor window.
    9.     // Creates a Data object and provides some
    10.     // UI to add/remove ListElements from it.
    11.  
    12.     public class ListWindow : EditorWindow
    13.     {
    14.         [SerializeField]
    15.         private Data data;
    16.  
    17.         private SerializedObject dataSO;
    18.         private SerializedProperty listProp;
    19.         private ReorderableList list;
    20.  
    21.         [MenuItem("Tests/List Window")]
    22.         private static void OpenWindow()
    23.         {
    24.             var window = EditorWindow.GetWindow<ListWindow>("List Window");
    25.             window.Show();
    26.         }
    27.  
    28.         private void OnEnable()
    29.         {
    30.             // This hide flag makes Unity not destroy the objects
    31.             // we work at when loading a scene or entering the play mode
    32.  
    33.             if (!data)
    34.             {
    35.                 data = Data.CreateInstance("My Data", HideFlags.DontSave);
    36.             }
    37.  
    38.             dataSO = new SerializedObject(data);
    39.             listProp = dataSO.FindProperty("list");
    40.             list = new ReorderableList(dataSO, listProp, draggable: true, displayHeader: false, displayAddButton: true, displayRemoveButton: true);
    41.  
    42.             list.onAddCallback += OnListElementAdded;
    43.             list.onRemoveCallback += OnListElementRemoved;
    44.             list.drawElementCallback += DrawListElement;
    45.  
    46.             Undo.undoRedoPerformed += OnUndoRedoPerformed;
    47.         }
    48.  
    49.         private void OnDestroy()
    50.         {
    51.             DestroyImmediate(data);
    52.             Undo.undoRedoPerformed -= OnUndoRedoPerformed;
    53.         }
    54.  
    55.         private void OnGUI()
    56.         {
    57.             dataSO.Update();
    58.  
    59.             list.DoLayoutList();
    60.             EditorGUILayout.LabelField($"Index: {list.index}/{list.count}");
    61.             DrawListElementLayout();
    62.  
    63.             dataSO.ApplyModifiedProperties();
    64.         }
    65.  
    66.         private void OnListElementAdded(ReorderableList list)
    67.         {
    68.             // Find the list index of a new element
    69.  
    70.             if (list.count > 0)
    71.             {
    72.                 list.index++;
    73.             }
    74.             else
    75.             {
    76.                 list.index = 0;
    77.             }
    78.  
    79.             listProp.InsertArrayElementAtIndex(list.index);
    80.             listProp.GetArrayElementAtIndex(list.index).managedReferenceValue = new ListElement();
    81.         }
    82.  
    83.         private void OnListElementRemoved(ReorderableList list)
    84.         {
    85.             listProp.DeleteArrayElementAtIndex(list.index);
    86.  
    87.             // Set the list index to the previous element
    88.  
    89.             if (list.count > 0)
    90.             {
    91.                 list.index = Mathf.Max(0, list.index - 1);
    92.             }
    93.             else
    94.             {
    95.                 list.index = -1;
    96.             }
    97.         }
    98.  
    99.         private void DrawListElement(Rect rect, int index, bool isActive, bool isFocused)
    100.         {
    101.             SerializedProperty elementProp = listProp.GetArrayElementAtIndex(index);
    102.  
    103.             if (elementProp.managedReferenceValue != null)
    104.             {
    105.                 EditorGUI.LabelField(rect, $"Element {index + 1}");
    106.             }
    107.             else
    108.             {
    109.                 EditorGUI.LabelField(rect, $"NULL");
    110.             }
    111.         }
    112.  
    113.         private void OnUndoRedoPerformed()
    114.         {
    115.             // UI doesn't seem to update immediately
    116.             // after an undo-redo, force it to do so
    117.             Repaint();
    118.         }
    119.  
    120.         private void DrawListElementLayout()
    121.         {
    122.             if (list.count == 0)
    123.             {
    124.                 EditorGUILayout.LabelField("List is empty");
    125.                 return;
    126.             }
    127.  
    128.             if (list.index < 0 || list.index >= list.count)
    129.             {
    130.                 EditorGUILayout.LabelField("No element is selected");
    131.                 return;
    132.             }
    133.  
    134.             SerializedProperty elementProp = listProp.GetArrayElementAtIndex(list.index);
    135.  
    136.             if (elementProp.managedReferenceValue == null)
    137.             {
    138.                 EditorGUILayout.LabelField("NULL");
    139.                 return;
    140.             }
    141.  
    142.             // Iterate over every child property of
    143.             // the ListElement and draw a field for it
    144.  
    145.             IEnumerator elementChildProp = elementProp.GetEnumerator();
    146.  
    147.             while (elementChildProp.MoveNext())
    148.             {
    149.                 EditorGUILayout.PropertyField(elementChildProp.Current as SerializedProperty);
    150.             }
    151.         }
    152.     }
    153. }