This example demonstrates how to use the PropertyVisitor
base class to create a property visitor. For an equivalent example that uses the IPropertyBagVisitor
and IPropertyVisitor
interfaces, refer to Use low-level APIs to create a property visitor.
This example includes step-by-step instructions to create a property visitor that prints the current state of an object to the console.
Assume you have the following type:
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 a utility method DebugUtilities
like this:
public static class DebugUtilities
{
public static void PrintObjectDump<T>(T value)
{
// Magic goes here.
}
}
Call the PrintObjectDump
method with the Data
object like this:
DebugUtilities.PrintObjectDump(new Data());
Would print the following to the console:
- 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
First, create a DumpObjectVisitor
class. Inside the class, use a StringBuilder to build a string that represents the current state of an object.
Create a DumpObjectVisitor
class that inherits from PropertyVisitor
.
Add a StringBuilder
field to the class.
Add a Reset
method that clears the StringBuilder
and resets the indent level.
Add a GetDump
method that returns the string representation of the current state of an object.
Your finished class looks like this:
// `PropertyVisitor` is an abstract class that you must subclass from it.
public class DumpObjectVisitor: 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();
}
}
Inside the DumpObjectVisitor
class, override the VisitProperty
method to visit each property of an object and log the property name. The PropertyVisitor
doesn’t require any members to implement and by default, it simply visits every property and performs no action.
Inside the DumpObjectVisitor
class, add the following override VisitProperty
method:
protected 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 the PrintObjectDump
method in the DebugUtilities
class to create a new DumpObjectVisitor
instance and use it to visit the properties of the given object:
public static class DebugUtilities
{
private static readonly DumpObjectVisitor 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());
}
}
This gets the following output:
- Name
- Vec2
- Colors
- Dict
The output from the previous section indicates that when you override the VisitProperty
method, it doesn’t automatically visit the sub-properties of an object. To get the sub-properties, use the PropertyContainer.Accept
method to apply the visitor on each of the values recursively.
Inside the DebugUtilities
class, update the VisitProperty
method to apply the visitor recursively on the value to nest in:
protected override void VisitProperty<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container, ref TValue value)
{
m_Builder.AppendLine($"{Indent}- {property.Name}");
++m_IdentLevel;
// Apply this visitor recursively on the value to nest in.
if (null != value)
PropertyContainer.Accept(this, ref value);
--m_IdentLevel;
}
This gets the following output:
- Name
- Vec2
- x
- y
- Colors
- 0
- r
- g
- b
- a
- 1
- r
- g
- b
- a
- Dict
- 5
- Key
- Value
Next, let’s 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. Here are the conventions for property names:
To make this distinction more explicit, encase the property name with square brackets.
Inside the DumpObjectVisitor
class, add the following method:
private static string GetPropertyName(IProperty property)
{
return property switch
{
// You can also treat `IListElementProperty`, `IDictionaryElementProperty`, and `ISetElementProperty` separately.
ICollectionElementProperty => $"[{property.Name}]",
_ => property.Name
};
}
Update the VisitProperty
method to use TypeUtility.GetTypeDisplayName
to retrieve the display name of a given type.
protected 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;
}
This gets the following output:
- 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
Because List<T>
doesn’t override the ToString()
method, the list value is displayed as System.Collections.Generic.List1[UnityEngine.Color]
. To reduce the amount of information displayed, update the VisitProperty
to use the TypeTraits.IsContainer
utility method to only display the value for types that don’t contain sub-properties, such as primitives, enums and strings.
Inside the DumpObjectVisitor
class, update the VisitProperty
method to use TypeTraits.IsContainer
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.
protected 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;
}
This gets the following output:
- 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
Tip:
To reduce the amount of information displayed, you can also use the following methods to override a Visit
specialization for the collection types:
protected override void VisitCollection<TContainer, TCollection, TElement>(Property<TContainer, TCollection> property, ref TContainer container, ref TCollection value) {}
protected override void VisitList<TContainer, TList, TElement>(Property<TContainer, TList> property, ref TContainer container, ref TList value) {}
protected override void VisitDictionary<TContainer, TDictionary, TKey, TValue>(Property<TContainer, TDictionary> property, ref TContainer container, ref TDictionary value) {}
protected override void VisitSet<TContainer, TSet, TValue>(Property<TContainer, TSet> property, ref TContainer container, ref TSet value) {}
These are similar to the VisitProperty
method, but they expose the generic parameters of their respective collections types.
Lastly, add per-type overrides to display Vector2
and Color
types in a more compact manner.
Use PropertyVisitor
along with IVisitPropertyAdapter
. Whenever an adapter is registered for a given type, if the targeted type is encountered during the visitation, the adapter is called instead of the VisitProperty
method:
Inside the DumpObjectVisitor
class, add the IVisitPropertyAdapter
for Vector2
and Color
:
public 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}");
}
}
The completed DumpObjectVisitor
class looks like this:
public class DumpObjectVisitor
: 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 DumpObjectVisitor()
{
// Constructor, it initializes the DumpObjectVisitor 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}");
}
}
When you run a visitor on data, by default, it starts the visitation on the given object directly. For any property visitor, to start the visitation on sub-properties of an object, pass a PropertyPath
to the PropertyContainer.Accept
method.
Update the DebugUtilities
method to take an optional PropertyPath
:
public static class DebugUtilities
{
private static readonly DumpObjectVisitor 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 the PrintObjectDump
method with the Data
object. This gets the desired output.