PropertyField children can only be queried after initial UI build

Heya,
I had been trying for the longest time to Query the child objects of different PropertyFields but was always end up with an empty result. I can query down to the actual property parent element itself, but no children. If I attempt to get a list of all descendants of a particular type such as ObjectField, I will get all of them, minus the ones that are child of a PropertyField.

You could clearly see that the elements exist in the hierarchy in the debugger, but at the time of Query (if you attempt to do it within CreateInspectorGUI(), that is), they are not found. This leads me to believe that they are not found because they simply have not been added to the hierarchy yet and are not added until after the CreateInspectorGUI() has completed?

What furthers the theory is that I found that delaying the query actually returns the expected results.

An example of this seen here:

VisualElement root;

public override VisualElement CreateInspectorGUI()
{
    root = new VisualElement();

    // -- This yields no results ----------------------------
    root.Query("PropertyField:myPropertyField").Descendents<ObjectField>().ForEach(x => x.AddToClassList("WITHOUT_DEFER"));
 
    // -- This works fine -----------------------------------
    root.schedule.Execute(ExecuteDeferredTask).StartingIn(0);
 
    return root;
}

private void ExecuteDeferredTask()
{
    root.Query("PropertyField:myPropertyField").Descendents<ObjectField>().ForEach(x => x.AddToClassList("WITH_DEFER"));
}

The end result would be, all child ObjectField elements of the myPropertyField property would have the “WITH_DEFER” class applied to them, but not the “WITHOUT_DEFER”.

Is this intended?

A side effect of having to resort to this workaround, at least in one particular instance I have encountered, resulted in what would best be described as “element popping” due to the action I was trying perform, not being able to be performed until after the tree was actually built.

Thanks,
-MH

Binding is asynchronous by design. The root element returned by CreateInspectorGUI() will be bound by serializedObject by the Inspector. During the binding process, the PropertyField will create the actual fields needed according to the SerializedProperty’s Type.

Why do you need to get the reference to the created ObjectField?

Thanks for the reply, I figured it was something along those lines.

I don’t particularly need the ObjectField anymore as I recreated all the field types in a new default editor, it was just an example.

Though, I mean, there could be many reasons that someone might want to more easily gain access to more granular control of a particular element within a particular PropertyField such as applying uss classes to specific elements of the child objects, or making adjustments to a set of query results, etc.

Over the last few weeks I had been trying to figure out why the queries kept coming back with 0 results. While I do not remember all of them off-hand, I do know that each time it was a separate result that I was trying to achieve.

I do recall one being, trying to change/remove the element_index (element_0, element_1, etc) label from arrays in order to allow the collection to become full width, or adjust the label to field ratio, etc without having to create a new field for it. It would have been very convenient to just query for those particular labels disable their display or remove them from the hierarchy, etc.

I am assuming that there is no point after the binding process, but before the actual completion and display of the inspector, that some changes could be made to the resulting fields?

— Edit —
I thought it was supposed to be able to work this way, and I was simply doing something wrong, ever since I saw this post:
2019.2.19f1 : PropertyField changed event?

The OP in that post seems like he was trying to do the same thing at first after stan-osipov mentioned it could be done, with out receiving any results. Then eventually just went back to IMGUI.

Thanks,
-MH

If you know which type of field the PropertyField will generate, you can always just create them directly, this way the entire VisualElements tree will be available up-front. With PropertyFields, to change sub-elements properties there are 2 ways you can do it. If it’s visual styling, or hiding components, you can do it via uss, something like:

#my-property-field > .unity-object-field > .unity-label
{
   display: none;
}

If you want to do it via code, you can delay the queries and changes until the first layout pass:

root.RegisterCallback<GeometryChangedEvent>(ExecuteDeferredTask);
//[...]
private void ExecuteDeferredTask(GeometryChangedEvent evt)
{
    root.UnregisterCallback<GeometryChangedEvent>(ExecuteDeferredTask);

    root.Query("PropertyField:myPropertyField").Descendents<ObjectField>().ForEach(x =>     x.AddToClassList("WITH_DEFER"));
}
1 Like

I appreciate the info. For my current project, I was already doing as you mentioned and created most field types ahead of time. I will have to give those a try.

I want to say I tried the first one but didn’t see a result, so I just assumed it didn’t work for some reason. It could be I just did something incorrectly, though. I will have to try again.

I will also give the second example a go and see how that works out.

Thanks again. As always, you have been MostHelpful.
-MH

P.S. Side note, if you recall, you shared some code prior for creating editable arrays which was similar to the following:

Even doing a straight copy-paste of it kept giving the following error in 2020.2 when trying to use either the add or remove methods: InvalidOperationException: The operation is not possible when moved past all properties (Next returned false)

I didn’t try it in prior versions of Unity, and I believe you shared it a while back, so not sure if version had anything to do with it, or if something had changed since then?

It probably uses the ListView: Class ListView | UI Toolkit | 1.0.0-preview.18

That element has a different usage as it automatically create what is displayed based on a source of data. Because the content is regenerated, any content added manually may conflict with the element regular behavior. Adding was blocked because of this.

You probably need to implement an itemsSource for that code to work.

Can you share the link of where it was posted?

Thanks for the reply. This one doesn’t use a list view. It’s the ArrayInspectorElement seen here : PropertyDrawer with UIElements, changes in array don’t refresh inspector

I changed a bit, but only so that it no longer builds propertyfields, but different field types depending on what the incoming property type is.

Example

private VisualElement MakeStringItem(SerializedProperty property, string propertyPath, int index, bool hideSelector = true)
{
    var container = new VisualElement();
    container.AddToClassList("makeStringItemContainer");

    var pf = new TextField
    {
        bindingPath = propertyPath,
        label = null,
        name = "valueTextField"
    }; // @formatter:off
    
    try {  pf.SetValueWithoutNotify(property.GetArrayElementAtIndex(index).stringValue); } 
    catch (Exception e) // @formatter:on
    {
        Debug.Log($"Item: {propertyPath} : {e}");
        throw;
    }

    pf.AddToClassList("makeStringItem");
    pf.AddToClassList("unity-base-field--no-label");
    pf.bindingPath = propertyPath;
    pf.style.flexGrow = new StyleFloat(1.0f);

    BuildContainers(container, index);
    container.Add(pf);
    return container;
}

The rest of it, as far as functionality of the add and remove, is the same.

I have to admit I haven’t run that code since then. I should keep these type of things in a package let people update them :slight_smile:

1 Like

A few pointers:

When you override a visualElement, you can set contentContainer to return where you want to add the elements inside.

By default, “this” is returned as we want other elements to go directly inside, but when you want the elements to be placed in a child (especially when having scroll bars) contentContainer will return the targeted container.

When contentContainer return null, it is meant to signify that you should not manually .Add() the elements.

There is probably a moment in the initialization sequence where contentContainer is null. The previous behaviour was to add to self when it was null that will now be blocked. If this is the issue, you can use .hierarchy.Add() to directly place the element without that redirecting behavior.

Let me know if it helps you.

1 Like

I believe I see what I need to change as I refactored a bit, I appreciate both of your help.

1 Like

Reopening this old thread because I keep hitting the same issue as the OC first stated. I’ve been using the GeometryChangedEvent as a way to go around the issue at first, but I’m at a point where BoxA is waiting for an info of BoxB who is waiting for an info of BoxC who is waiting for BoxD, a PropertyField (which is drawn with a PropertyDrawer) wether the binded property inside is a float, a text, or a boolean.

Fully explaining my goal would make quide a block of text, but basically:
I have a custom editor for a scriptable object, which has buttons, some toggles, etc. and then a big box (binding to a structure in the scriptable object), which contains different medium boxes (visually referencing different parts of said structure), and each medium box has different elements.
Each element (in C#) derives from a base class which contains a toggle and a string. Each element deriving from it has a “Value”. It could be a boolean, a string, an array of floats, a custom class, each needing their own data treatment afterwards, that’s why I cannot just group them all into a generic type class (some of them are though, such as simple floats and string)
Since they still have in common the same base class, I decided to do a BindableElement (I’ll call them BaseProperty), having the toggle and the label, and then make children of these for each different element (a list of scenes, exactly 4 enums, etc.) and among these elements is the generic type class, which needs to be drawn and binded with a PropertyField so it can be a float, string, etc. so I don’t have to make a child class of every single type (including the many different enum I might have).

The issue with this, is that the way it is designed, the big custom editor box needs some info from the middle boxes, which needs info from the elements, which need the property field to be “readable”.
I did get to make this work without separating the (toggle and label) and the (value) from the elements (meaning the generic type property drawer had a toggle, label, property field all in one), through delegates and events. The issue I had with this is that for every upcoming new element, I had to manually place the toggle and label (which is common for every element) manually in a new property drawer, making more or less sure that none did take more space than the others, they have the same color, font, etc. So I then thought of what I said before; having a bindable element with the toggle and label, and then every element have an editor deriving from it, generic type included. But that complicated even more the system of events and delegates I had before, and it’s getting too hard to follow what’s being queried before other things.

What would make this incredibly easy is to have some sort of Start, in the VisualElement class, that is called only when every PropertyField (EDIT: in the children) knows what’s under them(EDIT: thus, when the bindings are finished and we can, for example, hide some element because another element has or has not some other value), because the GeometryChangedEvent is just not enough, I’ve abused it as much as I could up until this point.

1 Like

If you don’t need access to property drawers, and you know the type of the properties you’re binding to, one quick way to avoid the issues with PropertyField, is to use the Typed Fields directly. ie: If a property is a float, you use the float field and assign its bindingPath.

Overall this is very valid feedback, if you can please share some code or screenshots via DM

FYI I’m currently running into this problem because I’m trying to workaround the various bugs in PropertyField (where it’s only half-implemented compared to the old IMGUI PropertyField - reacts differently on interpreting the ‘label’ arg, etc). In particular: currently stuck on PropertyFields always rendering the property’s name as the label-string, and trying to find a way to make it do what the API says (i.e. let you provide a label to the constructor).

Your callback trick works. In 2019 LTS it was disappointing that I needed 7 lines of code and an extra method, with a callback on GCE (which is needed for many other things already, to workaround other missing features in UIT), just to make the PropertyField work. I haven’t tested this on latest LTS builds yet - I’m hoping that PropertyField is less buggy there - but I’ve seen forum reports that even in 2021.x it’s not properly working yet.

Until PropertyField is fixed it seems we’ll need this kind of workaround. Thanks for sharing it!

Incidentally … the workaround ultimately fails because GCE already had to be overloaded to fix other bugs that were higher priority. Please can we stop using GCE as the catch-all for bugs in UIToolkit? Our source code is starting to get full to overflowing with things hooking GCE because it’s the only time that a lot of UIT API’s return stable, reliable, useable, correct, values.

…but then most code that is hooking GCE is also changing the render-tree (because we’re still constructing the tree - we had to pause because of UIT bugs, wait for a GCE to be able to continue), so each callback triggers a new GCE. And everything that unregistered after the first GCE doesn’t get run again.

It seems like the only valid way forward is to make a global GCE handler that all code submits lambdas to, and which schedules all the many, many GCE-callback-packets, and re-runs each one multiple times until UIToolkit stops changing.

Would it be possible to wait until all or a specific item is built?

I realize I’m replying to a very old post here, but I’m running into some issues with this in 2022.2. The proposed workaround works as expected for fields created initially, but fails for those created later.

My use-case: I have an editor that lays out a bunch of PropertyFields, one for each entry in a frames array. In its custom property drawer I am adding a button that should remove it from the list. Because this property drawer does not have access to the list itself, I am looking to add the button’s functionality from its parent property drawer. Like I said, this currently works, but only for pre-existing property fields. Forgive the long code sample, but this is as short as I can make it without losing context.

public class SpriteAnimationClipDrawer : PropertyDrawer {
    ScrollView scrollView;
    SerializedProperty spFrames;

    public override VisualElement CreatePropertyGUI(SerializedProperty property) {
        var root = new VisualElement();
   
        spFrames = property.FindPropertyRelative("frames");
        scrollView = new ScrollView(ScrollViewMode.Vertical);
   
        for(int i = 0; i < spFrames.arraySize; i++) {
              CreateFrameField(i); //Creating frame fields here works as expected
        }

        ObjectField dropField = new ObjectField();
   
        dropField.RegisterCallback<DragPerformEvent>(evt => {
            //Create another "dummy" frame field whenever the user drops an item on `dropField`
            //This one fails
            CreateFrameField(0);

            spFrames.serializedObject.ApplyModifiedProperties();
        });

        root.Add(dropField);
        root.Add(scrollView);

        return root;
    }

    void CreateFrameField(int index) {
        PropertyField frameField = new PropertyField(spFrames.GetArrayElementAtIndex(index));

        // NOT-A-FIX: Adding this seemingly superfluous line seems to sometimes fix things. See my edits below:
        frameField.BindProperty(spFrames.GetArrayElementAtIndex(index));

        frameField.RegisterCallback<GeometryChangedEvent>(ExecuteDeferredTask);

        void ExecuteDeferredTask(GeometryChangedEvent evt) {
            var button = frameField.Q<Button>();

            if(button == null) {
                Debug.Log("Deferred: still no button");
                return;
            }

            Debug.Log("Deferred: success");
            // Do stuff with `button` here...
        }

        scrollView.Add(frameField);
    }
}

What am I missing here? Any help would be greatly appreciated!

EDIT: Applying the modified changes before attempting to create the frame property field makes no difference

EDIT 2: I’m noticing a similar behaviour in a different part of the code; it almost seems as if property fields created after the initial creation and added to the tree later never get properly created?

EDIT 3: Strangely enough, rebinding the serialized property works. See the code above marked “FIX”. Weird, but sweet!

EDIT 4: I cheered too soon… It kind of works, sporadically. But something seems incredibly buggy here. Downloadering 2023.1b now to test

Yeah, don’t do that any more, if you try to use the old approaches with 2022+ then it’ll be broken horribly. Some changes are deliberate (the callback events no longer work properly, this was ‘by design’ as I understand it: because the new way makes more sense to newcomers apparently, which I can see (although maybe they should have added a new dispatch instead of arbitrarily breaking the existing one)). Other changes I think were accidental (UIToolkit just not testing many of the edge cases) - but it’s not worth chasing these bugs and reporting them, because …

… You need to avoid using GCE with PropertyFields, and instead use the (now, finally, documented) Binding system directly. In 2021+ it works fine now. Mostly. But more reliably than the old GCE-workarounds.

I have got old (fully working until Unity changed UIToolkit to break it) GCE-based code working again, but it was tortuous and not really worth the effort. I did a side-by-side ‘from scratch’ implementation using the new Binding system and that one worked in all the cases where GCE stuff was failing (but GCE used to work).

ETA: Also … doing everything with the Binding system produces much less convoluted code.

Thank you for your reply. The reason I went this route was I couldn’t get the binding to work. I may have misunderstood, but it seemed like binding only works with Unity Objects, whereas my classes are just plain, serialized ones. I’ll give it a closer look though!

You were absolutely right; I got it to work and this is so much better. Thank you so much!

1 Like

Would you mind explaining this a little bit? Even though we have the binding system, with Editor’s
CreateInspectorGUI and PropertyDrawer’s CreatePropertyGUI the binding is done automatically after those functions return. So, if I want to do something with an element that got created by a PropertyField, as a result of binding, it seems I still need GeometryChangedEvent to hook into something that gets called after the full hierarchy got constructed. Is this a wrong way of actions now?