Custom element with DateTime attribute

Greetings!

I have a custom UI element I’m building that will have a DateTime attribute. I implemented an UxmlAttributeConverter for it, but I keep getting ArgumentNullException thrown on the editor every time I select my custom element on the builder.

My most basic example looks like this:

[UxmlElement]
public partial class MyElement : VisualElement
{
   [UxmlAttribute]
   public DateTime date { get; set; } = DateTime.Today;
}

public class DateTimeUxmlAttributeConverter: UxmlAttributeConverter<DateTime>
{
   public override DateTime FromString(string v)
   {
      return (v == null || !DateTime.TryParse(v, CultureInfo.CurrentCulture, DateTimeStyles.None ,out DateTime value)) ? new DateTime(2024, 1, 1) : value;
   }

   public override string ToString(DateTime value)
   {
      return value.ToString(CultureInfo.CurrentCulture);
   }
}
Here is the Exception log
ArgumentNullException: Value cannot be null.
Parameter name: property
  at UnityEditor.UIElements.Bindings.DefaultSerializedObjectBindingImplementation.TrackPropertyValue (UnityEngine.UIElements.VisualElement element, UnityEditor.SerializedProperty property, System.Action`2[T1,T2] callback) [0x0000a] in <9b2f3888bcb940d9b1c9b5b968556914>:0 
  at UnityEditor.UIElements.BindingExtensions.TrackPropertyValue (UnityEngine.UIElements.VisualElement element, UnityEditor.SerializedProperty property, System.Action`2[T1,T2] callback) [0x00001] in <5d5fdc1eeb2a4f39b26caadc7b79ec7e>:0 
  at Unity.UI.Builder.BuilderUxmlAttributesView.TrackElementPropertyValue (UnityEngine.UIElements.VisualElement target, UnityEditor.SerializedProperty property) [0x00001] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderUxmlAttributesView.TrackElementPropertyValue (UnityEngine.UIElements.VisualElement target, System.String path) [0x0000e] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderUxmlAttributesView.CreateSerializedAttributeRow (UnityEditor.UIElements.UxmlSerializedAttributeDescription attribute, System.String propertyPath, UnityEngine.UIElements.VisualElement parent) [0x000ec] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderInspectorAttributes.CreateSerializedAttributeRow (UnityEditor.UIElements.UxmlSerializedAttributeDescription attribute, System.String propertyPath, UnityEngine.UIElements.VisualElement parent) [0x00001] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderUxmlAttributesView.GenerateSerializedAttributeFields (UnityEditor.UIElements.UxmlSerializedDataDescription dataDescription, Unity.UI.Builder.BuilderUxmlAttributesView+UxmlAssetSerializedDataRoot parent) [0x00094] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderUxmlAttributesView.GenerateSerializedAttributeFields () [0x0002c] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderUxmlAttributesView.Refresh () [0x00045] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderInspectorAttributes.Refresh () [0x0001b] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderInspector.<HierarchyChanged>b__134_0 () [0x00000] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at UnityEngine.UIElements.VisualElement+SimpleScheduledItem.PerformTimerUpdate (UnityEngine.UIElements.TimerState state) [0x0000c] in <a846d985d43b4514a3d8db65dbdc899a>:0 
  at UnityEngine.UIElements.TimerEventScheduler.UpdateScheduledEvents () [0x000f2] in <a846d985d43b4514a3d8db65dbdc899a>:0 
  at UnityEngine.UIElements.UIElementsUtility.UnityEngine.UIElements.IUIElementsUtility.UpdateSchedulers () [0x0003c] in <a846d985d43b4514a3d8db65dbdc899a>:0 
  at UnityEngine.UIElements.UIEventRegistration.UpdateSchedulers () [0x00018] in <a846d985d43b4514a3d8db65dbdc899a>:0 
  at UnityEditor.RetainedMode.UpdateSchedulers () [0x00001] in <9b2f3888bcb940d9b1c9b5b968556914>:0 

I have done this before on Unity 2022 using a TypedUxmlAttributeDescription, but I can’t make the same approach work on Unity 6.
I also played around with the example AttributeConverter on this docs page and it worked fine for me.

This is because DateTime is not a supported type and can not be Serialized, it wont work with a SerializedProperty so when the builder tries to show it an error occurs.
If the type wont appear in the inspector when added to a MonoBehaviour or ScriptableObject then its unsupported. We do have a few exceptions such as Type which are mentioned in the docs you referenced.

To work around this we use a type that can be serialized to wrap the field. For example use a string and then another property to do the conversion. The UxmlAttributeConverter is for converting the value for UXML use, its not used in the serialization.
You can also add a custom property drawer using an attribute so you get a nice DateTime picker.

E.G

[UxmlElement]
public partial class MyElement : VisualElement
{
   public DateTime date { get; set; } = DateTime.Today;

   [UxmlAttribute("date")]
   internal string dateString 
   {
      get
      {
         return date.ToString(CultureInfo.CurrentCulture);
      }

      set 
      {
         date = (v == null || !DateTime.TryParse(v, CultureInfo.CurrentCulture, DateTimeStyles.None ,out DateTime value)) ? new DateTime(2024, 1, 1) : value;
      }
   }
}

Ill create a bug so we can improve the way we handle unsupported types instead of throwing exceptions.

Hi, Karl, thanks for your response.

In that case it looks like upgrading to Unity 6 will loose functionality, am I correct?

In an existing project (we are preparing to upgrade to Unity 6) I wrote a DateTimeField that inherits from BaseField<DateTime>, with the aforementioned TypedUxmlAttributeDescription. This worked, and other devs could use it as any other field and access the DateTime value attribute as they saw fit.

I don’t see a way to have this work in Unity 6, we will have to use a BaseField<string> as a parent, and therefore the field value will not be of the type DateTime.
There will have to be another variable of the type DateTime that changes the BaseField string value.

Is there a more elegant way to go around this that I’m not thinking of? Please advise on the best way to proceed, this upgrade will happen soon.

Its still possible. The serialized data needs to be a string but the field can still be the same, you need to convert between them in the property drawer.

One way to do this is to use an attribute to link a property drawer:

public class MyDrawerAttribute : PropertyAttribute { }

[UxmlElement]
public partial class MyElement : VisualElement
{
   public DateTime date { get; set; } = DateTime.Today;

   [MyDrawerAttribute]
   [UxmlAttribute("date")]
   internal string dateString 
   {
      get => date.ToString(CultureInfo.CurrentCulture);
      set => date = (v == null || !DateTime.TryParse(v, CultureInfo.CurrentCulture, DateTimeStyles.None ,out DateTime value)) ? new DateTime(2024, 1, 1) : value;
   }
}

The property drawer can do something like this:

pseudo code(wont compile):

[CustomPropertyDrawer(typeof(MyDrawerAttribute))]
public class MyDrawerAttributePropertyDrawer : PropertyDrawer
{
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
       var field = new DateTimeField();
       field.value = property.stringValue to DateTime;

       // Undo/Redo support
       field.TrackPropertyValue(property, property =>
       {
         field.value = property.stringValue to DateTime;
       });

       field.RegisterChangeEvent(evt =>
       {
         property.stringValue = evt.newValue to string;
         property.serializedObject.ApplyModifiedProperties();
       });
       return field;
    }
}

All of this is for 2 reasons:

  1. Support editing in the UI Builder
  2. Allow for saving the uxml attribute value, this is done through Unity serialization so needs to be serializable.

The UxmlTraits system would apply the string value at runtime so you could do the conversion then, in the player. The new system does it all in the Editor and saves a serialized object which then gets applied in the runtime. So the type must be serializable or you risk losing data.

Thanks, Karl, I think I understand.

Just to be clear, my custom element could not inherit from BaseField<DateTime> anymore to make it work correctly, right?

And making it inherit from BaseField<string> will make the “value” attribute a string, so I would need to make my element inherit from VisualElement and implement the functionalities of a BaseField myself. So that “value” is a DateTime and other devs can use it like any other existing Field.

I have a follow-up question, then: If I decided to keep our old Factory code for a while longer, I still can’t make it work in Unity 6. This simpler element keeps throwing NullReferenceExceptions:

public class MyElement : BaseField<DateTime>
{
   public new class UxmlFactory : UxmlFactory<MyElement, UxmlTraits> { }

   public new class UxmlTraits : BaseFieldTraits<DateTime, UxmlDateTimeAttributeDescription>
   {  
   }

   public MyElement() : base(null, null)
   {  
   }
}
public class UxmlDateTimeAttributeDescription : TypedUxmlAttributeDescription<DateTime>
{   // [...]
}
Here is the Exception log
NullReferenceException: Object reference not set to an instance of an object
  at Unity.UI.Builder.BuilderUxmlAttributesView.CreateTraitsAttributeField (UnityEngine.UIElements.UxmlAttributeDescription attribute) [0x0001e] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderUxmlAttributesView.CreateTraitsAttributeRow (UnityEngine.UIElements.UxmlAttributeDescription attribute, UnityEngine.UIElements.VisualElement parent) [0x00001] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderUxmlAttributesView.GenerateUxmlTraitsAttributeFields () [0x00034] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderUxmlAttributesView.Refresh () [0x0003a] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderInspectorAttributes.Refresh () [0x0001b] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at Unity.UI.Builder.BuilderInspector.<HierarchyChanged>b__134_0 () [0x00000] in <dbb0fdfe7071441db865136fb095d0a0>:0 
  at UnityEngine.UIElements.VisualElement+SimpleScheduledItem.PerformTimerUpdate (UnityEngine.UIElements.TimerState state) [0x0000c] in <a846d985d43b4514a3d8db65dbdc899a>:0 
  at UnityEngine.UIElements.TimerEventScheduler.UpdateScheduledEvents () [0x000f2] in <a846d985d43b4514a3d8db65dbdc899a>:0 
  at UnityEngine.UIElements.UIElementsUtility.UnityEngine.UIElements.IUIElementsUtility.UpdateSchedulers () [0x0003c] in <a846d985d43b4514a3d8db65dbdc899a>:0 
  at UnityEngine.UIElements.UIEventRegistration.UpdateSchedulers () [0x00018] in <a846d985d43b4514a3d8db65dbdc899a>:0 
  at UnityEditor.RetainedMode.UpdateSchedulers () [0x00001] in <9b2f3888bcb940d9b1c9b5b968556914>:0 

Is there something I can change here to make it work until I have time to redo the element?

It really depends on what the end goal is for your element.
I wrote a blog post about the new system here GitHub - karljj1/UxmlSerializedDataBlogPost, it may help you to understand the restrictions.

The type used in BaseField<T> needs to be serializable if you want to use the element in UXML or the UI Builder, otherwise it wont be able to save the value field. There is however a way to workaround this limitation to allow you to continue to use BaseField<DateTime>. You can use an attribute override to change the type to a string.

[UxmlElement]
public partial class MyElement : BaseField<DateTime>
{
   // Override "value"
   [UxmlAttribute("value")] 
   string valueOverride 
   { 
        get => // convert .value to string
        set => // convert string to .value
    }
}

This will now let you serialize the value in UXML and the UI Builder but keep the field operating as a BaseField<DateTime>.
If you then want to have a custom property drawer for the “value” field in the UI Builder inspector you can do what I did in my earlier example.

Looks like a bug, please file a bug report Unity QA: Building quality with passion