This example demonstrates how to use the low-level APIs with the IPropertyBagVisitor
and IPropertyVisitor
interfaces to create a property visitor. This example is equivalent to the example that uses the PropertyVisitor
base class 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 that implements the IPropertyBagVisitor
. Inside the class, use a StringBuilder to build a string that represents the current state of an object.
Create a DumpObjectVisitor
class that implements the IPropertyBagVisitor
interfaces.
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 DumpObjectVisitor
class looks like this:
public class DumpObjectVisitor
: 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();
}
}
Inside the DumpObjectVisitor
class, override the IPropertyBagVisitor.Visit
method to loop through the properties of the container object. In the object dump visitor, display the values and delegate the visitation to the properties.
To call the Accept
method on the property using this
, implement the IPropertyVisitor
interface. This interface allows you to specify the visitation behavior when visiting a property, similar to the VisitProperty
method of the PropertyVisitor
class.
Inside the DumpObjectVisitor
class, add override the IPropertyBagVisitor.Visit
and IPropertyVisitor.Visit
methods.
void 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.
}
The IVisitPropertyAdapter
adapters used with the PropertyVisitor
base 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. Inside the DumpObjectVisito
class, update the implementation of the IPropertyVisitor
to use the adapter first:
// Create the following 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);
}
public class DumpObjectVisitor
: IPropertyBagVisitor
, IPropertyVisitor
, IPrintValue<Vector2>
, IPrintValue<Color>
{
public IPrintValue Adapter { get; set; }
public DumpObjectVisitor()
{
// 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)})");
}
}
The completed code looks like this:
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(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);
}
// DumpObjectVisitor class that implements various interfaces for property visiting and value printing
private class DumpObjectVisitor : IPropertyBagVisitor, IPropertyVisitor, IPrintValue<Vector2>, IPrintValue<Color>
{
// (Other members are omitted for brevity)
public IPrintValue Adapter { get; set; }
public DumpObjectVisitor()
{
// The Adapter property is set to this instance of DumpObjectVisitor
// This means the current DumpObjectVisitor can be used as a print value adapter for Vector2 and Color.
Adapter = this;
}
// 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, Indent, 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($"{Indent}- {propertyName} {{{typeName}}}");
else
m_Builder.AppendLine($"{Indent}- {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 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.
Did you find this page useful? Please give it a rating:
Thanks for rating this page!
What kind of problem would you like to report?
Thanks for letting us know! This page has been marked for review based on your feedback.
If you have time, you can provide more information to help us fix the problem faster.
Provide more information
You've told us this page needs code samples. If you'd like to help us further, you could provide a code sample, or tell us about what kind of code sample you'd like to see:
You've told us there are code samples on this page which don't work. If you know how to fix it, or have something better we could use instead, please let us know:
You've told us there is information missing from this page. Please tell us more about what's missing:
You've told us there is incorrect information on this page. If you know what we should change to make it correct, please tell us:
You've told us this page has unclear or confusing information. Please tell us more about what you found unclear or confusing, or let us know how we could make it clearer:
You've told us there is a spelling or grammar error on this page. Please tell us what's wrong:
You've told us this page has a problem. Please tell us more about what's wrong:
Thank you for helping to make the Unity documentation better!
Your feedback has been submitted as a ticket for our documentation team to review.
We are not able to reply to every ticket submitted.
When you visit any website, it may store or retrieve information on your browser, mostly in the form of cookies. This information might be about you, your preferences or your device and is mostly used to make the site work as you expect it to. The information does not usually directly identify you, but it can give you a more personalized web experience. Because we respect your right to privacy, you can choose not to allow some types of cookies. Click on the different category headings to find out more and change our default settings. However, blocking some types of cookies may impact your experience of the site and the services we are able to offer.
More information
These cookies enable the website to provide enhanced functionality and personalisation. They may be set by us or by third party providers whose services we have added to our pages. If you do not allow these cookies then some or all of these services may not function properly.
These cookies allow us to count visits and traffic sources so we can measure and improve the performance of our site. They help us to know which pages are the most and least popular and see how visitors move around the site. All information these cookies collect is aggregated and therefore anonymous. If you do not allow these cookies we will not know when you have visited our site, and will not be able to monitor its performance.
These cookies may be set through our site by our advertising partners. They may be used by those companies to build a profile of your interests and show you relevant adverts on other sites. They do not store directly personal information, but are based on uniquely identifying your browser and internet device. If you do not allow these cookies, you will experience less targeted advertising. Some 3rd party video providers do not allow video views without targeting cookies. If you are experiencing difficulty viewing a video, you will need to set your cookie preferences for targeting to yes if you wish to view videos from these providers. Unity does not control this.
These cookies are necessary for the website to function and cannot be switched off in our systems. They are usually only set in response to actions made by you which amount to a request for services, such as setting your privacy preferences, logging in or filling in forms. You can set your browser to block or alert you about these cookies, but some parts of the site will not then work. These cookies do not store any personally identifiable information.