Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Custom BindableElement

Discussion in 'UI Toolkit' started by Lecks, Oct 17, 2020.

  1. Lecks

    Lecks

    Joined:
    May 13, 2013
    Posts:
    18
    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 <InspectorList binding-path="arrayName" /> in a PropertyDrawer/Inspector). HandleEvent is the important part for binding.


    Code (CSharp):
    1.  
    2. public class InspectorList : BindableElement
    3. {
    4.     VisualElement listContainer;
    5.     Button addButton;
    6.     SerializedProperty array;
    7.     Label label;
    8.     public InspectorList()
    9.     {
    10.         label = new Label("Unbound List");
    11.         Add(label);
    12.         listContainer = new VisualElement();
    13.         listContainer.AddToClassList("inspector-list-container");
    14.         addButton = new Button(AddItem);
    15.         addButton.text = "Add";
    16.         addButton.AddToClassList("inspector-list-add-button");
    17.         Add(listContainer);
    18.         Add(addButton);
    19.         styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>("Assets/Editor/InspectorList.uss"));
    20.     }
    21.  
    22.     // Get the reference to the bound serialized object.
    23.     public override void HandleEvent(EventBase evt)
    24.     {
    25.         var type = evt.GetType(); //SerializedObjectBindEvent is internal, so need to use reflection here
    26.         if ((type.Name == "SerializedPropertyBindEvent") && !string.IsNullOrWhiteSpace(bindingPath))
    27.         {
    28.             var obj = type.GetProperty("bindProperty").GetValue(evt) as SerializedProperty;
    29.             array = obj;
    30.             // Updating it twice here doesn't cause an issue.
    31.             UpdateList();
    32.         }
    33.         base.HandleEvent(evt);
    34.     }
    35.  
    36.     // Refresh/recreate the list.
    37.     public void UpdateList()
    38.     {
    39.         listContainer.Clear();
    40.        
    41.         if (array == null)
    42.             return;
    43.         label.text = array.displayName;
    44.         for (int i = 0; i < array.arraySize; i++)
    45.         {
    46.             int index = i;
    47.             var item = new InspectorListItem(array, index);
    48.             var arr = array;
    49.             item.removeButton.RegisterCallback<PointerUpEvent>((evt) => {
    50.                 RemoveItem(index, arr);
    51.             });
    52.             listContainer.Add(item);
    53.         }
    54.     }
    55.  
    56.     // Remove an item and refresh the list
    57.     public void RemoveItem(int index, SerializedProperty array)
    58.     {
    59.         if(array != null)
    60.         {
    61.             array.DeleteArrayElementAtIndex(index);
    62.             array.serializedObject.ApplyModifiedProperties();
    63.         }
    64.  
    65.         UpdateList();
    66.     }
    67.  
    68.     // Add an item and refresh the list
    69.     public void AddItem()
    70.     {
    71.         if (array != null)
    72.         {
    73.             array.InsertArrayElementAtIndex(array.arraySize);
    74.             array.serializedObject.ApplyModifiedProperties();
    75.         }
    76.  
    77.         UpdateList();
    78.     }
    79.  
    80.     public new class UxmlFactory : UxmlFactory<InspectorList, UxmlTraits> { }
    81.  
    82.     public new class UxmlTraits : BindableElement.UxmlTraits
    83.     {
    84.     }
    85.  
    86. }
    87.  
    88. public class InspectorListItem : VisualElement {
    89.     public Button removeButton;
    90.     public InspectorListItem(SerializedProperty array, int index)
    91.     {
    92.         AddToClassList("inspector-list-item-container");
    93.         removeButton = new Button();
    94.         removeButton.name = "RemoveInspectorListItem";
    95.         removeButton.text = "-";
    96.         removeButton.AddToClassList("inspector-list-remove-button");
    97.         var property = array.GetArrayElementAtIndex(index);
    98.         var propertyField = new PropertyField(property);
    99.         propertyField.AddToClassList("inspector-list-item");
    100.         propertyField.Bind(property.serializedObject);
    101.        
    102.         Add(propertyField);
    103.         Add(removeButton);
    104.     }
    105. }
    106.  
    107.  
     
    yurdevor, oscarAbraham and net8floz like this.
  2. SimonDufour

    SimonDufour

    Unity Technologies

    Joined:
    Jun 30, 2020
    Posts:
    514
    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.
     
    tonytopper and Nexer8 like this.
  3. net8floz

    net8floz

    Joined:
    Oct 21, 2017
    Posts:
    37

    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

    Code (CSharp):
    1. protected override OnBind(SerializedObject obj){
    2.  
    3. }
    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.
     
    Last edited: Oct 20, 2020
    oscarAbraham likes this.
  4. broots

    broots

    Joined:
    Dec 20, 2019
    Posts:
    54
    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:


    Code (CSharp):
    1.         public void BindToList<T>(List<T> listOfObjects) where T : ScriptableObject
    2.         {
    3.             listViewer = root.Q<ListView>("listViewer");
    4.             listViewer.itemsSource = listOfObjects;
    5.             removeButtonLink.Clear();
    6.             listViewer.bindItem = BindListItem<T>;
    7.             RefreshList();
    8.         }
    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.
     
    Last edited: Oct 20, 2020
    oscarAbraham likes this.
  5. Lecks

    Lecks

    Joined:
    May 13, 2013
    Posts:
    18
    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?
     
  6. net8floz

    net8floz

    Joined:
    Oct 21, 2017
    Posts:
    37
    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.


    Code (CSharp):
    1.  
    2. // trying to be a good lad and use what unity provides me by overriding baseField
    3. public class BlackboardVariableField : BaseField<IBlackboardVariable> {
    4.         private VisualElement visualElement;
    5.         private PropertyField valueField;
    6.         private SerializedProperty property;
    7.  
    8.         public BlackboardVariableField(SerializedProperty property) : base(property.displayName, new VisualElement()) {
    9.             this.property = property;
    10.             this.bindingPath = property.propertyPath;
    11.             Initialize();
    12.         }
    13.  
    14.         private void Initialize() {
    15.             // look up the visual input because i have no other way to grab it
    16.             // am I misunderstanding the point of base field? Why isn't the private visualInput exposed?
    17.             visualElement = this[1];
    18.             visualElement.LoadStylesheet("Rousr/Blackboards/uss/blackboard-variable-field", "sr-blackboard-variable-field");
    19.  
    20.             // a simple name field I can just simply use binding paths
    21.      
    22.             var nameField = new TextField();
    23.             nameField.bindingPath =  "name";
    24.             nameField.AddToClassList("sr-blackboard-variable-field__name-field");
    25.             visualElement.Add(nameField);
    26.  
    27.             // this is bound to a managed reference and PropertyField decides it's
    28.             // field on bind
    29.             valueField = new PropertyField();
    30.             valueField.AddToClassList("sr-blackboard-variable-field__property-field");
    31.             valueField.bindingPath =  "backing.value";
    32.             visualElement.Add(valueField);
    33.  
    34.             // this is a hack  to refresh the value field as this class
    35.             // is inside of  a serialized property array and when re-ordering
    36.             // or deleting items for the serialized property array the property
    37.             // field will not notice that it has changed. classRef itself
    38.             // just happens to exist and is not relevant to the issue
    39.             var typeWatch = new BindableString();
    40.             typeWatch.bindingPath = "type._classRef";
    41.             visualElement.Add(typeWatch);
    42.  
    43.             typeWatch.RegisterValueChangedCallback(e => {
    44.                 // without serialized object there is no way to refresh this field
    45.                 // this pattern is observed in the unity source but i have no way
    46.                 // to ask for the parents binding I have to pass it in and manage it myself
    47.                 valueField.Unbind();
    48.                 valueField.Bind(property.serializedObject);
    49.             });
    50.         }
    51.     }
     
    Last edited: Oct 20, 2020
  7. net8floz

    net8floz

    Joined:
    Oct 21, 2017
    Posts:
    37
    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<T>.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.

    Code (CSharp):
    1.  public abstract class CustomBindable : BindableElement, INotifyValueChanged<MyType> {
    2.         [SerializeField]
    3.         protected MyType _value;
    4.         public virtual MyType value {
    5.             get {
    6.                 return _value;
    7.             }
    8.             set {
    9.                 if (_value == value) {
    10.                     // things usually have to happen when the value is set, for
    11.                     // instance the very first time its set.
    12.                     // and that's why we call this one anyways
    13.                     SetValueWithoutNotify(value);
    14.                     return;
    15.                 }
    16.          
    17.                 // in order for the serialization binding to update it's expecting you
    18.                 // to dispatch the event
    19.                 using (ChangeEvent<MyType> valueChangeEvent = ChangeEvent<MyType>.GetPooled(_value, value)) {
    20.                     valueChangeEvent.target = this; // very umportant
    21.                     SetValueWithoutNotify(value); // actually set the value and do any init with the value
    22.                     SendEvent(valueChangeEvent);
    23.                 }
    24.             }
    25.         }
    26.  
    27.  
    28.         public virtual void SetValueWithoutNotify(T newValue) {
    29.             _value = newValue;
    30.             // todo: whatever you want to do with the  value initially
    31.         }
    32.     }
     
    Last edited: Oct 20, 2020
  8. watsonsong

    watsonsong

    Joined:
    May 13, 2015
    Posts:
    555
    Hi, so the 'SerializedPropertyBindEvent' can be public?