ExposedReference with UI Toolkits PropertyField

I am in the process of making a node based tool, where I would like to add scene objects to my data (which in its root is a scriptable object).

After some researching, I found ExposedReference, which seems to be exactly what I want/need to get this to work.

After some searching around on how to use ExposedReference with the little to no documentation around on how to get this to work for Scriptable Objects, I managed to get something to work!

For the sake of completion and context of the question, these are the scripts involved:

This is the monobehaviour that implements the IExposedPropertyTable required to resolve the ExposedReference.

	public class StoryFlowGraphBehaviour : FlowGraphBehaviour, IExposedPropertyTable
	{
		[SerializeField] private StoryFlowAsset storyFlowAsset;

		// ReSharper disable once Unity.RedundantSerializeFieldAttribute
		[SerializeField] private List<PropertyName> propertyNames;
		[SerializeField] private List<Object> objects = new List<Object>();

		protected void Start()
		{
			StartFlow(storyFlowAsset);
		}

		protected override void PerformBindings(IDependencyContainer dependencyContainer)
		{
			dependencyContainer.Bind<IExposedPropertyTable>().ToInstance(this);
		}

		public void SetReferenceValue(PropertyName id, Object value)
		{
			if (PropertyName.IsNullOrEmpty(id))
			{
				return;
			}

			int index = propertyNames.IndexOf(id);

			if (index > -1)
			{
				propertyNames[index] = id;
				objects[index] = value;
			}
			else if (value == null)
			{
				propertyNames.Add(id);
				objects.Add(value);
			}
		}

		public Object GetReferenceValue(PropertyName id, out bool idValid)
		{
			Object result = null;
			idValid = false;

			int index = propertyNames.IndexOf(id);

			if (index > -1)
			{
				idValid = true;

				return objects[index];
			}

			return null;
		}

		public void ClearReferenceValue(PropertyName id)
		{
			int index = propertyNames.IndexOf(id);

			if (index > -1)
			{
				propertyNames.RemoveAt(index);
				objects.RemoveAt(index);
			}
		}
	}

This is the Scriptable Object with the ExposedReference in it.

	[CreateAssetMenu(fileName = "StoryFlowAsset", menuName = "Story/New StoryFlow Asset")]
	public class StoryFlowAsset : FlowGraphAsset
	{
		public ExposedReference<GameObject> testObject;
	}

And this is the Editor script to provide the context, which is the key to be able to reference scene objects in your scriptable object.

	[CustomEditor(typeof(StoryFlowGraphBehaviour))]
	public class StoryFlowGraphBehaviourEditor : Editor
	{
		private SerializedProperty asset;
		private SerializedObject assetSerializedObject;

		public override VisualElement CreateInspectorGUI()
		{
			VisualElement root = new VisualElement();

			if (asset == null)
			{
				asset = serializedObject.FindProperty("storyFlowAsset");
			}

			if (EditorGUI.EndChangeCheck())
			{
				if (asset != null && asset.objectReferenceValue != null)
				{
					assetSerializedObject = new SerializedObject(asset.objectReferenceValue, target);
				}
				else
				{
					assetSerializedObject = null;
				}
			}

			SerializedProperty prop1 = serializedObject.FindProperty("propertyNames");
			PropertyField propertyField1 = new PropertyField(prop1);
			root.Add(propertyField1);

			SerializedProperty prop2 = serializedObject.FindProperty("objects");
			PropertyField propertyField2 = new PropertyField(prop2);
			root.Add(propertyField2);

			if (assetSerializedObject != null)
			{
				SerializedProperty prop = assetSerializedObject.FindProperty("testObject");
				PropertyField field = new PropertyField();
				field.BindProperty(prop);

				root.Add(field);
			}

			return root;
		}

		public override void OnInspectorGUI()
		{
			EditorGUI.BeginChangeCheck();
		
			DrawDefaultInspector();
		
			if (asset == null)
			{
				asset = serializedObject.FindProperty("storyFlowAsset");
			}
		
			if (EditorGUI.EndChangeCheck())
			{
				if (asset != null && asset.objectReferenceValue != null)
				{
					assetSerializedObject = new SerializedObject(asset.objectReferenceValue, target);
				}
				else
				{
					assetSerializedObject = null;
				}
			}
		
			if (assetSerializedObject != null)
			{
				SerializedProperty prop = assetSerializedObject.FindProperty("testObject");
				EditorGUILayout.PropertyField(prop);
			}
		}
	}

I provided both the UGUI way and the UI Toolkit way, because simply put: It works as expected in UGUI and it doesn’t work with UI Toolkit.

When using EditorGUILayout.PropertyField(prop); I can see the data being added to the propertyName and object lists, which is needed to resolve this.

However, with the UI Toolkit PropertyField, the data doesn’t get added. I can add the scene objects just fine, but as the data is not added, I cannot resolve it.

So my question is… Why? I don’t know the exact inner workings of (either) properyfields, but I am assuming it has something to do with EditorGUILayout.PropertyField(prop); checking the properties context and calls the IExposedPropertyTable, while the UI Toolkit PropertyField doesn’t.

Is there something I can do to make this work out of the box without manually populating the 2 lists? Perhaps I am missing something?

Is there perhaps a better way to reference scene objects with UI Toolkit in the editor?

Considering the node based tool is all made with UI Toolkit, I am not planning on rewriting this for UGUI.

Any help, tips or insights are very much welcome.

I think you mean to say IMGUI. UGUI is the older game object based UI system.

Though honestly in my testing, at least with either a default IMGUI or UI Toolkit Inspector, the existing ExposedReference<T> property drawer doesn’t seem to work properly in either case.

You can see it here: UnityCsReference/Editor/Mono/Inspector/Core/ScriptAttributeGUI/Implementations/ExposedReferenceDrawer.cs at master · Unity-Technologies/UnityCsReference · GitHub

It has support for both IMGUI and UI Toolkit property drawers at the very least. It seems to also rely on this internal class: UnityCsReference/Editor/Mono/Inspector/Core/ScriptAttributeGUI/Implementations/ExposedReferenceObject.cs at master · Unity-Technologies/UnityCsReference · GitHub

Maybe you can decipher some of the intentions from the source code.

That said, if I needed a system like this, I would engineer my own to my own requirements and not try and rely on a rather undocumented part of Unity.

Yes. :slight_smile:

What’s not working?

With IMGUI, you first need to explicitly expose/unexpose the field (small button right to the field) before you can drag in scene objects. Granted, it is a bit of an annoyance, but it works.

UI Toolkit immediately provides the ability to drag in scene objects, but the referenced object gets lost (hence this post).

Thanks for these. Going to check it out, see if I can find something I might have overlooked.

Fair enough. It’s actually documented, but more in context of Timeline so for a complete solution with just Scriptable Objects, it’s only missing some explanation about the table.

Anyway, I got an “in between” solution at the moment by using the IMGUIContainer, which works as well. We’ll see how well it keeps working before I roll my own. Which I probably end up doing at some point.

Alright, I managed to get it working (and it probably worked the entire time…)

From the link you provided of the propertydrawer, I noticed they added a manipulator to the objectfield, which populates a context menu to expose/unexpose the object.

So that was basically it… it starts off as unexposed, but once you right click the element and select expose, it actually works!

1 Like