Create a property visitor with low-level APIs
Learn how to use the IPropertyBagVisitor and IPropertyVisitor interfaces to create a property visitor from an example.
Read time 8 minutesLast updated 10 days ago
This example demonstrates how to create a property visitor with the low-level and interfaces. It produces the same outcome as the Create a property visitor with the PropertyVisitor class example but with a visitor that implements these interfaces rather than one that derives from .
IPropertyBagVisitorIPropertyVisitorPropertyVisitorCreate data and utility classes
-
Create the sameand
Dataclasses as in the Create a property visitor with the PropertyVisitor class example.DebugUtilities -
Modify theclass to use the low-level visitor instead of the
DebugUtilities-derived visitor as follows:PropertyVisitorpublic static class DebugUtilities{ private static readonly LowLevelVisitor 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()); }}
Create the visitor
Create the low-level visitor class as follows:
-
Create aclass that implements the
LowLevelVisitorandIPropertyVisitorinterfaces.IPropertyBagVisitor -
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:LowLevelVisitorpublic class LowLevelVisitor : IPropertyBagVisitor , IPropertyVisitor{ private const int k_InitialIndent = 0; private readonly StringBuilder m_Builder = new StringBuilder(); private int m_IndentLevel = k_InitialIndent; public void Reset() { m_Builder.Clear(); m_IndentLevel = k_InitialIndent; } public string GetDump() { return m_Builder.ToString(); }}
Get the properties
To call the method on the property using , implement the interface. This interface allows you to specify the visitation behavior when visiting a property, similar to the method:
AcceptthisIPropertyVisitorPropertyVisitorVisitProperty-
In theclass, override the
LowLevelVisitorandIPropertyBagVisitor.Visitmethods:IPropertyVisitor.Visitvoid IPropertyBagVisitor.Visit<TContainer>(IPropertyBag<TContainer> propertyBag, ref TContainer container){ foreach (var property in propertyBag.GetProperties(ref container)) { property.Accept(this, ref container); }} void IPropertyVisitor.Visit<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container){ var value = property.GetValue(ref container); // Code goes here.} -
Theadapters used with the
IVisitPropertyAdapterbase class require access to the internal state of the visitor, so they can't be used outside of that class. However, you can define domain-specific adapters that have the necessary information. In the same file as thePropertyVisitorclass, defineLowLevelVisitorandIPrintValueinterfaces for these adapters and aIPrintValue<in T>struct to encapsulate the information the adapters need to format a property value as follows:PrintContext// Create the following struct with methods to encapsulate the formatting of the message and display the value.public readonly struct PrintContext{ private StringBuilder Builder { get; } private string Prefix { get; } public string PropertyName { get; } public void Print<T>(T value) { Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(value?.GetType() ?? typeof(T))}}} {value}"); } public void Print(Type type, string value) { Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(type)}}} {value}"); } public PrintContext(StringBuilder builder, string prefix, string propertyName) { Builder = builder; Prefix = prefix; PropertyName = propertyName; }}public interface IPrintValue{}public interface IPrintValue<in T> : IPrintValue{ void PrintValue(in PrintContext context, T value);} -
In theclass, override the adapter interface method to create type-specific adapters for
LowLevelVisitorandVector2, and update the implementation of theColorto use the adapter first:IPropertyVisitorpublic class LowLevelVisitor : IPropertyBagVisitor , IPropertyVisitor , IPrintValue<Vector2> , IPrintValue<Color>{ public IPrintValue Adapter { get; set; } public LowLevelVisitor() { // For simplicity Adapter = this; } void IPropertyVisitor.Visit<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container) { // Here, we need to manually extract the value. var value = property.GetValue(ref container); var propertyName = GetPropertyName(property); // We can still use adapters, but we must manually dispatch the calls. if (Adapter is IPrintValue<TValue> adapter) { var context = new PrintContext(m_Builder, Indent, propertyName); adapter.PrintValue(context, value); return; } // Fallback behaviour here } void IPrintValue<Vector2>.PrintValue(in PrintContext context, Vector2 value) { context.Print(value); } void IPrintValue<Color>.PrintValue(in PrintContext context, Color value) { const string format = "F3"; var formatProvider = CultureInfo.InvariantCulture.NumberFormat; context.Print(typeof(Color), $"RGBA({value.r.ToString(format, formatProvider)}, {value.g.ToString(format, formatProvider)}, {value.b.ToString(format, formatProvider)}, {value.a.ToString(format, formatProvider)})"); }}
Complete visitor code
The complete code for the low-level visitor class and the custom adapters for and looks as follows:
ColorVector2using UnityEngine; using System.Globalization; using System.Text; using Unity.Properties; public readonly struct PrintContext { // A context struct to hold information about how to print the property. private StringBuilder Builder { get; } private string Prefix { get; } public string PropertyName { get; } // Method to print the value of type T with its associated property name. public void Print<T>(T value) { Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(value?.GetType() ?? typeof(T))}}} {value}"); } // Method to print the value with a specified type and its associated property name. public void Print(System.Type type, string value) { Builder.AppendLine($"{Prefix}- {PropertyName} = {{{TypeUtility.GetTypeDisplayName(type)}}} {value}"); } // Constructor to initialize the PrintContext. public PrintContext(StringBuilder builder, string prefix, string propertyName) { Builder = builder; Prefix = prefix; PropertyName = propertyName; } } // Generic interface IPrintValue that acts as a marker interface for all print value adapters. public interface IPrintValue { } // Generic interface IPrintValue<T> to define how to print values of type T. // This interface is used as an adapter for specific types (Vector2 and Color in this case). public interface IPrintValue<in T> : IPrintValue { void PrintValue(in PrintContext context, T value); } // LowLevelVisitor class that implements various interfaces for property visiting and value printing. public class LowLevelVisitor : IPropertyBagVisitor, IPropertyVisitor, IPrintValue<Vector2>, IPrintValue<Color> { private const int k_InitialIndent = 0; private readonly StringBuilder m_Builder = new StringBuilder(); private int m_IndentLevel = k_InitialIndent; public IPrintValue Adapter { get; set; } public LowLevelVisitor() { // The Adapter property is set to this instance of LowLevelVisitor. // This means the current LowLevelVisitor can be used as a print value adapter for Vector2 and Color. Adapter = 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 a property bag (a collection of properties) void IPropertyBagVisitor.Visit<TContainer>(IPropertyBag<TContainer> propertyBag, ref TContainer container) { foreach (var property in propertyBag.GetProperties(ref container)) { // Call the Visit method of IPropertyVisitor to handle individual properties. property.Accept(this, ref container); } } // This method is called when visiting each individual property of an object. // It tries to find a suitable adapter (IPrintValue<T>) for the property value type (TValue) and uses it to print the value. // If no suitable adapter is found, it falls back to displaying the value using its type name. void IPropertyVisitor.Visit<TContainer, TValue>(Property<TContainer, TValue> property, ref TContainer container) { // Here, we need to manually extract the value. var value = property.GetValue(ref container); var propertyName = GetPropertyName(property); // We can still use adapters, but we must manually dispatch the calls. // Try to find an adapter for the current property value type (TValue). if (Adapter is IPrintValue<TValue> adapter) { // If an adapter is found, create a print context and call the PrintValue method of the adapter. var context = new PrintContext(m_Builder, m_IndentLevel.ToString(), propertyName); adapter.PrintValue(context, value); return; } // Fallback behavior here - if no adapter is found, handle printing based on type information. var type = value?.GetType() ?? property.DeclaredValueType(); var typeName = TypeUtility.GetTypeDisplayName(type); if (TypeTraits.IsContainer(type)) m_Builder.AppendLine($"{m_IndentLevel}- {propertyName} {{{typeName}}}"); else m_Builder.AppendLine($"{m_IndentLevel}- {propertyName} = {{{typeName}}} {value}"); // Recursively visit child properties (if any). ++m_IndentLevel; if (null != value) PropertyContainer.Accept(this, ref value); --m_IndentLevel; } // Method from IPrintValue<Vector2> used to print Vector2 values. void IPrintValue<Vector2>.PrintValue(in PrintContext context, Vector2 value) { // Simply use the Print method of PrintContext to print the Vector2 value. context.Print(value); } // Method from IPrintValue<Color> used to print Color values. void IPrintValue<Color>.PrintValue(in PrintContext context, Color value) { const string format = "F3"; var formatProvider = CultureInfo.InvariantCulture.NumberFormat; // Format and print the Color value in RGBA format. context.Print(typeof(Color), $"RGBA({value.r.ToString(format, formatProvider)}, {value.g.ToString(format, formatProvider)}, {value.b.ToString(format, formatProvider)}, {value.a.ToString(format, formatProvider)})"); } }
When you have the complete version of this code in your file, call the method with an instance of the class to verify that it produces the same console output as in the final stage of the Create a property visitor with the PropertyVisitor class example:
LowLevelVisitor.csDebugUtilities.PrintObjectDumpDatausing UnityEngine;public class MyMonoBehaviour : MonoBehaviour{ void Start() { DebugUtilities.PrintObjectDump(new Data()); }}