Custom BindableElement

G’day,

I’m trying to make custom BindableElements that function the same as built in controls. I’ve been successful at doing so but I have to use reflection to get at internal event classes to access the bound property.

How are we meant to be doing this? Can this class (SerializedObjectBindEvent/SerializedPropertyBindEvent) be made public so we can do it without reflection?

Here’s an example of what i’ve been doing (a list that i can use with in a PropertyDrawer/Inspector). HandleEvent is the important part for binding.

public class InspectorList : BindableElement
{
    VisualElement listContainer;
    Button addButton;
    SerializedProperty array;
    Label label;
    public InspectorList()
    {
        label = new Label("Unbound List");
        Add(label);
        listContainer = new VisualElement();
        listContainer.AddToClassList("inspector-list-container");
        addButton = new Button(AddItem);
        addButton.text = "Add";
        addButton.AddToClassList("inspector-list-add-button");
        Add(listContainer);
        Add(addButton);
        styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/InspectorList.uss"));
    }

    // Get the reference to the bound serialized object.
    public override void HandleEvent(EventBase evt)
    {
        var type = evt.GetType(); //SerializedObjectBindEvent is internal, so need to use reflection here
        if ((type.Name == "SerializedPropertyBindEvent") && !string.IsNullOrWhiteSpace(bindingPath))
        {
            var obj = type.GetProperty("bindProperty").GetValue(evt) as SerializedProperty;
            array = obj;
            // Updating it twice here doesn't cause an issue.
            UpdateList();
        }
        base.HandleEvent(evt);
    }

    // Refresh/recreate the list.
    public void UpdateList()
    {
        listContainer.Clear();
       
        if (array == null)
            return;
        label.text = array.displayName;
        for (int i = 0; i < array.arraySize; i++)
        {
            int index = i;
            var item = new InspectorListItem(array, index);
            var arr = array;
            item.removeButton.RegisterCallback<PointerUpEvent>((evt) => {
                RemoveItem(index, arr);
            });
            listContainer.Add(item);
        }
    }

    // Remove an item and refresh the list
    public void RemoveItem(int index, SerializedProperty array)
    {
        if(array != null)
        {
            array.DeleteArrayElementAtIndex(index);
            array.serializedObject.ApplyModifiedProperties();
        }

        UpdateList();
    }

    // Add an item and refresh the list
    public void AddItem()
    {
        if (array != null)
        {
            array.InsertArrayElementAtIndex(array.arraySize);
            array.serializedObject.ApplyModifiedProperties();
        }

        UpdateList();
    }

    public new class UxmlFactory : UxmlFactory<InspectorList, UxmlTraits> { }
  
    public new class UxmlTraits : BindableElement.UxmlTraits
    {
    }

}

public class InspectorListItem : VisualElement {
    public Button removeButton;
    public InspectorListItem(SerializedProperty array, int index)
    {
        AddToClassList("inspector-list-item-container");
        removeButton = new Button();
        removeButton.name = "RemoveInspectorListItem";
        removeButton.text = "-";
        removeButton.AddToClassList("inspector-list-remove-button");
        var property = array.GetArrayElementAtIndex(index);
        var propertyField = new PropertyField(property);
        propertyField.AddToClassList("inspector-list-item");
        propertyField.Bind(property.serializedObject);
       
        Add(propertyField);
        Add(removeButton);
    }
}
3 Likes

Hi,

It would indeed be interesting to have that exposed. It is planned, but we don’t have a specific ETA on the feature. We actually would prefer doing an event not specific to SerializedObject so that it would be, for example, compatible with the runtime.

2 Likes

Looking through the source a lot of the built in controls use serialized properties and since it’s all based around binding to serialized properties - yet we can’t access them - makes uielements quite difficult to use. I find myself needing access to the serialized property a lot and looking through the source so does unity.

Pretty pretty pretty please expose these or perhaps create some resources on how we’re intended to make custom controls without access to what unity has access to if I’m not supposed to use them. I would be much happier with refactoring my code when you guys decide you want to rename or remove an event and view the api as a “preview” or something than to have to constantly dig through the source and use reflection to do something as simple as access the property that I’m bound to.

I haven’t dug through all the binding source yet - is it possible for me to just implement my own binding provider ( or whatever terms you guys use around IBinding )?

IMO it should be as simple as

protected override OnBind(SerializedObject obj){

}

Since it’s assumed I at this point know my bindingPath

I know runtime stuff is going to be great in unity 2021 but 2019 editor only me needs love too.

1 Like

I’m echoing the others here as without access to serialized properties or some way to bind them there’s no good way to ensure your objects are serialized properely.
Just as an example:

        public void BindToList<T>(List<T> listOfObjects) where T : ScriptableObject
        {
            listViewer = root.Q<ListView>("listViewer");
            listViewer.itemsSource = listOfObjects;
            removeButtonLink.Clear();
            listViewer.bindItem = BindListItem<T>;
            RefreshList();
        }

Any changes made to the bound list are discarded after reloading unity, they do not persist. However, I can’t bind to the actual serialized property because it’s not exposed. This is a huge problem in the current rendition of UI elements. My only work around is literally copying all of the data in my objects to a new object as I have no other option to serialize.

1 Like

In the meantime, how do you advise binding serialized objects/properties to custom controls? Is there a way to actually use bindingPath/BindableElement/IBinding in custom controls without reflection?

I’ve been thinking about this topic tonight and went back and removed all the direct serialized property interaction from one of my tricky fields. It’s inside of an array and contains managed references. I was able to get it almost totally clean except there is no way to bind things.

It appears once my parent is bound and I add children to my parent they will not automatically inherit that serialized object ( turns out this is not entirely true ) binding so setting the bindingPath at this point is useless. Furthermore if I need to ensure something was properly bound like the PropertyField which will dynamically adjust it’s fields on bind I would be hopeless.

Here is an example:

As far as I can tell there would be no way to achieve this without either better managed reference support or direct access to the propertys. It could even just be a wrapped binding that doesn’t have any direct serialized property access but instead just a similar api that can under the hood either go to some runtime bindings or at editor time serialized properties.

Because if I could just manage the bindings a bit better and also be able to do the good old fashioned ApplyModifiedProperties() and UpdateScriptIfWhatever() then I really don’t care about accessing the properties as much. Though I also commonly need field info so I can get custom attributes or other drawer related info.

// trying to be a good lad and use what unity provides me by overriding baseField
public class BlackboardVariableField : BaseField<IBlackboardVariable> {
        private VisualElement visualElement;
        private PropertyField valueField;
        private SerializedProperty property;

        public BlackboardVariableField(SerializedProperty property) : base(property.displayName, new VisualElement()) {
            this.property = property;
            this.bindingPath = property.propertyPath;
            Initialize();
        }

        private void Initialize() {
            // look up the visual input because i have no other way to grab it
            // am I misunderstanding the point of base field? Why isn't the private visualInput exposed?
            visualElement = this[1];
            visualElement.LoadStylesheet("Rousr/Blackboards/uss/blackboard-variable-field", "sr-blackboard-variable-field");

            // a simple name field I can just simply use binding paths
     
            var nameField = new TextField();
            nameField.bindingPath =  "name";
            nameField.AddToClassList("sr-blackboard-variable-field__name-field");
            visualElement.Add(nameField);

            // this is bound to a managed reference and PropertyField decides it's
            // field on bind
            valueField = new PropertyField();
            valueField.AddToClassList("sr-blackboard-variable-field__property-field");
            valueField.bindingPath =  "backing.value";
            visualElement.Add(valueField);

            // this is a hack  to refresh the value field as this class
            // is inside of  a serialized property array and when re-ordering
            // or deleting items for the serialized property array the property
            // field will not notice that it has changed. classRef itself
            // just happens to exist and is not relevant to the issue
            var typeWatch = new BindableString();
            typeWatch.bindingPath = "type._classRef";
            visualElement.Add(typeWatch);

            typeWatch.RegisterValueChangedCallback(e => {
                // without serialized object there is no way to refresh this field
                // this pattern is observed in the unity source but i have no way
                // to ask for the parents binding I have to pass it in and manage it myself
                valueField.Unbind();
                valueField.Bind(property.serializedObject);
            });
        }
    }

You can get by a great deal with bindingPath alone though you lose control after a certain point. In most cases you should just be able to use the binding paths and then use IChangeEvent.value = to manage the value. This will automatically be synced with the serialized property so you do not have to call ApplyModified() or anything like that.

Here’s some code getting started code and how most controls are built. In most cases when the property is bound it will call .value = and you can process the value.

 public abstract class CustomBindable : BindableElement, INotifyValueChanged<MyType> {
        [SerializeField]
        protected MyType _value;
        public virtual MyType value {
            get {
                return _value;
            }
            set {
                if (_value == value) {
                    // things usually have to happen when the value is set, for
                    // instance the very first time its set.
                    // and that's why we call this one anyways
                    SetValueWithoutNotify(value);
                    return;
                }
          
                // in order for the serialization binding to update it's expecting you
                // to dispatch the event
                using (ChangeEvent<MyType> valueChangeEvent = ChangeEvent<MyType>.GetPooled(_value, value)) {
                    valueChangeEvent.target = this; // very umportant
                    SetValueWithoutNotify(value); // actually set the value and do any init with the value
                    SendEvent(valueChangeEvent);
                }
            }
        }


        public virtual void SetValueWithoutNotify(T newValue) {
            _value = newValue;
            // todo: whatever you want to do with the  value initially
        }
    }

Hi, so the ‘SerializedPropertyBindEvent’ can be public?