Hi, I'm currently creating some utility Attributes to handle the UI, so far everything is working fine but I think my implementation could be better. For example: I'm creating a ShowIf attribute (and CustomPropertyDrawer) that will hide or show a property if certain conditions are met Code (CSharp): public bool visible = false; [ShowIf(nameof(visible))] public string TestShowIf; In the code what I'm doing is get the current value of the variable "visible" and make "TestShowIf" visible or not, depending on the boolean, and then scheduling the same check to be done every 500 milliseconds: Code (CSharp): element.schedule.Execute(theAction).Every(500); I was wondering if there is a better way to do this. Sadly I can't share the code because it's under an NDA
Hi again, I tried to do something like this, but so far doesn't work: Code (CSharp): [CanEditMultipleObjects] [CustomEditor(typeof(Object), true, isFallback = true)] public class DefaultEditor : Editor { public override VisualElement CreateInspectorGUI() { var container = new VisualElement(); var iterator = serializedObject.GetIterator(); if (iterator.NextVisible(true)) { do { var propertyField = new PropertyField(iterator.Copy()) { name = "PropertyField:" + iterator.propertyPath }; if (iterator.propertyPath == "m_Script" && serializedObject.targetObject != null) { propertyField.SetEnabled(value: false); } container.Add(propertyField); } while (iterator.NextVisible(false)); } container.RegisterCallback<ChangeEvent<SerializedProperty>>(x => { Debug.Log("Changes to SerializedProperty"); }); return container; } } As I understand, the container.RegisterCallback<ChangeEvent<SerializedProperty>> should notify when changes are made to any of the SerializedProperties of the element, right? So far It's not working. However if I change the ChangeEvent to detect changes to a bool, it works as expected. container.RegisterCallback<ChangeEvent<bool>>
The specific type of the ChangeEvent<T> should be the type of the field, not the Serialized Property. So if you want to detect changes on the visible field, you do: Code (CSharp): PropertyField field = new PropertyField(this.serializedObject.FindProperty("visible")); field.RegisterCallback((ChangeEvent<bool> e) => { Debug.Log("Change in visibility"); }); And this works. However, I've tried going one step further and trying to detect changes on entire classes. For example, the following portion is inside a MonoBehavior class called "MyMonoBehavior.cs": Code (CSharp): [Serializable] public class MyClass { public string hello = "Hello World"; } public MyClass myClass = new MyClass(); And the Editor code: Code (CSharp): PropertyField field = new PropertyField(this.serializedObject.FindProperty("myClass")); field.RegisterCallback((ChangeEvent<MyMonoBehavior.MyClass> e) => { Debug.Log("Change detected!"); }); return field; But changing the values in the Inspector doesn't fire the change detection. Not sure if this is a bug or PropertyFields are not meant to detect changes deep in their hierarchy. Hopefully someone from Unity will pick this up.
To add, at this time, [CanEditMultipleObjects] (multi-selection editing) is not properly supported by UI Toolkit (formally UIElements) at this time. Just be aware when using those tags. They will work in the future though.
Hey thanks for your answer What I wanted to achieve is to detect changes to any field in the SerializedObject, more or less the UI Toolkit equivalent to the old EditorGUI.BeginChangeCheck / EditorGUI.EndChangeCheck, but I'm not sure if that is possible at the moment. Maybe @uDamian can throw some light into this. Thanks!
Yeah I'm on the same boat. Maybe not detecting any change in a SerializedObject, but in a managed class instance. Using a PropertyField like so (I've posted this above, but just so it's clearer) should, in theory, work. However it does not. Code (CSharp): [Serializable] public class MyClass { public string hello = "Hello World"; } public MyClass myClass = new MyClass(); Code (CSharp): PropertyField field = new PropertyField(this.serializedObject.FindProperty("myClass")); field.RegisterCallback((ChangeEvent<MyClass> e) => { Debug.Log("Change detected!"); }); @uDamian Can we expect this to work in the future? Or should we add change event listeners for each child property?
To clarify, the ChangeEvent is not fired by the PropertyField itself. It is instead fired by whatever UI fields the PropertyField created inside during the Bind(). So, in your case of creating a PropertyField of MyClass, you would see inside a TextField for the hello field. Therefore, the ChangeEvent you would get when the use makes a change would be a ChangeEvent<string>. Also, a note that you EditorGUI.BeginChangeCheck / EditorGUI.EndChangeCheck are no longer relevant for UI Toolkit as you don't need to manually update the SerializedObject yourself. If you used Bind() and/or CreateInspectorGUI() with PropertyField, all the updated to the SerializedObject should be automatic. The only reason to register for ChangeEvents would be to do something else in your UI when the user makes a change (like turn the field red if invalid, for example).
Ah, understood, so basically the PropertyField acts as a relay that takes any change from its child elements and bubbles it up. However, shouldn't the change type in PropertyField group the entire state of the object? Not only the changed field. Because for example, if you have the following class: Code (CSharp): public class MyClass { public string textA = "hello"; public string textB = "world"; public bool isOn = false; } public MyClass myClass = new MyClass(); And you want to detect any change made in the Inspector, you have to do something like this (where propertyField is a PropertyField type bound to the SerializedProperty of the myClass member): Code (CSharp): propertyField.RegisterCallback((ChangeEvent<string> e) => { Debug.Log("Change for either textA or textB"); }); propertyField.RegisterCallback((ChangeEvent<bool> e) => { Debug.Log("Change for isOn"); }); Instead. This isn't very versatile, because you need a change event listener for every type of your class member. It would be much more useful if we could simply ask for any change in any member of the class instance. Code (CSharp): propertyField.RegisterCallback((ChangeEvent<MyClass> e) => { Debug.Log("Change for either textA or textB or isOn"); }); Is this something that could be considered? The use case is, as you mention, validating that the values of a form are correct and painting them in red after validating each one of them. We're doing something similar, but also using polymorphic serialization (with the [SerializeReference] attribute), so we don't know which types the members of the class will have.
The PropertyField itself does not do anything regarding forwarding of events or tracking changes. Each individual UI field is responsible for this. For your example above, you'll have 2 TextFields and 1 Toggle. When the users changes a TextField, you get a ChangeEvent<string>. The PropertyField is not involved. Therefore, you cannot have a ChangeEvent<MyClass>, unless you had a monolithic custom C# field (inheriting from BaseField<T>) that is specialized to have value of type MyClass. (fyi, there are some limitations right now that prevent you from creating your own BaseField<T>, so that's not yet an option anyway). That said, we know this is a limitation. We might have the PropertyField send out a generic ChangeEvent in the future, but at this time, you need to register ChangeEvents for all supported primitive types. A reminder that you can do this at the root of your inspector to catch ALL fields in all PropertyFields inside. These events bubble up all the way to the root unless they are caught by an element. So you don't need to do this on every PropertyField. Using the ChangeEvent<T>, you can also look at the ChangeEvent.target and ChangeEvent.newValue to know what has changed (which property via bindingPath) and what the new value is (hence the need to have a specialized type T).
Thank you Damian for the detailed response. Makes sense, as I was thinking about detecting changes on the Serialized Property, but instead, changes are detected on each type of widget (the the thing that inherits from BaseField, not sure if it has a name). A generic ChangeEvent would certainly help. Meanwhile, we'll do as you suggested, and capture all primitive types. In case someone's interested, here's what I quickly put up. It hasn't been tested in depth, but should give a head start anyone struggling with the same: Code (CSharp): public void RegisterAnyChangeEvent(this VisualElement field, Action callback) { field.RegisterCallback((ChangeEvent<int> e) => callback()); field.RegisterCallback((ChangeEvent<bool> e) => callback()); field.RegisterCallback((ChangeEvent<float> e) => callback()); field.RegisterCallback((ChangeEvent<string> e) => callback()); field.RegisterCallback((ChangeEvent<Color> e) => callback()); field.RegisterCallback((ChangeEvent<UnityEngine.Object> e) => callback()); field.RegisterCallback((ChangeEvent<Enum> e) => callback()); field.RegisterCallback((ChangeEvent<Vector2> e) => callback()); field.RegisterCallback((ChangeEvent<Vector3> e) => callback()); field.RegisterCallback((ChangeEvent<Vector4> e) => callback()); field.RegisterCallback((ChangeEvent<Rect> e) => callback()); field.RegisterCallback((ChangeEvent<AnimationCurve> e) => callback()); field.RegisterCallback((ChangeEvent<Bounds> e) => callback()); field.RegisterCallback((ChangeEvent<Gradient> e) => callback()); field.RegisterCallback((ChangeEvent<Quaternion> e) => callback()); field.RegisterCallback((ChangeEvent<Vector2Int> e) => callback()); field.RegisterCallback((ChangeEvent<Vector3Int> e) => callback()); field.RegisterCallback((ChangeEvent<RectInt> e) => callback()); field.RegisterCallback((ChangeEvent<BoundsInt> e) => callback()); } To use it, simply use the RegisterAnyChangeEvent(callback) extension method. Like so: Code (CSharp): myPropertyField.RegisterAnyChangeEvent(() => { Debug.Log("Something changed in a child property!"); }); Also, this should be fairly easy to modify in the event (pun intended ) that Unity implements a generic ChangeEvent. Cheers and thanks again! EDIT: Made extension method more generic.
Seems there's still some fog around this. My bad. To reiterate, these RegisterCallbacks should not be done on each PropertyField. Events bubble up. So if all your PropertyFields are children of a ROOT element, then all these registrations should be done once on this ROOT element - not on each PropertyField. I would modify your extension method to work on any VisualElement, not just PropertyFields.
Definitely. I'll change the original text to avoid any confusion. Not necessarily on all cases. For example, in my case I want to detect any changes made on each element of a list, so that the header on each list item contains a summary of what's inside (taking into account its values). See the image below. So far, I think the question has been resolved. Thank you for taking the time to answer Although the new UI system is a bit tricky to get used to, so far I'm very happy with the direction its taking!
Wow that list you have there looks absolutely amazing! Can you share how you managed to do that? Does the drawer work on any kind of List? Or is a drawer for a specific class?
Sure! It's a drawer for any array-type that implements a specific interface, but also requires a custom visual element (so it's not that one-for-all solution). However, the basic structure is based on another thread where Matthew kindly provided a very neat way to deal with arrays: https://forum.unity.com/threads/pro...y-dont-refresh-inspector.747467/#post-5167271. Also, to implement the drag and drop, this part of the documentation was very useful: https://docs.unity3d.com/2020.1/Documentation/Manual/UIE-Events-DragAndDrop.html. Hope this helps!
I'm still not sure I understand this with the above info. How do I register for changes in an array/list? Say I have a public List<MyCustomScriptableObject> myList; and root.Add(new PropertyField(serializedObject.FindProperty("myList"))); how do I register for changes to this specific list? I tried root.RegisterCallback((ChangeEvent<MyCustomScriptableObject> x) and root.RegisterCallback((ChangeEvent<List<MyCustomScriptableObject>> x) but neither does anything. Neither does registering for int changes (as another thread suggested) to get the change of count.
Changes bubble up (meaning that the parent objects will receive notifications on any changes made to child properties). For example, if you have the following: Code (CSharp): class A { public B b = new B(); } class B { public C[] cs = { new C() }; } class C { public int value; } A PropertyField that is bound to A will receive event from changes done to A, B and C. A PropertyField bound to B will receive change events made on B and C (but not on A). C will receive change events made on the value field. However, I think there should be a generic event that registers any change, because a parent class may not be aware of its child object types (for example, if you use polymorphism). Something like RegisterCallback(ChangeEventAny e). Something that could be assessed @uDamian ? I can provide use cases where this would be very useful and there's no workaround (that I am aware of). Hope this helps!
I got the part about the bubbling up, but I don't receive any events for PropertyFields that have lists as content, no matter where I try to register for them.
Try making sure they are bound to a serialized object. Other than that, I don't see what else could be wrong (other than a bug in the UI Toolkit, but so far, this has worked out for me)
But what do you register? <List<MyType>>? Just <MyType>? Your code above only handles non-array types, as far as I can see.
Sorry to report that nothing suggested in this thread works. I just registered a change listener for every supported type on every VisualElement in my entire hierarchy and not a single one fires when I drag a new Object into a field bound with a PropertyField. Day 2 of disappointment with VisualElements!
I'm struggling here too. This seems basic functionality again being needlessly complicated. If I have a PropertyField, why can't I know when it has changed? Adding callbacks on every relevant type on a PropertyField gets no actual callbacks. What gives? In my particular application I would like to either know when a PropertyField changes so I can update an Image sprite value - or I would like to bind an Image to a SerializedProperty. Unfortunately Image has no .BindProperty method for some reason and PropertyField won't deliver on callbacks so what the heck is the actual approach here? If I use RegisterValueChangeCallback then I do get a callback, but its when the field is interacted with, and doesn't even fire when the actual value changes. This is confirmed because the field is serialized to the new value, but no event is fired when it does, it's fired when the field is interacted with. Finally to confirm this, the code propertyField.RegisterCallback<SerializedPropertyChangeEvent>(method); does not fire the callback when the serialized property actually does change it's value. This appears to be a bug, or at the very least some internal mixup between the VisualElement changing and the SerializedObject/Property changing. Either way, it does not seem to work as intended.