Skip to content

Instantly share code, notes, and snippets.

@CheapSk8
Created March 26, 2021 16:34
Show Gist options
  • Save CheapSk8/79ee3e8d0ac2599974702f47db092846 to your computer and use it in GitHub Desktop.
Save CheapSk8/79ee3e8d0ac2599974702f47db092846 to your computer and use it in GitHub Desktop.
Sample of Custom Content Area Renderer that permits multiple items per Visitor Group in a Personalized Content Area
using EpiSandbox.Helpers;
using EPiServer;
using EPiServer.Core;
using EPiServer.Core.Html.StringParsing;
using EPiServer.Personalization.VisitorGroups;
using EPiServer.Security;
using EPiServer.ServiceLocation;
using EPiServer.Web;
using EPiServer.Web.Mvc;
using EPiServer.Web.Mvc.Html;
using HtmlAgilityPack;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
namespace EpiSandbox.Business.Rendering
{
public class CustomContentAreaRenderer : ContentAreaRenderer
{
private readonly IVisitorGroupRoleRepository _visitorGroupRoleRepo;
private Lazy<IEnumerable<VisitorGroup>> _visitorGroups = new Lazy<IEnumerable<VisitorGroup>>(() => (ServiceLocator.Current.GetInstance<IVisitorGroupRepository>()).List());
public CustomContentAreaRenderer() : base()
{
_visitorGroupRoleRepo = ServiceLocator.Current.GetInstance<IVisitorGroupRoleRepository>();
}
public CustomContentAreaRenderer(IVisitorGroupRoleRepository visitorGroupRoleRepository, IContentRenderer contentRenderer, TemplateResolver templateResolver, IContentAreaItemAttributeAssembler attributeAssembler, IContentRepository contentRepository, IContentAreaLoader contentAreaLoader)
: base(contentRenderer, templateResolver, attributeAssembler, contentRepository, contentAreaLoader)
{
_visitorGroupRoleRepo = visitorGroupRoleRepository;
}
public override void Render(HtmlHelper htmlHelper, ContentArea contentArea)
{
// the majority of this block is boilerplate from Episerver's renderer
if (contentArea == null || contentArea.IsEmpty) { return; }
TagBuilder contentAreaTagBuilder = null;
var viewContext = htmlHelper.ViewContext;
if (!IsInEditMode(htmlHelper) && ShouldRenderWrappingElement(htmlHelper))
{
contentAreaTagBuilder = new TagBuilder(GetContentAreaHtmlTag(htmlHelper, contentArea));
AddNonEmptyCssClass(contentAreaTagBuilder, viewContext.ViewData["cssclass"] as string);
viewContext.Writer.Write(contentAreaTagBuilder.ToString(TagRenderMode.StartTag));
}
RenderContentAreaItems(htmlHelper, GetFilteredItems(contentArea)); // the change to use GetFilteredItems gives us better control of personalized content references
if (contentAreaTagBuilder != null)
{
viewContext.Writer.Write(contentAreaTagBuilder.ToString(TagRenderMode.EndTag));
}
}
/// <summary>
/// A new method for filtered personalized Items out of the content area in groups allowing
/// matches of multiple personalized groups in order of group ID reference.
/// </summary>
/// <param name="contentArea"></param>
/// <returns></returns>
private IEnumerable<ContentAreaItem> GetFilteredItems(ContentArea contentArea)
{
var itemList = new List<ContentAreaItem>();
// get all contnet items as ContentAreaItemDetail to iterate through
var detailedContentItems = contentArea.Fragments.Where(f => f as ContentFragment != null).Select(f => new ContentAreaItemDetail(f as ContentFragment)).ToList();
// for loop so we can manipulate the list and remove items as we process them. this prevents us from having to iterate every item
for (ContentAreaItemDetail fragmentItem; (fragmentItem = detailedContentItems.FirstOrDefault()) != null;)
{
// we can tell if the current item is a personalized group by checking the ContentGroup value. all personalized items reference a ContentGroup ID.
if (string.IsNullOrWhiteSpace(fragmentItem.ContentGroup))
{
// add if user has permission and remove from detail list to prevent duplicates
if (shouldUserSeeContent(fragmentItem.ContentFragment))
{
itemList.Add(new ContentAreaItem(fragmentItem.ContentFragment));
}
detailedContentItems.Remove(fragmentItem);
}
else
{
var groupItems = detailedContentItems.Where(i => i.ContentGroup == fragmentItem.ContentGroup);
// get the first visitor group ID the user is part of
var firstMatchID = filterVisitorGroupIds(groupItems?.SelectMany(i => i.VisitorGroupIds)?.Distinct()).FirstOrDefault();
// get all items matching our first matched visitor group ID or all items set for "Everyone else sees"
var visitorGroupItems = (
groupItems?.Where(i => i.VisitorGroupIds.Contains(firstMatchID)).NullIfEmpty()
?? groupItems.Where(i => !i.VisitorGroupIds.Any()) // the "Everyone else sees" items don't have Visitor Group IDs added to them
)?.Where(i => shouldUserSeeContent(i.ContentFragment));
// add any matches to our list and remove all group items to continue iterating through content area items
if (visitorGroupItems?.FirstOrDefault() != null)
{
itemList.AddRange(visitorGroupItems.Select(i => new ContentAreaItem(i.ContentFragment)));
}
detailedContentItems.RemoveAll(i => i.ContentGroup == fragmentItem.ContentGroup);
}
}
return itemList;
}
/// <summary>
/// Return only visitor group IDs the current user is a member of
/// </summary>
/// <param name="visitorGroupIds">The list of IDs to filter</param>
/// <returns>A list of VisitorGroup IDs as strings</returns>
private List<string> filterVisitorGroupIds(IEnumerable<string> visitorGroupIds)
{
var visitorGroups = from id in visitorGroupIds
join vg in _visitorGroups.Value
on id equals vg.Id.ToString()
select vg;
var filteredGroupIds = new List<string>();
foreach (var visitorGroup in visitorGroups)
{
if (_visitorGroupRoleRepo.TryGetRole(visitorGroup.Name, out var roleProvider) && roleProvider.IsInVirtualRole(PrincipalInfo.CurrentPrincipal, null))
{
filteredGroupIds.Add(visitorGroup.Id.ToString());
}
}
return filteredGroupIds;
}
/// <summary>
/// Check for access rights to view a specific ContentFragment
/// </summary>
/// <param name="fragment">The ContentFragment to check access rights against. This applies to Visitor Groups as well.</param>
/// <returns>True or False based on whether the current user has Read access</returns>
private bool shouldUserSeeContent(ContentFragment fragment)
{
var securable = fragment as ISecurable;
if (securable == null) { return true; }
var securityDescriptor = securable.GetSecurityDescriptor();
return securityDescriptor.HasAccess(PrincipalInfo.CurrentPrincipal, AccessLevel.Read);
}
/// <summary>
/// Represents a Content Area Item with more detail, like their visitor group assignments.
/// </summary>
private class ContentAreaItemDetail
{
/// <summary>
/// Gets the content group ID referenced by the Content Fragment
/// </summary>
public string ContentGroup
{
get
{
return this.ContentFragment.ContentGroup;
}
}
/// <summary>
/// All attributes provided as part of this item. These contain references to the Group ID and Visitor Group IDs assigned for personalization.
/// </summary>
public Dictionary<string, string> FragmentAttributes { get; set; } = new Dictionary<string, string>();
/// <summary>
/// The Content Fragment associated with this content item.
/// </summary>
public ContentFragment ContentFragment { get; private set; }
/// <summary>
/// The Content ID of the item as a GUID
/// </summary>
public Guid ItemGuid { get; private set; }
/// <summary>
/// A collection of Visitor Group IDs assigned to this item
/// </summary>
public List<string> VisitorGroupIds { get; set; } = new List<string>();
public ContentAreaItemDetail(ContentFragment fragment)
{
this.ContentFragment = fragment ?? throw new ArgumentNullException(nameof(fragment));
this.ItemGuid = fragment.ContentGuid;
// we need the reference to the Visitor Group IDs assigned to this item, but they are only available in the InternalFormat HTML markup
// so we need to access the attribute in the markup to get the values
var fragmentElement = HtmlNode.CreateNode(fragment.InternalFormat); // create HTML Node to access attributes cleanly
this.FragmentAttributes = fragmentElement.Attributes.ToDictionary(k => k.Name.ToString(), v => v.Value); // dictionary makes future operations a bit easier and normalized
if (FragmentAttributes.TryGetValue(FragmentHandlerHelper.GroupsAttributeName, out var groupIds)) // groups attribute defines the Visitor Groups assigned to this item
{
// each Visitor Group ID is added as a comma delimited string value to the Internal Format HTML
this.VisitorGroupIds = groupIds.Split(',').ToList();
}
}
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment