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.