- Home /
How can I get functionality similar to OnValidate for a custom class?
I have a class similar to as follows:
[Serializable]
public class MySerializableClass
{
[SerializeField]
private string someField;
public void OnValidate()
{
Debug.LogError("OnValidate()");
//perform validation code here
}
}
I would like the code in OnValidate to run, similarly to how it runs for MonoBehaviours and ScriptableObjects. However, I have a couple of issues achieving this:
This class is not and cannot be a MonoBehaviour nor a ScriptableObject, so OnValidate will not simply run.
The code I need to run requires methods on the Unity main thread, such as UnityEditor.AssetDatabase.GetAssetPath(). Because of this, I cannot use ISerializationCallbackReceiver.OnAfterDeserialized(), which runs on a separate thread.
Are there any options available for what I am trying to accomplish? I really want to avoid having to explicitly call OnValidate from any and every class which contains instances of MySerializableClass, as that would defeat the purpose of what I'm trying to do.
Answer by Bunny83 · Apr 26, 2017 at 09:39 AM
OnValidate is called by Unity on Components whenever a serialized property of that component is changed. That includes "sub-properties" in custom classes. Unity doesn't serialize custom classes on their own. They are simply treated like "structs". So all properties of a custom class simply become properties of the MonoBehaviour class that exposes your custom class.
So when you change a variable in the inspector, no matter if it's an actual field of the MonoBehaviour or if it's nested in a custom class, Unity will call the OnValidate method of the MonoBehaviour.
If you want to apply special handling in the custom class itself, you can simply add your own OnValidate method and call it from the MonoBehaviour that contains that class:
[System.Serializable]
public class CustomData
{
public int someValue;
public void OnValidate()
{
Debug.Log("ChildData::OnValidate");
}
}
public class MyComponent : MonoBehaviour
{
public int componentField = 42;
public CustomData data;
void OnValidate()
{
Debug.Log("MyComponent::OnValidate()");
if (data != null)
data.OnValidate()
}
}
So the OnValidate method inside CustomData now just works exactly the same as the OnValidate of the containing MonoBehaviour.
edit
If you set your asset serialization mode to "force text", scene assets (as well as prefabs) will be serialized as YAML text. Here's how my example looks like as serialized data:
MonoBehaviour:
m_ObjectHideFlags: 0
m_PrefabParentObject: {fileID: 0}
m_PrefabInternal: {fileID: 0}
m_GameObject: {fileID: 1140048734}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 001aa7c4a3d39e846af08362b2b2f495, type: 3}
m_Name:
m_EditorClassIdentifier:
componentField: 0
data:
someValue: 5
As you can see at the end of the internally serialized data there are our serialized fields. "componentField" is a direct member of our MonoBehaviour as well as "data". All the serialized fields of our "CustomData" class are simply sub-fields of the monobehaviour.
So if you simply do the validation in the MonoBehaviour, you don't have to do anything special. OnValidate will be called when you change sub-fields in the inspector. If you want to handle the specific validate code for your sub class inside that class, just "delegate" the OnValidate event like i showed in my code example.
I appreciate your detailed comment, however at the end of my question I stated:
"I really want to avoid having to explicitly call OnValidate from any and every class which contains instances of $$anonymous$$ySerializableClass, as that would defeat the purpose of what I'm trying to do."
I understand that OnValidate of the $$anonymous$$onoBehaviour will be called. I've used that plenty in the past. But that is not what I want. I would like to be able to have this class, $$anonymous$$ySerializableClass, to validate itself and not require me to explicitly call the method from the $$anonymous$$onoBehaviour which contains it.
That is why I attempted using ISerializationCallbackReceiver.OnAfterDeserialized(), because that method is automatically called when $$anonymous$$ySerializableClass is deserialized. The problem with that method is that I cannot access Unity code.
That's not possible. Classes don't do anything by themselfs. There are systems / framesworks which use certain patterns / interfaces / conventions and may invoke a method when a certain event happens. Unity treats custom classes like structs, pure data-containers. The ISerializationCallbackReceiver is one of those special things they implemented to allow special handling before and after serialization. There is no built-in concept for OnValidate for custom classes. So you have to roll your own solution.
I agree that it would be nice if Unity had more points in their internal event structure where you can "hook in". The introduction of the ISerializationCallbackReceiver some time ago was a huge improvement. However there's always room for improvements.
You can try posting a feature request. However unless "enough" people vote for it, it won't be implemented. Also keep in $$anonymous$$d that feature requests usually take at least a year (usually longer) to actually be implemented in an update. So if you need a solution now, you really need to roll your own solution for now.
You can still use ISerializationCallbackReceiver.OnBeforeSerialize but you would have to delegate the changes into a job queue that is processed by EditorApplication.update for example.
Another solution might be to implement a PropertyDrawer for your class. However inside a property drawer you only have direct access to the serialized data and not the actual instance. There are ways to get the actual instance but it's a quite hacky way.
These all seem like good suggestions. I'll try one of those two last things you suggested, and maybe put in a feature request. I have a friend who just now told me he did something very similar, and he did it with a PropertyDrawer but it was indeed very hacky. I'll mark your answer since this seems like the best thread.
Answer by andrew-lukasik · Sep 08, 2017 at 08:24 AM
Implement ISerializationCallbackReceiver
and enjoy simple, clean OnValidate
calls:
[System.Serializable]
public class ICanHazOnValidate : ISerializationCallbackReceiver
{
void OnValidate ()
{
/* (∩^o^)⊃━☆ */
}
void ISerializationCallbackReceiver.OnBeforeSerialize () => this.OnValidate();
void ISerializationCallbackReceiver.OnAfterDeserialize () {}
}
Answer by Bilelmnasser · Apr 26, 2017 at 07:53 AM
////Method 1 using delegate invoking :
public delegate void OnValidateDelegate();
public class MySerializableClass1
{
public OnValidateDelegate ValidationMethod;
[SerializeField]
private string someField;
public string someFieldProperty
{
get
{
return someField;
}
set
{
someField = value;
ValidationMethod.Invoke( );
}
}
}
public class Exampleusage : MonoBehaviour
{
void Start()
{
MySerializableClass1 obj = new MySerializableClass1( );
obj.ValidationMethod += new OnValidateDelegate( OnValidateExample );
}
public void OnValidateExample()
{
Debug.LogError( "OnValidate()" );
//perform validation code here
}
}
///Method 2 using direct call of code throw set properties:
public class MySerializableClass2
{
[SerializeField]
private string someField;
public string someFieldProperty
{
get
{
return someField;
}
set
{
someField = value;
OnValidate( );
}
}
public void OnValidate()
{
Debug.LogError( "OnValidate()" );
//perform validation code here
}
}
This doesn't work as the inspector works on the serialized data. Properties are completely ignored by Unity. This is a way to check for changes at runtime but not inside the editor / inspector at edit time.
There might be some confusion around the word "property". C# / .NET has "properties". However Unity's serialization system calls serialized fields also "properties". Well logically that's correct since they are properties of the class, but not in the C# /.NET sense but in a natural sense. The serialization system abstracts serialized fields as "UnityEditor.SerializedProperty"
Answer by tanoshimi · Apr 26, 2017 at 06:38 AM
OnValidate is called when a component's exposed properties are changed in the inspector. If your class is not a MonoBehaviour, those properties won't be exposed, so when do you expect this to get called?
Do you simply want to apply validation logic in the variable setter?
As a Serializable class, if $$anonymous$$ySerializableClass is contained by a $$anonymous$$onoBehaviour, then yes it does show up in the inspector.
public class $$anonymous$$yContainer : $$anonymous$$onoBehaviour
{
public $$anonymous$$ySerializableClass instance;
}
This code will expose $$anonymous$$ySerializableClass.someField in the inspector to be edited. I would like some way of calling this method when that field is edited. As get/set properties are not exposed in the inspector, perfor$$anonymous$$g validation through a set property is not possible.