DEV Community

Cover image for Why I built an Umbraco package that does "nothing"
Jason Elkin
Jason Elkin

Posted on

Why I built an Umbraco package that does "nothing"

Well, maybe not nothing, but definitely null.

Consider this problem: You have a temperature property on a page, and that property is optional. How do you distinguish between no value, and 0°C in your code?

By default, a blank decimal or integer property in the Umbraco backoffice will come out the other side of the default property value converter with the default value of 0. This means to be able to differentiate between an empty value and 0 you need to do some extra work.

How I used to do it

Animation of adding nested content in the Umbraco backoffice containing just one Temperature property

Nested content. It works out-of-the-box and is very intentional. The property inside the nested content needs to be required and the editor can choose to "add" a temperature value, or leave it empty.

But it's not pretty, and it's a rather clunky flow for editors.

The backoffice already knows the difference

The backoffice already distinguishes between an empty value and a 0 in a decimal/integer property. In this screenshot Temperature is "0", but Temperature 2 is empty.
Two Umbraco decimal properties, one is empty, the other is zero.

So all we need is a nice way of differentiating between empty and 0 values in our code.

Make those values nullable!

This is a good place to make use of nullable value types, because these are nullable values! Umbraco even already stores them as NULL in the database. So instead of our decimal property returning a decimal value type, we can make it return a decimal? type.

Do it yourself

To do this we'll need to use a new property value converter that can return a null.

Inherit from (or use the decorator pattern) to make a new class that replaces the ConvertSourceToIntermedia Method of the Umbraco.Cms.Core.PropertyEditors.ValueConverters class. Making it nullable is actually quite straightforward.

public object? ConvertSourceToIntermediate(
    IPublishedElement owner,
    IPublishedPropertyType propertyType,
    object? source,
    bool preview)
{
    if (source is null)
    {
        return null;
    }

    // is it already a decimal?
    if (source is decimal)
    {
        return source;
    }

    // is it a double?
    if (source is double sourceDouble)
    {
        return Convert.ToDecimal(sourceDouble);
    }

    // is it a string?
    if (source is string sourceString && decimal.TryParse(sourceString, NumberStyles.AllowDecimalPoint | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out decimal d))
    {
        return d;
    }

    // couldn't convert the source value - default to null
    return null;
}
Enter fullscreen mode Exit fullscreen mode

My site makes use of Nullable Reference Types so, as I am returning null, I need to change the return type from object to object?. This means I can't simply inherit, so my class is actually a decorator:

public class NullableDecimalConverter : IPropertyValueConverter
{
    readonly IPropertyValueConverter coreConverter = new DecimalValueConverter();
...
Enter fullscreen mode Exit fullscreen mode

You can see the full example of the class in my package's repo.

Because property value converters are type-scanned at startup, and you can only only have one property value converter for a type at a time, you will need to replace DecimalValueConverter with NullableDecimalConverter at startup.

There's a package for that

As well as decimals, there are a number of other property types that return value types that should probably be able to return null values.

  • Date Picker - default value is 0001-01-01 00:00:00
  • Integer - default value is 0
  • Labels

So I created a package called Emptiness:
Umbraco Emptiness

It adds nullable property value converters for the property types mentioned above, as well as a bonus...

Toggles (True/False)

Though toggles don't have a null state in the UI, they can be empty if:

  1. A property has been added to a content type, but some content of that type has not been (re)published since.
  2. Content has been created via the API, without the property value having been set.

In these cases the following converters might be useful:

  1. YesNoDefaultConverter which returns the "Initial State" (default) value that has been configured in the property editor's settings.
  2. NullableYesNoConverter, returns null if content hasn't yet been (re)published with the property.

Getting started

Just install it from NuGet and go.

> dotnet add package Our.Umbraco.Emptiness
Enter fullscreen mode Exit fullscreen mode

There's a default configuration that I think will make sense for most sites. It makes empty integer, decimal and date pickers null when empty and makes Toggle's return their default value.

After installing & configuring rebuild your ModelsBuilder models and you'll see the relevant properties have been made nullable (so you'd better null-check them 😉).

Feedback please 🙏

The package is still pretty new, and I'm only using it in anger on one site, so I really welcome any feedback/issues etc.

It's somewhat different to what we're used to when working with properties in Umbraco, but I've found nullable values are a much better way of taking what's happening in the backoffice editor UI and surfacing that in our website code.

Top comments (1)

Collapse
 
martinjgriffiths profile image
Martin

Hi Jason,

Sorry this is a little off-topic.

I was watching your excellent YouTube video on RCL in Umbraco 10 but I cannot, no matter how hard I try to get the razor views inside an RCL to recompile on save. I see the browser refresh, or if I run "dotnet watch run" I see the file change prompt. But nothing happens! I've tried it on a completely new install using VS2022. Do you have a github repo of the demo you showed on YouTube?