Hi,
my question is about reordering of ListView items when item is dragged. I have following setup:
1] TestSO - ScriptableObject with list of Test objects:
[CreateAssetMenu(fileName = "testSO", menuName = "Test SO")]
public class TestSO : ScriptableObject {
public List<Test> _testList;
}
2] Test - C# class with some test properties and 2 nested classes (Inner and InnerSO). Inner is plain C# class and InnerSO is another nested object.
[Serializable]
public class Test {
public string StringValue;
public float FloatValue;
public Inner Inner;
public InnerSO InnerSO;
}
3] Inner class:
[Serializable]
public class Inner {
public string strVal;
public int intVal;
}
4] InnerSO:
[CreateAssetMenu(fileName = "innerSO", menuName = "Inner SO")]
public class InnerSO : ScriptableObject {
public string strVal;
public int intVal;
}
Now, I want to show _testList from TestSO in ListView and allow user to reoder items with mouse dragging.
Whole code for my ListView is below and I started from this example .
- when I want to display properties of Inner class in ListView item, I can drill down with FindProperty() or FindPropertyRelative(). Or I can set binding path like this with dot between property names (see MakeItem()):
item.Add(new TextField() { bindingPath = "Inner.strVal" }); - but if nested object is ScriptableObject, then FindPropertyRelative returns null and binding with dot doesn’t work too. So, I can create nested BindableElement in MakeItem() like this…:
// InnerSO - nested Scriptable object
var nested = new BindableElement();
nested.name = "nested";
nested.Add(new TextField() { bindingPath = "strVal" });
nested.Add(new IntegerField() { bindingPath = "intVal" });
item.Add(nested);
…and bind it in BindItem() like this:
BindableElement nested = element.Q<BindableElement>("nested");
if (nested != null) {
SerializedProperty p = itemProp.FindPropertyRelative("InnerSO");
SerializedObject newSO = new SerializedObject(p.objectReferenceValue as InnerSO);
nested.Bind(newSO);
}
Up until now everything seems to work. This is image of list with test data:
Now,if user reorders 3rd and 2nd item with dragging, result is this:
Items were reodrered, except for nested ScriptableObject, that is bound to nested BindableElement.
Is this correct behaviour? Or is it bug?
Or am I missing something? Am I binding nested ScriptableObjects correctly?
How to bind complex object with nested ScriptableObjects as ListView item and manage correct reordering?
Here is full listing of ListViewTest class:
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
public class ListViewTest : EditorWindow {
private ListView _listView;
private SerializedObject _serializedObject;
// ---------------------------------------------------------
[MenuItem("Level Editor/ListView Test")]
public static void ShowExample() {
ListViewTest wnd = GetWindow<ListViewTest>();
wnd.titleContent = new GUIContent("ListView Test");
}
// ---------------------------------------------------------
public void CreateGUI() {
VisualElement root = rootVisualElement;
ObjectField of = new ObjectField("Insert TestSO");
of.objectType = typeof(TestSO);
of.RegisterValueChangedCallback(evt => {
TestSO testSO = evt.newValue as TestSO;
if (testSO) {
_serializedObject = new SerializedObject(testSO);
root.Bind(_serializedObject);
BindListView();
}
});
root.Add(of);
_listView = new ListView();
_listView.showBoundCollectionSize = false;
_listView.name = "Test listView";
rootVisualElement.Add(_listView);
}
// ---------------------------------------------------------
void BindListView() {
_listView.Unbind();
_listView.bindingPath = "_testList";
_listView.itemHeight = 150;
_listView.style.flexGrow = 1;
// make
_listView.makeItem = MakeItem;
// bind
_listView.bindItem = BindItem;
_listView.Bind(_serializedObject);
}
// ---------------------------------------------------------
VisualElement MakeItem() {
var item = new BindableElement(); //BindableElement so the default bind can assign the item's root property
var lbl = new Label("Custom Item");
item.Add(lbl);
// TestSO properties
item.Add(new TextField() { bindingPath = "StringValue" });
item.Add(new FloatField() { bindingPath = "FloatValue" });
// Inner (plain C# Serializable class)
item.Add(new TextField() { bindingPath = "Inner.strVal" });
item.Add(new IntegerField() { bindingPath = "Inner.intVal" });
// InnerSO - nested Scriptable object
var nested = new BindableElement();
nested.name = "nested";
nested.Add(new TextField() { bindingPath = "strVal" });
nested.Add(new IntegerField() { bindingPath = "intVal" });
item.Add(nested);
return item;
}
// ---------------------------------------------------------
void BindItem(VisualElement element, int index) {
//we find the first Bindable
var field = element as IBindable;
if (field == null) {
//we dig through children
field = element.Query().Where(x => x is IBindable).First() as IBindable;
}
// Bound ListView.itemsSource is a IList of SerializedProperty
var itemProp = _listView.itemsSource[index] as SerializedProperty;
field.bindingPath = itemProp.propertyPath;
element.Bind(itemProp.serializedObject);
BindableElement nested = element.Q<BindableElement>("nested");
if (nested != null) {
SerializedProperty p = itemProp.FindPropertyRelative("InnerSO");
SerializedObject newSO = new SerializedObject(p.objectReferenceValue as InnerSO);
nested.Bind(newSO);
}
}
}

