Blazor in Optimizely CMS 12 with .NET 8


How to enable support for Blazor components in Optimizely CMS 12 after upgrading to .NET 8.

Estimated read time : 3 minutes

Jump to

Key takeaways

  • Rendering of Blazor components fails when using PropertyFor
  • This specifically affects ContentArea and XhtmlString properties after upgrading to .NET 8
  • Issue is caused by an internal Optimizely class called WrappedHtmlContent
  • A custom property render type solves the problem without affecting edit mode

Summary

To enable rendering of Blazor components in Optimizely CMS 12 on .NET 8: copy the CustomPropertyRenderer class below into your project and use it to replace the built-in PropertyRenderer class.

The problem

After upgrading an Optimizely CMS 12 solution that makes heavy use of Blazor components to .NET 8, we encountered the following exception:

The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.

The exception itself is a bit of a red herring, as the problem has nothing to do with the Blazor components themselves, but rather how Optimizely renders properties.

The solution

Copy the following CustomPropertyRenderer class into your project:

using EPiServer.Web.Mvc;
using EPiServer.Web.Mvc.Html;
using Microsoft.AspNetCore.Html;
using Microsoft.AspNetCore.Mvc.Rendering;
using System.Text.Encodings.Web;

public class CustomPropertyRenderer : PropertyRenderer
{
    /// <summary>
    /// Replaces the internal class <see cref="EPiServer.Web.Mvc.Html.Internal.WrappedHtmlContent"/>.
    /// </summary>
    /// <remarks>Instead of implementing <see cref="IHtmlContent"/> we inherit <see cref="HtmlContentBuilder"/> to ensure proper rendering of Blazor components.</remarks>
    private class WrappedHtmlContent : HtmlContentBuilder
    {
        public WrappedHtmlContent(IHtmlContent content) => AppendHtml(content);

        public override string ToString()
        {
            using StringWriter stringWriter = new();
            WriteTo(stringWriter, HtmlEncoder.Default);
            return stringWriter.ToString();
        }
    }

    /// <summary>
    /// Exact copy of method from <see cref="PropertyRenderer"/>, except for the use of our custom <see cref="WrappedHtmlContent"/> class.
    /// </summary>
    protected override IHtmlContent GetHtmlForEditMode<TModel, TValue>(IHtmlHelper<TModel> html, string viewModelPropertyName, object editorSettings, Func<string, IHtmlContent> displayForAction, string templateName, string editElementName, string editElementCssClass, RouteValueDictionary additionalValues)
    {
        var _editHintResolver = html.ViewContext.HttpContext.RequestServices.GetRequiredService<IEditHintResolver>();

        if (!_editHintResolver.TryResolveEditHint(html.ViewContext, viewModelPropertyName, out var contentDataPropertyName))
        {
            if (!CurrentContentContainsProperty(html, viewModelPropertyName))
            {
                return new WrappedHtmlContent(displayForAction(templateName));
            }

            contentDataPropertyName = viewModelPropertyName;
        }

        HtmlContentBuilder htmlContentBuilder = new();

        using (CreateEditElement(html, "data-epi-property-name", contentDataPropertyName, editElementName, editElementCssClass, () => CustomSettingsAttributeWriter(additionalValues, "data-epi-property-rendersettings"), () => CustomSettingsAttributeWriter(new RouteValueDictionary(editorSettings), "data-epi-property-editorsettings"), htmlContentBuilder))
        {
            htmlContentBuilder.AppendHtml(displayForAction(templateName));
        }

        return new WrappedHtmlContent(htmlContentBuilder);
    }

    /// <summary>
    /// Exact copy of method from <see cref="PropertyRenderer"/>, except for the use of our custom <see cref="WrappedHtmlContent"/> class.
    /// </summary>
    protected override IHtmlContent GetHtmlForDefaultMode<TModel, TValue>(string propertyName, string templateName, string elementName, string elementCssClass, Func<string, IHtmlContent> displayForAction)
        => new WrappedHtmlContent(displayForAction(templateName));
}

Replace the built-in PropertyRenderer type with CustomPropertyRenderer in your Startup class:

services.AddTransient<PropertyRenderer, CustomPropertyRenderer>();

More details

This forum post on World does a great job describing the issue and how to reproduce it - thanks, Kevin!

It links to a GitHub repository with an Optimizely sample site exhibiting the faulty behavior.

The issue stems from the internal Optimizely class WrappedHtmlContent which is a really basic implementation of IHtmlContent.

To support Blazor component rendering, the IHtmlContent implementation must be buffered, which .NET 8 does through its internal sealed ViewBuffer class.

Since ViewBuffer implements IHtmlContentBuilder, we copied Optimizely's WrappedHtmlContent class and made it inherit HtmlContentBuilder instead of implementing IHtmlContent .

We expect the issue to be solved by Optimizely at some point, but until then this appears to be a valid workaround.