I’m trying to understand how the binding system works, so that I can make my UI respond properly when the object I’m drawing changes from a different Inspector.
After reading the source for a while, I came to the conclusion that if I used an object that implemented both IBindable and INotifyValueChanged, the system would be able to use my callback.
I don’t see why these are separate interfaces: when do you ever want to implement IBindable, but not INotifyValueChanged, and vice versa? (If you don’t have the binding path, how is the ChangeEvent<> ever going to hit the right INotifyValueChanged?, and if you don’t have a ‘value’ field, how is the bindingPath ever going to connect to anything?)
Here’s my attempt to pick up a basic message about the change:
private class ValueChangeTester<T> : IBindable, INotifyValueChanged<T> {
public T value {
get { return _value; }
set {
Debug.Log("value set: " + value);
_value = value;
}
}
private T _value;
public void SetValueWithoutNotify(T v) {
Debug.Log("SetValueWithoutNotify: " + v);
}
public IBinding binding { get; set; }
public string bindingPath { get; set; }
}
// .... somewhere else
var tester = new ValueChangeTester<bool>();
tester.bindingPath = "hasGroundPlane";
tester.BindProperty(serializedObject);
tester.RegisterValueChangedCallback(evt => Debug.Log($"value changed callback {evt.previousValue} => {evt.newValue}"));
No debug message is ever printed, so this isn’t working as I intended, but have I done something wrong on my side, or is this kind of code supposed to print when my property is changed from a debug inspector somewhere else?
Yeah, I also think the API could be leaner. There are reasons to keep those separate, though: On one hand, you can use INotifyValueChanged without the binding system, to simply have a callback when users change the value of a field. On the other hand, Unity uses IBindable to receive some internal events when a binding happens; for example, InspectorElement is IBindable but doesn’t use any of the properties of the interface, it just implements the interface to receive an internal event to know what object is to be inspected. That last part has some implementation details that are a bit ugly, but the fact that binding an element and notifiying of changes are different things still makes sense to me.
Ok, on your problem. It’s not the most elegant stuff, but you need to emit an event yourself. It’s important because the binding system also relies on that event being emitted. You also need to set _value in SetValueWithoutNotify:
private class ValueChangeTester<T> : IBindable, INotifyValueChanged<T> {
public T value {
get { return _value; }
set {
if (!EqualityComparer<T>.Default.Equals(_value, value)) {
if (panel != null) {
using (ChangeEvent<T> evt = ChangeEvent<T>.GetPooled(_value, value)) {
evt.target = this;
SetValueWithoutNotify(value);
SendEvent(evt);
}
}
else {
SetValueWithoutNotify(value);
}
}
}
}
private T _value;
public void SetValueWithoutNotify(T v) {
_value = v;
Debug.Log("SetValueWithoutNotify: " + v);
}
public IBinding binding { get; set; }
public string bindingPath { get; set; }
}
Unity usually only sends an event if the element is attached to a panel. I guess it’s because the binding system doesn’t update elements that aren’t attached to a a panel, so it keeps things consistent. But you can probably remove the panel check if you want.
Because my tester isn’t a BindableElement, I get these errors:
I have tried to use a BindableElement instead of IBindable, and it does cause the ‘value’ setter to be called appropriately (once added to rootVisualElement). But is it the case that I should forget all about IBindable then, because it just doesn’t work on its own?
Why is it weird? Visual Element is the base class for UIToolkit. It has everything that’s needed to display something, it’s like a div in html. You can’t add an element to a hierarchy if it isn’t a VisualElement, so even if there wasn’t a panel check in the code I wrote, you wouldn’t be able to use it.
In other words, for it to work properly, you need to add your ValueChangeTester to another element that ultimately gets added to a runtime panel, or an editorWindow, or an Inspector.
No problem, feel free to reach me if you get stuck with something else related to this topic.
It’s weird because it’s not a ui problem. What I want is essentially something like: serializedProperty.onChanged = () => Debug.Log("property changed in any open editor or debug inspector");
I’ve made a small library to aid in using UITK in the editor; it covers this, simple reorderable lists with elements of varying height, updating managed references, and some custom stuff like labels that are editable on double click. A lot of the elements can’t work with UXML though; they would need to access binding stuff with reflection and I’d prefer to avoid it until UITK’S internals seem more stable.
I’ll clean it up a little, make some documentation, and share it later. It could be until next week, though, because I need to make some time. I’ll drop you a message when I do in case it can be of help.