Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

[UI Elements] Binding & refresh issues

Discussion in 'UI Toolkit' started by mikapote, Mar 26, 2019.

  1. mikapote

    mikapote

    Joined:
    Oct 24, 2018
    Posts:
    28
    Hi there! I'm currently trying to switch to UIElements (in 2019.1 beta), and I'm experiencing many problems (most of them linked to my lack of experience I assume).

    First of all - I know that this is being worked on - but I suffer from the lack of documentation. The developper guide is nice, but it gives very few concrete examples. I'd like to see some "real-life" code snippets for the most common use cases.

    Now, regarding the two main issues I'm having (see my code below):

    1. Binding custom elements
    As you can see, in the custom editor, I'm simply iterating through my ObjectiveList to create one ObjectiveDrawer per item. Those ObjectiveDrawers are actually rendered, and the binding seems to partially work, as the fields are populated with the correct values... BUT if I try to update those fields in the editor, the changes are not persisted. It's as if the binding is working only one way.

    2. Refreshing the list after adding an item
    Second issue, when I'm adding an item to the list by calling CreateObjective, the list is not updated. As we're not running the OnGUI loop anymore, I'm assuming that the binding magic is not enough here and that I must trigger a rendering of the tree manually?

    As you can see, I'm a bit lost here! I've tried many things in the past days and I'm really looking forward to seeing some concrete examples and/or to receiving your advice. Thanks!

    My codes:

    Custom Editor
    Code (CSharp):
    1. [CustomEditor(typeof(ObjectiveList))]
    2. public class ObjectiveListEditor : Editor
    3. {
    4.     ObjectiveList objectiveList;
    5.     List<Objective> objectives;
    6.     TemplateContainer root;
    7.     VisualElement objectiveListContainer;
    8.  
    9.     public override VisualElement CreateInspectorGUI()
    10.     {
    11.         VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Objectives/ObjectiveList.uxml");
    12.         root = visualTree.CloneTree();
    13.         root.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Objectives/ObjectiveList.uss"));
    14.  
    15.         objectiveList = (ObjectiveList)target;
    16.    
    17.         objectives = objectiveList.objectives;
    18.  
    19.         objectiveListContainer = root.Q("objectives");
    20.  
    21.         RenderObjectives();
    22.  
    23.         if (objectives.Count == 0)
    24.         {
    25.             objectiveListContainer.Add(new Label("The list is empty"));
    26.         }
    27.  
    28.         // Attach action to button
    29.         VisualElement createObjectivesButton = root.Q("createObjectiveButton");
    30.         createObjectivesButton.RegisterCallback<MouseUpEvent>(CreateObjective);
    31.  
    32.         return root;
    33.     }
    34.  
    35.     void CreateObjective(MouseUpEvent evt)
    36.     {
    37.         Objective newObjective = (Objective)ScriptableObject.CreateInstance("Objective");
    38.         objectives.Add(newObjective);
    39.     }
    40.  
    41.     void RenderObjectives()
    42.     {
    43.         objectives.ForEach(objectiveObject => {
    44.             if (objectiveObject == null)
    45.             {
    46.                 // Something is wrong, remove entry from list
    47.                 Destroy(objectiveObject);
    48.             }
    49.             else
    50.             {
    51.                 objectiveListContainer.Add(new ObjectiveDrawer(objectiveObject));
    52.             }
    53.         });
    54.     }
    55. }
    Custom BindableElement
    Code (CSharp):
    1. public class ObjectiveDrawer : BindableElement
    2. {
    3.     public SerializedObject objective;
    4.  
    5.     public class Factory : UxmlFactory<BindableElement> {}
    6.  
    7.     public ObjectiveDrawer(Objective objective)
    8.     {
    9.         VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Objectives/ObjectiveDrawer.uxml");
    10.         this.objective = new SerializedObject(objective);
    11.         visualTree.CloneTree(this);
    12.         this.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Objectives/ObjectiveDrawer.uss"));
    13.  
    14.         this.Bind(this.objective);
    15.     }
    16. }
     
  2. mikapote

    mikapote

    Joined:
    Oct 24, 2018
    Posts:
    28
    Continuing my trials, I decided not to use Objective as a ScriptableObject but instead as a regular class. I'm now creating the VisualElements for my Objectives directly in the editor script, and I'm trying to redraw the list with the help of RegisterCallback<ChangeEvent>, which is supposed to trigger a callback every time a field changes. Here's my code:

    Code (CSharp):
    1. [CustomEditor(typeof(ObjectiveList))]
    2. public class ObjectiveListEditor : Editor
    3. {
    4.     List<Objective> objectives;
    5.     TemplateContainer root;
    6.     VisualElement objectiveListContainer;
    7.  
    8.     public override VisualElement CreateInspectorGUI()
    9.     {
    10.         VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>("Assets/Editor/Objectives/ObjectiveList.uxml");
    11.         root = visualTree.CloneTree();
    12.         root.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/Objectives/ObjectiveList.uss"));
    13.  
    14.         ObjectiveList objectiveList = (ObjectiveList)target;
    15.         objectives = objectiveList.objectives;
    16.  
    17.         objectiveListContainer = root.Q("objectives");
    18.  
    19.         UpdateObjectiveList();
    20.  
    21.         if (objectives.Count == 0)
    22.         {
    23.             objectiveListContainer.Add(new Label("The list is empty"));
    24.         }
    25.  
    26.         // Attach action to button
    27.         VisualElement createObjectivesButton = root.Q("createObjectiveButton");
    28.         createObjectivesButton.RegisterCallback<MouseUpEvent>(CreateObjective);
    29.  
    30.         // Register change event: NOT WORKING
    31.         root.RegisterCallback<ChangeEvent<Object>>(e => {
    32.             UpdateObjectiveList();
    33.         });
    34.  
    35.         root.Add(new PropertyField(serializedObject.FindProperty("objectives")));
    36.  
    37.         return root;
    38.     }
    39.  
    40.     VisualElement RenderObjectiveElement(SerializedProperty objective)
    41.     {
    42.         VisualElement objectiveContainer = new Box();
    43.         objectiveContainer.Add(new PropertyField(objective.FindPropertyRelative("desc")));
    44.         objectiveContainer.Add(new PropertyField(objective.FindPropertyRelative("objectiveType")));
    45.  
    46.         objectiveContainer.AddToClassList("objectiveContainer");
    47.         return objectiveContainer;
    48.     }
    49.  
    50.     void CreateObjective(MouseUpEvent evt)
    51.     {
    52.         SerializedProperty objectivesProp = serializedObject.FindProperty("objectives");
    53.         objectivesProp.arraySize++;
    54.         serializedObject.ApplyModifiedProperties();
    55.         UpdateObjectiveList(); // Call update here, as ChangeEvent on root isn't triggered
    56.     }
    57.  
    58.     void UpdateObjectiveList()
    59.     {
    60.         objectiveListContainer.Clear();
    61.         SerializedProperty objectivesPropery = serializedObject.FindProperty("objectives");
    62.  
    63.         for (int i = 0; i < objectives.Count; i++)
    64.         {
    65.             VisualElement objectiveElement = RenderObjectiveElement(objectivesPropery.GetArrayElementAtIndex(i));
    66.             objectiveListContainer.Add(objectiveElement);
    67.         }
    68.     }
    69. }

    My issues:

    1. The ChangeEvent is not triggered, the callback isn't called. As a workaround, I'm calling UpdateObjectiveList after the button press (creation of a new objective).
    2. The objective is created, but I'm hitting an error and the Box containing the Objective fields are drawn, but not their content. Here's the error:


    NullReferenceException: Object reference not set to an instance of an object
    UnityEditor.UIElements.BindingExtensions+SerializedObjectBindingBase.UpdateElementStyle (UnityEngine.UIElements.VisualElement element, UnityEditor.SerializedProperty prop) (at /Users/builduser/buildslave/unity/build/Editor/Mono/UIElements/Controls/BindingExtensions.cs:636)
    UnityEditor.UIElements.BindingExtensions+SerializedObjectBindingToBaseField`2[TValue,TField].Update () (at /Users/builduser/buildslave/unity/build/Editor/Mono/UIElements/Controls/BindingExtensions.cs:880)
    UnityEngine.UIElements.VisualTreeBindingsUpdater.UpdateBindings () (at /Users/builduser/buildslave/unity/build/Modules/UIElements/Controls/VisualTreeBindingsUpdater.cs:168)
    UnityEngine.UIElements.VisualTreeBindingsUpdater.Update () (at /Users/builduser/buildslave/unity/build/Modules/UIElements/Controls/VisualTreeBindingsUpdater.cs:136)
    UnityEngine.UIElements.VisualTreeUpdater.UpdateVisualTreePhase (UnityEngine.UIElements.VisualTreeUpdatePhase phase) (at /Users/builduser/buildslave/unity/build/Modules/UIElements/VisualTreeUpdater.cs:80)
    UnityEngine.UIElements.Panel.UpdateBindings () (at /Users/builduser/buildslave/unity/build/Modules/UIElements/Panel.cs:556)
    UnityEditor.RetainedMode.UpdateSchedulers () (at /Users/builduser/buildslave/unity/build/Editor/Mono/RetainedMode.cs:71)


    Thanks everyone for your help! I realize that working with serialized lists isn't really fun.
     
  3. mikapote

    mikapote

    Joined:
    Oct 24, 2018
    Posts:
    28
    For those interested, the issue with non-drawn element was linked to binding. Make sure to
    1. Unbind the parent element;
    2. Make the change on serialized data;
    3. Apply the change (serializedObject.ApplyModifiedProperties());
    3. And then rebind to see the UI properly updated.
     
  4. uDamian

    uDamian

    Unity Technologies

    Joined:
    Dec 11, 2017
    Posts:
    1,231
    I'll try to unstuck you by offering some general advice around bindings. Main thing is to make the binding system do for you as much as possible. For example, instead of manually going through your list of objectives and creating some UI elements, bind your list of objectives and let the binding system create the UI for each objective.

    Concretely, you should consider using custom PropertyDrawers. These are like custom Editors, but they overwrite the UI for an individual field in your Editor. See:
    https://docs.unity3d.com/2019.1/Documentation/ScriptReference/PropertyDrawer.html

    So first, try simply to allow your list of objectives to be bound and display (using the default UI for arrays), and provide a a custom PropertyDrawer for your Objective class. You'll get auto regeneration of the UI when objectives are added or removed magically from the binding system (because the array value change will trigger it). Then, if you wish, you can provide a custom PropertyDrawer for your list of objectives as well.

    Also, you should not be calling `Bind()` inside your `Editor`'s `CreateInspectorGUI()`. After your return your custom ui from `CreateInspectorGUI()`, the system calls `Bind()` on the entire inspector once (using the SerializedObject of the object you actually have selected). This global Bind() can conflict with your own Bind().
     
    net8floz likes this.
  5. mikapote

    mikapote

    Joined:
    Oct 24, 2018
    Posts:
    28
    Thanks uDamian for your help! I've managed to fix most of my issues, and came up with a nice, reactive and useful custom UI. Might not be the cleanest code ever, but it's good enough as an internal tool ;)
    Thanks again!
     
  6. cemleme

    cemleme

    Joined:
    Mar 24, 2017
    Posts:
    30
    hey mikapote, I'm working on something very similar and was working exactly like you did.

    Did you manage to do it using bindings and propertydrawers as uDamian told? is it possible if you share your code?

    thanks
     
    Emiles and MostHated like this.