Blog posts by Darren Sunderland2018-09-28T11:22:22.0000000Z/blogs/darren-sunderland/Optimizely WorldLazy-Loading blocks in an EpiServer Page/blogs/darren-sunderland/dates/2018/9/lazy-loading-blocks-in-an-episerver-page/2018-09-28T11:22:22.0000000Z<p>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.</p>
<p>Detailed below is a working version of the lazy-load solution, based on the Alloy template solution.</p>
<h2>Code</h2>
<h3>Block</h3>
<p>This is a standard block type, which was created to allow the editors to manage the hints for the page.</p>
<p><span style="text-decoration: underline;">Model (HintBlock.cs)</span></p>
<pre class="language-csharp"><code> 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; }
}</code></pre>
<p><span style="text-decoration: underline;">View (HintBlock.cshtml)</span></p>
<pre class="language-html"><code>@model HintBlock
<section>
@Html.ContentLink(Model.HintImage)
<div>
<h3>@Model.HintTitle</h3>
<p>@Html.DisplayFor(m => m.HintContent)</p>
</div>
</section></code></pre>
<h3>Page</h3>
<p>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.</p>
<p><span style="text-decoration: underline;">Model (HintInformationPage)</span></p>
<pre class="language-csharp"><code> 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; }
}</code></pre>
<p><span style="text-decoration: underline;">View (Index.cshtml)</span></p>
<pre class="language-html"><code>@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></code></pre>
<p><span style="text-decoration: underline;">Controller (HintListingPageController.cs)</span></p>
<p>This contains two regions - the "Rendering Methods" and the "Content Area Helper Methods" - in a production scenario the <span>"Content Area Helper Methods"</span> should be moved into an extension method to allow for use across all page types.</p>
<p>The GetHintBlocks method uses Linq to select the blocks, from the content area, which are required for the current display of the page. </p>
<p>The standard Index method loads the initial version of the page, with the number of blocks required.</p>
<p>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.</p>
<pre class="language-csharp"><code> 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
}</code></pre>
<h3>Extras</h3>
<p><span style="text-decoration: underline;">Partial View (HintList.cshtml)</span></p>
<p>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.</p>
<pre class="language-html"><code>@model List<HintBlock>
@foreach (var block in Model)
{
Html.RenderPartial("HintBlock", block);
}</code></pre>
<p><span style="text-decoration: underline;">Javascript (lazyLoading_HintInfo.js)</span></p>
<p>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.</p>
<pre class="language-csharp"><code>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();
}
}
</code></pre>
<p><br /><br /></p>Write uploaded media files to Amazon S3/blogs/darren-sunderland/dates/2018/4/write-uploaded-media-files-to-amazon-s3/2018-04-20T09:36:04.0000000Z<!DOCTYPE html>
<html>
<head>
</head>
<body>
<p>We recently had a need to allow the end users of our website to upload images as part of a competition. As the images are not part of the website content it seemed illogical to save the data on the EpiServer platform, as this would use up our content item space. After thinking through our options, we decided to write the uploaded images to an Amazon S3 bucket.</p>
<p>Below are the steps we followed to get our site writing to S3 – this has been tested in EpiServer 10 and above:</p>
<h3>Add required packages to the solution</h3>
<p>Using NuGet, install the following packages to connect to S3</p>
<ul>
<li>AWSSDK.S3</li>
<li>AWSSDK.Core</li>
</ul>
<h3>Create a new class in your solution to handle the Amazon connection and writing:</h3>
<pre class="language-csharp"><code>using System.IO;
using Amazon;
using Amazon.Runtime;
using Amazon.S3;
namespace MyCMS
{
public class AmazonS3Handler
{
private static string accessKey
{
get { return "ACCESS_KEY"; }
}
private static string secretKey
{
get { return "ACCESS_SECRET"; }
}
private static AmazonS3Client GetClient()
{
AmazonS3Config config = new AmazonS3Config
{
RegionEndpoint = RegionEndpoint.EUWest1
};
var creds = new BasicAWSCredentials(accessKey, secretKey);
AmazonS3Client clientout = new AmazonS3Client(creds, config);
return clientout;
}
/// <summary>
/// This method controls the file writing to the S3 bucket
/// </summary>
/// <param name="bucketname">String containing the name of the S3 Bucket</param>
/// <param name="filename">String containing the name for the artefact in S3</param>
/// <param name="mimetype">String containing the file extension type</param>
/// <param name="filecontent">Byte Array containing the content of the file</param>
public static void CreateMediaFile(string bucketname, string filename, string mimetype, byte[] filecontent)
{
var client = GetClient();
var request = new Amazon.S3.Model.PutObjectRequest
{
BucketName = bucketname,
Key = filename,
ContentType = mimetype,
InputStream = new MemoryStream(filecontent)
};
client.PutObject(request);
}
}
}</code></pre>
<h3><em></em>Create a new page type to handle the user uploads</h3>
<p>There are no specific property requirements for the new page type model.</p>
<p>In the new page type view, use HTML5 file upload component to allow the users to select and upload a file, see example:</p>
<pre class="language-html"><code><form enctype="multipart/form-data" method="post">
<label for="fileUpload">Click or Drop your receipt here to upload it</label>
<input type="file" id="fileUpload" name="fileUpload" multiple="multiple">
<br />
<input type="submit" value="Upload Files" />
</form></code></pre>
<p>In the new page type controller action method, use the following code to read the uploaded files and write them to S3</p>
<pre class="language-csharp"><code>string bucketname = "BUCKET_NAME";
string filename = "";
string mimetype = "";
for (int i = 0; i < Request.Files.Count; i++)
{
HttpPostedFileBase file = Request.Files[i];
int fileLen = file.ContentLength;
if (fileLen != 0)
{
string extension = Path.GetExtension(file.FileName);
if ((extension.ToUpper() == "JPG") || (extension.ToUpper() == "JPEG"))
{
mimetype = "image/jpeg";
}
else if (extension.ToUpper() == "PNG")
{
mimetype = "image/png";
}
else if (extension.ToUpper() == "GIF")
{
mimetype = "image/gif";
}
byte[] input = new byte[fileLen];
var fileContent = file.InputStream;
fileContent.Read(input, 0, fileLen);
filename = file.FileName;
AmazonS3Handler.CreateMediaFile(bucketname, filename, mimetype, input);
}
}
</code></pre>
<p>The above code snippets provide a guide on how to write to S3. To use this in a full EpiServer solution the following points should be considered:</p>
<ol>
<li>Amending the Access Key and Access Secret properties to read the values from a configuration source, this would allow different values to be used for different environments.</li>
<li>Creating a method to generate a unique file name for the uploaded file, this would prevent issues of different users uploading files with the same name (e.g. image1.jpg).</li>
</ol>
</body>
</html>