Blazor in Optimizely CMS 12 with .NET 8

Den här artikeln är inte översatt till svenska och visas därför på engelska istället.


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

Uppskattad lästid : 3 minuter

Gå till avsnitt

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.