So I have a ScriptableObject that defines a List of items that represent sprites.
I have a ListView that is binds to this list.
I want the items in the ListView to bind directly to the data in the list. That is, the object at a items index, not child properties of that items data.
All the examples I can find create ListView elements that contain sub elements that bind the properties of the data. I don’t want this, I want to bind the item to the element at the corresponding index.
Here’s some example code:
[CreateAssetMenu(fileName = "Test Data", menuName = "Test/Test Data")]
public class TestData : ScriptableObject {
public List<Item> items = new();
[Serializable]
public struct Item {
public Sprite sprite;
public Color color;
}
}
public class TestEditor : EditorWindow {
[SerializeField] private TestData testData;
[MenuItem("Window/ListViewTestWindow")]
static void Init() {
TestEditor window = EditorWindow.GetWindow<TestEditor>();
window.Show();
}
void CreateGUI() {
var listView = new ListView();
listView.makeItem = () => new ImageItem();
listView.dataSource = testData;
listView.bindingSourceSelectionMode = BindingSourceSelectionMode.AutoAssign;
listView.SetBinding("itemsSource", new DataBinding() { dataSourcePath = new PropertyPath("items") });
rootVisualElement.Add(listView);
}
class ImageItem : BindableElement {
[CreateProperty] // *** Removing this attribute causes the following error to spam the console: [UI Toolkit] Could not set value for target of type 'ImageItem' at path 'value': the path is either invalid or contains a null value. (TestEditor) ***
public TestData.Item value {
get => rawValue;
set {
rawValue = value;
image.sprite = value.sprite;
image.tintColor = value.color;
}
}
private Image image;
private TestData.Item rawValue;
public ImageItem() {
image = new Image();
image.style.width = 32;
image.style.height = 32;
Add(image);
SetBinding(nameof(value), new DataBinding() { bindingMode = BindingMode.ToTarget });
}
}
}
Now this works. My custom element ImageItem correctly gets the value of the corresponding data in the array. I can update the images sprite and color on the data and the UI correctly reflects this.
For the longest time I couldn’t get this to work, until I added the [CreateProperty] attribute on the “value” property. Without that attribute the console spams: “[UI Toolkit] Could not set value for target of type ‘ImageItem’ at path ‘value’: the path is either invalid or contains a null value. (TestEditor)”
I also need to set: listView.bindingSourceSelectionMode = BindingSourceSelectionMode.AutoAssign; otherwise the same warning logs.
Can someone explain why this is the case? Why this attribute is needed? And is this the correct way to bind items to array elements directly?
Another potential issue is that value is being changed every frame.
Event if my data class implements INotifyBindablePropertyChanged and IDataSourceViewHashProvider the list is constantly refreshing the value. How can I avoid this?
For you example, since it’s relatively simple and you are already writing most of the code, I would use the makeItem/bindItem directly, something like that:
listView.itemsSource = testData.items;
listView.makeItem = new ImageItem();
listView.bindItem = (element, index) => (element as ImageItem).vakye = testData.items[i];
For the longest time I couldn’t get this to work, until I added the [CreateProperty] attribute on the “value” property. Without that attribute the console spams: “[UI Toolkit] Could not set value for target of type ‘ImageItem’ at path ‘value’: the path is either invalid or contains a null value. (TestEditor)”
I also need to set: listView.bindingSourceSelectionMode = BindingSourceSelectionMode.AutoAssign; otherwise the same warning logs.
What this does is assign the listView.itemsSource as the dataSource and set the proper dataSourcePath for each list item automatically for you. For simple cases where you want to map the list item directly to a custom element, I would prefer to use makeItem/bindItem.
Event if my data class implements INotifyBindablePropertyChanged and IDataSourceViewHashProvider the list is constantly refreshing the value. How can I avoid this?
In this case, you would need both the collection type and the item type to implement change tracking interfaces. We currently have only partial support for this. You can find more information in this post.
listView.bindItem = (element, index) => (element as ImageItem).value = testData.items[i];
is not being invoked when data inside the list is changed.
It does invoke if I create a new list everytime the data changes.
[CreateAssetMenu(fileName = "Test Data", menuName = "Test/Test Data")]
public class TestData : ScriptableObject, INotifyBindablePropertyChanged, IDataSourceViewHashProvider {
public List<Item> items = new();
[NonSerialized] private int hash;
public event EventHandler<BindablePropertyChangedEventArgs> propertyChanged;
public long GetViewHashCode() {
return hash;
}
private void OnValidate() {
Debug.Log("On Validate");
items = new List<Item>(items); //Have to create a new list, otherwise ListView won't rebind elements
propertyChanged?.Invoke(this, new BindablePropertyChangedEventArgs(nameof(items)));
hash++;
}
[Serializable]
public struct Item {
public Sprite sprite;
public Color color;
}
}
I don’t want to create a new List everytime some data changes. This is why I went down the binding rabbit hole on listView items.
ListView won’t track your items to check if they changed. It doesn’t know which part of the item you are using. You can however tell it to refresh, so instead of:
items = new List<Item>(items); //Have to create a new list, otherwise ListView won't rebind elements
Yes, but as stated, binding with collection types is currently partial and while it is possible to use it, it will update on every tick.
I propose the alternative because your case is simple.
That’s the problem though. Now my data needs to know about the view.
Your data still implements INotifyBindablePropertyChanged. You could use it from your view to listen to changes to items and call RefreshItems() when you know it has changed without making your data know about the view.