LogoLogo
  • Home
  • Projects
  • About
  • Contact

Using Attributes for Property Validation

Devon O. · December 24, 2019 · Unity3D · 0 comments
3

Programmers are always looking for ways to write safer more bullet proof code. For that reason, at work it was a convention for us to use Unity’s OnValidate() callback to validate the public and serialized properties of our MonoBehaviour components. So for example we would typically write code like:

C#
1
2
3
4
5
6
7
8
9
10
11
using UnityEngine;
    
public class BaseBehaviour : MonoBehaviour
{
    [SerializeField] private Transform someTransform = null;
    
    private void OnValidate()
    {
        Debug.Assert(someTransform != null, "[BaseBehaviour] someTransform property cannot be null", this);
    }
}

Now, generally speaking, this works great. Essentially, what the above snippet will do is log an error in the Unity console every time the a property of the Monobehaviour is updated, the code is changed (i.e. the scripts reloaded), or until, (the desired goal), the property is set using the Unity Inspector and someTransform is no longer null. This is an excellent way to be sure properties that are meant to be set in the Inspector window are kept to expected parameters (e.g. not null).

Programmers, though, are also very lazy. It wasn’t long before the nice clean assumption above turned into something more like:

1
Debug.Assert(someTransform);

Logically speaking, that works just as well and does the basically the same thing and works just as well (sans the nice output), but it wasn’t long before even that condensed version felt like too much boilerplate code to be worth writing. I wanted something which did the same thing, but was quick and easy with no boilerplate. So I turned to looking at attributes. What I really wanted was to be able to write something like what’s below and skip the OnValidate() call altogether.

C#
1
2
3
4
5
6
using UnityEngine;
 
public class BaseBehaviour : MonoBehaviour
{
    [SerializeField, NotNull] private Transform someTransform = null;
}

After a weekend or so of mucking around, I managed to get just that and thought I’d share how here.


The Attributes

We’ll start by creating a base abstract attribute and an abstract subclass attribute that has a Validate() method and an ErrorMessage property to display if the Validate() fails.

C#
1
2
3
4
5
6
7
8
9
10
using System;
 
public abstract class OneByOneAttribute : Attribute
{}
 
public abstract class ValidationAttribute : OneByOneAttribute
{
    public abstract bool Validate(System.Reflection.FieldInfo field, UnityEngine.Object instance);
    public abstract string ErrorMessage { get; }
}

The idea being something will call this Validate() method on the attribute, and, if it returns false, will show an error containing the attribute’s ErrorMessage.

Now we’ll create a couple concrete attribute implementations that will cover the two assertions I find myself making the most often – that a property is not null and that an array (or list) property contains x number of elements.

Our NotNull attribute will look like this:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
using System;
using UnityEngine;
 
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class NotNull : ValidationAttribute
{
    public override string ErrorMessage => error;
    private string error = string.Empty;
 
    public override bool Validate(System.Reflection.FieldInfo field,  UnityEngine.Object instance)
    {
        bool isValid;
        MonoBehaviour mb = instance as MonoBehaviour;
        error = $"Property: {field.Name}\non GameObject: {mb.name}\ncannot be NULL";
        try
        {
            var value = field.GetValue(instance);
            isValid = !(value.Equals(null));
        }
        catch (Exception)
        {
            isValid = false;
        }
        return isValid;
    }
}

One thing to note here is that we want to error message to be as descriptive as possible. One drawback to using attributes for validation rather than just using assertions in OnValidate() is that when we double click the error in the Unity console, we will no longer open the exact spot of the problem. We can mitigate that issue a little though with (a) a detailed error message and (b) by passing a context argument to the Debug.LogError() call – this will mean when we single click on the error message in the console, the context object will be highlighted temporarily in the editor window.

All that in mind, here is a ContainsAtLeast attribute validator:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using System.Collections;
 
using UnityEngine;
 
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class ContainsAtLeast : ValidationAttribute
{
    public override string ErrorMessage =>; error;
    private string error = string.Empty;
 
    private int Count;
 
    public ContainsAtLeast(int minimumItemCount)
    {
        Count = minimumItemCount;
    }
 
    public override bool Validate(System.Reflection.FieldInfo field, UnityEngine.Object instance)
    {
        var value = field.GetValue(instance);
        var mb = instance as MonoBehaviour;
        var isValid = false;
        try
        {
            error = $"Object: {field.Name}\non GameObject: {mb.name}\nMust contain AT LEAST ({Count}) item(s)";
            int i = 0;
            foreach(var o in (value as IEnumerable))
            {
                i++;
                if (i >= Count)
                {
                    isValid = true;
                    break;
                }
            }
        }
        catch (Exception)
        {
            isValid = false;
            if (typeof(IEnumerable).IsAssignableFrom(field.FieldType))
                error = $"Object: {field.Name}\non GameObject: {mb.name}\nMust contain AT LEAST ({Count}) item(s)";
            else
                error = $"Item: {field.Name}\non GameObject: {mb.name}\nMust be Array, List, or String to use attribute properly";
        }
 
        return isValid;
    }
}

Also, since developers are lazy, here’s a quick attribute that’s a shortcut to writing ContainsAtLeast(1):

C#
1
2
3
4
5
6
7
8
9
using System;
 
 
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public class NotEmpty : ContainsAtLeast
{
    public NotEmpty() : base(1)
    {}
}


The Editor

Next we’ll need the editor script that will actually validate our custom attributes. I won’t bother going too deeply into the code, but generally speaking what we want is an editor that will:

  • Work on all Unity Objects
  • If there are no fields with custom attributes, just do what a default editor will do
  • If there are custom attributes on or more fields, it should validate them and display errors

To more closely replicate the replaced OnValidate() call, it should also:

  • Validate all fields on script reload
  • Display errors (if there are any) when any property is updated in the editor

All that in mind, here is the final editor script I wrote:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
 
using UnityEditor;
using UnityEngine;
 
[CanEditMultipleObjects]
[CustomEditor(typeof(UnityEngine.Object), true)]
public class OneByOneAttributeEditor : Editor
{
    private bool useDefaultInspector = false;
    private IEnumerable<FieldInfo> fields;
 
    private bool shouldShowErrors = true;
 
    private void OnEnable()
    {
        fields = GetAllFields(target.GetType());
        useDefaultInspector = fields.All(f => f.GetCustomAttributes(typeof(OneByOneAttribute), true).Length == 0);
    }
 
    public override void OnInspectorGUI()
    {
        if (useDefaultInspector)
        {
            base.OnInspectorGUI();
            return;
        }
 
        this.serializedObject.Update();
 
        foreach (var f in fields)
        {
            ValidateField(f);
        }
 
        shouldShowErrors = this.serializedObject.ApplyModifiedProperties();
    }
 
    private void ValidateField(FieldInfo field)
    {
        var prop = GetSerializedProperty(field);
        if (prop == null)
            return;
 
        object[] atts = field.GetCustomAttributes(typeof(ValidationAttribute), true);
        foreach (var att in atts)
        {
            ValidateAttribute(att as ValidationAttribute, field);
        }
 
        DrawProperty(prop);
    }
 
    private void DrawProperty(SerializedProperty prop)
    {
        EditorGUILayout.PropertyField(prop, true);
    }
 
    private void ValidateAttribute(ValidationAttribute attribute, FieldInfo field)
    {
        if (!attribute.Validate(field, this.target))
        {
            EditorGUILayout.HelpBox(attribute.ErrorMessage, MessageType.Error, true);
            if (shouldShowErrors)
                ShowError(attribute.ErrorMessage, this.target);
        }
    }
 
    private SerializedProperty GetSerializedProperty(FieldInfo field)
    {
        // Do not display properties marked with HideInInspector attribute
        object[] hideAtts = field.GetCustomAttributes(typeof(HideInInspector), true);
        if (hideAtts.Length > 0)
            return null;
 
        return this.serializedObject.FindProperty(field.Name);
    }
 
    public static IEnumerable<FieldInfo> GetAllFields(Type t)
    {
        if (t == null)
            return Enumerable.Empty<FieldInfo>();
 
        BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic |
                             BindingFlags.Static | BindingFlags.Instance  |
                             BindingFlags.DeclaredOnly;
 
        return t.GetFields(flags).Concat(GetAllFields(t.BaseType));
    }
 
    private static void ShowError(string msg, UnityEngine.Object o)
    {
        Debug.LogError(msg, o);
    }
 
    [UnityEditor.Callbacks.DidReloadScripts]
    private static void OnScriptReload()
    {
        MonoBehaviour[] behaviours = MonoBehaviour.FindObjectsOfType<MonoBehaviour>();
        foreach(var b in behaviours)
        {
            var fields = GetAllFields(b.GetType());
            foreach(var f in fields)
            {
                var atts = f.GetCustomAttributes(typeof(ValidationAttribute), true);
                foreach(var a in atts)
                {
                    var vatt = a as ValidationAttribute;
                    if(!vatt.Validate(f, b))
                    {
                        ShowError(vatt.ErrorMessage, b);
                    }
                }
            }
        }
    }
}

Drop that script into a folder named Editor, create a test MonoBehaviour component marking some properties with the NotNull, NotEmpty, ContainsAtLeast attribute and check it out in the inspector.

For example, Add this script to a game object named “Custom Attributes” in the scene and this is what you’ll get in the Inspector Window.

And in the Console Window:

Actually add some items to the component via the Inspector and just watch those errors disappear.

Hope this might help out.


Enjoy the holidays, all!

  Facebook   Pinterest   Twitter   Google+
Editor Scripting
  • Starling Filter Collection
    July 05, 2013 · 9 comments
    11084
    27
    Read more
  • Asdoc GUI and CommandProxy
    June 01, 2008 · 14 comments
    3102
    3
    Read more
  • MOTION – FLUID – SOUND
    October 22, 2010 · 1 comments
    2160
    7
    Read more

Leave a Comment! Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Devon O. Wolfgang

AIR | Unity3D | AR/VR

Unity Certified Developer

Technical Reviewer of “The Essential Guide to Flash CS4 AIR Development” and “Starling Game Development Essentials”

Reviewer of “The Starling Handbook”

Unity Engineer at Touch Press.

Categories
  • Actionscript (95)
  • AIR (16)
  • Flash (99)
  • Games (7)
  • Liberty (13)
  • Life (53)
  • Shaders (20)
  • Unity3D (21)
Recent Comments
  • Devon O. on Unity Ripple or Shock Wave Effect
  • Feral_Pug on Unity Ripple or Shock Wave Effect
  • bavvireal on Unity3D Endless Runner Part I – Curved Worlds
  • Danielius Vargonas on Custom Post Processing with the LWRP
  • Luca G on Unity Ripple or Shock Wave Effect
Archives
  • December 2020 (1)
  • December 2019 (1)
  • September 2019 (1)
  • February 2019 (2)
  • December 2018 (1)
  • July 2018 (1)
  • June 2018 (1)
  • May 2018 (2)
  • January 2018 (1)
  • December 2017 (2)
  • October 2017 (1)
  • September 2017 (2)
  • January 2017 (1)
  • July 2016 (1)
  • December 2015 (2)
  • March 2015 (1)
  • September 2014 (1)
  • January 2014 (1)
  • August 2013 (1)
  • July 2013 (1)
  • May 2013 (1)
  • March 2013 (2)
  • December 2012 (1)
  • November 2012 (1)
  • September 2012 (3)
  • June 2012 (2)
  • May 2012 (1)
  • April 2012 (1)
  • December 2011 (2)
  • October 2011 (3)
  • September 2011 (1)
  • August 2011 (1)
  • July 2011 (1)
  • May 2011 (2)
  • April 2011 (2)
  • March 2011 (1)
  • February 2011 (1)
  • January 2011 (2)
  • December 2010 (3)
  • October 2010 (5)
  • September 2010 (1)
  • July 2010 (2)
  • May 2010 (5)
  • April 2010 (2)
  • March 2010 (7)
  • February 2010 (5)
  • January 2010 (5)
  • December 2009 (3)
  • November 2009 (1)
  • October 2009 (5)
  • September 2009 (5)
  • August 2009 (1)
  • July 2009 (1)
  • June 2009 (2)
  • May 2009 (6)
  • April 2009 (4)
  • March 2009 (2)
  • February 2009 (4)
  • January 2009 (1)
  • December 2008 (5)
  • November 2008 (2)
  • September 2008 (1)
  • August 2008 (6)
  • July 2008 (6)
  • June 2008 (9)
  • May 2008 (4)
  • April 2008 (3)
  • March 2008 (4)
  • February 2008 (9)
  • January 2008 (7)
  • December 2007 (6)
Copyright © 2017 Devon O. Wolfgang