For a long time now I’ve been struggling with trying to write a custom Drawer (or Field) for Serializable classes with members that can’t be bound to directly for various reasons. One of my classes is immutable. Another contains a List that I want to modify by toggling items that could go inside it instead of the usual adding/removing of items directly. In either case, I can’t bind the data inside the class, but I can change the whole value completely using SerializedProperty.boxedValue.
Most of my attempts have been trying to create a custom Field that inherits from BindableElement and INotifyValueChanged, but for some reason custom data types aren’t supported for that, even though boxedValue is now easily accessible in the big switch statement that silently (why???) dismisses SerializedPropertyType.Generic (which has been most of my headache for multiple weeks).
I don’t want to crack open the UnityEditor.UIElements assembly to support INotifyValueChanged, so currently I’m just trying to figure out how I would go about writing a Drawer for my classes that doesn’t try to use bindings, and I have it technically working, except for two specific things (so far):
-
Hitting Undo doesn’t immediately refresh the Drawer (although the data does get successfully undo’d, as switching the inspector to something else and then back again shows the previous value). I could handle this myself honestly, but I don’t know where
-
Prefabs with overrides don’t indicate that the value has been changed (with the blue marker and bold text), and doesn’t give me a way to Reset or Apply the value by right-clicking. For sub fields this makes sense, as there’s no value bound to them, but I would expect that the entire box would at least be changed since the returned element from the Drawer is bound to the whole value. It should be pretty easy to tell when the object is dirty anyway, since, again, boxedValue is a thing, and should be able to be used the same way as the other types of accessors.
Here’s the simplest example code I could pull up for my current attempt, using Unity 2022.2.1f1:
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
[Serializable]
public class CustomDataType
{
// Immutable data type, shouldn't bind directly to m_foo
[SerializeField]
private string m_foo;
public string foo => m_foo;
public CustomDataType() : this("default foo text") {}
public CustomDataType(string foo)
{
m_foo = foo;
}
}
[CustomPropertyDrawer(typeof(CustomDataType))]
public class CustomDataTypeDrawer : PropertyDrawer
{
public override VisualElement CreatePropertyGUI(SerializedProperty property)
{
// Give it the default value if it's null
if (property.boxedValue is null)
{
property.boxedValue = new CustomDataType();
property.serializedObject.ApplyModifiedPropertiesWithoutUndo();
}
Debug.Log($"Making {(property.boxedValue as CustomDataType).foo}");
// Simple layout for making the drawer look like a field
VisualTreeAsset asset = Resources.Load<VisualTreeAsset>("CustomDataTypeDrawer");
TemplateContainer drawer = asset.Instantiate(property.propertyPath);
drawer.Q<Label>("Label").text = property.displayName;
// A text field that can't be bound to a property, and an additional label
TextField fooTextField = drawer.Q<TextField>();
Label fooLabel = drawer.Q<Label>("FooLabel");
// Helper for displaying the correct values in the inspector
void UpdateUI(CustomDataType value)
{
fooTextField.SetValueWithoutNotify(value.foo);
fooLabel.text = "foo: " + value.foo;
}
// Immediately show the current values
UpdateUI(property.boxedValue as CustomDataType);
// Helper for setting values
// Similar to INotifyValueChanged.value_set or SetValueWithoutNotify if, you know, *that would actually work for Generic serialized types*
void SetValue(CustomDataType newValue)
{
property.boxedValue = newValue;
property.serializedObject.ApplyModifiedProperties();
UpdateUI(newValue);
}
// Make the fields actually change the
fooTextField.RegisterValueChangedCallback(e => SetValue(new CustomDataType(e.newValue)));
return drawer;
}
}
So, how can I either bind to Generic boxedValue data (optimal, but would probably need fixing the UnityEditor.UIElements assembly), or fix my Undo/Prefab issues?
Or should I just learn IMGUI instead?