A comment from the Umbraco Source Code

Implementing Property Value Converters for Rich Text Editors and The Importance of One Comment!

As part of something I’m working on for a future blog post/technical demo, today I had the need to implement some custom Property Value Converters in Umbraco 13 to allow me to modify the HTML from them in one place. To do so I had to implement converters for both the Umbraco Markdown editor and the Umbraco Richtext editor, and this was not as straightforward as expected. So I’m hoping by writing this up it can help at least one person who may be reading this save themselves some time going down the wrong pathway.

Although there are detailed pages in the docs giving information on building up value converters from scratch, if you’re looking to work with an existing built in editor and just make some minor modifications, one of the best ways is often just to start by basing your code off whichever existing property value converter in the core codebase you’re looking to extend. For the Markdown control, this was fairly straightforward. It was possible to take the existing MarkdownEditorValueConverter.cs code from the Umbraco source and largely reimplement this all the same, just adding some extra lines to the final method responsible for output, namely the ‘ConvertIntermediateToObject’ method. Indeed probably the most complicated part of all this I ran into is that despite the original Umbraco Markdown property editor sitting in the Umbraco.Core namespace, the codefile for it is not found in the PropertyEditors folder within the Umbraco.Core project as you might expect, but instead is found within an equivalent folder inside the Umbraco.Infrastructure project. After doing that and adding my own lines of code in the one method, this all just worked straight away. Great, this is easy, right…

Alas, implementing a property editor for the included TinyMCE Rich Text editor was a very different story. Looking at the most likely candidates in the Umbraco source, it seemed most likely that to implement this you could start off with the SimpleTinyMceValueConverter code, this time found in the Umbraco.Core project. So I followed the same approach, copied across all the same methods and once again added my own code to the ‘ConvertIntermediateToObject’ method and got… well a blob of json thrown back out rather than the nice HTML I’d expected. Attaching the debugger it was clear the new code was definitely hooking on the Rich Text control and firing, but the output coming back remained way off what you’d expect. Perplexed I took out all my own custom Property Values again, hooked the debugger on to an Umbraco composer and inspected what default core converters Umbraco injects in during its startup. When doing this, I could immediately see the default MarkdownEditorValueConverter among the list of various other value converters, but there was no sign of SimpleTinyMceValueConverter anywhere to be found.

As this seemed odd and wasn’t something I could readily explain, I went back to the Umbraco source code again and actually checked where it was referenced. A key comment in the Umbraco codebase, followed by some lines of code explains what is going on.

    // both TinyMceValueConverter (in Core) and RteMacroRenderingValueConverter (in Web) will be
    // discovered when CoreBootManager configures the converters. We will remove the basic one defined
    // in core so that the more enhanced version is active.
    builder.PropertyValueConverters()
        .Remove<SimpleTinyMceValueConverter>();

So to implement a converter for a Rich Text Editor, you actually need to work off the code for RteMacroRenderingValueConverter.cs, despite its naming suggesting this may be something to do with Macros. However unlike with the equivalent Markdown code, it is not as trivial a case of copying the code for this and adding a bit into one method. The implementation for RteMacroRenderingValueConverter is a much bigger beast, with a stack of methods, some public and some private, as the class is designed to implement all manner of different Rich Text implementations across lots of different scenarios. After some thinking, I concluded that the best approach if wanting to just do a simple manipulation of the output HTML for this one seems to be to inherit from their existing RteMacroRenderingValueConverter, override only the ConvertIntermediateToObject method and otherwise inherit everything from their base converter. To do this there are also a few constructor methods you also need to inherit, however Visual Studio’s Quick Actions can implement these for you. In Umbraco 13 there are currently three such constructor methods, two of which are marked as deprecated with notes saying they’ll be removed in Umbraco 14 and Umbraco 15. In actual fact, it appears both have been removed in the Umbraco 14 codebase and the whole class renamed as ‘RteBlockRenderingValueConverter.cs’ to better reflect the removal of macros in v14+. So if you’re on newer versions you’ll need to work from a different initial class and the constructor implementation required should be simpler, but otherwise the approach should be similar. However as I’m still on the LTS-eses here at the moment, the resultant code for this in Umbraco 13 led to a very thin class similar to the below (with apologies for some of the very long lines here – this tech blogger theme is really not great for code display)…

namespace UmbracoScrub13.PropertyValueConverters
{
    public class CustomRteMacroRenderingValueConverter : RteMacroRenderingValueConverter
    {
        public CustomRteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser) : base(umbracoContextAccessor, macroRenderer, linkParser, urlParser, imageSourceParser)
        {
        }

        public CustomRteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser, IOptionsMonitor<DeliveryApiSettings> deliveryApiSettingsMonitor) : base(umbracoContextAccessor, macroRenderer, linkParser, urlParser, imageSourceParser, apiRichTextElementParser, apiRichTextMarkupParser, deliveryApiSettingsMonitor)
        {
        }

        public CustomRteMacroRenderingValueConverter(IUmbracoContextAccessor umbracoContextAccessor, IMacroRenderer macroRenderer, HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser, IPartialViewBlockEngine partialViewBlockEngine, BlockEditorConverter blockEditorConverter, IJsonSerializer jsonSerializer, IApiElementBuilder apiElementBuilder, RichTextBlockPropertyValueConstructorCache constructorCache, ILogger<Umbraco.Cms.Core.PropertyEditors.ValueConverters.RteMacroRenderingValueConverter> logger, IOptionsMonitor<DeliveryApiSettings> deliveryApiSettingsMonitor) : base(umbracoContextAccessor, macroRenderer, linkParser, urlParser, imageSourceParser, apiRichTextElementParser, apiRichTextMarkupParser, partialViewBlockEngine, blockEditorConverter, jsonSerializer, apiElementBuilder, constructorCache, logger, deliveryApiSettingsMonitor)
        {
        }

        public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType,
            PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
        {
            var html = (HtmlEncodedString)base.ConvertIntermediateToObject(owner, propertyType, referenceCacheLevel, inter, preview);

            return html.ReplaceImages(); // My custom modification
        }

    }
}

With this implementation in place, my own custom Property Value Converter for the Rich Text Editor then correctly fired, and resulted in HTML coming back complete with my own HTML modifications. I’ve tested this approach in multiple places, such as with a basic TinyMCE Umbraco Rich Text control, adding in a macro (if anyone still uses those), and inside blockgrid and all seemed to work correctly. But if working from this idea, you are of course advised to always do your own additional testing depending on how you use the CMS.

And finally, a big thank you and a #H5YR to whoever originally put that simple comment into the Umbraco codebase, however long ago it was now. Without it, I may still have been looking!

One thought on “Implementing Property Value Converters for Rich Text Editors and The Importance of One Comment!

Comments are closed.