Darren Sunderland
Sep 28, 2018
  3536
(0 votes)

Lazy-Loading blocks in an EpiServer Page

In one of our sites we were requested to create a new page type, which the content editors could use to host a list of hints/tips to maximise what is available through the website.  The initial concept is a simple task to achieve in EpiServer, but we realised fairly quickly that there could be a potential issue with page load time when the editors informed us that there would be over 100 hints/tips added to the page.  This led us to look into the option of lazy-loading blocks into the page to speed up the load time, and also to improve the user experience.

Detailed below is a working version of the lazy-load solution, based on the Alloy template solution.

Code

Block

This is a standard block type, which was created to allow the editors to manage the hints for the page.

Model (HintBlock.cs)

    public class HintBlock : BlockData
    {
        [Display(GroupName = SystemTabNames.Content, Order = 1, Name = "Hint Title")]
        public virtual string HintTitle { get; set; }

        [Display(GroupName = SystemTabNames.Content, Order = 2, Name = "Hint Content")]
        [Required]
        public virtual XhtmlString HintContent { get; set; }

        [Display(GroupName = SystemTabNames.Content, Order = 3, Name = "Hint Image")]
        [UIHint(UIHint.Image)]
        public virtual ContentReference HintImage { get; set; }
    }

View (HintBlock.cshtml)

@model HintBlock

<section>
    @Html.ContentLink(Model.HintImage)
    <div>
        <h3>@Model.HintTitle</h3>
        <p>@Html.DisplayFor(m => m.HintContent)</p>
    </div>
</section>

Page

This is a new block type, which was created to allow the editors to organise the hints to display on the page.  Notice the last property of the model (HintsForDisplay), this is to allow the view to display the required number of blocks during the initial page load as opposed to loading all blocks in the content area - this property is populated within the controller for the page.

Model (HintInformationPage)

    public class HintListingPage : SitePageData
    {
        [Display(GroupName = SystemTabNames.Content, Order = 1, Name = "Page Title")]
        public virtual String PageTitle { get; set; }

        [Display(GroupName = SystemTabNames.Content, Order = 2, Name = "Page Introduction Text")]
        public virtual XhtmlString PageSubText { get; set; }

        [Display(GroupName = SystemTabNames.Content, Order = 3, Name = "Page Hint Content")]
        public virtual ContentArea HintContentArea { get; set; }

        [Display(GroupName = SystemTabNames.Content, Order = 4, Name = "Hints Per Page")]
        public virtual int HintsPerPage { get; set; }

        [Ignore]
        public virtual List<HintBlock> HintsForDisplay { get; set; }
    }

View (Index.cshtml)

@model PageViewModel<HintListingPage>

@{ Layout = "~/Views/Shared/Layouts/_LeftNavigation.cshtml"; }

<h2>@Html.DisplayFor(t => t.CurrentPage.PageTitle)</h2>
<p>@Html.DisplayFor(t => t.CurrentPage.PageSubText)</p>

<div id="hints__blockarea">
    @{
        foreach (var item in Model.CurrentPage.HintsForDisplay)
        {
            Html.RenderPartial("HintBlock", item);
        }
    }
</div>
<br />
<center>
    <div id="loading">
        <p id="loading__message" style="color: red;"><b>Loading Next Hints...</b></p>
    </div>
</center>
<div>
    <button id="load__more__elements" onclick="buttonHandler();">Show More</button>
</div>
<label id="lbl_finished">No more records to load.</label>

<script type="text/javascript">
    var pageID = @Model.CurrentPage.ContentLink.ID.ToString();
    var url = '@Request.Url.ToString()' + "LazyLoad";
    document.getElementById("loading__message").style.display = "NONE";
    document.getElementById("lbl_finished").style.display = "NONE";
</script>
<script src="~/Static/js/lazyLoading_HintInfo.js"></script>

Controller (HintListingPageController.cs)

This contains two regions - the "Rendering Methods" and the "Content Area Helper Methods" - in a production scenario the "Content Area Helper Methods" should be moved into an extension method to allow for use across all page types.

The GetHintBlocks method uses Linq to select the blocks, from the content area, which are required for the current display of the page. 

The standard Index method loads the initial version of the page, with the number of blocks required.

The LazyLoad method is called by an Ajax call from the view, and identifies the next batch of blocks which need to be added into the page. The return of this is the HTML required to render the blocks for the end page.

    public class HintListingPageController : PageController<HintListingPage>
    {
        private static HintListingPage _currentPage = new HintListingPage();

        #region Page Rendering Methods
        public ActionResult Index(HintListingPage currentPage)
        {
            _currentPage = currentPage;
            var model = PageViewModel.Create(currentPage);
            model.CurrentPage.HintsForDisplay = GetHintBlocks(null);
            return View(model);
        }

        [System.Web.Http.HttpGet]
        public ActionResult LazyLoad(int pageID, int? pageNum)
        {
            _currentPage = (HintListingPage)DataFactory.Instance.GetPage(new PageReference(pageID));

            var blocks = GetHintBlocks(pageNum);
            return PartialView("HintList", blocks);
        }

        public List<HintBlock> GetHintBlocks(int? pageNum)
        {
            int lazyLoadBlocks = 5;
            try
            {
                lazyLoadBlocks = _currentPage.HintsPerPage == 0 ? lazyLoadBlocks : _currentPage.HintsPerPage;
            }
            catch (Exception) { }

            List<HintBlock> rawItems = GetFilteredItemsOfType<HintBlock>(_currentPage.HintContentArea);
            var displayItems = from ri in rawItems
                               select ri;

            pageNum = pageNum ?? 1;
            int firstElement = ((int)pageNum - 1) * lazyLoadBlocks;

            return displayItems.Skip(firstElement).Take(lazyLoadBlocks).ToList();
        }
        #endregion

        #region Content Area Helper Methods
        public static bool AreaIsNullOrEmpty(ContentArea contentArea)
        {
            return contentArea == null || !contentArea.FilteredItems.Any();
        }

        public static List<HintBlock> GetFilteredItemsOfType<HintBlock>(ContentArea contentArea)
        {
            var items = new List<HintBlock>();

            if (AreaIsNullOrEmpty(contentArea))
            {
                return items;
            }

            var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();

            foreach (var contentAreaItem in contentArea.FilteredItems)
            {
                IContentData item;
                if (!contentLoader.TryGet(contentAreaItem.ContentLink, out item))
                {
                    continue;
                }
                if (item is HintBlock)
                {
                    items.Add((HintBlock)item);
                }
            }

            return items;
        }
        #endregion
    }

Extras

Partial View (HintList.cshtml)

This is the partial HTML to display a list of the blocks.  This is used by the lazy-load method to return the formatted code to be injected into the page.

@model List<HintBlock>

@foreach (var block in Model)
{
    Html.RenderPartial("HintBlock", block);
}

Javascript (lazyLoading_HintInfo.js)

This Javascript handles the button click, from the end page, to load additional blocks for display.  This makes an Ajax call to the LazyLoad method of the controller, and injects the returned content into the div element of the page where the blocks are rendered.  There is also logic in place to show a loading message whilst the work is taking place and there is also logic to show/hide the load more hints button OR a message stating all hints are loaded.

var page = 1;
var isCallback = false;

function buttonHandler() {
    loadHintData(url);
}

function loadHintData(loadMoreRowsUrl) {
    if (page > -1 && !isCallback) {
        isCallback = true;
        page++;
        isCallback = false;

        document.getElementById("loading__message").style.display = "BLOCK";

        dataUrl = loadMoreRowsUrl + "?pageID=" + pageID + "&pageNum=" + page;
        dataUrl = dataUrl.replace("/?", "?");
        var xhttp = new XMLHttpRequest();
        xhttp.onreadystatechange = function () {
            if (xhttp.readyState !== 4) return;

            if (xhttp.status === 200) {
                var web_response = xhttp.responseText;
                if (web_response.toString().trim().length !== 0) {
                    var main = document.getElementById("hints__blockarea");
                    main.insertAdjacentHTML('beforeend', web_response);
                    document.getElementById("load__more__elements").style.display = "BLOCK";
                }
                else {
                    document.getElementById("load__more__elements").style.display = "NONE";
                    document.getElementById("lbl_finished").style.display = "BLOCK";
                }

                document.getElementById("loading__message").style.display = "NONE";
            }
            else {
                page = -1;
                document.getElementById("loading__message").style.display = "NONE";
                document.getElementById("load__more__elements").style.display = "NONE";
                document.getElementById("lbl_finished").style.display = "BLOCK";
            }
        };
        xhttp.open("GET", dataUrl, true);
        xhttp.send();
    }
}



Sep 28, 2018

Comments

Casper.Rasmussen
Casper.Rasmussen Sep 28, 2018 12:23 PM

Hi Darren.

Thanks for the inspiration. I like the thinking!

Blogged about something similar a while back, which used ASP.NET WebApi to render the blocks. It might be worth for you to look at, to get the benefits of being more RESTfull in your approach. Have a look!

http://fellow.aagaardrasmussen.dk/2016/11/01/how-to-render-an-episerver-contentreference-via-your-webapi/

/Casper Aagaard Rasmussen 

Please login to comment.
Latest blogs
Plug-in manager is back in CMS 12

Plug-in manager is back in the UI, what is it and how can i use it?

Luc Gosso (MVP) | Oct 6, 2022 | Syndicated blog

Display Child Pages in Content Delivery API Response

The below example will implement an instance of IContentConverterProvider to customise the serialisation of PageData and output child pages in the...

Minesh Shah (Netcel) | Oct 4, 2022

Bring the Report Center back in Optimizely CMS 12

The Report Center has been a part of Optimizely CMS since its first debut in version 5R2 in 2008, but in CMS 12, it's removed! Don't despair! Make...

Tomas Hensrud Gulla | Oct 4, 2022 | Syndicated blog

Customizing Property Lists in Optimizely CMS

Generic property lists is a cool editorial feature that has gained a lot of popularity - in spite of still being unsupported (officially). But if y...

Allan Thraen | Oct 2, 2022 | Syndicated blog

Optimizely names Luminary Senior Developer, Ynze Nunnink, OMVP

Luminary Senior Developer and Optimizely Lead, Ynze Nunnink has secured the coveted position of Optimizely MVP. Earning a Platinum badge for his...

Ynze | Oct 2, 2022 | Syndicated blog

Content Delivery API – The Case of the Duplicate API Refresh Token

Creating a custom refresh provider to resolve the issues with duplicate tokens in the DXC The post Content Delivery API – The Case of the Duplicate...

David Lewis | Sep 29, 2022 | Syndicated blog