How to implement Reorderable List in custom propertyDraw for a simple list of class instances.

TLDR; How do you implement reorderablList in a classes custom property draw. Class is a serialisable class, not a scriptable object or Monobehaviour.

Hi,
During this transition from IMGUI to UIToolkit I’m struggling to find concise documentation and example that covers displaying List properties with custom layouts.

i’ve found some examples of implementing reorderableList, but the issue is the examples all do this within a custom editor, not a custom propertyDraw, and as i understand it there are issues with custom property draws because you can’t instantiate the reorderableList in an OnEnable() because that method doesn’t exist. So i’m thinking that you’d need to do this with perhaps a dictionary of reorderableList instances keyed of the list properties name/parent. (trying to find the refence example where i think someones sort of achieved this).

In the mean time, below is pretty much the kind of scenario I have.

I have the following in psuedo code.

class Mission : ScribtableObject
{
    public MainTask mainTask;
}

class MainTask
{
   public List<SubTask> subTasks;
}

class SubTask
{
   public string decription;
   public bool complete;
}

// Note, It was my original intention to have  the following, but of course you immediately get into Unitys limitation on serialisation this sort of scenario. 
class Task {
   public string decription;
   List<Task> subTasks;
}

The editor classes look like

/// Mission Editor, nothing special here.
[CustomEditor(typeof(Mission), false)]
public class MissionEditor : UnityEditor.Editor {

    private void OnEnable()
   {
      // in other examples this method can be used to create an instance of ReorderableList
      // Which by all accounts might work, I'll at some point implemented it just to know that it does.
      // However, as you can see, its my MainTask class that has a list of subTasks, and its this i want to be reorderable. 
      // subTasks = new ReorderableList(serializedObject, 
      //     serializedObject.FindProperty("subTasks"), 
      //     true, true, true, true);
   }

   
    // As I understand it this is the old way and wouldn't use if doing it the new way with UIElements (
    public override void OnInspectorGUI() {
   //     serializedObject.Update();
   //     EditorGUILayout.PropertyField(descriptionProp);
   //     EditorGUILayout.PropertyField(taskProp);
   //     serializedObject.ApplyModifiedProperties();
   }

    // This is the new way, and this is something i got from an example to simply render properties with default UIElements.
    public override VisualElement CreateInspectorGUI()
    {
    var container = new VisualElement();

    var iterator = serializedObject.GetIterator();
    if (iterator.NextVisible(true))
    {
        do
        {
            var propertyField = new PropertyField(iterator.Copy()) { name = "PropertyField:" + iterator.propertyPath };

            if (iterator.propertyPath == "m_Script" && serializedObject.targetObject != null)
                propertyField.SetEnabled(value: false);

            container.Add(propertyField);
        }
        while (iterator.NextVisible(false));
    }

    return container;
    }
}

// I've currently made assumption that i need to create a PropertyDrawer for the MainTask class and not just a custom Editor? 
// I'm wondering if i can just extend MainTask with scriptable object, despite not really needing it to be a scriptable object? I don't feel the need currently to use scriptable objects.

[CustomPropertyDrawer(typeof(MainTask))]
public class MainTaskDrawer: PropertyDrawer
{
    private ReorderableList subTasks;
   
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        // Create property container element.
        VisualElement container = new VisualElement();


        // Task fields

        PropertyField nameField = new PropertyField(property.FindPropertyRelative("name"));                    
        PropertyField subTasksField = new PropertyField(property.FindPropertyRelative("subTasks"));
        
        // Add fields to the container.
        container.Add(nameField);
        container.Add(subTasksField);

        // So the question is, How, or can you? implement reorderable list
        // Where to do the initialisation of the ReorderableList as in the Editor example? 
        // How to add ReorderableList to the Container? and where/how to call DoLayoutList()?
        
        var listProperty = property.FindPropertyRelative("subTasks");
        subTasks = new ReorderableList( property.serializedObject,
            listProperty,
                true, true, true, true);
       
        subTasks.drawHeaderCallback = (Rect rect) => {
            EditorGUI.LabelField(rect, "List header");
        };
        //subTasks?.DoLayoutList();

        return container;
    }
}

Articles i’m looking through that imlpement the IMGUI version of Reorderable list and some articales that talk about doing something similar with UIElements. They have sort of helped…a bit.

Hello,

To comment on the specific implementation of your drawer, what doesn’t work is that the ReorderableList is an IMGUI control. Its “DoLayoutList()” is meant to be invoked during an IMGUI callback, whereas UI Toolkit works by create elements which are rendered implicitly every frame.

UI Toolkit is not yet a full replacement for the ReorderableList. The ListView class has a “reorderable” attribute but ultimately it is made to render any number of elements in a scroll view with a fixed height and not take up as much vertical space as needed. It also expects a fixed height for each element, which wouldn’t work at all.

So in order to achieve this “nested lists” UI you will have to stick to IMGUI.

By the way, the [SerializedReference] attribute now lifts the limitations related to “recursive” serialization, so modelling a “tree” like structure of tasks is trivial (introduced in 2019 IIRC)

To do some tests, I was able to get this structure to work:

    public class Mission : ScriptableObject
    {
        public Task mainTask;
    }
    [Serializable]
    public class Task : ISerializationCallbackReceiver {
        public bool complete;
        public string decription;
        [SerializeReference]
        public List<Task> subTasks;

        public Task()
        {
        }

        public void OnBeforeSerialize()
        {
        }

        public void OnAfterDeserialize()
        {
            if (subTasks == null)
                subTasks = new List<Task>();
        }
    }

You can find a screenshot of the default inspector for this (no custom editor or drawer). It is almost functional but unfortunately adding new instances does not work out of the box. So a custom drawer will be needed.

Cool, thanks for the SerialiseReference example. Very handy. I’d looked at OnBforeSerialize/onAferDeserialize but was missing that important [serializeReference] attribute.

I was pondering if i could use the IMGUIContainer to implement the ReorderableList with the classic implementation. Was going to look at how that might work in conjunction with the Task.

I’m thinking that it should be possible to just implement the propertyDraw with the old style and then call the IMGUIContainer to wrap that specific list. Just not entirely sure how to actually implementat that.

The main problem with IMGUIContainer is that it won’t be able to deal with UI Toolkit drawers.

We support UI Toolkit → IMGUI but not UI Toolkit → IMGUI → UI Toolkit. So your custom drawer will not picked up in this case.

Switched to the [SerializeReference] solution, removed all custom editors and all i get know is the following (see sub Tasks) only displays Elemen 0, Element 0.
I delete Library thinking that perhaps something was cached, no luck.

Never mind, i see what you were saying about serialised reference now.
I’ll get there eventually.