Using Generic List with serializedProperty Inspector

I haven’t found a simple answer and example for working with generic lists. Say I have a class with public List things and I want to make my inspector work with multiple objects selected in the Editor.

[I’ll update this example with any replies]

public class CompoundTargetTrackerInspector : Editor
{
    private SerializedProperty things
		
    private void OnEnable()
    {
	    this.thingsProperty = this.serializedObject.FindProperty("things");
    }

    public override void OnInspectorGUI()
    {
        this.serializedObject.Update();
        
        // 1. How do I cast back to a generic list?
        List<Something> things = (List<Something>)this.thingsProperty;  // <--Doesn't work

        // 2. How do I iterate over the contents as "Something" types
        bool clicked = GUILayout.Button("Click Me");
        if (clicked)
        {
            for  or foreach ?
            {
                ...???...

                Debug.Log(thing.name);
            }
        }
        
        serializedObject.ApplyModifiedProperties();
    }
}

this.thingsProperty is not the List variable. Its returning a SerializedProperty object. You will have to get the contents of your list from the SerializedProperty object. Generally, SerializedProperty will store your data in one of its many data fields based on the type of the data. For example, bool data values are stored in the [SerializedProperty.boolValue][1] field. There are other types for ints, floats, etc. References to objects are stored in [.objectReferenceValue][2] So the basic way of working with SerializedProperties is to get the property, then get the value from the appropriate field depending on the data type you’re expecting.

SerializedProperty stores arrays in a special way (and it just treats a list as an array). It doesn’t just store the array as an object which you can easily grab and work with. Instead, it stores a bunch of data for the array in various fields. Basically, you test if the field is an array with isArray, then step through the fields with SerializedProperty.Next(true) to get to the parts of the array like the length, and finally the data fields. Once you get the data, you still have to know its type and access it through SerializedProperty.boolValue or whatever type it is.

Here is an example of one to get the data from an array/list in a SerialzedObject:

[CustomEditor(typeof(MyTestType))]
public class SOListtest : Editor {

    private SerializedProperty thingsProperty;

    private void OnEnable() {
        this.thingsProperty = this.serializedObject.FindProperty("_things");
    }
    
    public override void OnInspectorGUI() {
        this.serializedObject.Update();

        SerializedProperty sp = thingsProperty.Copy(); // copy so we don't iterate the original

        if(sp.isArray) {
            int arrayLength = 0;

            sp.Next(true); // skip generic field
            sp.Next(true); // advance to array size field

            // Get the array size
            arrayLength = sp.intValue;

            sp.Next(true); // advance to first array index

            // Write values to list
            List<int> values = new List<int>(arrayLength);
            int lastIndex = arrayLength - 1;
            for(int i = 0; i < arrayLength; i++) {
                values.Add(sp.intValue); // copy the value to the list
                if(i < lastIndex) sp.Next(false); // advance without drilling into children
            }

            // iterate over the list displaying the contents
            for(int i = 0; i < values.Count; i++) {
                EditorGUILayout.LabelField(i + " = " + values*);*

}
}

this.DrawDefaultInspector(); // show the default inspector so we can set some values
}
}

And here is the MonoBehaviour that the inspector inspects:
public class MyTestType : MonoBehaviour {

public List things {
get {
return _things;
}
set {
_things = value;
}
}

[SerializeField]
private List _things;

}
FYI, there are also other methods you can use to access and work with arrays in SerializedProperty:
[SerializedProperty.ClearArray][3],
[SerializedProperty.DeleteArrayElementAtIndex][4],
[SerializedProperty.GetArrayElementAtIndex][5],
[SerializedProperty.InsertArrayElementAtIndex][6],
[SerializedProperty.MoveArrayElement][7]
Additionally, you can directly access the contents of an array via [SerializedObject.FindProperty][8] by passing it a string to the path using this syntax:
“fieldName.Array.data[0].fieldName”
Below are some methods I made to help with working with SerializedProperties. You can see how arrays are stored and how to iterate through all the properties to discover the structure.
public static void LogProperties(SerializedObject so, bool includeChildren = true) {
// Shows all the properties in the serialized object with name and type
// You can use this to learn the structure
so.Update();
SerializedProperty propertyLogger = so.GetIterator();
while(true) {
Debug.Log("name = " + propertyLogger.name + " type = " + propertyLogger.type);
if(!propertyLogger.Next(includeChildren)) break;
}
}
// variablePath may have a structure like this:
// “meshData.Array.data[0].vertexColors”
// So it uses FindProperty to get data from a specific field in an object array
public static void SetSerializedProperty(UnityEngine.Object obj, string variablePath, object variableValue) {

  •  SerializedObject so = new SerializedObject(obj);*
    
  •  SerializedProperty sp = so.FindProperty(variablePath);*
    
  •  if(sp == null) {*
    
  •  	Debug.Log("Error setting serialized property! Variable path: \"" + variablePath + "\" not found in object!");*
    
  •  	return;*
    
  •  }*
    
  •  so.Update(); // refresh the data*
    
  •  //SerializedPropertyType type = sp.propertyType; // get the property type*
    
  •  System.Type valueType = variableValue.GetType(); // get the type of the incoming value*
    
  •  if(sp.isArray && valueType != typeof(string)) { // serialized property is an array, except string which is also an array*
    
  •  	// assume the incoming value is also an array*
    
  •  	if(!WriteSerializedArray(sp, variableValue)) return; // write the array*
    
  •  } else { // not an array*
    
  •  	if(!WriteSerialzedProperty(sp, variableValue)) return; // write the value to the property*
    
  •  }*
    
  •  so.ApplyModifiedProperties(); // apply the changes*
    
  • }*

  • private static bool WriteSerialzedProperty(SerializedProperty sp, object variableValue) {*

  •  // Type the property and fill with new value*
    
  •  SerializedPropertyType type = sp.propertyType; // get the property type*
    
  •  if(type == SerializedPropertyType.Integer) {*
    
  •  	int it = (int)variableValue;*
    
  •  	if(sp.intValue != it) {*
    
  •  		sp.intValue = it;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.Boolean) {*
    
  •  	bool b = (bool)variableValue;*
    
  •  	if(sp.boolValue != b) {*
    
  •  		sp.boolValue = b;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.Float) {*
    
  •  	float f = (float)variableValue;*
    
  •  	if(sp.floatValue != f) {*
    
  •  		sp.floatValue = f;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.String) {*
    
  •  	string s = (string)variableValue;*
    
  •  	if(sp.stringValue != s) {*
    
  •  		sp.stringValue = s;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.Color) {*
    
  •  	Color c = (Color)variableValue;*
    
  •  	if(sp.colorValue != c) {*
    
  •  		sp.colorValue = c;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.ObjectReference) {*
    
  •  	Object o = (Object)variableValue;*
    
  •  	if(sp.objectReferenceValue != o) {*
    
  •  		sp.objectReferenceValue = o;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.LayerMask) {*
    
  •  	int lm = (int)variableValue;*
    
  •  	if(sp.intValue != lm) {*
    
  •  		sp.intValue = lm;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.Enum) {*
    
  •  	int en = (int)variableValue;*
    
  •  	if(sp.enumValueIndex != en) {*
    
  •  		sp.enumValueIndex = en;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.Vector2) {*
    
  •  	Vector2 v2 = (Vector2)variableValue;*
    
  •  	if(sp.vector2Value != v2) {*
    
  •  		sp.vector2Value = v2;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.Vector3) {*
    
  •  	Vector3 v3 = (Vector3)variableValue;*
    
  •  	if(sp.vector3Value != v3) {*
    
  •  		sp.vector3Value = v3;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.Rect) {*
    
  •  	Rect r = (Rect)variableValue;*
    
  •  	if(sp.rectValue != r) {*
    
  •  		sp.rectValue = r;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.ArraySize) {*
    
  •  	int aSize = (int)variableValue;*
    
  •  	if(sp.intValue != aSize) {*
    
  •  		sp.intValue = aSize;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.Character) {*
    
  •  	int ch = (int)variableValue;*
    
  •  	if(sp.intValue != ch) {*
    
  •  		sp.intValue = ch;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.AnimationCurve) {*
    
  •  	AnimationCurve ac = (AnimationCurve)variableValue;*
    
  •  	if(sp.animationCurveValue != ac) {*
    
  •  		sp.animationCurveValue = ac;*
    
  •  	}*
    
  •  } else if(type == SerializedPropertyType.Bounds) {*
    
  •  	Bounds bounds = (Bounds)variableValue;*
    
  •  	if(sp.boundsValue != bounds) {*
    
  •  		sp.boundsValue = bounds;*
    
  •  	}*
    
  •  } else {*
    
  •  	Debug.Log("Unsupported SerializedPropertyType \"" + type.ToString() + " encoutered!");*
    
  •  	return false;*
    
  •  }*
    
  •  return true;*
    
  • }*

  • private static bool WriteSerializedArray(SerializedProperty sp, object arrayObject) {*

  •  System.Array[] array = (System.Array[])arrayObject; // cast to array*
    
  •  sp.Next(true); // skip generic field*
    
  •  sp.Next(true); // advance to array size field*
    
  •  // Set the array size*
    
  •  if(!WriteSerialzedProperty(sp, array.Length)) return false;*
    
  •  sp.Next(true); // advance to first array index*
    
  •  // Write values to array*
    
  •  int lastIndex = array.Length - 1;*
    
  •  for(int i = 0; i < array.Length; i++) {*
    

_ if(!WriteSerialzedProperty(sp, array*)) return false; // write the value to the property*_
* if(i < lastIndex) sp.Next(false); // advance without drilling into children }*

* return true;*
* }*
// A way to see everything a SerializedProperty object contains in case you don’t
// know what type is stored.
public static void LogAllValues(SerializedProperty serializedProperty) {
Debug.Log("PROPERTY: name = " + serializedProperty.name + " type = " + serializedProperty.type);
Debug.Log("animationCurveValue = " + serializedProperty.animationCurveValue);
Debug.Log("arraySize = " + serializedProperty.arraySize);
Debug.Log("boolValue = " + serializedProperty.boolValue);
Debug.Log("boundsValue = " + serializedProperty.boundsValue);
Debug.Log("colorValue = " + serializedProperty.colorValue);
Debug.Log("depth = " + serializedProperty.depth);
Debug.Log("editable = " + serializedProperty.editable);
Debug.Log("enumNames = " + serializedProperty.enumNames);
Debug.Log("enumValueIndex = " + serializedProperty.enumValueIndex);
Debug.Log("floatValue = " + serializedProperty.floatValue);
Debug.Log("hasChildren = " + serializedProperty.hasChildren);
Debug.Log("hasMultipleDifferentValues = " + serializedProperty.hasMultipleDifferentValues);
Debug.Log("hasVisibleChildren = " + serializedProperty.hasVisibleChildren);
Debug.Log("intValue = " + serializedProperty.intValue);
Debug.Log("isAnimated = " + serializedProperty.isAnimated);
Debug.Log("isArray = " + serializedProperty.isArray);
Debug.Log("isExpanded = " + serializedProperty.isExpanded);
Debug.Log("isInstantiatedPrefab = " + serializedProperty.isInstantiatedPrefab);
Debug.Log("name = " + serializedProperty.name);
Debug.Log("objectReferenceInstanceIDValue = " + serializedProperty.objectReferenceInstanceIDValue);
Debug.Log("objectReferenceValue = " + serializedProperty.objectReferenceValue);
Debug.Log("prefabOverride = " + serializedProperty.prefabOverride);
Debug.Log("propertyPath = " + serializedProperty.propertyPath);
Debug.Log("propertyType = " + serializedProperty.propertyType);
Debug.Log("quaternionValue = " + serializedProperty.quaternionValue);
Debug.Log("rectValue = " + serializedProperty.rectValue);
Debug.Log("serializedObject = " + serializedProperty.serializedObject);
Debug.Log("stringValue = " + serializedProperty.stringValue);
Debug.Log("tooltip = " + serializedProperty.tooltip);
Debug.Log("type = " + serializedProperty.type);
Debug.Log("vector2Value = " + serializedProperty.vector2Value);
Debug.Log("vector3Value = " + serializedProperty.vector3Value);
}

_[1]: https://docs.unity3d.com/Documentation/ScriptReference/SerializedProperty-boolValue.html*_
_
[2]: https://docs.unity3d.com/Documentation/ScriptReference/SerializedProperty-objectReferenceValue.html*_
_[3]: http://docs.unity3d.com/Documentation/ScriptReference/SerializedProperty.ClearArray.html*_
_
[4]: http://docs.unity3d.com/Documentation/ScriptReference/SerializedProperty.DeleteArrayElementAtIndex.html*_
_[5]: http://docs.unity3d.com/Documentation/ScriptReference/SerializedProperty.GetArrayElementAtIndex.html*_
_
[6]: http://docs.unity3d.com/Documentation/ScriptReference/SerializedProperty.InsertArrayElementAtIndex.html*_
_[7]: http://docs.unity3d.com/Documentation/ScriptReference/SerializedProperty.MoveArrayElement.html*_
_
[8]: http://docs.unity3d.com/Documentation/ScriptReference/SerializedObject.FindProperty.html*_

I do not fully understand your question, but I have put together a few examples which I thought might be helpful for you:

  • The default list editor with multi-editing and undo support.
  • Modifying the data directly using Undo.RecordObjects.
  • The default text field with multi-editing and undo support.
  • Converting existing editor GUI controls into multi-editing versions.

Also, if you really need to you can enumerate arrays using SerializedProperty:

  • yourProp.arraySize
  • yourProp.GetArrayElementAtIndex
  • yourProp.InsertArrayElementAtIndex
  • yourProp.DeleteArrayElementAtIndex
  • yourProp.MoveArrayElement

Example Behaviour:

using UnityEngine;

using System.Collections.Generic;

public class Example : MonoBehaviour {

    public List<string> things = new List<string>();
    public string label1 = "";
    public string label2 = "";

}

Example Editor:

using UnityEngine;
using UnityEditor;

[CustomEditor(typeof(Example)), CanEditMultipleObjects]
public class ExampleEditor : Editor {

    private SerializedProperty thingsProp;
    private SerializedProperty label1Prop;
    private SerializedProperty label2Prop;

    private void OnEnable() {
        thingsProp = serializedObject.FindProperty("things");
        label1Prop = serializedObject.FindProperty("label1");
        label2Prop = serializedObject.FindProperty("label2");
    }

    public override void OnInspectorGUI() {
        serializedObject.Update();

        // Editable list of things.
        EditorGUILayout.PropertyField(thingsProp, new GUIContent("Things"), true);

        if (GUILayout.Button("Append Item")) {
            // Undo/redo support for changes made to behaviour fields.
            Undo.RecordObjects(targets, "Append Item");

            // You can loop through list as normal here!
            foreach (Example target in targets) {
                target.things.Add("Appended!");

                // You can use either `foreach` or `for`.
                foreach (var thing in target.things)
                    Debug.Log(thing);

                // Any changes made to the array will be undoable.
            }
        }

        // Multi-object editable label (the easy way):
        EditorGUILayout.PropertyField(label1Prop, new GUIContent("Label 1"));

        // Multi-object editable label (the manual way):
        EditorGUI.showMixedValue = label2Prop.hasMultipleDifferentValues;
        EditorGUI.BeginChangeCheck();
        string newValue = EditorGUILayout.TextField("Label 2", label2Prop.stringValue);
        if (EditorGUI.EndChangeCheck())
            label2Prop.stringValue = newValue;
        EditorGUI.showMixedValue = false;

        serializedObject.ApplyModifiedProperties();
    }

}

Aside:

You might be interested in my reorderable list control. Whilst I haven’t tested it for multi-object editing, I suspect that it could be used (perhaps with some minor alterations):

https://bitbucket.org/rotorz/reorderable-list-editor-field-for-unity

I hope that this helps a little anyhow. Feel free to follow up with comments :slight_smile:

For those who are
interested there is an array handler class in
UnityEditorInternal namespace. However there is ZERO docs on it , mostly because UnityDevs don’t want us to use it or something.

Inside the namespace there is a class called ReOrderableList which handles your array for example

using System.Collections.Generic;
using UnityEngine
[System.Serializable]
class ClassThatHasAList {
    [SerializeField]
    List<int> myIntList = new List<int>();
}

We’ve already seen how hard using editor script when dealing with Generic Types, its made worse with the way their handled in Serialized Property.

  using UnityEngine;
    using UnityEditor;
    using UnityEditorInternal; 
    using System.Collections;
    using System.Collection.Generic;
    
    [CustomEditor("ClassThatHasAList")]
    class ListIntEditor : Editor {
    
    ReorderableList list;
    
    public void OnEnable() {

    list = new ReorderableList(serializedObject, 
                    serializedObject.FindProperty("myIntList"),
                    true,
                    true,
                    true,
                    true);
        
    }   
    
    public override void OnInspectorGUI () {
    
    list.DoLayoutList();
    
    }
}

This should Give You something Similar you see in the Event Trigger Component,
It also has several call backs you can use Lambda expressions to customize how it is drawn and behaves.

I found some more info on them here:

Hope it Helps