Need help with binding

Hi,

I need some help with binding nested lists of data to a custom editor. I have tried to simplify the problem here as much as possible so it’s easier to understand.

The data consists of a scriptable object that contains info about a list of characters. When I double click this scriptable object I open an editor window.

public class CharactersDataSO : ScriptableObject
{
    public List<CharacterInfo> m_Characters;
}

Each character has a name and a list of abilities.

public class CharacterInfo
{
    public string m_Name;
    public List<Ability> m_Abilities;
}

Each ability has a name.

public class Ability
{
    public string m_Name;
}

When I select one of the characters in the first list, I want to display the abilities that it contains in the second tab. Also in the future, if I click on an ability, I would like to display info about that ability in a third tab, and be able to edit it.

As you can see in the gif I attached, the first list of data (the characters list) is binding just fine, but I can’t seem to be able to bind the subsequent data when I select something on the first list.

This is how the EditorWindow script that takes care of everything looks like:

using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;

public class CharactersDataWindow : EditorWindow
{
    [SerializeField] private VisualTreeAsset m_RootUXML;

    private VisualElement m_LeftListRoot;
    private VisualElement m_RightListRoot;

    private CharactersDataSO m_Model;

    public static void Open(CharactersDataSO model)
    {
        CharactersDataWindow wnd = GetWindow<CharactersDataWindow>();
        wnd.titleContent = new GUIContent("Characters data");
        wnd.Init(model);
    }

    private void Init(CharactersDataSO model)
    {
        m_Model = model;

        rootVisualElement.Clear();

        VisualElement tabsRoot = m_RootUXML.CloneTree();
        rootVisualElement.Add(tabsRoot);

        m_LeftListRoot = tabsRoot.Q<VisualElement>("left-view");
        m_RightListRoot = tabsRoot.Q<VisualElement>("right-view");

        m_LeftListRoot.Add(new Label("Characters"));
        // Add characters list
        MultiColumnListView characterListView = CreateCharactersList();
        m_LeftListRoot.Add(characterListView);

        rootVisualElement.Bind(new SerializedObject(model));
    }

    private void OnCharacterSelected(int idx)
    {
        m_RightListRoot.Clear();

        m_RightListRoot.Add(new Label("Abilities"));

        // Add abilities list
        MultiColumnListView abilitiesListView = CreateAbilitiesList();
        abilitiesListView.dataSource = m_Model.m_Characters[idx];
        m_RightListRoot.Add(abilitiesListView);
    }

    private MultiColumnListView CreateCharactersList()
    {
        MultiColumnListView characterListView = new MultiColumnListView();
        characterListView.bindingPath = nameof(CharactersDataSO.m_Characters);

        characterListView.selectionChanged += (_) => OnCharacterSelected(characterListView.selectedIndex);

        characterListView.columns.Add(new Column
        {
            title = "Name",
            stretchable = true,
            makeCell = () => new Label(),
            bindingPath = nameof(CharacterInfo.m_Name)
        });

        characterListView.columns.Add(new Column
        {
            title = "Empty column",
            stretchable = true,
            makeCell = () => new Label("Empty"),
        });

        return characterListView;
    }

    private MultiColumnListView CreateAbilitiesList()
    {
        MultiColumnListView abilitiesListView = new MultiColumnListView();
        abilitiesListView.bindingPath = nameof(CharacterInfo.m_Abilities);

        abilitiesListView.columns.Add(new Column
        {
            title = "Name",
            stretchable = true,
            makeCell = () => new Label(),
            bindingPath = nameof(Ability.m_Name)
        });

        abilitiesListView.columns.Add(new Column
        {
            title = "Empty column",
            stretchable = true,
            makeCell = () => new Label("Empty"),
        });

        return abilitiesListView;
    }
}

I would really appreciate if you could provide some assistance on how to handle situations like this where I have multiple nested lists that I would want to bind and display.

Thanks!

9823392--1412100--3d10fbccb54143e88c89a265007c2988.gif

Link to the super simple example I’m mentioning:

9823401–1412112–UI Bindings Test.zip (53.3 KB)

Hi! Just a few changes needed to get the above working:

  1. You can remove the dataSource assignment on the abilityListView - it is not doing anything in this scenario

  2. You need to rebind your data when creating the ability list:

abilitiesListView.Bind(new SerializedObject(m_Model))

  1. Update the bindingPath for your abilitiesListView:

abilitiesListView.bindingPath = $"m_Characters.Array.data[{index}].m_Abilities"

Hope this helps!

This worked great, thanks! I see that abilitiesListView.SetBinding wasn’t needed either but I’m not sure why. In what cases would that be needed? Could I replicate the same behavior just using SetBinding?

I’m really struggling to make a mental map of the different ways to bind. There’s so many different ways (.Bind, .SetBinding, .itemsSource, .dataSource, .bindingPath, .dataSourcePath, etc.) and it’s hard to know when to use each of them.

Hi @Finijumper ,

dataSource, dataSourcePath and SetBinding are part of the new binding system that can bind on arbitrary data.

bindingPath is part of the specialized serialized object binding system that exclusively work on top of Unity serialization data (thus editor only).

You can find information distinguishing the two here.

In your case, since you are using serialized types and you are inside the editor, you should probably use the serialized object binding system directly. That should give you some nifty features such as undo/redo and access to custom editors/drawers for the data.

Hope this helps!

1 Like