eGandalf
Oct 11, 2016
  4075
(2 votes)

Rendering a ContentFolder as a Block to List Assets

Update: I'm an idiot. Fixed it up.

For content-heavy clients, we get the occasional request for listing file assets on the site for download. From what I can tell, there are a couple of options that require a bit of author effort, but I really wanted to provide something that is more suited to the best authoring experience offered by Episerver - drag and drop within Content Areas.

Knowing I can drag and drop most content types into a ContentArea and expect something to happen, I decided to give it a try using one of the asset panel ContentFolders. So I hover my mouse over a folder, casually hold down the mouse button, and drag the folder into an open area of the page. The field highlights! The folder drops in! It works!

Almost.

According to Episerver's documentation, "a content folder is used to structure content and has no visual appearance on the site." Meaning that the ContentFolder type has no Controller and no View.

Great. I can fix that. Code below.

Here's the part where I was an idiot. I was using the wrong parameter name in my Index method and therefore thought that the system was always returning null. Key lesson: make sure you're aligning with convention. So I deleted where I was explaining my workaround that I didn't need and am just including the final code below. This isn't production, but feel free to point out my other mistakes.

Controller:

using EPiServer.Core;
using EPiServer.Web.Mvc;
using System;
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
using EPiServer;
using EPiServer.ServiceLocation;
using Alloy.Models.ViewModels;
using Alloy.Models.Media;
using Alloy.Business;

namespace Alloy.Controllers
{
    public class ContentFolderController : PartialContentController
    {
        private IContentRepository contentRepository => ServiceLocator.Current.GetInstance();
        // GET: ContentFolder
        public override ActionResult Index(ContentFolder currentContent)
        {
            if(currentContent != null)
            {
                var model = new ContentFolderViewModel();
                model.Name = currentContent.Name;

                var documents = contentRepository.GetChildren(currentContent.ContentLink);

                model.Documents = documents.Select(d => new DocumentViewModel() {
                    Name = d.Name,
                    Description = d.Description,
                    Url = d.GetUrl(),
                    FileSize = d.BinaryData.GetSize(),
                    EveryoneHasAccess = d.IsAvailableToEveryone()
                });

                return PartialView(model);
            }
            return PartialView();
        }
    }
}

View:

@model Alloy.Models.ViewModels.ContentFolderViewModel

<h3>@Model.Name</h3>
<ul>
@foreach (var doc in Model.Documents)
{
    <li>
        <a href="@doc.Url">@doc.Name</a>
        @if (!string.IsNullOrWhiteSpace(doc.Description))
        { <div>@Html.Raw(doc.Description)</div> }
        <div style="font-style: italic; color: #777">File size: @doc.FileSize</div>
    </li>
}
</ul>

ViewModels:

using System.Collections.Generic;

namespace Alloy.Models.ViewModels
{
    public class ContentFolderViewModel
    {
        public string Name { get; set; }
        public IEnumerable<DocumentViewModel> Documents { get; set; }
    }
}
namespace Alloy.Models.ViewModels
{
    public class DocumentViewModel
    {
        public string Name { get; set; }
        public string FileSize { get; set; }
        public string Url { get; set; }
        public string Description { get; set; }
        public bool EveryoneHasAccess { get; set; }
    }
}

Extensions:

using EPiServer.Core;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web.Routing;
using System;
using System.Security.Principal;

namespace Alloy.Business
{
    public static class MediaExtensions
    {
        private static UrlResolver urlResolver => ServiceLocator.Current.GetInstance<UrlResolver>();
        private static PermissionService permissionService => ServiceLocator.Current.GetInstance<PermissionService>();

        public static string GetSize(this EPiServer.Framework.Blobs.Blob blob)
        {
            using (var blobReader = blob.OpenRead())
            {
                var l = blobReader.Length;
                if(l < 1000)
                {
                    return $"{l} bytes";
                }
                if(l < 1000000)
                {
                    return $"{l / 1024} KB";
                }
                return $"{l / 1056478} MB";
            }
        }

        public static Boolean IsAvailableToEveryone<T>(this T content) where T : IContent
        {
            return content.RoleHasAccess(new[] { "Everyone" }, AccessLevel.Read);
        }

        public static Boolean RoleHasAccess<T>(this T content, string[] roles, AccessLevel accessLevel) where T : IContent
        {
            var securedContent = content as ISecurable;
            var descriptor = securedContent.GetSecurityDescriptor();
            var identity = new GenericIdentity("doesn't matter");
            var principal = new GenericPrincipal(identity, roles);
            return descriptor.HasAccess(principal, accessLevel);
        }

        public static string GetUrl<T>(this T content) where T : IContent
        {
            return urlResolver.GetUrl(content.ContentLink);
        }
    }
}
Oct 11, 2016

Comments

Per Magne Skuseth
Per Magne Skuseth Oct 11, 2016 06:52 PM

Hi! The reason "folder" is always null is because of the parameter name.
For Episerver controllers, the model binder will check and see if the name of the parameter is currentPage/currentBlock/currentContent. If it is, it will get the current content from the routeData.
So, changing the name from "folder" to "currentContent" will get you get current ContentFolder.

eGandalf
eGandalf Oct 11, 2016 08:05 PM

Thanks - I'm an idiot. While it should be obvious to anyone with more Epi experience, I'm leaving the code for more novice searchers (like myself).

Oct 12, 2016 12:39 PM

Maybe worth adding a Log.Info in Episerver if a controller is missing a matching parameter to aid in finding that error? I think most developers have dont that at least once.

At least I have.. :)

It can take a few hours to find if you have never seen it before...

valdis
valdis Oct 12, 2016 01:30 PM

Adding my 2 cents :)

I would move Length calculation at the moment when media is saved, not when it's rendered.

And rendering the description property in the template could be also wrapped into some generic `ifnotempty` helper method.

Aaaaand :) If extension is defined for IContent, should it be located in MediaExtensions class? ;)

Cheers!

eGandalf
eGandalf Oct 12, 2016 03:02 PM

Hey Valdis,

I completely agree with every point!

This is just a PoC I put together for a project, nowhere near production code, so I'm willing to live with some shortcuts in favor of that. I'm changing the extension a bit to add some validation (see comments in other post). When I'm satisfied with it, I'll update this as well.

Please login to comment.
Latest blogs
Integrating Optimizely DAM with Your Website

This article is the second in a series about integrating Optimizely DAM with websites. It discusses how to install the necessary package and code t...

Andrew Markham | Sep 28, 2024 | Syndicated blog

Opticon 2024 - highlights

I went to Opticon in Stockholm and here are my brief highlights based on the demos, presentations and roadmaps  Optimizely CMS SaaS will start to...

Daniel Ovaska | Sep 27, 2024

Required fields support in Optimizely Graph

It's been possible to have "required" properties (value must be entered) in the CMS for a long time. The required metadata haven't been reflected i...

Jonas Bergqvist | Sep 25, 2024

How to write a bespoke notification management system

Websites can be the perfect vehicle for notifying customers of important information quickly, whether it’s the latest offer, an operational message...

Nicole Drath | Sep 25, 2024

Optimizely DAM – An Introduction

I presented a talk about the Optimizely DAM at the OMVP summit during Opticon 2024 in Sweden. I have now converted that talk into two blog posts....

Andrew Markham | Sep 25, 2024 | Syndicated blog

Simple and Effective Personalization with Optimizely Data Platform (ODP)

As we dive into the amazing capabilities of Optimizely One, let’s shine a spotlight on the Optimizely Data Platform (ODP). This simple tool unifies...

Alex Harris - Perficient | Sep 24, 2024 | Syndicated blog