This thread: UIElements ListView with serializedProperty of an array mentions that 2019.3 should support the Bind() function on ListView. I haven’t been able to track down any examples that use it, and I’d rather not track down the correct setup through trial and error.
Is it as simple as setting the data key on the ListView to match an array/list field name, then call Bind() and everything automagically works?
Do I need to set the itemSource, or does Bind() take care of that?
If I have a list of something more complicated than primitive fields, will that automatically work if I have the binding paths set correctly on the item elements, or do I need to provide a bindItem callback to manually bind them?
This should work in 19.3.
A few quick answers:
itemSource will get filled by a list of SerializedProperties.
makeItem() will default to a PropertyField, if you provide your own, make sure to fill the proper binding-paths to the fields you create.
bindItem will wrap any user-provided bind and Bind() with the proper array element. So if binding-paths were properly set in bake item, you can leave this to null.
Excellent! Thanks for the quick response. That sounds fantastically user-friendly compared to the code for manually setting up the binding in the linked post.
Alright, that got me kind of, sort of, almost rolling.
I’m binding my ListView to an array field, which is an array of simple objects with a few string fields (specifically, a localized text entry).
The initial ListView bindings display correctly, minus a warning in the console about it trying to bind to the array size property of the SerializedProperty associated with the array. Then when scrolling through the list, the items aren’t rebinding, so I just get the same initial set of items repeating over and over.
Also worth noting - after hooking up the data binding, any action (just simple stuff like expanding a watch variable, or stepping to the next line) in the VS debugger takes on the order of minutes to complete, and the only way to stop debugging is to kill the process for both VS & Unity.
Here’s the bulk of the code:
using System.IO;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor.UIElements;
public class TestElementWindow : EditorWindow
{
public LocalizationStringData _data = null;
private SerializedObject _serializedObject = null;
private ListView _listView = null;
private VisualTreeAsset _itemPrototype = null;
private StyleSheet _itemStyle = null;
private string _filePath;
[MenuItem("Window/UIE Test/TestElementWindow")]
public static void ShowExample()
{
TestElementWindow wnd = GetWindow<TestElementWindow>();
}
public void OnEnable()
{
_filePath = Locale.StringFilePath;
LoadStrings();
CreateUI();
}
private void CreateUI()
{
titleContent = new GUIContent("Localized Text Database");
// Each editor window contains a root VisualElement object
VisualElement root = rootVisualElement;
// Load layout structure and style
var visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/UI/LocalizedStringsWindow.uxml");
root.Add(visualTree.CloneTree());
var styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/UI/LocalizedStringsWindow.uss");
root.styleSheets.Add(styleSheet);
_itemPrototype = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/UI/LocStringEntry.uxml");
_itemStyle = AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/UI/LocStringEntry.uss");
_listView = root.Q<ListView>("string-entry-list");
_listView.parent.style.flexGrow = 1; // Make parent container grow to fit the window, so that the list view grows to fit the window
_listView.makeItem = CreateListItem;
rootVisualElement.Bind(_serializedObject);
_listView.Refresh();
}
private VisualElement CreateListItem()
{
var element = _itemPrototype.CloneTree();
element.styleSheets.Add(_itemStyle);
return element;
}
private void LoadStrings()
{
if (File.Exists(_filePath))
{
string dataAsJson = File.ReadAllText(_filePath);
_data = JsonUtility.FromJson<LocalizationStringData>(dataAsJson);
}
else
{
_data = new LocalizationStringData();
}
_serializedObject = new SerializedObject(this);
}
}
Any feedback on this? From what I’ve gathered, if you let it use the default property drawers for an array, everything works ok - and by default it displays a size property as the first thing in the list.
This is not a terribly user friendly way to work with lists of data though - and I’d really like to be able to customize the way list items are displayed, and still retain undo/redo and all that goodness afforded by working through SerializedObject, rather than directly with the data it wraps.
Coming with 2020.1 latest(next?) beta, ListView has showBoundCollectionSize. Setting it to false will skip the ArraySize as the first element.
1 Like
Excellent. Thanks for that. I look forward to putting it to use.
For the time being I got my use-case working by skipping the data binding and manually setting my ‘raw’ list data, then pushing to or from the SerializedObject manually via SerializedObject.Update or ApplyModifiedProperties() as appropriate. I’m willing to bet this approach is non-ideal for a number of reasons though.
I’ve been banging my head over this for a couple of hours, wondering why it keeps showing the size. Simple solution. I also have it working with the binding path set up in UIBuilder, and all I needed to touch code for it was to try and debug was it wasn’t working right. Now I have no code that touches the ListView.
Edit: Scratch that no code thing I still needed the selected index.
This was hours of pain.
The way ListView.makeItem works when showBoundCollectionSize = true is kinda broken.
Why?
Let’s say you define makeItem callback not knowing about showBoundCollectionSize:
VisualElement makeItem()
{
return new Label("YOLO");
}
And when you go to inspector you’ll see more “YOLO’s” than items in the array. Even if array is empty. You won’t see array size, only Label from the callback. Then your bindItem callback starts to throw out of bounds errors.
Moreover at somepoint i tried to set showBoundCollectionSize = true in uxml and wondered why its didn’t work.
That looks like a bug. Can you please report it using the Bug Reporter?
Does the ListView work in game? What’s the proper way to set up?
Using the debugger I can see that #unity-content-viewport and #unity-content-container inside the Listview have 0 width. Although the parent ListView (.unity-list-view) does have a definite width and height.
Here is the code, based on the example in the docs:
public void Show(bool enable)
{
gameObject.SetActive(enable);
if (enable)
{// Create some list of data, here simply numbers in interval [1, 1000]
const int itemCount = 1000;
var items = new List<string>(itemCount);
for (int i = 1; i <= itemCount; i++)
items.Add(i.ToString());
// The "makeItem" function will be called as needed
// when the ListView needs more items to render
Func<VisualElement> makeItem = () =>
{
var l = new Label();
l.style.width = 64; // without this labels had also 0 width
l.style.color = Color.white;
return l;
};
// As the user scrolls through the list, the ListView object
// will recycle elements created by the "makeItem"
// and invoke the "bindItem" callback to associate
// the element with the matching data item (specified as an index in the list)
Action<VisualElement, int> bindItem = (e, i) => (e as Label).text = items[i];
var listView = root.Q<ListView>("ComponentList");
listView.selectionType = SelectionType.Multiple;
listView.onItemsChosen += obj => Debug.Log(obj);
listView.onSelectionChange += objects => Debug.Log(objects);
listView.style.flexGrow = 1.0f;
listView.makeItem = makeItem;
listView.bindItem = bindItem;
listView.itemsSource = items;
listView.Refresh();
}
}
Yep, ListView should work at runtime. I can’t see anything wrong with your code. Make sure to set the ListView mode at creation time or in UXML (ie. Vertical, Horizontal, or both). Otherwise, that seems odd. For testing, give ListView a hard-coded width and height (say 200px by 200px) and see if that works.