CustomPropertyDrawer for a Class with a Generic Type

I have a class which acts like a dictionary but uses two lists instead:

using UnityEngine;
using System.Collections;
using System.Collections.Generic;

public class ListDictionary<T>
{
	private List<string> _keys;
	private List<T> _values;
	
	public ListDictionary()
	{
		_keys = new List<string>();
	    _values = new List<T>();
	}
        
        // ... etc
}

I want to write a CustomPropertyDrawer for it but I’m getting the following error:

Here’s my property drawer:

using UnityEditor;
using UnityEngine;
using System.Collections;
using System.Collections.Generic;

[CustomPropertyDrawer(ListDictionary<T>)]
public class ListDictionaryDrawer : PropertyDrawer
{
	public override void OnGUI (Rect position, SerializedProperty property, GUIContent label)
	{
        EditorGUI.BeginProperty (position, label, property);
        
        
        EditorGUI.EndProperty ();
	}
}

I tried variations of the class name (ListDictionary, ListDictionary<>, and etc) but I haven’t gotten it to work. How do I go about fixing this? On a related note, are there any tutorials on creating a custom property drawer for C#? I’m currently going off one that is for javascript.

I would also like to know if this is possible.
Thanks!

You are on c#, aren’t you missing the “typeof” part?

[CustomPropertyDrawer(typeof(List))]

For example

I tried this before and decided it didn’t work. The only thing that I could do to make it work was to derive a non-generic class from the generic class, fixing its type parameters, and make the MonoBehaviours use that instead of using the generic type directly. You do need to proxy constructors, but most other things work fine. You also need the middle-man class to be [Serializable].

It also means that your PropertyDrawer can only deal with one type - it can’t be generic - though that’s not quite the case either, as you can pull the same trick, defining a non-generic derived class and doing the [CustomPropertyDrawer] markup on that instead.

So you end up with engine code:

[Serializable]
public class ListDictionary_int : ListDictionary<int>
{
    // ... constructors, at least ...
}

And editor code:

// No attributes needed
public class ListDictionaryPropertyDrawer<T> : PropertyDrawer
{
    public override void OnGui(...)
    // ... etc ...
}

[CustomPropertyDrawer(typeof(ListDictionary_int))]
public class ListDictionaryPropertyDrawer_int : ListDictionaryPropertyDrawer<int>
{
    // nothing needed here
}
1 Like

Try this [CustomPropertyDrawer(typeof(ListDictionary<>))]

3 Likes

@Xappek : I can’t believe you even tested that, since it has never worked, and still (over a year later) doesn’t work.

2 Likes

Hey there,

Unfortunately (from my experience) you are not able to do this. It has to do with how the inspector works and serialization works.

Generic types are not serialized by Unity. When an inspector goes to preview an object it does not look at the class. It tells the class to serialize itself. It will then go ahead and show the results. Since Generics are not serialized there are no results to display.

In short you can’t :frowning:

Too bad, I was also just trying this…

Unity does serialize and display T[ ] or List for example.

Support for this would really be appreciated, because the alternative doesn’t look very pretty. My goal is simply to replace the current editor for arrays with a version that is expanding automatically to reduce the need to set the size up front. This works perfectly fine for specific value types like int[ ], float[ ] and string[ ], but for any array of Component instances it would be nice to be able to define a generic property drawer.

(The property drawer is not set for int[ ] directly, because that’s also not possible. If have an ArrayInt class that can be implicitly cast to int[ ]. The expanding int array property drawer is placed on this class.)

1 Like

So it is somewhat possible. Maybe not directly but there is a workaround for it. And I know I’m reviving old thread, but it was first answer in Google.

PropertyDrawers does not support generic types. It is because Unity does not serialize generic types different than List.

So if you have:

[Serializable]
public class GenericClass<T>
{
    [SerializeField]
    private T GenericVariable;
}

And MonoBehaviour with this class as a field:

public class GenericPropertyTestController : MonoBehaviour
{
    public GenericClass<int> GenericField;
}

Unity will not serialize it. Period. Maybe someday we’ll get such support but it would require changing such a base thing as Unity Serializer so we might never have it.

But one can do a workaround.

Basically you need to declare not generic class and your generic class will need to inherit from it. Then you can create your CustomPropertyDrawer for this class, but you need to set useForChildren as true in CustomPropertyDrawer attribute:

[Serializable]
public class GenericClassParent
{

}

public class GenericClass<T> : GenericClassParent
{
    [SerializeField]
    private T GenericVariable;
}

[CustomPropertyDrawer(typeof(GenericClassParent), true)]
public class GenericClassDrawer : PropertyDrawer
{
    //Property drawer code here.
}

But it still won’t show in Inspector, because we’re still trying to serialize the GenericClass in our MonoBehaviour. Unity allow us to serialize types derived from generic classes though. So we can use this feature like this:

[Serializable]
public class IntGenericClass : GenericClass<int>
{ }

public class GenericPropertyTestController : MonoBehaviour
{
    public IntGenericClass GenericField;
}

This will allow Unity to serialize GenericField in GenericPropertyTestController and for drawing it’ll use GenericClassDrawer.

13 Likes

Yes, that is also how I do things now. So for the automatically expanding int array, I have the following classes:

  • Array (abstract, Serializable, provides implicit casting to T[ ])
  • ArrayInt : Array (really just an empty class, but Serializable)
  • ArrayDrawer : PropertyDrawer (abstract, provides a general automatically expanding array drawer)
  • ArrayIntDrawer : ArrayDrawer (CustomPropertyDrawer(typeof(ArrayInt)))

If you use my method and do it like this:

class Array{}

class Array<T> : Array {}

[CustomPropertyDrawer(typeof(Array), true)]

ArrayDrawer : PropertyDrawer {}

Then, with a bit of help from reflections, you can create one property drawer for all types of Array.

3 Likes

That is an interesting approach. It won’t work out for this specific Array class, because it needs type specific handling, but I’ll check to see whether I can apply this in other places. I didn’t know this was possible.

Actually, somewhere further along the array line I have classes to handle standard types of Unity:
ArrayObject : Array where T : Object (also implements an interface to prevent circular references.)
ArrayComponent : ArrayObject where T : Component
ArrayAudioSource : ArrayComponent

It would be nice to be able to handle ArrayAudioSource with a PropertyDrawer that is general for ArrayComponent, because in this case there is nothing type specific beyond Component. That won’t be possible however, because ArrayComponent does have to inherit from Array in some way.

I might split up Array into type specific variants for things like Array and for object references like Array that can have a single PropertyDrawer.

I’m actually running into a scenario where this is very useful, but it does not seem to work. The custom property drawer is simply not called for the children. It’s called for the base class, but not for the child class.

Both the base class and the generic class are serializable and the fields are too.

Edit: If I make a non generic subclass of the base class, it does work. So this way also simply doesn’t work for generic classes.

Hi, sorry for late answer.

Yes, this will only work if you have class derived from the genric one. It doesn’t work for generic class probably because of the same reasons why generic classes are not serialized.

My solution just allows to have one Property Drawer for all of these classes. But you still need to create them manually i.e. my example with IntGenericClass.

So if you wanted to create ArrayDrawer for all Array types you can do this. You’ll need to use reflections to get the type of T inside your drawer. But if you’ll work through it then you can have one ArrayDrawer for all classes that derive from Array e.g: IntArray : Array or FloatArray : Array.

I too benefited from the answers here, and I thank you.
My follow up question is: could anyone tell how is the inspector drawer made in the case of List<> type ? I am guessing it is more low level than we would have acces from project scripts ?

1 Like

+1 @Danielpunct

@Danielpunct and @Whatever560 I believe they hardcoded support for List, and pretend it’s (for the most part) an array during serialization. Despite not really knowing the full details, I know they did NOT support true generics at the time.

However, YAY UNITY 2020.1.0a18!!!
Unity 2020.1 supports generic serialization, without needing to derive a concrete child type. For example, I can use Blah<float> and it serializes just fine now. :slight_smile:

using System;
using UnityEngine;

[Serializable]
public class Blah<T> {
    [SerializeField] private T value;
}

//Example in-use:
public class ExampleMonoBehaviour : MonoBehaviour {
    [SerializeField] private Blah<float> someFloat;
}

Really, thank you Unity team… makes me on the verge of tears (due to happiness).

13 Likes

This is indeed worth updating for, I hope the feature will be more or less stable, 2019 and the serialize ref feature had some issues for when i first tried them

1 Like

So that means that we can create a CustomPropertyDrawer for generic types? Like a Dictionary<,> or a List<>?

3 Likes

If we were only as lucky. After nearly 8 years, Unity still hasn’t solved this.

Here’s what I’ve tried:

[System.Serializable]
public class MyGenericClass<T>
{
    public T someField;
}
using UnityEngine;

public class MyClass : MonoBehaviour
{
    public MyGenericClass<string> stringClass;
    public MyGenericClass<int> intClass;
    public MyGenericClass<float> floatClass;
}

This is how it looks in the inspector:
6489677--729578--upload_2020-11-4_12-6-51.png
Now, that’s what Unity 2020 enabled us to do. You don’t have to create a class that derives from MyGenericClass and use that as a variable type. You can use a generic class out of the box.
That’s cool, but, in our case, still useless without a property drawer.

If we create a custom property drawer, we get an error.

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(MyGenericClass<string>))]
public class MyPropertyDrawer<TKey> : PropertyDrawer
{
    private MyGenericClass<TKey> reference;
 
    protected static readonly Dictionary<Type, Func<Rect, object, object>> fields =
        new Dictionary<Type, Func<Rect, object, object>>()
        {
                { typeof(int), (rect, value) => EditorGUI.IntField(rect, (int)value) },
                { typeof(float), (rect, value) => EditorGUI.FloatField(rect, (float)value) },
                { typeof(string), (rect, value) => EditorGUI.TextField(rect, (string)value) },
        };

    protected static T DoField<T>(Rect rect, Type type, T value)
    {
        if (fields.TryGetValue(type, out Func<Rect, object, object> field))
        {
            return (T)field(rect, value);
        }

        Debug.LogError("Type not supported: " + type);
        return value;
    }

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if(reference == null)
        {
            reference = (MyGenericClass<TKey>)InspectorUtil.GetTargetObjectOfProperty(property);
        }

        reference.someField = DoField(position, typeof(TKey), reference.someField);
    }
}

6489677--729581--upload_2020-11-4_12-11-10.png

This is what works:

using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(MyGenericClass<string>))]
public class ThisIsStupid : MyPropertyDrawer<string> { }

public class MyPropertyDrawer<TKey> : PropertyDrawer
{
    private MyGenericClass<TKey> reference;
    ...

This is what it looks like in the editor:
6489677--729584--upload_2020-11-4_12-13-24.png

Why this hasn’t been addressed in almost a decade is beyond me.

6 Likes