TL;DR;:
Hello, I am wondering if there is any way to tell whether some element was added or removed to a VisualElement’s children (contentContainer)?
As far as I can tell, there has to be some way of doing this, since the ScrollView would need something like that to display/hide the scrollbar and/or change the scroll position…
Details:
I am implementing something similar to the ShaderGraph’s grouping selection. It’s a node editor, where a node can have other nested nodes:
Basically you have a graph area (that’s the RHS in the image), which is just a container. Inside the graph area are nodes which can be positioned anywhere by dragging (so their position is absolute as there is no layouting to be done). Then inside each node another nodes can be nested. In the image you can see “Parent” having nested two nodes “Other” and “Node”.
Now when I drag the the nested nodes around their parent nodes, the parent changes its size to accomodate them. That’s why you can see the “Other” and “Node” in the corners. If I move the “Node” closer to the center the parent will resize like this:
I have implemented this resizing by manually registering the GeometryChangedEvent callback on the child nodes, which were already in the parent statically in the UXML. In the callback I just set the parent width and that’s it. But now I want to do it dynamically (ie add and remove children nodes at runtime), in a clean way and encapsulate it inside its own FloatingResizeContainer class which would inherit from VisualElement. Then I would imagine that whenever a child gets added/removed I would just register/unregister the events.
The problem is after lots of hours of searching for the solution I wasn’t able to find out if there is any way to actually tell if a child was added or removed from an element.
We actually don’t have such an event. I see to main 2 main solutions.
Use GeometryChangedEvent. This should be sent regardless of elements being added after creation from UXML or elements added later. This should work fine for something that should react to resizing, although this does not work in all situations this is fairly generic.
Introduce some coupling. This is what we do in our graph implementations at Unity. Our main container for the GraphView has a type that every node knows about. When a node detects it’s added (with AttachToPanelEvent) it walks its parent hierarchy until it finds a GraphView and registers itself.
Alternatively you could force your whole system to call a “AddNode” method in your main Graph class.
In the future, we could expose another mechanism we have internally to detect hierarchy changes, which does not rely on events and is a bit more efficient. Let us know if you think that’d be useful.
I think this won’t work in my case, since the Parent registers a GeometryChangedEvent on the child Nodes and after that reacts to their geometry changes. But this won’t work unless the parent can dynamically register the callback on whatever is being added.
I will try to apply this solution, although, when there are multiple possible places for this (multiple nodes can have multiple children), it’s not going to be as straightforward.
Yup, this also crossed my mind and it’s something I wanted to avoid.Although if the solution through 2) will end up being too complex, I will do it this way.
I definitely think that these kind of hierarchy changes events would be useful for other applications as well. So if they could be exposed, that would be great! On the other hand, what would also work is if the VisualElement’s Add method was virtual and could be overriden in the subclasses. Then the client could just call base implementation and add anything he wants after that (something like a decoration).
Hi all, I ran into this issue too. I wanted to have CSS pseudo classes like :first-child and :last-child. I created a manipulator that sends a ChildChangeEvent but it does require some kind of polling. One can provoke it manually or set an interval to check for changes. Given this stylesheet:
@antoine-unity Could you guide us on that “internal mechanism” you were refering to? Maybe we can get to it using reflections to cover some edge cases.
UPDATE:
Have done a bit of digging and it seems the Panel class actually has an event for hierachy changes. Though it’s a real pain to get to it (lots of reflections). I’ve also only tested this in the Editor, so not sure how compatible this is with the runtime.
public void SubscribeToHierarchyChange(IPanel panel)
{
// Find everything we need.
// See: https://github.com/Unity-Technologies/UnityCsReference/blob/master/Modules/UIElements/Core/Panel.cs#L647
var panelAssembly = typeof(IPanel).Assembly;
var panelType = panelAssembly.GetType("UnityEngine.UIElements.BaseVisualElementPanel");
var changeType = panelAssembly.GetType("UnityEngine.UIElements.HierarchyChangeType");
EventInfo eventInfo = panelType.GetEvent("hierarchyChanged", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.FlattenHierarchy);
var callbackMethod = GetType().GetMethod("OnHierarchyChanged", BindingFlags.Static | BindingFlags.Public);
// Create the method
DynamicMethod method = new DynamicMethod("dynOnHierarchyChanged", typeof(void), new Type[] { typeof(VisualElement), changeType });
var generator = method.GetILGenerator(256);
generator.EmitCall(OpCodes.Call, callbackMethod, null);
generator.Emit(OpCodes.Ret);
// Create and assign event handler
Delegate handler = method.CreateDelegate(eventInfo.EventHandlerType);
// Do it like this to allow private access.
var addMethod = eventInfo.GetAddMethod(true);
addMethod.Invoke(panel, new[] { handler });
}
public static void OnHierarchyChanged()
{
Debug.Log("Hierarchy changed");
}
Running in to this issue right now … An event when the hierarchy of a VisualElement changes should be implemented, this is very common to know when that happens. The GeometryChangeEvent doesn’t fire when a child is added.
My favourite part about this missing feature is that a hierarchy changed event exists within the BaseVisualElementPanel, and is fired from any hierarchy.insert call, but of course, it’s internal so we can’t use it
I’m currently getting around this by handling it in reverse for my use case, by using AttachToPanel events on the children. However, this only works in a very limited subset of scenarios, and doesn’t work quite how I’d like.
Ideally, it’d be great to be able to control the behaviour of adding children via UXML as well, to handle things such as slotting and more advanced child-parsing behaviour.
It is a bit hacky but it works great. It use IL Post Processor and a bit of Reflection to get an access to internal stuff, and there is almost no cost at runtime. It also works for IL2CPP.
Only tested on 2022.3.7f1. You will find an example in the repo.