Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

How are you supposed to connect CustomPropertyDrawers to an EditorWindow?

Discussion in 'UI Toolkit' started by wheee09, Jun 20, 2020.

  1. wheee09

    wheee09

    Joined:
    May 21, 2018
    Posts:
    68
    EDIT: I think I figured out it out - see my reply for a code example, but I do have some follow up questions there.

    Hi all,

    I've been struggling with how to connect a CustomPropertyDrawer to an EditorWindow with UIElements.

    In the documentation https://docs.unity3d.com/ScriptReference/PropertyDrawer.html (2019.4), it shows an example of how to create a CustomPropertyDrawer for a custom Serialized class Ingredient which is great.

    But no where in these code snippets or in any of the associated documentation does it show you how to actually connect the EditorWindow to the "Recipe" Monobehaviour script and that's the part that is stumping me.

    So my questions are:

    1. What are you supposed to put in the EditorWindow.OnEnable so that it uses the "Recipe" script and renders the CustomPropertyDrawer for Ingredient? Is it something like:
    Code (CSharp):
    1.  
    2. var inspector = new InspectorElement(recipe);
    3. root.Add(inspector);
    4.  
    2. Are there other ways?

    3. What if I want to use a ScriptableObject instead of a Monobehaviour script for Recipe? I tried to do that and using the script above and instantiating a ScriptableObject (wrapper) in EditorWindow.OnEnable.

    Even though the documentation states:
    What I found was that the CustomProperyDrawer.CreatePropertyGUI was NOT called, and would show me an error about having nothing to render. And when I would override CustomPropertyDrawer.OnGUI, it would use that instead.

    Note that the EditorWindow was using UIElements.

    4. Is there a way to simply make the Ingredient Serializable class a SerializedObject and have the EditorWindow directly render and bind it without the need for a Recipe? This is probably my main question - the reason is that in my particular use case, all of my game data comes from a database and is stored in memory as simple classes.

    Thanks!
     
    Last edited: Jun 20, 2020
  2. wheee09

    wheee09

    Joined:
    May 21, 2018
    Posts:
    68
    Ok I think I figured it out for the most part, but I do have some followup questions and would appreciate some feedback - perhaps there's a better way of doing this.


    1. Simple Serializable class to store data (that would be provided by a database in my case):
    Code (CSharp):
    1.  
    2. [Serializable]
    3. public class TestDataObject {
    4.     public string Name;
    5.     public int Value;
    6.  
    7.     public TestDataObject(string name, int value) {
    8.         Name = name;
    9.         Value = value;
    10.     }
    11. }
    12.  

    2. A CustomPropertyDrawer for the simple Serializable class (see #1 above):
    Code (CSharp):
    1.  
    2. [CustomPropertyDrawer(typeof(TestDataObject))]
    3. public class TestDataObjectDrawer : PropertyDrawer {
    4.     public override VisualElement CreatePropertyGUI(SerializedProperty property) {
    5.         var container = new VisualElement();
    6.         container.Add(new Label("Inside TestDataObjectDrawer"));
    7.  
    8.         var nameField = new PropertyField(property.FindPropertyRelative("Name"));
    9.         container.Add(nameField);
    10.  
    11.         var valueField = new PropertyField(property.FindPropertyRelative("Value"));
    12.         container.Add(valueField);
    13.  
    14.         return container;
    15.    }
    16. }
    17.  

    3. A View Model (think MVVM) that is specific to the requirements of the EditorWindow (see #5):
    Code (CSharp):
    1.  
    2. public class TestViewModel : ScriptableObject {
    3.     public string ViewString;
    4.     public TestDataObject TestDataObject;
    5. }
    6.  

    4. A Custom Editor for the View Model - showing the different ways to render the property fields for the View Model (including the actual data model object nested inside):
    Code (CSharp):
    1.  
    2. [CustomEditor(typeof(TestViewModel))]
    3. public class TestViewModelEditor : UnityEditor.Editor {
    4.     public override VisualElement CreateInspectorGUI() {
    5.         var container = new VisualElement();
    6.  
    7.         container.Add(new Label("TestViewModel -> View String"));
    8.         var valueField = new PropertyField(serializedObject.FindProperty("ViewString"));
    9.         container.Add(valueField);
    10.  
    11.         var dataObjectProp = serializedObject.FindProperty("TestDataObject");
    12.         container.Add(new Label("TestViewModel -> TestDataObject -> Name"));
    13.         var dataObjectNameField = new PropertyField(dataObjectProp.FindPropertyRelative("Name"));
    14.         container.Add(dataObjectNameField);
    15.  
    16.         container.Add(new Label("TestViewModel -> TestDataObject -> Value"));
    17.         var dataObjectValueField = new PropertyField(dataObjectProp.FindPropertyRelative("Value"));
    18.         container.Add(dataObjectValueField);
    19.  
    20.         container.Add(new Label("TestViewModel -> TestDataObject via CustomPropertyDrawer"));
    21.         var dataObjectField = new PropertyField(dataObjectProp);
    22.         container.Add(dataObjectField);
    23.  
    24.         return container;
    25.     }
    26. }
    27.  

    5. The EditorWindow to tie it altogether:
    Code (CSharp):
    1.  
    2. public class TestEditorWindow : EditorWindow {
    3.     [MenuItem("Test/TestEditorWindow _%#T")]
    4.     public static void ShowWindow() {
    5.         var window = GetWindow<TestEditorWindow>();
    6.  
    7.         window.titleContent = new GUIContent("TestEditorWindow");
    8.         window.minSize = new Vector2(250, 50);
    9.     }
    10.  
    11.     private void OnEnable() {
    12.         var root = rootVisualElement;
    13.  
    14.         var viewModel = CreateInstance<TestViewModel>();
    15.         viewModel.ViewString = "View String";
    16.         viewModel.TestDataObject = new TestDataObject("string", 1);
    17.  
    18.         var inspector = new InspectorElement(viewModel);
    19.         root.Add(inspector);
    20.     }
    21. }
    22.  
    This is how it looks like:
    Screen Shot 2020-06-20 at 1.12.42 PM.png


    Followup Question:
    Is it possible to have multiple CustomPropertyDrawers (see #2) for the same model class (#1) - which is used by different EditorWindows?

    An example use case is:
    1. an Admin CustomPropertyDrawer for the model where all fields are visible and editable
    2. a Non-Admin CustomPropertyDrawer where certain fields are not visible and most fields are readonly
    Caveat: It is ok to have multiple View Models (#3) and Custom Editors for the View Model (#4), but I should only have 1 version of the data model (#1).

    Reasoning: I don't want a huge monolithic CustomPropertyDrawer class that is handling all of the different scenarios.


    Hopefully the above code example will benefit other people trying to figure this stuff out and save them many hours. Unfortunately the documentation, videos and guides out there don't cover this use case.
     
    Last edited: Jun 20, 2020
    MostHated likes this.
  3. MostHated

    MostHated

    Joined:
    Nov 29, 2015
    Posts:
    1,235
    +10 Big thanks for sharing this. I was having some troubles with a few small parts of what I was working on and this example put them all together for me.

    Next is to figure out how to used the PropertyDrawer to iterate over the items of a collection that is placed within TestDataObject so each value can be drawn a certain way.

    I had gotten a ProperyDrawer to iterate over the actual containing serialzedObject using an example from another thread, but but not the values within a collection. Perhaps doing it this way and not starting out with an initial iteration over the properties might help. This definitely is getting me closer.


    ----- Edit, nice, I actually just figured it out a few minutes after I posted this.

    The main issue was I kept creating a new PropertyField and adding the List to it, as I was with everything else, I had to find the List as a SerializedProperty, then I was able to do it, as that gave me access to an arraySize property:

    Code (CSharp):
    1. var scroller = new ScrollView();
    2. SerializedProperty listProperty = property.FindPropertyRelative("intList");
    3. listProperty.serializedObject.ApplyModifiedProperties();
    4.  
    5. for (int i = 0; i < listProperty.arraySize; i++)
    6. {
    7.     var layerEntry = new VisualElement {focusable = true, name = $"Entry: {i}", style = {flexDirection = FlexDirection.Row, justifyContent = Justify.FlexStart}};
    8.    
    9.     var listElem = new PropertyField(listProperty.GetArrayElementAtIndex(i));
    10.     var labelData = new Label($"Entry_{i.ToString()}");
    11.  
    12.     layerEntry.Add(labelData);
    13.     layerEntry.Add(listElem);
    14.     scroller.Add(layerEntry);
    15. }
    16.  
    17. container.Add(scroller);
     
    Last edited: Sep 5, 2020