Pages

Monday, October 25, 2021

The Problem with Nested Blocks and AdditionalViewData

It's not uncommon to nest blocks in Optimizely to create more controlled and complex layouts on pages. It's also sometimes necessary to provide additional data to the blocks you render. When using the PropertyFor helper this is done by specifying an additionalViewData object. However ViewData persists to nested views and this can mess up your layout. This is how to identify when this happens, and how to work around it.

It helps to understand the problem

This issue can happen with any data passed with PropertyFor as additionalViewData. It can be as minor as a persisting CssClass, or more major when rendering a ContentArea as a list. Using CustomTag and ChildrenCustomTagName to render the ContentArea as a UL with LI children will affect the markup of nested ContentAreas. This article identifies other settings that can affect things.

To demonstrate, I created a simple HomePage with MainContentArea rendered using PropertyFor. A CssClass is specified to wrap the rendered ContentArea in an orange border, and I am rendering it as a list as well using the additionalViewData argument. I have also created a HeroCollectionBlock with a LeftContentArea property rendered using PropertyFor and no additional data. I placed a single TeaserBlock in LeftContentArea.

[pre class="brush:razor" title="HomePage.cshtml"]
@using BaseEpiserverSite.Models.Pages
@using BaseEpiserverSite.Models.ViewModels

@model IPageViewModel<HomePage>

<div class="content-wrapper">
    <h1>This is my homepage content</h1>
    <div class="main-content">
        @Html.PropertyFor(m => m.CurrentPage.MainContent, new {CssClass = "highlighted", CustomTag = "ul", ChildrenCustomTagName = "li" })
    </div>
</div>
[/pre]


[pre class="brush:razor" title="HeroCollectionBlock.cshtml"]
@model BaseEpiserverSite.Models.Blocks.HeroCollectionBlock

<h4>This is my hero collection block</h4>

<div class="collection-row" style="width: 800px;display: grid;grid-template-columns:repeat(2,50%)">
    <div class="collection-row-left">
        @Html.PropertyFor(m=> m.LeftContentArea)
    </div>
    <div class="collection-row-right">
        @Html.PropertyFor(m=> m.RightContent)
    </div>
</div>
[/pre]


The screenshot below shows the page and the nested blocks rendered on it. The highlighted class specified from HomePage adds an orange border to MainContentArea, and the tags specified render it as an unordered list. The LeftContentArea from HeroCollectionBlock also renders as an unordered list with the border around it.

This happens because the additionalViewData object is persisted in the context ViewBag. Each nested block will receive the same data unless overridden by different view data.

Work around this by resetting the ViewBag data

Since the values specified from additionalViewData objects are added to the ViewBag, I created a HtmlHelper to reset it. I based this on the options listed in the PropertyFor article linked above, but you can add additional ones you might use in your templates.

[pre class="brush:csharp;class-name:collapse-box"]
using System.Web.Mvc;

namespace BaseEpiserverSite.Extensions
{
    public static class HtmlHelpers
    {
        public static void ResetAdditionalViewData(this HtmlHelper helper)
        {
            helper.ViewBag.ChildrenCssClass = "";
            helper.ViewBag.ChildrenCustomTagName = "";
            helper.ViewBag.HasContainer = true;
            helper.ViewBag.CustomTag = "";
            helper.ViewBag.CssClass = "";
        }
        public static void ResetAdditionalViewData<T>(this HtmlHelper<T> helper)
        {
            (helper as HtmlHelper).ResetAdditionalViewData();
        }
    }
}
[/pre]

 

Use this helper after you read any values you need from the ViewData object. Doing this will prevent the values from affecting nested layouts.

[pre class="brush:razor;class-name:collapse-box"]
@using BaseEpiserverSite.Extensions
@model BaseEpiserverSite.Models.Blocks.HeroCollectionBlock

@{
    var cssClass = (ViewData["CssClass"] ?? "").ToString();
    var otherDataValue = (ViewData["otherDataValue"] ?? "").ToString();
    Html.ResetAdditionalViewData();
}

<h4>This is my hero collection block</h4>
<div class="collection-row" style="width: 800px;display: grid;grid-template-columns:repeat(2,50%)">
    <div class="collection-row-left">
        @Html.PropertyFor(m=> m.LeftContentArea)
    </div>
    <div class="collection-row-right">
        @Html.PropertyFor(m=> m.RightContent)
    </div>
</div>
[/pre]

 

I used cssClass and otherDataValue here to show when to use the helper. Note in the screenshot below, the LeftContentArea doesn't render as a list and it is not highlighted. Also, while I only covered a couple values in these examples and focused on ContentArea, other ViewData values will persist, and can also affect other nested layouts.


No comments:

Post a Comment

Share your thoughts or ask questions...