Search Unity

Bug Can't create bindings for an enum - "not compatible"

Discussion in 'UI Toolkit' started by Baste, Aug 14, 2019.

  1. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    Code (csharp):
    1. public enum MyEnum
    2. {
    3.     A,
    4.     B,
    5.     C
    6. }
    7.  
    8. public class Test : MonoBehaviour
    9. {
    10.     public MyEnum myEnum;
    11. }
    12.  
    13. [CustomEditor(typeof(Test))]
    14. public class TestEditor : Editor
    15. {
    16.     private VisualElement rootElement;
    17.  
    18.     private void OnEnable()
    19.     {
    20.         rootElement = new VisualElement();
    21.     }
    22.  
    23.     public override VisualElement CreateInspectorGUI()
    24.     {
    25.         rootElement.Clear();
    26.  
    27.         var enumField = new EnumField("My Enum");
    28.         enumField.bindingPath = "myEnum";
    29.  
    30.         rootElement.Add(enumField);
    31.         rootElement.Bind(serializedObject);
    32.  
    33.         return rootElement;
    34.     }
    35. }
    Gives:
    Code (csharp):
    1. Field type UnityEditor.UIElements.EnumField is not compatible with Enum property "myEnum"
    2. UnityEditor.UIElements.BindingExtensions:Bind(VisualElement, SerializedObject)
    3. TestEditor:CreateInspectorGUI() (at Assets/Editor/TestEditor.cs:23)
    4. UnityEditor.InspectorWindow:RedrawFromNative()
    I tried making a PopupField<MyEnum>, that gave the same result.

    This should work, right?
     
  2. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    Hello,

    This is an oversight that is supposed to be fixed in 2019.3, but as it turned out I have encountered a different issue while trying out your script in both 2019.3 and 2019.2. We've added that to our list of issues.

    Which version of Unity were you trying this with ?
     
  3. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    2019.2.0f1
     
  4. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    I worked around it by using a PropertyField for the property, but that runs into a different issue; I'm not able to get
    RegisterCallback to do anything for a PropertyField that targets an enum:

    Code (csharp):
    1.  
    2. using System;
    3. using UnityEditor;
    4. using UnityEditor.UIElements;
    5. using UnityEngine;
    6. using UnityEngine.UIElements;
    7.  
    8. [CustomEditor(typeof(Test))]
    9. public class TestEditor : Editor
    10. {
    11.     private VisualElement rootElement;
    12.  
    13.     private void OnEnable()
    14.     {
    15.         rootElement = new VisualElement();
    16.     }
    17.  
    18.     public override VisualElement CreateInspectorGUI()
    19.     {
    20.         rootElement.Clear();
    21.  
    22.         var enumProp = new PropertyField(serializedObject.FindProperty("myEnum"));
    23.         var intProp  = new PropertyField(serializedObject.FindProperty("val"));
    24.  
    25.         // none of these gets called
    26.         enumProp.RegisterCallback<ChangeEvent<MyEnum>>(Callback_MyEnum);
    27.         enumProp.RegisterCallback<ChangeEvent<Enum  >>(Callback_Enum);
    28.         enumProp.RegisterCallback<ChangeEvent<int   >>(Callback_Int);
    29.         enumProp.RegisterCallback<ChangeEvent<object>>(Callback_Object);
    30.  
    31.         // this gets called
    32.         intProp.RegisterCallback<ChangeEvent<int>>(Callback_Int);
    33.  
    34.         rootElement.Add(enumProp);
    35.         rootElement.Add(intProp);
    36.         rootElement.Bind(serializedObject);
    37.         return rootElement;
    38.     }
    39.  
    40.     private void Callback_MyEnum(ChangeEvent<MyEnum> evt) => Debug.Log("My enum");
    41.     private void Callback_Int   (ChangeEvent<int>    evt) => Debug.Log("int");
    42.     private void Callback_Enum  (ChangeEvent<Enum>   evt) => Debug.Log("enum");
    43.     private void Callback_Object(ChangeEvent<object> evt) => Debug.Log("object");
    44. }
     
    Xarbrough likes this.
  5. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    Thanks!

    I'd really prefer not to leak details about the editor into the runtime code, but I can hide those details behind #if's, so I'll look into if it's viable..
     
  6. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    Hello!

    So here's the whole picture for this issue. The message
    Field type UnityEditor.UIElements.EnumField is not compatible with Enum property "myEnum"
    is unfortunately accurate for 2019.2. The work for making EnumField truly on par with IMGUI was done only for 2019.3 and backporting seems unlikely as it required quite a lot of refactoring. This was required to make binding work correctly.

    In the mean time it's possible to bind a PopupField<string> to an enum property. You can register for
    ChangeEvent<string>
    to detect changes. If you need to make sense of the value, the
    index
    property of the
    PopupField
    object can be used in conjunction with
    SerializedProperty.enumValueIndex
    .

    Right in both 2019.2 and 2019.3 PopupField suffers the issue I've described earlier about a NRE due to the specific order of operations in the script you've shared (EnumField has this issue in 2019.3 as well).

    We'll fix this of course but the workaround is to remove the call to
    Bind()
    inside
    CreateInspectorGUI()
    as this already done by the system when calling this method.

    So here is something that should work in 2019.2 :

    Code (CSharp):
    1. using UnityEngine;
    2. using UnityEditor;
    3. using UnityEditor.UIElements;
    4. using UnityEngine.UIElements;
    5.  
    6. public enum MyEnum
    7. {
    8.     A,
    9.     B,
    10.     C
    11. }
    12. public class Test : MonoBehaviour
    13. {
    14.     public MyEnum myEnum;
    15. }
    16. [CustomEditor(typeof(Test))]
    17. public class TestEditor : Editor
    18. {
    19.     private VisualElement rootElement;
    20.     private void OnEnable()
    21.     {
    22.         rootElement = new VisualElement();
    23.     }
    24.     public override VisualElement CreateInspectorGUI()
    25.     {
    26.         rootElement.Clear();
    27.         var enumField = new PopupField<string>("My Enum");
    28.         enumField.bindingPath = "myEnum";
    29.         rootElement.Add(enumField);
    30.         // This hits a bug in Unity but an explicit Bind() is not necessary anyways      
    31.         // rootElement.Bind(serializedObject);
    32.  
    33.         rootElement.RegisterCallback<ChangeEvent<string>>(OnChangeEvt);
    34.         return rootElement;
    35.     }
    36.  
    37.     private void OnChangeEvt(ChangeEvent<string> evt)
    38.     {
    39.         Debug.Log(evt.newValue);
    40.     }
    41. }
    Hope this helps
     
    Xarbrough likes this.
  7. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    Thanks!

    Backports are not an issue, this project won't have any issues updating to a newer Unity version. You could consider adding a better error message to 2019.2 - "enum isn't supported yet" or something along those lines.

    I think I called bind explicitly because that was done in some example? Not sure. If we don't need to do that - when does the binding happen? There's nothing in that code that relates the SerializedObject to the VisualElement tree.
     
  8. antoine-unity

    antoine-unity

    Unity Technologies

    Joined:
    Sep 10, 2015
    Posts:
    780
    The binding happens automatically after the system calls
    CreateInspectorGUI()
    .
    I'll look out for a mistake in our examples.
     
    Baste likes this.
  9. Baste

    Baste

    Joined:
    Jan 24, 2013
    Posts:
    6,338
    It's probably my bad - I got it from the UI Elements Examples repo, and there the SerializedObjects are edited in an EditorWindow instead of an Editor in the Inspector Comparer Window.

    It's not super-intuitive that the binding happens automatically, but I'm super in favour of it. I've pretty much always wondered why the hell the default IMGUI Editor didn't automatically do serializedObject.ApplyModifiedProperties at the end of OnInspectorGUI.
     
  10. ayellowpaper

    ayellowpaper

    Joined:
    Dec 8, 2013
    Posts:
    52
    Glad I found this. Incredibly frustrating issue but at least there is a workaround.
     
  11. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    I'm getting this in 2020LTS. It appears to happen when you use UIToolkit team's provided code for emulating RadioButtonGroup to display/edit an Enum (That was missing from UIToolkit originally) and enable the missing Bind() calls (that are necessary to prevent UIToolkit corrupting data - only appears to be a problem for prefabs, but prefabs actively lose data if you don't correct this).

    What I expected to happen: I can take an Enum field, make my own RadionButtonGroup (that wraps multiple Toggle instances), and which implements IBindable etc, and have the Toggles map 1:1 with the possible values of the Enum -- user clicks a Toggle, it sets the value of the Enum field to that -- and it should all Just Work.

    What happened: all the above, except with this message spamming the console. It appears to be incorrect/meaningless: there's nothing wrong here. Worse: without adding this code, Unity corrupts prefabs; with this code, Unity stops corrupting prefabs.
     
  12. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    @antoine-unity What is the correct way to "remove the call to Bind()" (your instruction above) in cases where Unity requires a call to Bind()?

    UPDATE: removing the call to Bind() doesn't fix this problem, Unity ends up still triggering it when resolving bindings internally. This message is infuriating: it does nothing, and just creates spam.

    I can't find the source code with this incorrect message, and I can't ship this and have every user spammed because of Unity's incorrect (unavoidable!) spam. I can't remove Bind becaus without it Unity corrupts prefabs.

    So. How do we bypass this?
     
    Last edited: Sep 11, 2022
  13. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    I seem to have found a solution: it requires a pile of horrendous hacks to workaround UIToolkit's weird (ab)use of Enum.

    Step 1: You have to do 'the wrong thing' and make your VisualElement implement "INotifyValueChanged<string>" even though strings are 100% not involved anywhere (this is the workaround for UIToolkit's bugs).

    Step 2: You have to do that WHILE ALSO implementing the typesafe, correct, 'not allowed by UIToolkit', "INotifyValueChanged<T>"

    Step 3: ... but Microsoft won't allow you to compile a class that implements both of those. So now you have to workaround bugs in the design of C# in order to get a class that implements both. There are multiple ways you can do this, I went with: "Create a fake superclass that implements one interface, and delegates implementation to subclass, then write the subclass to implement the OTHER interface, and also to provide the delegate body".

    Final setup, here's my base class (that you can re-use yourself; make your 'real' VisualElement class extend this).

    Code (CSharp):
    1. public abstract class UnityUghs : BindableElement, INotifyValueChanged<string>
    2.     {
    3.         protected abstract void UnityUgh1( string s );
    4.      
    5.         protected abstract void UnityUgh2( string s );
    6.         protected abstract string UnityUgh2();
    7.      
    8.         void INotifyValueChanged<string>.SetValueWithoutNotify(string newValue)
    9.         {
    10.             UnityUgh1( newValue );
    11.         }
    12.  
    13.         string INotifyValueChanged<string>.value
    14.         {
    15.             get => UnityUgh2();
    16.             set => UnityUgh2(value);
    17.         }
    18.     }
    And your class should look something like:
    Code (CSharp):
    1.  
    2. public class MyCustomElement<T>: UnityUghs, INotifyValueChanged<T>
    3.  
    ...where T is the Enum you're trying to display.

    Net effects:

    Unity's spam message disappears, because it was listening out for a STRING (UIToolkit's requirement)
    You can safely use Bind(), making Unity stop corrupting prefabs
    Your code (unlike UIToolkit) is still typesafe.
     
    Last edited: Sep 11, 2022