Named Values

Named values are a huge part of the power and convenience of value and action resolvers. In short, they're contextual values that can be accessed by the string that is being resolved. When the resolved string is an expression, named values can be accessed by using the $named_value syntax, and when the resolved string is a referenced method, named values can be passed into parameters on that method.

All named values have a type and a name, and by default there are two named values that all resolvers have:

  • InspectorProperty property: The property instance that the string is being resolved on.
  • TValue value: The value of the property that the string is being resolved on - note that this named value only exists on properties that have values, and the type of the named value changes based on the value type of the property!

The property named value is very, very convenient - it means that any resolved string can always access details about its inspector context! For example, in the following example, Title will always be equal to the full path of the property it's on. This is done using the $named_value syntax of attribute expressions to access the named property value, and by referencing a method that takes an InspectorProperty argument called property.

// The two following examples are functionally equivalent.
// Expressions are shorter and more handy, but methods can hold more complex logic!

[Title("@$property.Path")]
public string expressionTitle;

[Title("$GetTitle")]
public string methodTitle;

private string GetTitle(InspectorProperty property)
{
    return property.Path;
}

Confused? Let's take a look at an example of an attribute that uses value resolvers with named values that most of you will be familiar with: ValidateInput.

To refresh your memory, the ValidateInput attribute can be used like this:

[ValidateInput("ValidateSomeString")]
public string someString;

private bool ValidateSomeString(string value, ref string message, ref InfoMessageType? messageType)
{
    if (string.IsNullOrEmpty(value))
    {
        message = "Value must not be empty!";
        messageType = InfoMessageType.Error;
        return false;
    }
    else if (value.Length < 10)
    {
        message = "The value is less than 10 characters long - are you sure this is right?";
        messageType = InfoMessageType.Warning;
        return false;
    }

    return true;
}

As you can see, the referenced method accepts several parameters that are unique to the ValidateInput attribute! However, if you try doing this as in the basic examples given in Using Value Resolvers and Using Action Resolvers, you'll simply get an error complaining about the extra parameters in the ValidateSomeString method.

What is happening here is that the implementing logic for the ValidateInput attribute (namely, the ValidateInputAttributeValidator class) has created a bunch of custom named values that its resolved strings can optionally make use of. Without getting into the full implementation of ValidateInputAttributeValidator, the relevant part of the code essentially looks like this:

var context = ValueResolverContext.CreateDefault<bool>(this.Property, this.Attribute.Condition, new NamedValue[]
{
    new NamedValue("message", typeof(string)),
    new NamedValue("messageType", typeof(InfoMessageType?)),
});

context.SyncRefParametersWithNamedValues = true;

this.validationChecker = ValueResolver.GetFromContext<bool>(ref context);

There's a few new things going on here.

First, it's adding two extra named values: message of type string, and messageType of type InfoMessageType? (a nullable enum value). Further down in its implementation, it then makes use of them. But wait, where's the value named parameter? Well, as we mentioned above, it will always exist when there is a value - as such, there's no need to create it again.

Second, we're creating our own resolver context up front, as well as using a new resolver call: ValueResolver.GetFromContext<T>(ref ValueResolverContext context). This lets us set values on the context before we pass it into the resolver system. We wouldn't need to do this if we just wanted extra named values, though; the "quick and dirty" resolver APIs all let you pass in a params array of named values. The reason we're doing it here is that we need to specify SyncRefParametersWithNamedValues as being true.

What is up with setting SyncRefParametersWithNamedValues = true on the resolver context? This is used because ValidateInputAttributeValidator doesn't just pass down values for the validation logic to make use of; it expects the validation logic to change those values when they're passed by ref or out, and then it makes use of those changed parameters. Setting this value to true tells the resolver that it should be fetching any changes made to those parameters, such that ValidateInputAttributeValidator can use them to determine which message and error type to display. We need to tell it this value up front because it changes the constraints of how named values can be matched to method parameters; if you try to change SyncRefParametersWithNamedValues after a resolver has been created, you will simply get an exception.

If that seems complicated, don't worry! It's rare that you'll need to make use of this option.

Let's try to make our own, slightly more simple and straight-forward example: a formatted time displaying attribute that provides the current time in hours, minutes and seconds and displays the result as a label in the inspector. Let's start with just the basic setup that we need for this.

You can see this tutorial for the basics of creating attribute drawers:

// DisplayFormattedDateAttribute.cs
using System;

public class DisplayFormattedDateAttribute : Attribute
{
    public string FormattedDate;

    public DisplayFormattedDateAttribute(string formattedDate)
    {
        this.FormattedDate = formattedDate;
    }
}

// DisplayFormattedDateAttributeDrawer.cs
using Sirenix.OdinInspector.Editor;
using Sirenix.OdinInspector.Editor.ValueResolvers;
using UnityEngine;

public class DisplayFormattedDateAttributeDrawer : OdinAttributeDrawer<DisplayFormattedDateAttribute>
{
    private ValueResolver<string> formattedDateResolver;
    
    protected override void Initialize()
    {
        this.formattedDateResolver = ValueResolver.GetForString(this.Property, this.Attribute.FormattedDate);
    }
    
    protected override void DrawPropertyLayout(GUIContent label)
    {
        if (this.formattedDateResolver.HasError)
        {
            this.formattedDateResolver.DrawError();
        }
        else
        {
            GUILayout.Label(this.formattedDateResolver.GetValue());
        }
        
        this.CallNextDrawer(label);
    }
}

Here we have a simple attribute implementation that will take a string, resolve it, and then display it as a label above a given property. Now let's add the named values that we discussed, and update them just before we resolve the string every frame.

// DisplayFormattedDateAttributeDrawer.cs
using Sirenix.OdinInspector.Editor;
using Sirenix.OdinInspector.Editor.ValueResolvers;
using UnityEngine;

public class DisplayFormattedDateAttributeDrawer : OdinAttributeDrawer<DisplayFormattedDateAttribute>
{
    private ValueResolver<string> formattedDateResolver;
    
    protected override void Initialize()
    {
        this.formattedDateResolver = ValueResolver.GetForString(this.Property, this.Attribute.FormattedDate, new NamedValue[]
        {
            new NamedValue("hour", typeof(int)),
            new NamedValue("minute", typeof(int)),
            new NamedValue("second", typeof(int)),
        });
    }
    
    protected override void DrawPropertyLayout(GUIContent label)
    {
        if (this.formattedDateResolver.HasError)
        {
            this.formattedDateResolver.DrawError();
        }
        else
        {
            var time = DateTime.Now;
            this.formattedDateResolver.Context.NamedValues.Set("hour", time.Hour);
            this.formattedDateResolver.Context.NamedValues.Set("minute", time.Minute);
            this.formattedDateResolver.Context.NamedValues.Set("second", time.Second);
            
            GUILayout.Label(this.formattedDateResolver.GetValue());
        }
        
        this.CallNextDrawer(label);
    }
}

Now we'll be able to use these named values when we declare the attribute:

// Example.cs
using UnityEngine;

public class Example : MonoBehaviour
{
    [DisplayFormattedDate("@$hour + \":\" + $minute + \":\" + $second")]
    public string datedString;
}

And there we are - an attribute that provides custom contextual named values to its resolved string.