How to know when all Visual Element PropertyField/ListView items are rendered?

I bound a property field element to a serialized object. This is dynamically generating a list view. I am adding some classes to a specific list view items after they’re rendered. However, not all list view children are accessible.

I’ve been querying list view items using the code below.


            var items = listView.Query<VisualElement>(className: "unity-list-view__item").ToList();

I tried using the geometry changed event to detect when the children of the property field are rendered, apply the classes, then unregister the callback. This is not predictably working in all cases.

EventCallback<GeometryChangedEvent> geometryChangedCallback = null;
geometryChangedCallback = (evt) =>
{
    // add classes

};
propertyField.RegisterCallback(geometryChangedCallback);

How to dynamically apply classes to property fields that don’t have elements already? I’m doing this inside of an editor window. I tried adding a delay but that’s varying a lot based on loading times.

Could you provide pseudo code so people can understand what you’re trying to achieve?

I think it would make more sense to use ListView.makeItem to return a visual element with the class applied to it already.

 var items = listView.Query<VisualElement>(className: "unity-list-view__item").ToList();
if(items.Count > 0) {
items[0].AddToClassList("dynamic-class");
}

I defined the styles of dynamic-class in a separate uss file. By dynamically applying I just mean I’m adding the classes or defining styles during run time after rendering.

@spiney199 I cannot do that because these list view items are generated by the property field after the property field binds to the serialized object. I’m not creating the list view myself.

First the items children of this ListView are generated automatically when you bind the ListView to a serialized property, right?

Then where does this ListView come from? Or more exactly who creates it?

Right, the list view is automatically generated when I bind the property field to a serialized object. It is created by the property field when i call bind on it.

propertyField.BindProperty(property);

So, I’m not making these elements using makeItem. It’s created by Unity’s internal logic - similar to what happens when we draw an inspector.

Property fields are a black box honestly, particularly because of the async loading of elements. Until Unity give us a way to make them load synchronously, there’s no good way of doing this. The way I’ve always worked around this, is to just make visual elements that I have control over.

Since we can’t react upon the creation of a child item, your approach probably can’t work reliably. I think the better way maybe observing the change on the layout of ListView itself.

The GeometryChangedEvent is sent when either the position or the dimensions of an element changes.

This is the recommended approach (until we have a better option). What cases does this not reliably work for you?

One thing to try is to use hierarchical USS Selectors (ie. my-class > .unity-label) to apply styles to the generated elements without explicitly adding classes in C# to those created elements.

But where should we observe that event? On ListView itself as I said in previous reply, or on children of ListView?

I’d really just like a boolean property on PropertyField that makes it bind synchronously. Non breaking, opt-in, and would solve a lot of problems for everyone.

Yes, the ListView itself. By the time the first GeometryChangeEvent fires, bindings should be done.

It’s something we’re considering. As an alternative, we might have a more robust event/callback to late you know exactly when bindings are done.

2 Likes

Adding the classes in the GeometryChangeEvent of the list view instead of the property field’s GeometryChangeEvent does the job. Thanks @uDamian :slight_smile:
However, I consistently need to update the classes when the list view is scrolled because list view items seem to be rendered on demand as the user scrolls and those that aren’t visible are removed. Due to this, the indices change on scrolling and the classes are occassionally on the wrong list view element when the user scrolls. I’ve been re-adding the classes in this callback

listView.Q<ScrollView>().verticalScroller.valueChanged += value =>
{
};

is there a better way to ensure a class always remains on a specific list view element? I don’t think i can use USS selectors for this because I’m dynamically calculating which list view items to apply the class to.

After adding the dynamic element, u can send a fake GeometryChangedEvent using: schedule.Execute() to force List View update. And use this to add class after that.

I found a way around this by not using list view at all. I generated a separate property field and bound it to each list view element individually. Then I put it in a scrollView. I added classes to the property field using the geometry change event callback. This way, I don’t have to draw out each specific field individually and can take advantage of the binding while I apply dynamic classes. I defined the dynamic class in USS. The classes do not change upon scrolling because the property field items are not virtualized (removed when not visible) like in list views.

var serializedComponent = new SerializedObject(component);
var listProperty = serializedComponent.FindProperty(propertyName); 

if (listProperty != null && listProperty.isArray)
{
    for (int i = 0; i < listProperty.arraySize; i++)
    {
        var listElement = listProperty.GetArrayElementAtIndex(i);
        PropertyField propertyField = new (listElement);
        propertyField.BindProperty(listElement);
        propertyField.AddToClassList("class");
    }
}
1 Like