Pages

Wednesday, 1 July 2020

Umbraco - Enabling Language Fallback by default

Sometimes you need a fresh pair of eyes to see an obvious solution when you're stuck looking in the wrong direction.

(Skip the backstory to the technical implementation if you prefer technical stuff over the story)

Backstory

Earlier, I talked about using Modelsbuilder partial classes to enable Language Fallback on all variant properties, so your View Designers would have an easier time of using fallback without even realizing it.

The reason I want this is:

I want (Razor) View Designers to be able to use

Model.Title

and not

Model.Value<string>("title", fallback: Fallback.ToLanguage)

I want them to
  • not need to know HOW to get fallback on a poperty
  • not need to know WHEN to use it (when a property is variant)
  • not have an excuse to FORGET to use it
I ALSO don't want to use my previously discussed approach where I need to implemented each Variant property in a partial class.

I wanted to find a better solution. I wanted Modelsbuilder to generate the .Value() method using Language Fallback by default.
I went as far as implementing it and making a Pull Request for ModelsBuilder, based on an AppSetting to enable this behaviour.

The PR was basically refused and a long discussion ensued wether or not what I wanted had merit and my solution was the correct approach.

Until one day, Ronald Barendse commented (paraphrased) "Why don't you just override the default Umbraco behaviour?"

My eyes opened and I went to work (and completed it unexpectedly fast).

Implementation

The default behaviour resides in the PublishedValueFallback class. To enable a solution just for this situation, they made the TryGetValue method virtual, so you can override it with whatever you need.

using System.Linq;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.Services;
using Umbraco.Web.Models.PublishedContent;

namespace Dpw.Eworld2.Foundation.Umbraco.PublishedContent

{
    public class CustomPublishedValueFallback : PublishedValueFallback
    {
        public CustomPublishedValueFallback(ServiceContext serviceContext, IVariationContextAccessor variationContextAccessor)
            : base(serviceContext, variationContextAccessor)
        {
            
        }

        public override bool TryGetValue<T>(IPublishedContent content, string alias, string culture, string segment, Fallback fallback, T defaultValue, out T value, out IPublishedProperty noValueProperty)
        {
            //When no fallback, use ToLanguage by default
            if (!fallback.Any(f => f == Fallback.DefaultValue || f == Fallback.Language || f == Fallback.Ancestors))
            {
                fallback = Fallback.ToLanguage;
            }

            return base.TryGetValue(content, alias, culture, segment, fallback, defaultValue, out value, out noValueProperty);
        }
    }
}

Bascially, what I do is : when no fallback of any sort is requested, I put fallback to Language Fallback. Nothing else needs to change.

To enable this custom class instead of the default one, you register it in your Startup class as follows:

composition.RegisterUnique<IPublishedValueFallback, CustomPublishedValueFallback>();

I was then able to toss my PR, my custom built ModelsBuilder-version, all remaining partial classes and never need to even think about which property will be Variant.