Search Unity

Question OnGeometryChange Race condition. Help me please.

Discussion in 'UI Toolkit' started by LarsLundh, Jan 31, 2023.

  1. LarsLundh

    LarsLundh

    Joined:
    Sep 6, 2022
    Posts:
    22
    I try my best to be concise.

    This is the template I see in tutorials for constructing a custom VisualElement

    Code (CSharp):
    1. public new class UxmlFactory : UxmlFactory<ToggleButton, UxmlTraits> { }
    2.  
    3.        // There is no way to pass in any arguments.
    4.         public ToggleButton()
    5.         {
    6.             this.RegisterCallback<GeometryChangedEvent>(OnGeometryChange);
    7.         }
    8.  
    9.         private void OnGeometryChange(GeometryChangedEvent evt)
    10.         {
    11.             // Construct the VisualElement here.
    12.  
    13.             this.UnregisterCallback<GeometryChangedEvent>(OnGeometryChange);
    14.         }
    The actual constructor is unable to be used for custom VisualElements.
    *there is no way to pass in any data to the constructor of a VisualElement.
    Because you have to use the following In order to construct a custom visual element.
    Code (CSharp):
    1. var toggleButton = Addressables.LoadAssetAsync<VisualTreeAsset>("ToggleButton").WaitForCompletion().Instantiate();
    sure, then just do everything in OnGeometryChange...
    But... there is a timing issue with that. because OnGeometryChange happens only when (see documentation)..
    So in the situation where you provide a Public SetX(ThingToSet thingToSet);
    SetX can fail because of null reference unless OnGeometryChange has been called...
    aka the object needs to be constructed before you interact with it...

    How are you guys solving this?
    How can I ensure the custom VisualElement exists in its finished state?

    As far as I can tell, there is no way to safely provide public methods to interact with a custom VisualElement.
    What did I miss?

    p.s this is not an issue for custom VisualElements that have been added statically.
    this only happens for dynamically generated (created by code) custom VisualElements that have been added with.
    Example: this.Add(toggleButton);
     
    Last edited: Jan 31, 2023
  2. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    Geometry Changed Event can be used like that to setup things that need to know about the initial layout of the UI (things like positions and sizes). But that's not the main way of initializing custom elements.

    If you are creating single elements from code, you can certainly pass parameters through constructors. You don't have to create single elements from UXML assets, you can use constructors with parameters to make those.

    You do need to use parameterless constructors when creating elements in UXML assets, but you can still reference and initialize them through their properties and methods. For example, if your ToggleButton VissualTreeAsset contains a ToggleButton custom element, you can do this:
    Code (CSharp):
    1.  
    2. var template = Addressables.LoadAssetAsync<VisualTreeAsset>("ToggleButton").WaitForCompletion().Instantiate();
    3. var actualToggleButton = template.Q<ToggleButton>();
    4. actualToggleButton.someProperty = "Some value";
    5. actualToggleButton.SomeInitializationMethod("Some parameters");
    6.  
    If you are creating elements as part of UXML asset instantiations, you can pass parameters from the UXML as attributes using UXMLFactory and UXMLTraits. You override UxmlTraits.Init to pass the values from UXML attributes to your custom element through its properties.

    As an example, here's the code for a custom element of mine that does that: EditableLabel. It also overrides uxmlChildElementsDescription to specify that this custom element can't have children, but you can ignore that part if you don't need it.
     
    Last edited: Feb 1, 2023
    LarsLundh likes this.
  3. LarsLundh

    LarsLundh

    Joined:
    Sep 6, 2022
    Posts:
    22
    Thank you for your suggestions, they are awesome!
    It confirms that I didn't miss anything,
    I have been doing as you suggest above.

    so I have to solve race conditions in another way...

    It would be cool if you could add arguments to Instantiate() --> Instantiate(ArgumentForConstructor);
    Like this...
    Addressables.LoadAssetAsync<VisualTreeAsset>("ToggleButton").WaitForCompletion().Instantiate(ArgumentForConstructor);
     
  4. oscarAbraham

    oscarAbraham

    Joined:
    Jan 7, 2013
    Posts:
    431
    But, how would the system know which of the elements inside the VisualTreeAsset is supposed to receive the constructor arguments?
     
    LarsLundh likes this.
  5. LarsLundh

    LarsLundh

    Joined:
    Sep 6, 2022
    Posts:
    22
    I don't know what happens internally, I assumed that Instantiate calls ToggleButton toggleButton = new ToggleButton() on my behalf. but from your comment it sounds like its a lot more complex.
     
  6. LarsLundh

    LarsLundh

    Joined:
    Sep 6, 2022
    Posts:
    22
    I have since found the solution to not get race conditions on accessing public methods of a custom visual element that is being added to the uxml tree at runtime.

    [Example where race conditions are possible]
    Code (CSharp):
    1.             var customToggleButton = Addressables.LoadAssetAsync<VisualTreeAsset>("MyToggleButton").WaitForCompletion().Instantiate();
    2.             this.Add(customToggleButton);
    3.             var script = customToggleButton.Q<ToggleButton>("MyToggleButton");
    4.             script.SetParameters("Title", true); // Example Setter that could be sensitive
    [Example that prevents race conditions]
    Code (CSharp):
    1.             var customToggleButton = Addressables.LoadAssetAsync<VisualTreeAsset>("MyToggleButton").WaitForCompletion().Instantiate();
    2.        
    3.             EventCallback<AttachToPanelEvent> attachToPanelCallback = null;
    4.             attachToPanelCallback = (evt) => {
    5.                 customToggleButton.UnregisterCallback<AttachToPanelEvent>(attachToPanelCallback);
    6.                 // Do the public methods stuff
    7.                 var script = customToggleButton.Q<ToggleButton>("MyToggleButton");
    8.                 script.SetParameters("Title", true); // The custom element wont give null reference.
    9.             };
    10.  
    11.             customToggleButton.RegisterCallback<AttachToPanelEvent>(attachToPanelCallback);
    12.             this.Add(customToggleButton);
     
    Last edited: Apr 6, 2023