Create a property visitor with the PropertyVisitor class
Learn how to use the PropertyVisitor base class to create a property visitor from an example.
Read time 10 minutesLast updated 14 days ago
This example demonstrates how to use the base class to create a property visitor. For an equivalent example that uses the and interfaces, refer to Use low-level APIs to create a property visitor.
PropertyVisitorIPropertyBagVisitorIPropertyVisitorCreate data and utility classes
Create a simple data class and a utility class that uses the visitor to visit and print its properties:
-
Create the following C# class calledwith properties that can be visited:
Datausing System.Collections.Generic;using UnityEngine;public class Data{ public string Name = "Henry"; public Vector2 Vec2 = Vector2.one; public List<Color> Colors = new List<Color> { Color.green, Color.red }; public Dictionary<int, string> Dict = new Dictionary<int, string> { { 5, "zero" } };} -
Create the following utility class called:
DebugUtilitiesusing UnityEngine;public static class DebugUtilities{ public static void PrintObjectDump<T>(T value) { // Magic goes here. }}
Create the visitor
Create the visitor class as follows:
-
Create a class namedthat inherits from
ObjectVisitor.PropertyVisitor -
Add afield to the class. This can be used to build a string that represents the current state of an object.
StringBuilder -
Add amethod that clears the
Resetand resets the indent level.StringBuilder -
Add amethod that returns the string representation of the current state of an object.
GetDumpTheclass now looks as follows:ObjectVisitor// `PropertyVisitor` is an abstract class that you must derive from. public class ObjectVisitor: PropertyVisitor{ private const int k_InitialIndent = 0; private readonly StringBuilder m_Builder = new StringBuilder(); private int m_IndentLevel = k_InitialIndent; private string Indent => new (' ', m_IndentLevel * 2); public void Reset() { m_Builder.Clear(); m_IndentLevel = k_InitialIndent; } public string GetDump() { return m_Builder.ToString(); }}
Get the top-level properties
Inside the class, override the method to visit each property of an object and log the property name. The doesn't require any members to implement and by default, it simply visits every property and performs no action.
ObjectVisitorVisitPropertyPropertyVisitor-
Inside theclass, add the following override
ObjectVisitormethod:VisitPropertyprotected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value){ m_Builder.AppendLine($"- {property.Name}");} -
Now that you have a minimal visitor, you can implement the utility method. Update themethod in the
PrintObjectDumpclass to create a newDebugUtilitiesinstance and use it to visit the properties of the given object:ObjectVisitorpublic static class DebugUtilities{ private static readonly ObjectVisitor s_Visitor = new(); public static void PrintObjectDump<T>(T value) { s_Visitor.Reset(); // This is the main entry point to run a visitor. PropertyContainer.Accept(s_Visitor, ref value); Debug.Log(s_Visitor.GetDump()); }} -
From an appropriate place in your code, call themethod and pass a
PrintObjectDumpobject to it. For example, from a simple MonoBehaviour component this might look as follows:Datausing UnityEngine;public class MyMonoBehaviour : MonoBehaviour{ void Start() { DebugUtilities.PrintObjectDump(new Data()); }}This prints the following output to the console:- Name- Vec2- Colors- Dict
Get the sub-properties
As the output from the previous section shows, when you override the method, it doesn't automatically visit the sub-properties of an object. To get the sub-properties, use the method to apply the visitor on each of the values recursively.
VisitPropertyPropertyContainer.AcceptIn the class, update the method to apply the visitor recursively on the value to nest in:
ObjectVisitorVisitPropertyprotected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value){ m_Builder.AppendLine($"{Indent}- {property.Name}"); ++m_IndentLevel; // Apply this visitor recursively on the value to nest in. if (null != value) PropertyContainer.Accept(this, ref value); --m_IndentLevel;}
The console output is now as follows:
- Name- Vec2- x- y- Colors- 0 - r - g - b - a- 1 - r - g - b - a - Dict- 5 - Key - Value
Display more information about each property
To further enhance the printed output, get the property name of collection items, and the type and value of each property. Certain properties have special names, especially when dealing with collection items. The conventions for property names are as follows:
- For list items, the name corresponds to the index.
- For dictionaries, the name is derived from the string version of the key value.
- For sets, the name is based on the string version of the value.
To make this distinction more explicit, enclose the property name in square brackets.
-
Inside theclass, add the following method:
ObjectVisitorprivate static string GetPropertyName(IProperty property){ return property switch { // You can also treat `IListElementProperty`, `IDictionaryElementProperty`, and `ISetElementProperty` separately. ICollectionElementProperty => $"[{property.Name}]", _ => property.Name };} -
Update themethod to use
VisitPropertyto retrieve the display name of a given type.TypeUtility.GetTypeDisplayNameprotected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value){ var propertyName = GetPropertyName(property); // Get the concrete type of the property or its declared type if value is null. var typeName = TypeUtility.GetTypeDisplayName(value?.GetType() ?? property.DeclaredValueType()); m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}"); ++m_IndentLevel; if (null != value) PropertyContainer.Accept(this, ref value); --m_IndentLevel;}The console output is now as follows:- Name = {string} Henry- Vec2 = {Vector2} (1.00, 1.00)- x = {float} 1- y = {float} 1- Colors = {List<Color>} System.Collections.Generic.List`1[UnityEngine.Color]- [1] = {Color} RGBA(0.000, 1.000, 0.000, 1.000) - r = {float} 0 - g = {float} 1 - b = {float} 0 - a = {float} 1- [1] = {Color} RGBA(1.000, 0.000, 0.000, 1.000) - r = {float} 1 - g = {float} 0 - b = {float} 0 - a = {float} 1- Dict = {Dictionary<int, string>} System.Collections.Generic.Dictionary`2[System.Int32,System.String]- [5] = {KeyValuePair<int, string>} [5, five] - Key = {int} 5 - Value = {string} five
Display less information for collection types
Because doesn't override the method, the list value is displayed as . To reduce the amount of information displayed, update the to use the utility method to only display the value for types that don't contain sub-properties, such as primitives, enums, and strings.
List<T>ToString()System.Collections.Generic.List1[UnityEngine.Color]VisitPropertyTypeTraits.IsContainerIn the class, update the method to use to determine whether the value is a container type. If it is, display the type name without the value. Otherwise, display the type name and the value:
ObjectVisitorVisitPropertyTypeTraits.IsContainerprotected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value){ var propertyName = GetPropertyName(property); var type = value?.GetType() ?? property.DeclaredValueType(); var typeName = TypeUtility.GetTypeDisplayName(type); // Only display the values for primitives, enums and strings. if (TypeTraits.IsContainer(type)) m_Builder.AppendLine($"{Indent}- {propertyName} {{{typeName}}}"); else m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}"); ++m_IndentLevel; if (null != value) PropertyContainer.Accept(this, ref value); --m_IndentLevel;}
The console output is now as follows:
- Name = {string} Henry- Vec2 {Vector2}- x = {float} 1- y = {float} 1- Colors {List<Color>}- [0] {Color} - r = {float} 0 - g = {float} 1 - b = {float} 0 - a = {float} 1- [1] {Color} - r = {float} 1 - g = {float} 0 - b = {float} 0 - a = {float} 1- Dict {Dictionary<int, string>}- [5] {KeyValuePair<int, string>} - Key = {int} 5 - Value = {string} five
PropertyVisitor.VisitCollectionPropertyVisitor.VisitListPropertyVisitor.VisitDictionaryPropertyVisitor.VisitSet
These are similar to the method, but they expose the generic parameters of their respective collections types.
VisitPropertyAdd type-specific overrides
Use along with . When an adapter is registered for a specific type, if the targeted type is encountered during the visitation, the adapter is called instead of the method.
PropertyVisitorIVisitPropertyAdapterVisitPropertyIn the class, add the for and :
ObjectVisitorIVisitPropertyAdapterVector2Colorpublic class DumpObjectVisitor : PropertyVisitor , IVisitPropertyAdapter<Vector2> , IVisitPropertyAdapter<Color>{ public DumpObjectVisitor() { AddAdapter(this); } void IVisitPropertyAdapter<Vector2>.Visit<TContainer>(in VisitContext<TContainer, Vector2> context, ref TContainer container, ref Vector2 value) { var propertyName = GetPropertyName(context.Property); m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Vector2)}}} {value}"); } void IVisitPropertyAdapter<Color>.Visit<TContainer>(in VisitContext<TContainer, Color> context, ref TContainer container, ref Color value) { var propertyName = GetPropertyName(context.Property); m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Color)}}} {value}"); }}
Start visitation on sub-properties
When you run a visitor on data, by default, it starts the visitation on the top-level object. For any property visitor, to start the visitation on sub-properties of an object, pass a to the method.
PropertyPathPropertyContainer.Accept-
Update themethod to take an optional
DebugUtilities:PropertyPathpublic static class DebugUtilities{ private static readonly ObjectVisitor s_Visitor = new(); public static void PrintObjectDump<T>(T value, PropertyPath path = default) { s_Visitor.Reset(); if (path.IsEmpty) PropertyContainer.Accept(s_Visitor, ref value); else PropertyContainer.Accept(s_Visitor, ref value, path); Debug.Log(s_Visitor.GetDump()); }} -
Call themethod with the
PrintObjectDumpobject. The console output is now as follows:Data- Name {string} = Henry- Vec2 {Vector2} = (1.00, 1.00)- Colors {List<Color>} - [0] = {Color} RGBA(0.000, 1.000, 0.000, 1.000) - [1] = {Color} RGBA(1.000, 0.000, 0.000, 1.000)- Dict {Dictionary<int, string>} - [5] {KeyValuePair<int, string>} - Key {int} = 5 - Value {string} = five
Complete visitor code
The complete code for the visitor class now looks as follows:
using System.Text; using Unity.Properties; using UnityEngine; public class ObjectVisitor : PropertyVisitor , IVisitPropertyAdapter<Vector2> , IVisitPropertyAdapter<Color> { private const int k_InitialIndent = 0; // StringBuilder to store the dumped object's properties and values. private readonly StringBuilder m_Builder = new StringBuilder(); private int m_IndentLevel = k_InitialIndent; // Helper property to get the current indentation. private string Indent => new(' ', m_IndentLevel * 2); public ObjectVisitor() { // Constructor, it initializes the ObjectVisitor and adds itself as an adapter // to handle properties of type Vector2 and Color. AddAdapter(this); } // Reset the visitor, clearing the StringBuilder and setting indentation to initial level. public void Reset() { m_Builder.Clear(); m_IndentLevel = k_InitialIndent; } // Get the string representation of the dumped object. public string GetDump() { return m_Builder.ToString(); } // Helper method to get the property name, handling collections and other property types. private static string GetPropertyName(IProperty property) { return property switch { // If it's a collection element property, display it with brackets ICollectionElementProperty => $"[{property.Name}]", // For other property types, display the name as it is _ => property.Name }; } // This method is called when visiting each property of an object. // It determines the type of the value and formats it accordingly for display. protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value) { var propertyName = GetPropertyName(property); // Get the type of the value or property. var type = value?.GetType() ?? property.DeclaredValueType(); var typeName = TypeUtility.GetTypeDisplayName(type); // Only display the values for primitives, enums, and strings, and treat other types as containers. if (TypeTraits.IsContainer(type)) m_Builder.AppendLine($"{Indent}- {propertyName} {{{typeName}}}"); else m_Builder.AppendLine($"{Indent}- {propertyName} = {{{typeName}}} {value}"); // Increase indentation level before visiting child properties (if any). ++m_IndentLevel; if (null != value) PropertyContainer.Accept(this, ref value); // Decrease indentation level after visiting child properties. --m_IndentLevel; } // This method is a specialized override for Vector2 properties. // It displays the property name and its value as a Vector2. void IVisitPropertyAdapter<Vector2>.Visit<TContainer>(in VisitContext<TContainer, Vector2> context, ref TContainer container, ref Vector2 value) { var propertyName = GetPropertyName(context.Property); m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Vector2)}}} {value}"); } // This method is a specialized override for Color properties. // It displays the property name and its value as a Color. void IVisitPropertyAdapter<Color>.Visit<TContainer>(in VisitContext<TContainer, Color> context, ref TContainer container, ref Color value) { var propertyName = GetPropertyName(context.Property); m_Builder.AppendLine($"{Indent}- {propertyName} = {{{nameof(Color)}}} {value}"); } }