Fixing block rendering issues in Alloy Templates for EPiServer 7

New Alloy templates package for EPiServer 7 is a good example of building adaptive site and using new EPiServer features like shared blocks. It implements custom content area rendering to arrange blocks in rows and fill all available space depending on current browser size. There is couple of known issues related to these rendering tricks.

Symptoms

Postback handler in block control is not triggered.

Controls in block template are not always initialized properly.

Steps to reproduce

Add dummy block definition:

[ContentType]
public class PostbackBlock : BlockData
{
}

Create user control as rendering template for this block type. Add some input controls and submit button in block template. Let’s ping Santa:

<%@ Control Language="C#" AutoEventWireup="true" 
    CodeBehind="PostbackBlockControl.ascx.cs"
    Inherits="EPiServer.Templates.Alloy.Views.Blocks.PostbackBlockControl" %>
<asp:Panel runat="server">
    <asp:TextBox runat="server" ID="Message"></asp:TextBox>
    <asp:RequiredFieldValidator runat="server" Text="*" 
        ControlToValidate="Message"  />
    <asp:Button runat="server" Text="Send" OnClick="OnClick" />
</asp:Panel>
<asp:Panel ID="SentMessagePanel" runat="server" Visible="false">
    <p>Ok, it looks like you've been good boy/girl this year. Sent to Santa:</p>
    <p><asp:Literal runat="server" ID="SentMessage"></asp:Literal></p>
</asp:Panel>

Implement postback handler for button click event.

public partial class PostbackBlockControl : BlockControlBase<PostbackBlock>
{
    protected void OnClick(object sender, EventArgs e)
    {
        if (Page.IsValid)
        {
            // TODO: Deliver message to Santa

            SentMessagePanel.Visible = true;
            SentMessage.Text = Message.Text;
        }
    }
}

In Edit mode create new shared block, drop it to the content area on the page, publish and try to submit the form in view mode. Note that on-click handler is not executed.

Solution

Only a couple of code lines in Alloy project needs to be fixed, please see it highlighted below.

Creating controls in time

All block controls in Alloy project implement IBlockControl interface to define block width and ensure nice Tetris-like effect when block is rendered. If your block control does not implement IBlockControl interface it is automatically wrapped in GenericIBlockControlWrapper control to provide default minimum and maximum width. The side effect of this wrapping is that controls are created too late (usually on PreRender stage) and postback handlers in wrapped block controls are never triggered.

First you have to add public method to ensure that child controls are created in GenericIBlockControlWrapper:

public class GenericIBlockControlWrapper : WebControl, IBlockControl
{
    // ...
    // Ommited GenericIBlockControlWrapper code...
    // ...

    /// <summary>
    /// Ensures that child controls are created.
    /// </summary>
    public void EnsureChildControlsCreated()
    {
        EnsureChildControls();
    }
}

Then you need to call this method when block control is wrapped in GetIBlockControl method of SitePropertyContentAreaControl:

public class SitePropertyContentAreaControl : PropertyContentAreaControl
{
    // ...
    // Ommited SitePropertyContentAreaControl code...
    // ...

    /// <summary>
    /// Gets the control responsible for rendering content and tries to cast it to <see cref="IBlockControl"/>.
    /// If the control does not implement this <see cref="IBlockControl"/> we create a wrapper that handles this for us.
    /// </summary>
    private static IBlockControl GetIBlockControl(ContentRenderer contentRenderer)
    {
        var blockControl = contentRenderer.CurrentControl as IBlockControl;

        if (blockControl != null)
        {
            return blockControl;
        }

        var genericIBlockControlWrapper = new GenericIBlockControlWrapper();

        genericIBlockControlWrapper.InnerControl = contentRenderer.CurrentControl;
        genericIBlockControlWrapper.CurrentData = contentRenderer.CurrentData;
        genericIBlockControlWrapper.ContentRenderer = contentRenderer;
        contentRenderer.CurrentControl = genericIBlockControlWrapper;

        // Create controls in time:
        genericIBlockControlWrapper.EnsureChildControlsCreated();

        return genericIBlockControlWrapper;            
    }

    // ...
    // Ommited SitePropertyContentAreaControl code...
    // ...

}

Now block controls are created in time (before Load stage on postback) and ASP.NET can find and trigger postback handlers.

Avoiding adding block row twice

Second problem is that last row of the blocks in the content area is mistakenly added to control tree twice. It’s not so obvious because blocks in that row are rendered only once, but it can cost various problems with control initialization. In my case there were issues with view state and validation in complex web control.

Find GetBlockGroups method in SitePropertyContentAreaControl class and clean block group list when last row is created. Here is full listing of GetBlockGroups method, updated code is highlighted:

public class SitePropertyContentAreaControl : PropertyContentAreaControl
{
    // ...
    // Ommited SitePropertyContentAreaControl code...
    // ...

    /// <summary>
    /// Composes groups where each group represents a Bootstrap row of blocks
    /// </summary>
    /// <param name="contentRenderers"></param>
    /// <returns></returns>
    private List<List<ContentRenderer>> GetBlockGroups(IList<ContentRenderer> contentRenderers)
    {
        var groups = new List<List<ContentRenderer>>();

        var group = new List<ContentRenderer>();

        var minimumWidthOfBlockControlsAddedToCurrentGroup = 0;

        for (int i = 0; i < contentRenderers.Count; i++)
        {
            var contentRenderer = contentRenderers[i];

            // Add block controls to the current group

            var blockControl = GetIBlockControl(contentRenderer);

            // There is no point in showing the default block control in preview mode, or if the current user isn't an editor
            if (blockControl is DefaultBlockControl)
            {
                if (Page is BlockPreview || !CurrentPage.QueryDistinctAccess(AccessLevel.Edit))
                {
                    continue;
                }
            }

            if (blockControl.MinimumWidth > RowWidth)
            {
                _logger.WarnFormat("Block control '{0}' has a minimum width of {1} and won't properly fit within the maximum row width which is {2}", blockControl.GetType().Name, blockControl.MinimumWidth, RowWidth);

                if (Page is BlockPreview) // In block preview mode we skip block controls that won't fit within the current row width
                {
                    continue;
                }
            }

            blockControl.Width = blockControl.MinimumWidth;

            group.Add(contentRenderer);

            minimumWidthOfBlockControlsAddedToCurrentGroup += blockControl.MinimumWidth;

            if (i == contentRenderers.Count - 1) // The last block has been added, so we're done creating groups
            {
                // Fill out the last row
                AdjustWidthOfBlocksToFillRow(group);
                groups.Add(group);
                // Clean the last row list
                group = null;
                break;
            }

            var nextContentRenderer = contentRenderers[i + 1];

            if (nextContentRenderer != null)
            {
                var nextBlockControl = GetIBlockControl(nextContentRenderer);

                if (minimumWidthOfBlockControlsAddedToCurrentGroup + nextBlockControl.MinimumWidth <= RowWidth)
                {
                    continue;
                }

                // The next block won't fit in this group, try to fill the group by increasing the size of already added blocks
                AdjustWidthOfBlocksToFillRow(group);
            }

            groups.Add(group);

            group = new List<ContentRenderer>();

            minimumWidthOfBlockControlsAddedToCurrentGroup = 0;
        }

        // Check that row list is not null
        if (group != null && group.Count > 0)
        {
            //If we somehow escaped the loop without adding the last group lets do it now.
            AdjustWidthOfBlocksToFillRow(group);
            groups.Add(group);
        }

        return groups;
    }

    // ...
    // Ommited SitePropertyContentAreaControl code...
    // ...
}

Result

Now you can post a message using shared block:

Message to Santa

More information

Bug #91643 is filed for these issues and the fix should be available in the next Alloy templates update. Until then you can use described workaround. Please let us know if it works for you.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.