Rendering preview blocks with Advanced Reviews while using React for Episerver websites

We are trying out the Advanced CMS add-on Advanced Reviews for one of our Episerver websites at my current client. The thing is that we are also using JOS Content Serializer for turning our Episerver content items into serializable objects in order to feed our React frontend (described in previous articles). This caused issues for external reviewers while trying to preview ContentArea blocks in view mode as they only saw published content.

The reason for this is quite simple. As our solution needs to turn Episerver content into serializable content, the part of Advanced Review responsible for selecting the latest draft of ContentArea blocks was not being used.

JOS property handler using Advanced Review

The solution is to override the default content area property handler being supplied by the JOSContentSerializer, implementing a custom one doing almost the same thing, apart from also considering a context for external reviewers. This may be done using a standard configuration initializer as below.

[InitializableModule]
[ModuleDependency(typeof(ContentSerializerInitalizationModule))]
public class JosPropertyHandlerInitialization : IConfigurableModule
{
  public void Initialize(InitializationEngine context) { }
  public void Uninitialize(InitializationEngine context) { }

  public void ConfigureContainer(ServiceConfigurationContext context)
  {
    context.Services.AddSingleton<IPropertyHandler<ContentArea>, JosContentAreaPropertyHandler>();
  }
}

The new property handler is very similar to the default one supplied by the JOS Content Serializer. Create a new class implementing the IPropertyHandler interface as below.

public class JosContentAreaPropertyHandler : IPropertyHandler<ContentArea>
{
  private readonly IContentLoader _contentLoader;
  private readonly IPropertyManager _propertyManager;
  private readonly DraftContentAreaLoader _draftContentAreaLoader;
  private readonly IContentSerializerSettings _contentSerializerSettings;

  public JosContentAreaPropertyHandler(
    IContentLoader contentLoader,
    IPropertyManager propertyManager,
	IContentSerializerSettings contentSerializerSettings,
    DraftContentAreaLoader draftContentAreaLoader)
  {
    _contentLoader = contentLoader ?? throw new ArgumentNullException(nameof(contentLoader));
    _propertyManager = propertyManager ?? throw new ArgumentNullException(nameof(propertyManager));
    _draftContentAreaLoader = draftContentAreaLoader ?? throw new ArgumentNullException(nameof(draftContentAreaLoader));
    _contentSerializerSettings = contentSerializerSettings ?? throw new ArgumentNullException(nameof(contentSerializerSettings));
  }

The parts that are different may be found in the GetContentAreaItems implementation. First, the contentArea null-check have been moved to the top, rather than doing it in the FilteredItems-if statement further down. However, the interesting part is the IsInExternalReviewContext check. This boolean will be true if the visitor is in a review context (both with and without the Advanced Review project-functionality).

  /// <summary>
  /// This method is the only one slightly edited (original in JOS.ContentSerializer.Internal.Default.ContentAreaPropertyHandler)
  /// For external review context, we return items using GetContentAreaItemsReviewContext.
  /// </summary>
  private IEnumerable<IContentData> GetContentAreaItems(ContentArea contentArea)
  {
    if(contentArea == null)
    {
      return Enumerable.Empty<IContentData>();
    }
    if (AdvancedExternalReviews.ExternalReview.IsInExternalReviewContext)
    {
      return GetContentAreaItemsReviewContext(contentArea);
    }

    // JOS Default below.

    if (contentArea.FilteredItems == null || !contentArea.FilteredItems.Any())
    {
      return Enumerable.Empty<IContentData>();
    }

    var content = new List<IContentData>();
    foreach (var contentAreaItem in contentArea.FilteredItems)
    {
      var loadedContent = this._contentLoader.Get<ContentData>(contentAreaItem.ContentLink);
      if (loadedContent != null)
      {
        content.Add(loadedContent);
      }
    }

    return content;
  }

If we’re not in a review context, the default implementation will continue, serving blocks from the FilteredItems collection.

If we are in a review context however, we will need to involve parts of the Advanced Review add-on’s code. First, instead of using FilteredItems, we need to be looking at the Items collection directly.

If you look at the Advanced Review code at GitHub you will find that the add-on is using a custom implementation of the IContentAreaLoader interface to fetch drafts of content residing in content areas. This is the functionality that we will need to add to our new content area property handler.

It is rather straight forward. Inject Advanced Review’s DraftContentAreaLoader via the constructor, and use it’s Get method on each of the items in the collection. See below.

  /// <summary>
  /// Getting content from the Items collection using the review addon's method for review context.
  /// Follows same pattern as the JOS version.
  /// </summary>
  private IEnumerable<IContentData> GetContentAreaItemsReviewContext(ContentArea contentArea)
  {
    if(contentArea.Items == null || !contentArea.Items.Any())
    {
      return Enumerable.Empty<IContentData>();
    }
    var content = new List<IContentData>();
    foreach(var contentAreaItem in contentArea.Items)
    {
      var loadedContent = _draftContentAreaLoader.Get(contentAreaItem.CreateWritableClone());
      if(loadedContent != null)
      {
        content.Add(loadedContent);
      }
    }
    return content;
  }

The rest of the class is just the default implementation, duplicated due to visibility constraints.

  /// <summary>
  /// This method is a copy of the one in JOS.ContentSerializer.Internal.Default.ContentAreaPropertyHandler
  /// It is duplicated in order to change the private GetContentAreaItems method.
  /// </summary>
  public object Handle(ContentArea contentArea, PropertyInfo propertyInfo, IContentData contentData)
  {
    if (contentArea == null)
    {
      return null;
    }
    var contentAreaItems = GetContentAreaItems(contentArea);
    if (WrapItems(contentArea, this._contentSerializerSettings))
    {
      var items = new Dictionary<string, List<object>>();
      foreach (var item in contentAreaItems)
      {
        var result = this._propertyManager.GetStructuredData(item, this._contentSerializerSettings);
        var typeName = item.GetOriginalType().Name;
        result.Add(this._contentSerializerSettings .BlockTypePropertyName, typeName);
        if (items.ContainsKey(typeName))
        {
          items[typeName].Add(result);
        }
        else
        {
          items[typeName] = new List<object> { result };
        }
      }
    
      return items;
    }
    else
    {
      var items = new List<object>();
      foreach (var item in contentAreaItems)
      {
        var result = this._propertyManager.GetStructuredData(item, this._contentSerializerSettings);
        result.Add(this._contentSerializerSettings .BlockTypePropertyName, item.GetOriginalType().Name);
        items.Add(result);
      }
      
      return items;
    }
  }
  
  /// <summary>
  /// This method is a copy of the one in JOS.ContentSerializer.Internal.Default.ContentAreaPropertyHandler
  /// It is duplicated in order to change the private GetContentAreaItems method.
  /// </summary>
  private static bool WrapItems(ContentArea contentArea, IContentSerializerSettings contentSerializerSettings)
  {
    var wrapItemsAttribute = contentArea.GetType() .GetCustomAttribute<ContentSerializerWrapItemsAttribute>();
    var wrapItems = wrapItemsAttribute?.WrapItems ?? contentSerializerSettings.WrapContentAreaItems;
    return wrapItems;
  }
}