Hello, I’m implementing a “Condition” class in my project for which I have built a custom display UXML and controller script. I need many other classes to contain an instance of a Condition so each of those will require a custom editor to use my custom Condition display. However, I don’t want to write all of the boilerplate to display other data from the classes for which the default inspector view is fine. I’ve figured out how to use InspectorElement.FillDefaultInspector to avoid this problem on simple classes but when the Condition is buried within a list of serializable classes, that method breaks down. Here’s an example:
At the lowest level we have a serializable “NpcAction” class with some data. A ScritableObject “Activity” contains a list of these.
public class Activity : ScriptableObject
{
[SerializeField] public List<NpcAction> actionList = new List<NpcAction>();
//...other basic type data
}
[System.Serializable]
public class NpcAction
{
[SerializeField] string argument;
//...other basic type data
}
A serializable “ActivityWrapper” contains one Activity and a Condition (the one piece of data that I want a custom display for). An “ActivityList” MonoBehaviour contains a list of ActivityWrappers and other basic type data.
public class ActivityList : MonoBehaviour
{
[SerializeField] [HideInInspector] public List<ActivityWrapper> activities;
//...other basic type data
[System.Serializable]
public class ActivityWrapper
{
[SerializeField] public Condition condition;
[SerializeField] public Activity activity;
}
}
The CustomEditor of ActivityList builds a ListView to display the ActivityWrapper list. Then it builds the default inspector to display any other data.
[CustomEditor(typeof(ActivityList))]
public class ActivityListEditor : Editor
{
[SerializeField] VisualTreeAsset _inspectorTemplate;
[SerializeField] VisualTreeAsset _listEntryTemplate;
[SerializeField] VisualTreeAsset _conditionTemplate;
ListView _listView;
ActivityList _selectedParentItem;
VisualElement _rootVisualElement;
VisualElement _defaultInspectorRoot;
public override VisualElement CreateInspectorGUI()
{
_rootVisualElement = new VisualElement();
_inspectorTemplate.CloneTree(_rootVisualElement);
_selectedParentItem = Selection.activeObject as ActivityList;
// Build a list view to display the ActivityWrappers contained within the selected ActivityList
SetupListView();
// Build the custom inspector to display the rest of the data in the ActivityList
_defaultInspectorRoot = _rootVisualElement.Q<Foldout>("default-inspector-root");
InspectorElement.FillDefaultInspector(_defaultInspectorRoot, serializedObject, this);
return _rootVisualElement;
}
private void SetupListView()
{
_listView = _rootVisualElement.Q<ListView>("list-view");
_listView.makeItem = () =>
{
VisualElement newListEntry = _listEntryTemplate.Instantiate();
ActivityListEntryController newListEntryController = new();
newListEntry.userData = newListEntryController;
newListEntryController.SetVisualElement(newListEntry, _conditionTemplate);
return newListEntry;
};
_listView.bindItem = (item, index) =>
{
if (item.userData is ActivityListEntryController newListEntryController)
{
newListEntryController.SetData(_selectedParentItem.activities[index]);
}
};
_listView.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
_listView.itemsSource = _selectedParentItem.activities;
}
}
The controller for each item in the ActivityWrapper list instantiates the custom Condition controller script (which handles building it’s own UI internally).
public class ActivityListEntryController
{
VisualElement _conditionRoot;
VisualTreeAsset _conditionListTemplate;
ConditonTreeController _conditionTreeController = new();
public void SetVisualElement(VisualElement visualElement, VisualTreeAsset conditionListTemplate)
{
_conditionListTemplate = conditionListTemplate;
_conditionRoot = visualElement.Q<VisualElement>("condition-root");
}
public void SetData(ActivityList.ActivityWrapper itemData)
{
// Build a custom UI element to display the ConditionTree of this ActivityWrapper
_conditionTreeController.SetupRootVisualElement(_conditionRoot, _conditionListTemplate, itemData.condition);
// FIXME: Still need to display other data in ActivityWrapper such as Activity
}
}
So what I have implemented above displays the basic data contained in ActivityList (using the default inspector) and manually builds a list of ActivityWrappers, each of which has a the custom Condition display. I still need to display the other ActivityWrapper field, the Activity, which consists of a List of NpcActions.
I could write all of the boilerplate to draw another list view for the NpcActions within each item of the ActivityWrapper list view but that is a lot of work just to draw something that is already well served by the default inspector. I have not been able to figure out how to call InspectorElement.FillDefaultInspector from within ActivityListEntryController to display the Activity list because I would need to somehow get the ActivityWrapper that is passed into SetData as a SerializedObject, which I have not been able to do.
I looked a little into CustomPropertyDrawer which I hoped would allow me to automatically replace the display of Condition instances without defining a custom editor for everything that uses a Condition but it does not seem that framework supports complicated UI style and behavior defined in C# scripting.
I am hoping there is some piece of UI toolkit that I am just not aware of that will help me solve this problem efficiently. Thank you very much for taking the time to read my post and for any feedback you can provide.

