How the heck are we supposed to use BindableElement, IBindable, and IBinding in Editor?

It kinda seems to me like nobody is using UI Toolkit editor binding correctly. The problem here is that there doesn’t seem to be any usable documentation on the topic. I’ve been scouring the internet trying to find the correct way to create our own BindableElement for use in-editor, with SerializedObject and SerializedProperty, but I’ve yet to turn up any usable results.

For example, say I wanted to set up a complex PropertyDrawer and an associated VisualElement

First we need to create CustomDrawer, a simple PropertyDrawer used to create the element, bind it, and return it:

public class CustomDrawer : PropertyDrawer
{
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        CustomField field = new CustomField();
        field.BindProperty(property);

        return field;
    }
}

From there, we create CustomElement (a VisualElement) to visualize our object in-editor. This is the part I’m stuck on.

Up until now, I’ve just implemented my own BindProperty(SerializeProperty p) method, without inheriting from BindableElement, or implementing IBindable or IBinding. I know this is the wrong way to go about it and I’d love to know the right way to do it.

What is the intended way to set this up? The documentation seems to be completely lacking here. How do we make our own VisualElement with similar editor Binding functionality to FloatField, ObjectField, PropertyField, etc?

2 Likes

Hi wildertony, I agree - it’s pretty unacceptable that such a vital topic has been left essentially unexplained. It took me a long time to figure this out myself. This is a bit ad-hoc and I’m only talking about the details that helped me with my project: making a set of drawers + fields for every System integer type.

Internally, IBindable (IBinding IBindiable.binding) contains a reference to the bound SerializedObject.

First, you’ll want to inherit from BindableElement and INotifyValueChanged<T>. The former inherits from IBindable, and gives you access to BindingExtensions.BindProperty().

When BindProperty is called, it analyzes the field. There is a large case switch that switches based on the property argument’s SerializedPropertyType. For each type, there are a set of valid INotifyValueChanged<T>s. If it finds one, the bindingPath and binding will be updated (otherwise, null).

Once that happens, you need to do a bit more. INotifyValueChanged<T>.value’s setter needs to send out a value changed event to notify the undo system, and then call INotifyValueChanged<T>.SetValueWithoutNotify().

You can do a bit of this stuff with TextValueField<T>, which implicitly inherits from INotifyValueChanged<T>, but there are a few pathetic caveats with integers. The binding system for uint8 - uint32 will all bind to INotifyValueChanged<int> and so you’ll have to implement that in addition to the one that TextValueField<T> implicitly uses. uint64 is a bit of a lost cause, because it also binds to int, so you’ll have to do some ugly gymnastics to get that to work.

3 Likes

You need to implement INotifyValueChanged in addition to IBindable. It only works with types included in the SerializedPropertyType enum. The C# reference in GitHub can be of help to understand how to implement it: UnityCsReference/Modules/UIElementsEditor/Controls/ProgressBar.cs at 61f92bd79ae862c4465d35270f9d1d57befd1761 · Unity-Technologies/UnityCsReference · GitHub

There are two caveats to the way it works that are worth mentioning: One, you usually want to call Bind in your root element. Each call to bind generates a new tracking object internally, so if all your elements are bound separately, it could have a significant performance impact.

Two, I’d recommend avoiding nesting elements that need to be bound to different serialized objects, because, if both objects have properties with the same name, you could overwrite the bindings of the nested elements when binding the parent.

Sadly, there’s no way of preventing these now, because we can’t assign serialized objects directly to elements, and we don’t have access to binding events. I suspect it might change when they add support for runtime bindings.

EDIT
I just saw the previous post; it’s correct. I didn’t mean to repeat information; it was posted while I was writing this. I’d still recommend to look at the GitHub repository; it’s very easy to understand it from there.

EDIT
HI from the future. Doing multiple binding calls isn’t a performance issue anymore as long as:

  • All the binding calls are made in the same frame,
  • Or any binding calls made later are on elements with an ancestor that has already been bound to the same SerializedObject.

The advice on avoiding nested elements bound to different SerializedObjects still stands.

4 Likes

It seems the document has been updated.

You can find the guideline in section.

1 Like

Found out an inconvenient situation for combination of serialization of private members and binding while making things for the unity editor. I want this to be working:

// MyBehavior class inherited from MonoBehavior
[field: SerializeField]
public string Id { get; set; }

// MyInspector class
public class MyInspector : Editor
{
	public override VisualElement CreateInspectorGUI()
	{
		var idField = new DropdownField()
		{
			bindingPath = nameof(MyBehavior.Id),
		};
	}
}

But it doesn’t work - editor shows the values in drop down, but doesn’t remember it after the inspector is redrawn.

So, I want:

  • non-public field
  • auto property
  • editor serializable field
  • “nameof” to evade hard coding the property name

And I can’t achieve this.

This doesn’t work also:

[field: SerializeField]
public string Id { get; set; }

bindingPath = "id",

This works, but hardcoded. For using “nameof” to evade hardcoding I need a public member.

[field: SerializeField]
string id;

bindingPath = "id",

This works, but too cumbersome:

[field: SerializeField]
string id;
public string Id
{
	get => id;
	set => id = value;
}

var propName = nameof(MyBehavior.Id);
var fieldName = char.ToLowerInvariant(propName[0]) + propName[1..];
bindingPath = fieldName,

It seems like bindingPath works only with fields, although its comment says it works with properties

image

The field should be written in obvious way, without any backing field generations for auto properties. And [field: SerializeField] means nothing for bindingPath.

And if you want the field to be non-public, but use the nameof to evade hardcoding, the only way is to use the combersome combination I wrote above. Ridiculous!

So, the issue is - Unity uses the wrong description of what is meant under “bindingPath” - it is field path actually?

It seems like bindingPath works only with fields, although its comment says it works with properties

Property in that context, is refering to the “SerializedProperty”, not to C# properties. Your problem is that u might be misunderstanding how auto-properties and the [field:...] attribute work.

Auto-properties are just syntactic sugar for generating a backing field, and referencing that through the property.

And the prefix [field:...]on a auto-property attribute means “Apply this attribute to the backing field of this property”.

So what is really happening is that you have

[SerializeField]
private string <Id>k__BackingField;

public string Id {
  get => <Id>k__BackingField;
  set => <Id>k__BackingField = value;
}

Binding paths work for SerializedProperties. And unity only serializes fields. So your binding path should be

var idField = new DropdownField()
{
    bindingPath = $"<{nameof(MyBehavior.Id)}>k__BackingField",
};

Anyways, i dont recommend u doing that, just write the backing field manually

1 Like

I understood about generated names for fields of auto properties. And the one of the most convenient ways (without reflection, expressions, and such a things) to use the combination of “bindingPath” and “nameof” is writing the whole property syntax with getter and setter, then lowercasing the first letter of the property name - what I have done in my last code snippet above?

I would just go without any “nameof” approach. Dont serialize auto-properties, just write them fully with a manual field, then write the name of the field directly “id” or “_id” or “m_Id” or whichever you want.

I get what u are trying to do, but to be honest, there is only ever 1 place where you might reference a field by name, and that is the Editor of that particular Type, hardly somewhere else. Those 2 classes are always going to be tightly coupled, so even if everyone on the internet will tell you that referencing a name with a string is an anti-pattern (which it is), is not really a problem when you know those 2 are working together in that way. Its very unlikely that u will ever write that property with a string anywhere else.

Another way is to use public fields - very short

public string id;

bindingPath = nameof(MyBehavior.id),

It is so “Unity way” and so contra to “C# and OOP way”.

I get what u are trying to do, but to be honest, there is only ever 1 place where you might reference a field by name, and that is the Editor of that particular Type, hardly somewhere else. Those 2 classes are always going to be tightly coupled, so even if everyone on the internet will tell you that referencing a name with a string is an anti-pattern (which it is), is not really a problem when you know those 2 are working together in that way. Its very unlikely that u will ever write that property with a string anywhere else.

The main thing to use “nameof” is refactoring-renaming. You say, these two classes are tightly coupled, but they are located even in two different projects - Assembly-CSharp and Assembly-CSharp-Editor. So, I should keep always in mind - “don’t forget to rewrite all these hardcoded strings in the editor assembly if you renamed something”.

Anyway, thank you for your explanations. But I decided to stay with my cumbersome solution for a while - with “nameof” and lowercasing the name of the property.

1 Like

Its just a choice. Serializing is a very particular area that is usually full of string-based approachs. Is either that or have very strong naming conventions. If you ever change your naming convention, then your “char.ToLowerInvariant” might break, cause for any reason you now decide to use underscore to prefix private fields. or now you decided to use “m_XXX” naming convention, or you decided to make the field public so the naming convetion changes, etc.

Just take your pick :stuck_out_tongue: there is no perfect solution

1 Like