Search Unity

  1. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Resolved ArgumentNullException when adding PropertyField to any VisualElement.

Discussion in 'UI Toolkit' started by Fep310, Oct 18, 2023.

  1. Fep310

    Fep310

    Joined:
    Apr 30, 2018
    Posts:
    10
    I'm creating an "Expose" field attribute using UI Toolkit (CreatePropertyGUI from PropertyDrawer). Its purpose is to expose an object's field (serialized property) in the inspector without having to travel to the object to edit it. I will mainly use this attribute to directly edit ScriptableObjects while a MonoBehaviour is selected.

    I've managed to successfully code this using IMGUI - the older editor UI system; but, I'm reaching an unexpected issue with UI Toolkit.

    Basic code explanation:
    1. Create root VIsualElement;
    2. Create, setup and add a normal PropertyField;
    3. Get an UXML file for the custom Foldout;
    4. Query and set up the Foldout;
    5. Create a SerializedObject based on the PropertyDrawer's property object reference value;
    6. Iterate through every SerializedObject's properties and copy them to an array (ignores some properties);
    7. Use this array to create and add PropertyFields; <- The add is where the error is coming from!

    The PropertyDrawer class:
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEditor;
    3. using UnityEditor.UIElements;
    4. using UnityEngine;
    5. using UnityEngine.UIElements;
    6.  
    7. [CustomPropertyDrawer(typeof(ExposeAttribute))]
    8. public class ExposeAttributePropertyDrawer : PropertyDrawer
    9. {
    10.     private VisualElement _root;
    11.     private SerializedProperty[] _serializedProperties;
    12.  
    13.     private const string k_foldoutUxmlGuid = "c370dc7d5a39e92428503c5c195c612b";
    14.  
    15.     private static string s_foldoutUxmlPath;
    16.     private static string FoldoutUxmlPath
    17.     {
    18.         get
    19.         {
    20.             if (string.IsNullOrEmpty(s_foldoutUxmlPath))
    21.                 s_foldoutUxmlPath = AssetDatabase.GUIDToAssetPath(k_foldoutUxmlGuid);
    22.  
    23.             return s_foldoutUxmlPath;
    24.         }
    25.     }
    26.  
    27.     public override VisualElement CreatePropertyGUI(SerializedProperty property)
    28.     {
    29.         _root = new VisualElement();
    30.  
    31.         var propField = new PropertyField(property);
    32.         propField.BindProperty(property);
    33.  
    34.         _root.Add(propField);
    35.  
    36.         if (property.objectReferenceValue == null)
    37.             return _root;
    38.  
    39.         var foldoutUxml = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(FoldoutUxmlPath);
    40.  
    41.         var foldoutRoot = foldoutUxml.Instantiate();
    42.         var foldout = foldoutRoot.Q<Foldout>();
    43.         foldout.text = $"Edit \"{property.objectReferenceValue.name}\"";
    44.  
    45.         using var serializedObject = new SerializedObject(property.objectReferenceValue);
    46.  
    47.         GetProperties(serializedObject);
    48.  
    49.         if (foldout.value)
    50.         {
    51.             for (int i = 0; i < _serializedProperties.Length; i++)
    52.             {
    53.                 var prop = _serializedProperties[i];
    54.  
    55.                 var propPropField = new PropertyField(prop);
    56.                 propPropField.BindProperty(prop);
    57.  
    58.                 // The next line of code doesn't work.
    59.                 // If I comment the next line of code, the error goes away.
    60.                 foldoutRoot.Add(propPropField);
    61.             }
    62.         }
    63.  
    64.         _root.Add(foldoutRoot);
    65.  
    66.         serializedObject.ApplyModifiedProperties();
    67.  
    68.         return _root;
    69.     }
    70.  
    71.     private void GetProperties(SerializedObject serializedObject)
    72.     {
    73.         serializedObject.UpdateIfRequiredOrScript();
    74.  
    75.         var prop = serializedObject.GetIterator();
    76.  
    77.         if (!prop.Next(true))
    78.             return;
    79.  
    80.         int currentPropIndex = 0;
    81.  
    82.         List<SerializedProperty> serializedProperties = new();
    83.  
    84.         do
    85.         {
    86.             if (currentPropIndex > 9)
    87.                 serializedProperties.Add(prop.Copy());
    88.  
    89.             currentPropIndex++;
    90.         }
    91.         while (prop.Next(false));
    92.  
    93.         _serializedProperties = serializedProperties.ToArray();
    94.     }
    95. }


    The error:
    upload_2023-10-18_7-50-39.png

    The error doesn't directly point to line 60, but when removing it, the error gets never printed. I tried to add the PropertyFields to other VisualElements and the error gets printed anyway. The IMGUI version does literally the same thing, but it works (take this into account; I may post the IMGUI code if necessary).

    Am I missing something? Thank you.
     
  2. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,272
    There's a problem that repeats all over that code - you're using UI Toolkit as if it was an immediate mode UI framework. It's not, `CreatePropertyGUI` is called once
    So when you do this:
    Code (csharp):
    1.  
    2.         if (property.objectReferenceValue == null)
    3.             return _root;
    4.  
    That means that if you open the editor with the object reference not set to anything, and then update the object reference to something else, the UI won't change.

    Similarly, here:
    Code (csharp):
    1.        if (foldout.value)
    2.         {
    3.             for (int i = 0; i < _serializedProperties.Length; i++)
    That will only run once, so depending on what setting the foldout has in the default when you load it from the uxml, you will generate or not generate the fields.


    What you need to do in order to work with UI Toolkit is to set up callbacks when UI elements change, and then update the data based on that.

    You'll need to eg. register a value changed callback in order to edit what's in the UI when the property field changes:
    Code (csharp):
    1. var propField = new PropertyField(property);
    2. propField.RegisterValueChangeCallback(changeEvent =>
    3. {
    4.     CreateUIFor(changeEvent.changedProperty);
    5.  
    6. });
    7.  
    8. if (property.objectReferenceValue != null)
    9.     CreateUIFor(property);
    10.  
    11. void CreateUIFor(SerializedProperty prop)
    12. {
    13.     ...
    14. }
    In CreateUIFor, you'll want to delete any old UI you have, and create completely new UI.

    Your foldout should similarly not do
    if (foldout.value) { ... }
    . Instead, it should always create the UI for the child SerializedProperties, and then set the display of the child elements to Display.Flex if the foldout is folded out and Display.None if it's not. You probably want to create a root for the content so you don't have to iterate all of them to set the display, but can just set it on the root instead.
     
    Fep310 likes this.
  3. Fep310

    Fep310

    Joined:
    Apr 30, 2018
    Posts:
    10

    Thanks a ton!


    Your post was the push I needed to finally start creating tools with UI Toolkit.
    I now have a reference to an UXML UI Document that has everything that I need, minus the code generated PropertyFields.

    If you or someone reading this is interested in how everything is working, I'll leave a pastebin link to the code and a screenshot of the final result.

    Pastebin link:
    https://pastebin.com/v49H5GSh

    Screenshot:
    upload_2023-10-18_16-31-38.png