Search Unity

Question Custom C# VisualElement children question.

Discussion in 'UI Toolkit' started by Ashfid, Sep 24, 2021.

  1. Ashfid

    Ashfid

    Joined:
    Jun 8, 2019
    Posts:
    20
    Hey all. So, I have a doubt on custom C# Visual Element that adds child when created. Is it not possible for the child element to accept another child element when this custom visual element is dropped in UI Builder? It is grayed out, but can you override that somehow?

    I am trying to create tab container that takes tab buttons that should be under TabButtonGroup child element and other visual elements under Content child element that is placed according to order of tab buttons.
     
  2. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    I am going to reference a slightly undocumented feature called the contentContainer.

    Basically, overriding the contentContainer property from your custom element class and return the element to act as the parent of anything added with the "Add()" method, including what comes from UXML and thus the UI Builder.

    This is how something like the ScrollView is actually implemented.
     
  3. Ashfid

    Ashfid

    Joined:
    Jun 8, 2019
    Posts:
    20
    That worked beautifully! However, my tab panel element works this way:
    Tab Container
    - TabButtonGroup (this is the contentContainer now to add buttons)
    - TabContents (I can’t add to this because contentContainer takes one element of course)

    Do you suggest atomizing those 2 children and then add it on UI Builder (TabButtonGroup and TabContents are separate components then) or any other way?
     
  4. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    I am not sure if this is what you are asking but you should use use this.hierarchy.Add() to bypass the contentContainer mechanism.
     
  5. bloodthirst69

    bloodthirst69

    Joined:
    Oct 1, 2017
    Posts:
    28
    I walked into the same issue (also trying to make a tabview custom control lol) and i think i got it working in a clean way , so ill share it here since it may help other people in the futur.

    so here's the situation , i wanted to create a custom tab component that would look like this uxml-wise if i wanted to use it

    Here's an example of what it would look like when used :
    Code (CSharp):
    1. <!-- This would be the TabView element , so pretty much is should "host" the content of the its child tabs -->
    2.                 <custom:TabUI name="Tabs">
    3.            
    4.                     <!-- this is how i would prefer to define a single tab : a single element with a label attribute that specifies the tab title , with the content of the tab being inside it -->
    5.                     <custom:TabElement name="ManageTab" label="Manage">
    6.                      
    7.                         <views:CreateView name="CreateView" class="shrink-0" />
    8.  
    9.                         <views:BrowseView name="BrowseView" />
    10.  
    11.                     </custom:TabElement>
    12.  
    13.                     <!-- This is another tab-->
    14.                     <custom:TabElement name="EditTab" label="Edit" />
    15.                
    16.                     <!-- This is another tab aswell-->
    17.                     <custom:TabElement name="CheckTab" label="Check" />
    The problem here is that the "child-tab" is basically two elements when implemented : the content of the tab and the tab-buttom used to select the tab, but since they both belong to the same tab , it makes sense to declare them with a single element instead of separating them everytime we need to add a tab.

    The way i went about this is to separate the whole thing like this :
    - TabUI : the class that represents the TabView , or the parent of the tabs
    - TabElement : a custom control that we can add to the xml doc , just like the example above.
    - TabButton : the tab we click on to select the button , we don't add this to the xml doc , this gets added automatically
    - TabContent : tthe content of the tab , we don't add this to the xml doc , this gets added automatically

    whenver we re-evaluate the TabUI content , we check if we have a TabElement in there , if we do then we take it , convert it to a TabButton , TabContent and do the whole setup , then we remove the TabElement since it's now "treated" correcty.

    And now to the TabUI code
    Code (CSharp):
    1. using System.Collections.Generic;
    2. using UnityEditor;
    3. using UnityEngine.UIElements;
    4.  
    5. namespace Bloodthirst.Editor.CustomComponent
    6. {
    7.     public class TabUI : VisualElement
    8.     {
    9.         public new class UxmlFactory : UxmlFactory<TabUI, UxmlTraits> { }
    10.         public new class UxmlTraits : VisualElement.UxmlTraits
    11.         {
    12.             UxmlChildElementDescription allowedChildren = new UxmlChildElementDescription(typeof(TabElement));
    13.             public override IEnumerable<UxmlChildElementDescription> uxmlChildElementsDescription
    14.             {
    15.                 get
    16.                 {
    17.                     yield return allowedChildren;
    18.                 }
    19.             }
    20.  
    21.             public override void Init(VisualElement ve, IUxmlAttributes bag, CreationContext cc)
    22.             {
    23.                 base.Init(ve, bag, cc);
    24.                 TabUI tab = ve as TabUI;
    25.                 tab.CheckForTabs();
    26.             }
    27.         }
    28.  
    29.         private const string UXML_PATH = "Packages/com.bloodthirst.bloodthirst-core/Editor/CustomComponent/TabUI/TabUI.uxml";
    30.         private const string USS_PATH = "Packages/com.bloodthirst.bloodthirst-core/Editor/CustomComponent/TabUI/TabUI.uss";
    31.  
    32.         public int CurrentTabIndex { get; private set; }
    33.  
    34.         public override VisualElement contentContainer => TabContainer;
    35.  
    36.         private List<TabButton> _AllButtons { get; set; } = new List<TabButton>();
    37.         private List<TabContent> _AllContents { get; set; } = new List<TabContent>();
    38.         private VisualElement TabContainer => this.Q<VisualElement>(nameof(TabContainer));
    39.         private VisualElement ContentContainer => this.Q<VisualElement>(nameof(ContentContainer));
    40.  
    41.         public TabUI()
    42.         {
    43.             SetupElement();
    44.             InitializeUI();
    45.         }
    46.  
    47.         private void SetupElement()
    48.         {
    49.             // import USS
    50.             StyleSheet styleSheet = AssetDatabase.LoadAssetAtPath<StyleSheet>(USS_PATH);
    51.  
    52.             // Import UXML
    53.             VisualTreeAsset visualTree = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(UXML_PATH);
    54.             visualTree.CloneTree(this);
    55.  
    56.             styleSheets.Add(styleSheet);
    57.             styleSheets.Add(EditorConsts.GlobalStyleSheet);
    58.         }
    59.  
    60.         public void Select(int tabIndex)
    61.         {
    62.             CurrentTabIndex = tabIndex;
    63.  
    64.             ContentContainer.Clear();
    65.  
    66.             TabContent newContent = _AllContents[tabIndex];
    67.  
    68.             ContentContainer.Add(newContent);
    69.  
    70.             for (int i = 0; i < _AllButtons.Count; i++)
    71.             {
    72.                 TabButton curr = _AllButtons[i];
    73.  
    74.                 curr.UnSelected();
    75.             }
    76.  
    77.             _AllButtons[tabIndex].Select();
    78.         }
    79.  
    80.         private void InitializeUI()
    81.         {
    82.             CheckForTabs();
    83.             TabContainer.RegisterCallback<GeometryChangedEvent>(HandleContentChanged);
    84.             TabContainer.RegisterCallback<AttachToPanelEvent>(HandleAttachedToPanel);
    85.         }
    86.  
    87.         private void HandleAttachedToPanel(AttachToPanelEvent evt)
    88.         {
    89.             CheckForTabs();
    90.         }
    91.  
    92.         private void HandleContentChanged(GeometryChangedEvent evt)
    93.         {
    94.             CheckForTabs();
    95.         }
    96.  
    97.         public void CheckForTabs()
    98.         {
    99.             List<TabButton> newTabs = new List<TabButton>();
    100.             List<TabContent> newContents = new List<TabContent>();
    101.             int index = 0;
    102.  
    103.             int i = 0;
    104.  
    105.             // the goal of this loop is to check if we have any "TabElement" children that haven't been converted to a "TabButton"-"TabContent" pair
    106.             // if we find any , we convert them into a TabButton and TabContent and we remove the TabElement since it's now "Setup" correctly
    107.             while (i < contentContainer.childCount)
    108.             {
    109.                 VisualElement curr = contentContainer[i];
    110.  
    111.                 // if we found an "uncoverted" tab
    112.                 // we do the setup
    113.                 if (curr is TabElement t)
    114.                 {
    115.                     // create button
    116.                     TabButton newTab = new TabButton(this, t.Title, index);
    117.  
    118.                     // copy content
    119.                     TabContent tabContent = new TabContent(this, index);
    120.  
    121.                     // we take the content form the TabElement and we move it to the TabContent
    122.                     while(t.contentContainer.childCount != 0)
    123.                     {
    124.                         VisualElement tabC = t.contentContainer[0];
    125.  
    126.                         // NOTE : apprentyl the "Add" method already removes the element from the previous parent
    127.                         // so no need to do something like : oldParent.Remove(child) ; newParent.Add(child)
    128.                         // we just add directly
    129.                         tabContent.Add(tabC);      
    130.                     }
    131.  
    132.                     newTabs.Add(newTab);
    133.                     newContents.Add(tabContent);
    134.  
    135.                     index++;
    136.  
    137.                     contentContainer.RemoveAt(i);
    138.                 }
    139.  
    140.                 else
    141.                 {
    142.                     i++;
    143.                 }
    144.  
    145.  
    146.             }
    147.  
    148.  
    149.             // we add all the buttons to the "button" header
    150.             // and to a buttons collection
    151.             foreach (TabButton b in newTabs)
    152.             {
    153.                 _AllButtons.Add(b);
    154.                 TabContainer.Add(b);
    155.             }
    156.  
    157.             // add to a contents collection
    158.             // then the THIS tabUI will decide which tab content to show
    159.             foreach (TabContent c in newContents)
    160.             {
    161.                 _AllContents.Add(c);
    162.             }
    163.         }
    164.     }
    165. }
    166.  
    i added some comments to explain the logic , but i think the whole idea is clear : create a "Placeholder" TabElement that says "hey ! i am a tab that isn't yet setup correcly" , we scan for those placeholders to see if we need to correctly setup any tabs , and that's it really
    Here are screenshot of the uxml layout VS what you get in your layout when spawned : tab.PNG uxml.PNG
     
  6. BillyWM

    BillyWM

    Joined:
    Dec 29, 2018
    Posts:
    14
    This is the same as the appendChild DOM method. It's nice that in many places they didn't break parity with the web equivalent.

    (pretty much expected anyway since you're working with an object - a reference type. You can imagine multiple parents having a reference to the object as their child, but thinking about it in reverse - the child having more than a single 'parent' reference back - is unexpected)
     
    Last edited: Jun 11, 2022
  7. bloodthirst69

    bloodthirst69

    Joined:
    Oct 1, 2017
    Posts:
    28
    Yeah i agree it makes sense in the layout manipulation context , i just felt like mentioning that since whenever i work with containers that provide an Add and Remove method i tend to assume that i have to do the cleanup myself when moving from container to another , so i thought it would be good to mention that
     
  8. uMathieu

    uMathieu

    Unity Technologies

    Joined:
    Jun 6, 2017
    Posts:
    398
    The Remove(VisualElement) method is an artifact of the old times when VisualElement implemented IEnumerable<VisualElement>. While it seemed like a good idea at first, it lead to more usability problems than we anticipated.

    The recommended way nowadays is to use child.RemoveFromHierarchy()