November Happy Hour will be moved to Thursday December 5th.

eGandalf
Apr 6, 2015
  14359
(1 votes)

Building a Carousel using Global Razor Templates in EPiServer MVC

I’m currently working on my first EPiServer project - one that I hope will serve as a well-commented reference and starter app. Going through this process somewhat slowly and retyping all of the code that I’m borrowing from other places is really helping me with some deep-learning of the platform.

In this process, I’m really starting to appreciate the extensibility of the platform and the combined power of EPiServer and MVC. While I still feel like I have more questions than answers when it comes to specifics of the technology, I also am getting a firm grasp of EPiServer development and am appreciating how it is simultaneously awesome for developers and for business users alike.

An Ektron and EPiServer comparison blog may be forthcoming, but for the moment I wanted to share a really cool little trick that I read about on Pride Parrot, Simplifying html generation in code using Razor templates.

The HTML template I’m using 

Carousel slides would be created by dragging and dropping ArticlePage items into the ContentArea. Displaying those items using a carousel rendering was obvious for an individual item, but in order to display them as a carousel, I needed to treat them as a list of similar objects and the traditional PagePartial rendering didn’t seem to fit the bill.

A traditional @helper template approach would do the job, provided I could correctly parse the information inside the ContentArea and pass it over, but I really didn’t like the idea of having what is essentially a PagePartial rendering residing as a template within my StartPage Index rendering.

The article referenced above provides a simple way to take the template I created for my list of articles and move it into a place where it can be used and called globally from my other views. If you’re interested in also calling the template from code, see the above article, though I have not (yet) taken it that far in my own project.

The process is exceedingly simple. 

First, if you don’t already have an App_Code folder in your project, then add one by going to Visual Studio, open the Solution panel and right-click on the EPiServer project. Then choose Add > Add ASP.NET Folder > App_Code. 

Within the App_Code folder, I created a new view named Templates.cshtml. I pasted my helper into this view, like so:

@helper CarouselTemplate(List<BlogSample.Helpers.CarouselItem> carouselItems)
{
    <div class="col-md-12">
        <div class="content-article">
            <div id="home-slider" class="carousel slide" data-ride="carousel">
                <ol class="carousel-indicators">
                    @for (int i = 0; i < carouselItems.Count(); i++)
                    {
                        <li data-target="#home-slider" data-slide-to="@i" class="@(i == 0 ? "active" : string.Empty)"></li>
                    }
                </ol>
                <div class="carousel-inner">
                    @for (int i = 0; i < carouselItems.Count(); i++)
                    {
                        <div class="item @(i == 0 ? "active" : string.Empty)">
                            <img src="@carouselItems[i].ImageUrl" alt="@carouselItems[i].TitleText" />
                            <div class="carousel-caption">
                                <h3><small><span class="caption-meta-date">@carouselItems[i].PublicationDate.ToString("MM/dd")</span> <span>@carouselItems[i].PublicationDate.Year</span></small> @carouselItems[i].TitleText</h3>
                                <p>@carouselItems[i].Tagline</p>
                            </div>
                        </div>
                    }
                </div>
                <a class="left carousel-control" href="#home-slider" role="button" data-slide="prev">
                    <span class="glyphicon glyphicon-chevron-left"></span>
                </a>
                <a class="right carousel-control" href="#home-slider" role="button" data-slide="next">
                    <span class="glyphicon glyphicon-chevron-right"></span>
                </a>
            </div>
        </div>
    </div>
}

After that, calling the global template just works. The View name becomes a namespace of sorts, so the format for calling the template in my case is Templates.CarouselTemplate, and I use it just as I would any other razor template, except that it’s globally accessible.

In my case, however, simply using it as a template wasn’t enough to get the results I want. Primarily, my carousel will be made up of ArticlePage items. I have no intention of doing otherwise, but that doesn’t mean I don’t want that flexibility in the future. So what I’ll do instead is create my own Html Helper that will take the items within the ContentArea - whatever they are - and process them to be used as carousel slides. Here’s my logic for providing a drag and drop area versus the carousel helper:

@if (EPiServer.Editor.PageEditing.PageIsInEditMode)
{
    @Html.PropertyFor(m => m.CurrentPage.FeaturedContentArea, new { CssClass = "row", tag = BlogSample.Global.ContentAreaTags.Carousel })
}
else
{
    @Html.Carousel(Model.CurrentPage.FeaturedContentArea != null ? Model.CurrentPage.FeaturedContentArea.Items : new List<ContentAreaItem>(), Templates.CarouselTemplate)
}

I do this because I want the author to retain the drag and drop UI plus be able to view the slides individually for reordering. The else condition will provide a non-editable view for the site visitor to see a fully-functional carousel.

In order to build the HTML helper, I first need all of my “carousel enabled” items to have the same properties. So I created an inheritable class that would contain those content properties and present them in a Carousel tab via the Properties view.

using System;
using System.ComponentModel.DataAnnotations;
using EPiServer.Core;
using EPiServer.DataAbstraction;
using EPiServer.DataAnnotations;
using EPiServer.SpecializedProperties;

namespace BlogSample.Models.Pages
{
    [ContentType(DisplayName = "CarouselPageData", GUID = "684c86d7-6a66-44f3-82d5-9b3a6277ab90", Description = "")]
    [AvailableContentTypes( 
        Availability = Availability.None,
        ExcludeOn = new[] { typeof(StartPage), typeof(BlogPage), typeof(ArticlePage) })]
    public class CarouselPageData : PageBase
    {
        [Display(Name = "Short Title",
            Description = "Used in places where the actual title might be too long. For example, the home page carousel.",
            GroupName = Global.GroupNames.Carousel,
            Order = 100)]
        public virtual string ShortTitle { get; set; }

        [Display(Name = "Featured Image",
            Description = "Will appear in places like the Homepage Carousel.",
            GroupName = Global.GroupNames.Carousel,
            Order = 200)]
        public virtual ContentReference FeaturedImage { get; set; }
    }
}

All I need do to make sure my Article page is available for this Carousel is to have it inherit from my CarouselPageData class and make sure that it has a PagePartial rendering configuration with the same tag as my ContentArea Property.

Inside ArticlePage.cs:

public class ArticlePage : CarouselPageData

Inside TemplateCoordinator.cs:

viewTemplateModelRegistrator.Add(typeof(ArticlePage), new TemplateModel

{

    Name = "ArticleAsCarouselSlide",

    DisplayName = "Article as Carousel Slide",

    Default = false,

    Tags = new[] { Global.ContentAreaTags.Carousel },

    AvailableWithoutTag = false,

    Path = PartialPath("ArticlePageCarouselSlide")

});

I now have the right properties, a rendering for when I drag and drop, and the global template I need for rendering the visitor view. What’s lacking is the Html.Carousel helper itself, which is used in the view above as well as a common ViewModel for the display within that template.

The ViewModel is rather simple, in the way of most ViewModels. The only extra logic in this one is a bit to get the publication date as I want the author to be able to set a publication date on this content manually (helpful for imported content - unless there’s another solution there I’ve not yet discovered). If a manual date has been set, then I want to use it, otherwise fallback to the content item’s initial publish date. Obviously, this is still a work in progress and there will be no hard-coded text in the final product.

using BlogSample.Business.Extensions;
using BlogSample.Models.Pages;
using EPiServer.Core;
using EPiServer.Web.Routing;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BlogSample.Helpers
{
    public class CarouselItem
    {
        public PageData Page { get; set; }
        public string PageUrl { get; set; }
        public string TitleText { get; set; }
        public string ImageUrl { get; set; }
        public string Tagline { get; set; }
        public DateTime PublicationDate { get; set; }

        public CarouselItem(CarouselPageData carouselContent)
        {
            this.Page = carouselContent;
            this.PageUrl = UrlResolver.Current.GetUrl(carouselContent.ContentLink);
            this.TitleText = carouselContent.ShortTitle;
            this.ImageUrl = carouselContent.FeaturedImage.ImageUrl();
            this.Tagline = "Can't stop me now!";
            this.PublicationDate = GetPublicationDate(carouselContent);
        }

        private DateTime GetPublicationDate(CarouselPageData carouselContent)
        {
            if (carouselContent is ArticlePage)
            {
                var articleContent = carouselContent as ArticlePage;
                if (DateTime.Compare(articleContent.PublicationDate, DateTime.MinValue) != 0)
                {
                    return articleContent.PublicationDate;
                }
            }

            return carouselContent.StartPublish;
        }
    }
}

I borrowed what I could from several places to create my Carousel helper, but mostly from the Alloy HtmlHelpers class used for menu items.

public static IHtmlString Carousel(
    this HtmlHelper helper,
    IList<ContentAreaItem> itemList,
    Func<List<CarouselItem>, HelperResult> itemTemplate = null)
{
    itemTemplate = itemTemplate ?? GetDefaultCarouselItemTemplate(helper);
    var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>();
    var contentAreaContentLinks = itemList.Select(c => c.ContentLink).ToList();

    var contentList = new List<CarouselItem>();
    CarouselPageData cdata;
    foreach (var item in itemList)
    {
        cdata = null;
        contentLoader.TryGet<CarouselPageData>(item.ContentLink, out cdata);
        if (cdata != null)
        {
            contentList.Add(new CarouselItem(cdata));
        }
    }

    var buffer = new StringBuilder();
    var writer = new StringWriter(buffer);
    itemTemplate(contentList).WriteTo(writer);

    return new MvcHtmlString(buffer.ToString());
}

private static Func<IList<CarouselItem>, HelperResult> GetDefaultCarouselItemTemplate(HtmlHelper helper)
{
    return x => new HelperResult(writer => writer.Write(helper.PageLink(x.First().Page)));
}

All together, I get an editor view with drag and drop placement, but I also get a fully functional Carousel rendering that is available globally so I can call it from multiple templates, should I need to do so. In addition, all any content item needs is to inherit my CarouselPageData class in order to be used as a slide.

This is still very early in development of this project and any or all of the code above is subject to change as the project proceeds and I continue to learn about development in EPiServer and MVC. The entire project is being published to GitHub as well, so you can see what I’m up to and check in periodically on my progress.

Happy coding!

Apr 06, 2015

Comments

Apr 7, 2015 08:06 PM

I like the approach you took here, thanks for sharing!

Please login to comment.
Latest blogs
Optimizely SaaS CMS + Coveo Search Page

Short on time but need a listing feature with filters, pagination, and sorting? Create a fully functional Coveo-powered search page driven by data...

Damian Smutek | Nov 21, 2024 | Syndicated blog

Optimizely SaaS CMS DAM Picker (Interim)

Simplify your Optimizely SaaS CMS workflow with the Interim DAM Picker Chrome extension. Seamlessly integrate your DAM system, streamlining asset...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Optimizely CMS Roadmap

Explore Optimizely CMS's latest roadmap, packed with developer-focused updates. From SaaS speed to Visual Builder enhancements, developer tooling...

Andy Blyth | Nov 21, 2024 | Syndicated blog

Set Default Culture in Optimizely CMS 12

Take control over culture-specific operations like date and time formatting.

Tomas Hensrud Gulla | Nov 15, 2024 | Syndicated blog

I'm running Optimizely CMS on .NET 9!

It works 🎉

Tomas Hensrud Gulla | Nov 12, 2024 | Syndicated blog

Recraft's image generation with AI-Assistant for Optimizely

Recraft V3 model is outperforming all other models in the image generation space and we are happy to share: Recraft's new model is now available fo...

Luc Gosso (MVP) | Nov 8, 2024 | Syndicated blog