Search Unity

Resolved Confused about binding

Discussion in 'UI Toolkit' started by highlyinteractive, Aug 4, 2021.

  1. highlyinteractive

    highlyinteractive

    Joined:
    Sep 6, 2012
    Posts:
    116
    I've been able to bind simple UIElement controls without a problem, but now that I'm trying to create more complicated interfaces, I'm struggling to grok how it's supposed to work.

    I was able to bind a ListView to a struct by following this post, but what I'd like to do is bind a Toggle to a boolean within that struct:

    Code (CSharp):
    1.  
    2. using System;
    3. using System.IO;
    4. using System.Collections.Generic;
    5. using UnityEditor;
    6. using UnityEngine;
    7. using UnityEngine.UIElements;
    8. using UnityEditor.UIElements;
    9.  
    10. public class ScenesToggleList : EditorWindow
    11. {
    12.     private TemplateContainer _ui;
    13.     private SerializedObject _serializedObject;
    14.  
    15.     [Serializable]
    16.     public struct SceneStruct
    17.     {
    18.         public string SceneName;
    19.         public string ScenePath;
    20.         public string SceneGUID;
    21.         public bool ToggleValue;
    22.  
    23.         public SceneStruct (string guid, bool b = false)
    24.         {
    25.             SceneGUID = guid;
    26.             ScenePath = AssetDatabase.GUIDToAssetPath(guid);
    27.             SceneName = Path.GetFileNameWithoutExtension(ScenePath);
    28.             ToggleValue = b;
    29.         }
    30.     }
    31.  
    32.     [SerializeField] private List<SceneStruct> _sceneList;
    33.  
    34.     [MenuItem("Window/UI Toolkit/ScenesToggleListTest")]
    35.     public static void OpenWindow ()
    36.     {
    37.         ScenesToggleList wnd = GetWindow<ScenesToggleList>();
    38.         wnd.titleContent = new GUIContent("ScenesToggleList");
    39.     }
    40.  
    41.     private void OnEnable ()
    42.     {
    43.         _serializedObject = new SerializedObject(this);
    44.  
    45.         //Find all scenes in project
    46.         string[] scenes = AssetDatabase.FindAssets("t:Scene");
    47.  
    48.         //Set up list of scenes
    49.         _sceneList = new List<SceneStruct>();
    50.  
    51.         int i = 0;
    52.  
    53.         //Add scene data to list & create toggles
    54.         foreach (string guid in scenes)
    55.         {
    56.             SceneStruct s = new SceneStruct(guid);
    57.             _sceneList.Add(s);
    58.  
    59.             Toggle t = new Toggle(s.SceneName);
    60.             t.bindingPath = $"_sceneList.Array.data[{i}].ToggleValue";
    61.             rootVisualElement.Add(t);
    62.  
    63.             t.RegisterCallback<ChangeEvent<bool>>(OnSceneClick);
    64.  
    65.             i++;
    66.         }
    67.  
    68.         rootVisualElement.Bind(_serializedObject);
    69.     }
    70.  
    71.     private void OnSceneClick (ChangeEvent<bool> evt)
    72.     {
    73.         Toggle t = evt.target as Toggle;
    74.         Debug.Log($"<color=green>Clicked toggle {t.label}</color>");
    75.  
    76.         foreach (SceneStruct s in _sceneList)
    77.         {
    78.             Debug.Log(s.ToggleValue);
    79.         }
    80.     }
    81. }
    82.  
    Can anyone help me get this working? I'm using Unity 2021.1.12f1
     
  2. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    Can you help us understand what is not working in this case?
     
  3. highlyinteractive

    highlyinteractive

    Joined:
    Sep 6, 2012
    Posts:
    116
    Ah, apologies.

    This editor window should list all scenes in the project and create a Toggle for each.

    I would like the value of the toggle(s) to be bound to the ToggleValue booleans of the structs in _sceneList.

    I can make it work by binding the full struct to a ListView (code below) but I can't work out how to just create Toggles and have them affect the boolean output (logged in the OnSceneClick function at the end)

    Code (CSharp):
    1. using System;
    2. using System.IO;
    3. using System.Collections.Generic;
    4. using UnityEditor;
    5. using UnityEngine;
    6. using UnityEngine.UIElements;
    7. using UnityEditor.UIElements;
    8.  
    9. public class ScenesToggleList : EditorWindow
    10. {
    11.     private TemplateContainer _ui;
    12.     private SerializedObject _serializedObject;
    13.  
    14.     [Serializable]
    15.     public struct SceneStruct
    16.     {
    17.         public string SceneName;
    18.         public string ScenePath;
    19.         public string SceneGUID;
    20.         public bool ToggleValue;
    21.  
    22.         public SceneStruct (string guid, bool b = false)
    23.         {
    24.             SceneGUID = guid;
    25.             ScenePath = AssetDatabase.GUIDToAssetPath(guid);
    26.             SceneName = Path.GetFileNameWithoutExtension(ScenePath);
    27.             ToggleValue = b;
    28.         }
    29.     }
    30.  
    31.     [SerializeField] private List<SceneStruct> _sceneList;
    32.  
    33.     [MenuItem("Window/UI Toolkit/ScenesToggleListTest %g")]
    34.     public static void OpenWindow ()
    35.     {
    36.         ScenesToggleList wnd = GetWindow<ScenesToggleList>();
    37.         wnd.titleContent = new GUIContent("ScenesToggleList");
    38.     }
    39.  
    40.     private void OnEnable ()
    41.     {
    42.         _serializedObject = new SerializedObject(this);
    43.  
    44.         //Find all scenes in project
    45.         string[] scenes = AssetDatabase.FindAssets("t:Scene");
    46.  
    47.         //Set up list of scenes
    48.         _sceneList = new List<SceneStruct>();
    49.  
    50.         //Add scene data to list
    51.         foreach (string guid in scenes)
    52.         {
    53.             SceneStruct s = new SceneStruct(guid);
    54.             _sceneList.Add(s);
    55.         }
    56.  
    57.         ListView list = new ListView();
    58.         list.showBoundCollectionSize = false;
    59.         list.itemHeight = 100;
    60.         list.style.height = 400;
    61.         list.bindingPath = nameof(_sceneList);
    62.         rootVisualElement.Add(list);
    63.  
    64.         list.RegisterCallback<ChangeEvent<bool>>(OnSceneClick);
    65.  
    66.  
    67.         rootVisualElement.Bind(_serializedObject);
    68.     }
    69.  
    70.     private void OnSceneClick (ChangeEvent<bool> evt)
    71.     {
    72.         foreach (SceneStruct s in _sceneList)
    73.         {
    74.             Debug.Log(s.ToggleValue);
    75.         }
    76.     }
    77. }
    78.  
     
  4. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    The first script you shared works fine for me (the logged value of "ToggleValue" match the UI).
    Which version of Unity are you using? It could be a bug that only exists in earlier versions.
     
  5. highlyinteractive

    highlyinteractive

    Joined:
    Sep 6, 2012
    Posts:
    116
    I'm using Unity 2021.1.12f1

    So you get a value of True when you check the toggle?

    Does it still work when you have more than one scene in your project?

    I have four scenes, and the values in the console remain False if I check or uncheck any of the toggles
     
  6. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    Tested with 2 scenes, I don't think more would matter.

    I don't know what could be wrong, I'd suggest submitting a bug.

    In the mean time, could try to see if resolving the serialized property works?

    I will share with the you the documentation page we are working on, related to exactly this topic.
    In particular, the code for TexturePackEditor should be of interest to you. You can bind the toggle to a SerializedProperty instead of setting a binding path.
    Specficially, look at how each property is bound with BindProperty(). Disregard the TexturePreviewElement class, it's just a bindable element just like Toggle. BindProperty() is an extension method.

    The recommended way to bind UI to lists is to use the ListView element. However, it may not be suitable for all situations.

    In order to manage a list directly, you can still leverage some of the functionality of the binding system, by interacting directly with the SerializedObject & SerializedProperty API. Specifically, each visual element can be bound to an item in the source array of the SerializedProperty object representing the array. You must also track the value of the array size in case it changes outside the UI (for example, doing “Reset” operation on asset or undoing).


    The following example demonstrates how to create a ScrollView with an horizontal layout, where items can wrap to the next line when needed. This will operate inside a custom Editor for a new type of asset which holds textures that we use in our made up game.

    To use this example, copy the scripts and assets below in your project and from the top menu select “Assets > Create > UIElementsExamples > TexturePackAsset”, then select the newly created asset. You might also want to import some textures and assign them to the different entries in the list. You can find some example textures in the free Playground asset store plugin: https://assetstore.unity.com/packages/essentials/tutorial-projects/unity-playground-109917


    First, let’s define the TexturePackAsset which holds the list of textures:

    Code (CSharp):
    1. using System;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4.  
    5. [CreateAssetMenu(menuName = "UIElementsExamples/TexturePackAsset")]
    6. public class TexturePackAsset : ScriptableObject
    7. {
    8.     public List<Texture2D> textures;
    9.  
    10.     public void Reset()
    11.     {
    12.         textures = new List<Texture2D>() { null, null, null, null };
    13.     }
    14. }

    Then,we define a UI document which will be used in the Editor for TexturePackAsset.
    It holds a ScrollView with a container where the list of UI elements for each texture will be added, and a Button which allows adding more entries to the list.

    Code (CSharp):
    1. <ui:UXML xmlns:ui="UnityEngine.UIElements" xmlns:uie="UnityEditor.UIElements" xmlns="UnityEngine.UIElements" example="UIElementsExamples" editor-extension-mode="True">
    2.     <ui:ScrollView>
    3.         <ui:VisualElement class="preview-container" style="flex-wrap: wrap; flex-direction: row; justify-content: space-around;" />
    4.     </ui:ScrollView>
    5.     <ui:Button name="add-button" text="Add" />
    6. </ui:UXML>
    Each item of the list is represented by a TexturePreviewElement which is provided as part of the “Custom elements and data binding” section.

    From the custom Editor for TexturePackAsset, we load this UI Document and populate it by iterating the list directly from the SerializedProperty, inside the SetupList() method.

    In addition to populating the list when the Editor is first created, we must also track the size of the array. This is facilitated by using the TrackPropertyValue extension method which allows one to specify a callback function to be invoked whenever a property changes. In this case, we simply call the SetupList() method again.

    This method provides the heavy lifting of keeping the container in synchronization with the SerializedProperty. Elements are reused or created as needed, and then bound directly to each property representing each item of the texture array.

    Finally, we also add a callback to increase the size of the list. There are several ways to accomplish this, but one way is simply to increment the SerializedProperty.arraySize property.

    The code for TexturePackEditor:

    Code (CSharp):
    1. using System;
    2. using System.Linq;
    3. using NUnit.Framework;
    4. using UIElementsExamples;
    5. using UnityEditor;
    6. using UnityEditor.UIElements;
    7. using UnityEngine;
    8. using UnityEngine.Pool;
    9. using UnityEngine.UIElements;
    10.  
    11. [CustomEditor(typeof(TexturePackAsset))]
    12. public class TexturePackEditor : Editor
    13. {
    14.     public override VisualElement CreateInspectorGUI()
    15.     {
    16.         var visualTreeAsset = Resources.Load<VisualTreeAsset>("texture_pack_editor");
    17.         var editor = visualTreeAsset.CloneTree();
    18.    
    19.         var container = editor.Q(className: "preview-container");
    20.    
    21.         SetupList(container);
    22.    
    23.         // Watch the array size to handle the list being changed    
    24.         var propertyForSize = serializedObject.FindProperty(nameof(TexturePackAsset.textures) + ".Array");
    25.         propertyForSize.Next(true); // Expand to obtain array size
    26.         editor.TrackPropertyValue(propertyForSize, prop => SetupList(container));
    27.    
    28.         editor.Q<Button>("add-button").RegisterCallback<ClickEvent>(OnClick);
    29.  
    30.         return editor;
    31.     }
    32.  
    33.     void SetupList(VisualElement container)
    34.     {
    35.         var property = serializedObject.FindProperty(nameof(TexturePackAsset.textures) + ".Array");
    36.  
    37.         var endProperty = property.GetEndProperty();
    38.  
    39.         property.NextVisible(true); // Expand the first child.
    40.  
    41.         int childIndex = 0;
    42.    
    43.         // Iterate each property under the array and populate the container with preview elements
    44.         do
    45.         {
    46.             // Stop if we've reached the end of the array
    47.             if (SerializedProperty.EqualContents(property, endProperty))
    48.                 break;
    49.  
    50.             // Skip the array size property
    51.             if (property.propertyType == SerializedPropertyType.ArraySize)
    52.                 continue;
    53.  
    54.             TexturePreviewElement element;
    55.  
    56.             // Find an existing element or create one
    57.             if (childIndex < container.childCount)
    58.             {
    59.                 element = (TexturePreviewElement)container[childIndex];
    60.             }
    61.             else
    62.             {
    63.                 element = new TexturePreviewElement();
    64.                 container.Add(element);
    65.             }
    66.  
    67.             element.BindProperty(property);
    68.  
    69.             ++childIndex;
    70.         }
    71.         while (property.NextVisible(false));   // Never expand children.
    72.    
    73.         // Remove excess elements if the array is now smaller
    74.         while (childIndex < container.childCount)
    75.         {
    76.             container.RemoveAt(container.childCount-1);
    77.         }
    78.     }
    79.  
    80.     void OnClick(ClickEvent evt)
    81.     {
    82.         var property = serializedObject.FindProperty(nameof(TexturePackAsset.textures));
    83.         property.arraySize += 1;
    84.         serializedObject.ApplyModifiedProperties();
    85.     }
    86. }
     
    Last edited: Aug 4, 2021
  7. highlyinteractive

    highlyinteractive

    Joined:
    Sep 6, 2012
    Posts:
    116
    Thanks for the help

    Adding this to my code just returns Null:
    Code (CSharp):
    1. var property = _serializedObject.FindProperty("_sceneList.Array.data[0].ToggleValue");
    2. Debug.Log(property);
    ...so I assume the issue is accessing/serializing the data within the struct.

    If it's working for you, what version of Unity are you using?
     
  8. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    I am using 2022.1.
    The fact that this returns null means that the issue is with the property path. I am not sure why that would be, but iterating the children of the "_sceneList" SerializedProperty would probably work, instead of reconstructing the full path by hand.
     
    Last edited: Aug 5, 2021
  9. highlyinteractive

    highlyinteractive

    Joined:
    Sep 6, 2012
    Posts:
    116
    I think I've found the issue: I'm creating the serializedObject before initialising the List. Calling serializedObject.Update() seems to fix the issue.

    There must be something in Unity 2022 that refreshes serialized objects differently.

    Thanks for your help / sanity checking. I'll try to implement the iteration method you suggested
     
    antoine-unity likes this.