Search Unity

  1. Unity 2019.2 is now released.
    Dismiss Notice

UXML could be easily replaced with C#

Discussion in 'UIElements' started by Kamyker, Sep 8, 2019.

  1. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    85
    I've spent few hours learning about UIElements and trying to convert one editor tool. I came to the conclusion that UXML is not really needed. I don't like how to access single VisualElement I have to Query (or Q) and write same object name twice in UXML and C#.

    I'll show what I was trying to do and how I converted it to C#. I'll skip few parts that are same in both examples. (I've decided to inline specific styling if it's not repetitive as writing object name to .USS file for that reason seems like an overkill.)

    Old UXML:
    Code (CSharp):
    1. <?xml version="1.0" encoding="utf-8"?>
    2. <UXML
    3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4.     xmlns="UnityEngine.UIElements">
    5.     <Box name="header">
    6.         <Image name="playfablogo" style="background-image: url(Images/playfablogo.png);"/>
    7.         <Box style="flex-direction: row; align-items: center;">
    8.             <Button name="gMText" class="gameManagerBtn" text="GAME MANAGER"/>
    9.             <Button name="gMIcon" class="gameManagerBtn" />
    10.         </Box>
    11.     </Box>
    12.     <IMGUIContainer name="progressBar"/>
    13.     <IMGUIContainer name="mainIMGUI" style="flex-grow: 1;"/>
    14. </UXML >
    Old C#:
    Code (CSharp):
    1.  
    2. private Box header;
    3. private VisualElement gMText;
    4. private IMGUIContainer mainIMGUI;
    5.  
    6. void OnEnable()
    7. {
    8.     root = rootVisualElement;
    9.     root.Clear();
    10.     rootVisualElement.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(Path.Combine(Strings.PATH_UI, "styles.uss")));
    11.     var template = AssetDatabase.LoadAssetAtPath<VisualTreeAsset>(Path.Combine(Strings.PATH_UI, "mainView.uxml"));
    12.     template.CloneTree(root);
    13.     header = root.Q<Box>("header");
    14.     mainIMGUI = root.Q<IMGUIContainer>("mainIMGUI");
    15.     gMText = root.Q<VisualElement>("gMText");
    16. }
    17.  
    18. void Update ()
    19. {
    20.     //actions on header, gMText, mainIMGUI
    21. }
    22.  
    Here's converted to full C# with help of few extension methods:
    Code (CSharp):
    1.  
    2. //this class in seperate file like .uxml
    3. public class MainView
    4. {
    5.     public Box Header;
    6.     public VisualElement GMText;
    7.     public IMGUIContainer MainIMGUI;
    8.     public VisualElement[] Elements;
    9.  
    10.     public MainView()
    11.     {
    12.         Elements = new VisualElement[] {
    13.             Box.Set(name: "header").AssignTo(out Header).AddRange(
    14.                 Image.Set(name: "playfablogo", background_image: Strings.PATH_UI_IMG("playfablogo.png")),
    15.                 Box.Set(flexDirection: FlexDirection.Row, alignItems: Align.Center).AddRange(
    16.                     Button.Set(name: "gMText", _class: "gameManagerBtn", text: "GAME MANAGER").AssignTo(out GMText),
    17.                     Button.Set(name: "gMIcon", _class: "gameManagerBtn")
    18.                 )
    19.             ),
    20.             IMGUIContainer.Set(name: "progressBar"),
    21.             IMGUIContainer.Set(name: "mainIMGUI", flexGrow: 1).AssignTo(out MainIMGUI)
    22.         };
    23.     }
    24. }
    25.  
    26. //Editor class file
    27.  
    28. private MainView mainView;
    29.  
    30. void OnEnable()
    31. {
    32.     root = rootVisualElement;
    33.     root.Clear();
    34.     rootVisualElement.styleSheets.Add(AssetDatabase.LoadAssetAtPath<StyleSheet>(Path.Combine(Strings.PATH_UI, "styles.uss")));
    35.     mainView = new MainView();
    36.     root.AddRange(mainView.Elements);
    37. }
    38.  
    39. void Update ()
    40. {
    41.     //actions on mainView.Header, mainView.GMText, mainView.MainIMGUI
    42. }
    43.  
    Yes uxml looks slightly better for ex.
    flex-direction: row;
    vs
    flexDirection: FlexDirection.Row
    but that could be improved with better methods/enums. Other than that C# is far more powerful. I could go further and for ex. type:
    new Box().Set(name: nameof(Header)
    instead of
    new Box().Set(name: "header")
    that way I wouldn't have any problems with renaming this object in the future. As you can also see I'm using simple AssignTo to bind elements to fields.
    Code (CSharp):
    1.  
    2. public static VisualElement AssignTo(this VisualElement v, out VisualElement reference)
    3. {
    4.     reference = v;
    5.     return v;
    6. }
     
    Last edited: Sep 8, 2019
  2. Cence99

    Cence99

    Joined:
    Apr 14, 2013
    Posts:
    39
    I actually do the same thing and just create the UI in C#. Seems more intuitive to me, but I can see XML being a bit better if you have a big layout and/or a lot of elements that don't need to be queried like Labels.

    Edit: The most important aspect of this is probably splitting the concerns (hope this is the correct expression), so you do not end up with a lot of UI creation code in C# and can mostly just focus on the logic.
     
  3. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,340
    UXML is probably a better target for code-generation, if you want to create a framework for outputting UI. But other than that, yeah, I also prefer writing the editors with C#/uss. I imaging that people who have a background in html/css and the web in general would prefer it the other way round.
     
  4. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    85
    Here are the extension methods that I'm using. A lot of null checks but that's probably faster than parsing uxml anyway.
    https://github.com/kamyker/PlayFabU...itor/Scripts/Utils/VisualElementExtensions.cs

    Code (CSharp):
    1. public static class VisualElementExtensions
    2.     {
    3.         public static T Set<T>(this T v,
    4.             string name = null,
    5.             string _class = null,
    6.             FlexDirection? flexDirection = null,
    7.             Justify? justifyContent = null,
    8.             Align? alignItems = null,
    9.             string background_image = null,
    10.             float? flexGrow = null,
    11.             float? maxHeight = null,
    12.             float? maxWidth = null,
    13.             float? height = null,
    14.             float? width = null,
    15.             Color? color = null,
    16.             ScaleMode? unityBackgroundScaleMode = null,
    17.             DisplayStyle? display = null) where T : VisualElement
    18.         {
    19.             if (name != null)
    20.                 v.name = name;
    21.             if (_class != null)
    22.                 v.AddToClassList(_class);
    23.             if (flexDirection.HasValue)
    24.                 v.style.flexDirection = new StyleEnum<FlexDirection>(flexDirection.Value);
    25.             if (alignItems.HasValue)
    26.                 v.style.alignItems = new StyleEnum<Align>(alignItems.Value);
    27.             if (flexGrow.HasValue)
    28.                 v.style.flexGrow = new StyleFloat(flexGrow.Value);
    29.             if (background_image != null)
    30.                 v.style.backgroundImage = new Background(AssetDatabase.LoadAssetAtPath<Texture2D>(background_image));
    31.             if (maxHeight.HasValue)
    32.                 v.style.maxHeight = maxHeight.Value;
    33.             if (maxWidth.HasValue)
    34.                 v.style.maxWidth = maxWidth.Value;
    35.             if (height.HasValue)
    36.                 v.style.height = height.Value;
    37.             if (width.HasValue)
    38.                 v.style.width = width.Value;
    39.             if (justifyContent.HasValue)
    40.                 v.style.justifyContent = new StyleEnum<Justify>(justifyContent.Value);
    41.             if (color.HasValue)
    42.                 v.style.color = new StyleColor(color.Value);
    43.             if (unityBackgroundScaleMode.HasValue)
    44.                 v.style.unityBackgroundScaleMode = new StyleEnum<ScaleMode>(unityBackgroundScaleMode.Value);
    45.             if (display.HasValue)
    46.                 v.style.display = display.Value;
    47.             return v;
    48.         }
    49.  
    50.         public static T Set<T>(this T v, string text = null) where T : TextElement
    51.         {
    52.             if (text != null)
    53.                 v.text = text;
    54.             return v;
    55.         }
    56.  
    57.         public static T AssignTo<T>(this T v, out T reference) where T : VisualElement
    58.         {
    59.             reference = v;
    60.             return v;
    61.         }
    62.  
    63.         public static VisualElement AddRange(this VisualElement v, params VisualElement[] elements)
    64.         {
    65.             foreach (var el in elements)
    66.                 v.Add(el);
    67.             return v;
    68.         }
    69.     }
     
    Last edited: Sep 9, 2019
    Stardog likes this.
  5. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    85
    Maybe but probably not for majority of unity programmers that are used to C# and especially for beginners that barely know C# and suddenly also have to learn xml.
    Yes but the same can be achieved with C#. Not only that but in the end you end up with simpler solution.

    Let's say I'm making fairly complex page. I'd rather split it to small C# files like header.cs, body.cs, footer.cs and just add them all up in page.cs. It's up to me if for ex. body is very complex and I will split it to bodyModel.cs, bodyController.cs etc. With uxml you can do the same but it's... worse?

    You can make page.cs and page.uxml, you've "splitted the concerns" but you didn't split the page making it far more difficult to maintain and develop. Let's say you are working in team it's easier to say u work on header and u on body than uxml/cs split.

    Ok so let's say you will split everything making header.uxml, body.uxml, footer.uxml and handle logic in page.cs or separate scripts (body.cs). You end up using Query a lot making it more complex than plain C#.

    Another concern is that uxml is resolved at runtime. At start I thought that's good as my code won't recompile and Ill be able to see changes immediately. Yes but I've also realized that there's no code completion, no pre-compile null checks (if I remove something from uxml and I was using it in C# query everything breaks), no names check. It's like writing usual C# script but having to declare all the fields in separate .txt file.
     
  6. jonathanma_unity

    jonathanma_unity

    Unity Technologies

    Joined:
    Jan 7, 2019
    Posts:
    61
    Hi Kamkyker,

    One thing that you must be aware of is that when you do "v.style =" you're writing the style "inline".
    Inline style have a "hidden" cost and are not the most optimal way to use UIElements.
    I won't go into the gritty details but if you have many elements that share the same styles, you'll have some performance gain by using USS since they will share the same data...
     
  7. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    85
    Good to know but as I said:
    Just wanted to show that C# can do same thing as uxml. Whole styling part can be ignored in this thread I'm focusing more about logic/data and c#/uxml
     
  8. jonathanma_unity

    jonathanma_unity

    Unity Technologies

    Joined:
    Jan 7, 2019
    Posts:
    61
    Yes you can do everything from C#, if that is what you prefer you're totally free to do it.
    But I just wanted to let you know that if you have a complex UI with many elements you might have a performance hit if all styles are inline.
    This is something we are aware of and want to improve over the next releases.
     
  9. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,374
    I ended up just creating everything apart from my main layout in C# too. I would prefer to use UXML, but CloneTree doesn't do what I want. I just use an MVC pattern and build the UI in code instead of loading the UXML...

    I still want to use USS, so the only thing I added was a function that lets me add multiple classes to an element, instead of the regular AddToClassList which only allows one.
    Code (CSharp):
    1. _nodeVE = new VisualElement();
    2. _nodeVE.AddToClassList("node-container");
    3.  
    4. _titleVE = new VisualElement();
    5. _titleVE.AddToClassList("title-container");
    6. _nodeVE.Add(_titleVE);
    7.  
    8. _nameVE = new Label { text = Model.Name };
    9. _nameVE.AddToClassList("name");
    10. _titleVE.Add(_nameVE);
    11.  
    12. _resizerVE = new VisualElement();
    13. _resizerVE.AddToClassList("resizer");
    14. _nodeVE.Add(_resizerVE);
    Code (CSharp):
    1. <VisualElement class="node-container">
    2.  
    3.     <VisualElement class="title-container">
    4.         <Label text="Name" class="name" />
    5.     </VisualElement>
    6.  
    7.     <VisualElement class="resizer" />
    8.      
    9. </VisualElement>
     
    Last edited: Sep 10, 2019
  10. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    85
    With extensions you can write this:
    Code (CSharp):
    1. VisualElement _nodeVE;
    2. VisualElement _titleVE;
    3. VisualElement _resizerVE;
    4.  
    5. new VisualElement().Set(_class: "node-container").AssignTo(out _nodeVE).AddRange(
    6.     new VisualElement().Set(_class: "title-container").AssignTo(out _titleVE).AddRange(
    7.         new Label().Set(_class: "name").Set(text: "Name")
    8.     ),
    9.     new VisualElement().Set(_class: "resizer").AssignTo(out _resizerVE)
    10. );
     
  11. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    85
    But there's no way of writing it easily out of box. Hmm maybe it would be enough if VisualElement constructor could parse text that's usually inside uxml <>.
    For ex:
    Code (CSharp):
    1. <Label text="Name" class="name" />
    would become:
    Code (CSharp):
    1. new Label("text='Name' class='name'")
    I guess this may not be possible as normally it happens during uxml import (right?).

    Trying to figure out some kind of solution that wouldn't require writing extension methods (that may stop working in the future) and wouldn't be tough to implement from Unity's perspective.
     
  12. jonathanma_unity

    jonathanma_unity

    Unity Technologies

    Joined:
    Jan 7, 2019
    Posts:
    61
    Yes, UXML is parsed at import.
     
  13. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    4,340
    Any chance we could get to invoke the uxml importer ourselves? Something like:

    Code (csharp):
    1. VisualTreeAsset tree = UIElementUtil.Parse(someString);
    That would make building things on top of frameworks be a lot easier, and I don't imagine it would be too hard to implement?
     
  14. benoitd_unity

    benoitd_unity

    Unity Technologies

    Joined:
    Jan 2, 2018
    Posts:
    99
    Another advantage of using UXML over C# for creating UI is the opportunity to use the upcoming UI Builder, which authors UXML/USS assets only.
     
  15. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    85
    Ok, I'm starting to understand how uxml makes sense. It's similar to what .yaml files (prefabs/scenes) do for usual Unity UI.

    I still didn't like the Q() so I've made small tool to generate C# classes based on uxml (or rather VisualAssetTree). Ill post it soon.

    Ex from the first post:
    TestPage.uxml:
    Code (CSharp):
    1. <?xml version="1.0" encoding="utf-8"?>
    2. <UXML
    3.     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    4.     xmlns="UnityEngine.UIElements">
    5.     <Box name="header" style="--csName: gMIcon">
    6.         <Image name="playfablogo" style="background-image: url(Images/playfablogo.png);"/>
    7.         <Box style="flex-direction: row; align-items: center;">
    8.             <Button name="gMText" class="gameManagerBtn" text="GAME MANAGER" style="--csName: gMIcon" />
    9.             <Button name="gMIcon" class="gameManagerBtn" />
    10.         </Box>
    11.     </Box>
    12.     <IMGUIContainer name="progressBar"/>
    13.     <IMGUIContainer name="mainIMGUI" style="flex-grow: 1; --csName: mainIMGUI" />
    14. </UXML >
    After creating uxml I right click it and select Create C# class. TestPageConverted.cs is generated.

    C# (again from 1st post):
    Code (CSharp):
    1. private TestPageConverted page = new TestPageConverted(); //this does CloneTree and assigns fields
    2. void OnEnable()
    3. {
    4.     root = rootVisualElement;
    5.     root.Add(page);
    6. }
    7. void Update ()
    8. {
    9.     //actions on page.header, page.gMText, page.mainIMGUI
    10. }
    This looks pretty good, I don't have to Q(uery) or write code load the uxml/VisualAssetTree. "--csName" custom style is used to generate C# fields. Couldn't use "name" as it's used for ex. in unity's toggle.
     
    joelybahh likes this.
  16. Kamyker

    Kamyker

    Joined:
    May 14, 2013
    Posts:
    85
    benoitd_unity likes this.
  17. Creta_Park

    Creta_Park

    Joined:
    Mar 11, 2016
    Posts:
    48
    I feel UIElements should be improved for same use in code by naming uxml elements, like .NET's WPF or UWP(xaml).
    When write a layout in WPF and UWP, the compiler writes new C# code(extension of *.g.cs, *.g.i.cs) for the layout. (Like you made)
    So then even IntelliSense detect it name as property with element type.

    xaml
    Code (CSharp):
    1. <Page
    2.     x:Class="MyApp.MyPage"
    3.     ...
    4.     >
    5.     <StackPanel>
    6.  
    7.         <Button x:Name="SomeButton"
    8.                  Content="Click me"
    9.                  Click="OnSomeButtonClick"/>
    10.  
    11.     </StackPanel>
    12. </Page>
    c#
    Code (CSharp):
    1. namespace MyApp {
    2.  
    3.     public sealed partial class MyPage : Page {
    4.      
    5.         public MyPage() {
    6.          
    7.             this.InitializeComponent();
    8.          
    9.         }
    10.      
    11.         int count = 0;
    12.         void OnSomeButtonClick(object sender, RoutedEventArgs e) {
    13.          
    14.             SomeButton.Content = $"You clicked {++count} time(s)";
    15.         }
    16.      
    17.     }
    18.  
    19. }
    In other respects, however, this follows a similar web development approach, so UIElements appears to be this way.
    I think because the official documentation says that UQuery is similar to jQuery (commonly used JS library) as an example.
     
    Last edited: Sep 18, 2019