Friday, October 3, 2014

Adding Expression Target Groups support to the Reference Implementation

Now days “Context” is a key part on almost every SDL Tridion implementation, the Reference Implementation is not a exception, the first version of the Reference Implementation supports Contextual Images Delivery. However it doesn’t support Dynamic Vocabulary (Expression Target Groups)

In this post I am describing the process to extend the Reference Implementation in order to add support for Dynamic Vocabulary. If you need more details on what is a Dynamic Vocabulary, please refer to this blog post Context Expression Extension

Adding Dynamic Vocabulary support for the Reference Implementation involves adding / changing some DD4T and Reference implementation areas.


Publishing Model

1.       Update the IComponentPresentation interface. Add a new property that holds Expressions

namespace DD4T.ContentModel
{
    public interface IComponentPresentation
    {
        IComponent Component { get; }
        IComponentTemplate ComponentTemplate { get; }
        IPage Page { get; set; }
        bool IsDynamic { get; set; }
        string RenderedContent { get; }
        int OrderOnPage { get; set; }
        IList<ICondition> Conditions { get; }
        IList<string> Expressions { get; set; }
    }
}

2.       Update the ComponentPresentation class. Implement the Expressions property

public class ComponentPresentation : IComponentPresentation
{
    [XmlIgnore]
    public IPage Page { get; set; }
    public Component Component { get; set; }
    [XmlIgnore]
    IComponent IComponentPresentation.Component
    {
        get { return Component as IComponent; }
    }
    public ComponentTemplate ComponentTemplate { get; set; }
    [XmlIgnore]
    IComponentTemplate IComponentPresentation.ComponentTemplate
    {
        get { return ComponentTemplate as IComponentTemplate; }
    }
    public string RenderedContent { get; set; }
    public bool IsDynamic { get; set; }

    [XmlIgnore]
    public int OrderOnPage { get; set; }

    public List<Condition> Conditions { get; set; }

    public List<string> Expressions { get; set; }

    [XmlIgnore]
    IList<ICondition> IComponentPresentation.Conditions
    {
        get { return Conditions.ToList<ICondition>(); }
    }

    [XmlIgnore]
    IList<string> IComponentPresentation.Expressions {
        get {
            throw new NotImplementedException();
        }
        set {
            throw new NotImplementedException();
        }
    }
}

3.       Add Context Target Groups TBB. New TBB to be created / added in order to populate the Expressions property with Target Groups / Component Presentations mapping

namespace DD4T.Templates {
    [TcmTemplateTitle("Add Expression Target Groups")]
    public class AddExpressionTargetGroups : BasePageTemplate {
        protected override void TransformPage(Page page) {
            Tcm.Page tcmPage = GetTcmPage();

            int index = 0;
            foreach (var componentPresentation in tcmPage.ComponentPresentations) {
                if (componentPresentation.Conditions != null && componentPresentation.Conditions.Count > 0) {
                    page.ComponentPresentations[index].Expressions = componentPresentation.Conditions.Select(s => s.TargetGroup.Title).ToList();
                }
                index += 1;
            }
        }
    }
}

4.  Update the Render Page Content Composite TBB in order to use Add Expressions Target Group


Context Expression Extension

1.  Create a set of Expressions Target Groups to test the functionality




2.  Apply Context Target Groups to Component Presentations. In this case I have applied them to the Article in the Reference Implementation Home Page



3.  Publish the Expressions Target Groups so that they can be part of the Dynamic Vocabulary



Reference Implementation

1.       Add a new class ExpressionsEngine to Sdl.Web.Tridion. This class will call the JEXL engine in order to validate the expressions while Rendering Items

namespace Sdl.Web.Tridion.Context {
    public static class ExpressionsEngine {
        public static bool EvaluateExpression(string expression) {
            using (ClaimStoreExpressionEngine expressionEngine = new ClaimStoreExpressionEngine(Com.Tridion.Ambientdata.AmbientDataContext.GetCurrentClaimStore(), new ValueConverter())) {
                Java.Lang.Boolean result = expressionEngine.EvaluateBooleanExpression(expression);
                if (result != null) return result.BooleanValue();
                return false;
            }
        }

        public static string WriteExpression(string expression) {
            using (ClaimStoreExpressionEngine expressionEngine = new ClaimStoreExpressionEngine(Com.Tridion.Ambientdata.AmbientDataContext.GetCurrentClaimStore(), new ValueConverter())) {
                return expressionEngine.EvaluateStringExpression(expression);
            }
        }
    }
}

2.       Update the RenderEntity method in the DD4TRenderer class in order to evaluate expressions before entities are written

public override MvcHtmlString RenderEntity(object item, HtmlHelper helper, int containerSize = 0, List<string> excludedItems = null)
{
    var cp = item as ContentModel.ComponentPresentation;
    if (cp.Expressions != null && cp.Expressions.Count > 0) {
        foreach (string expression in cp.Expressions) {
            if (!ExpressionsEngine.EvaluateExpression(expression)) {
                return null;
            }
        }
    }

    var mvcData = ContentResolver.ResolveMvcData(cp);
    if (cp != null && (excludedItems == null || !excludedItems.Contains(mvcData.ViewName)))
    {
        var parameters = new RouteValueDictionary();
        int parentContainerSize = helper.ViewBag.ContainerSize;
        if (parentContainerSize == 0)
        {
            parentContainerSize = SiteConfiguration.MediaHelper.GridSize;
        }
        if (containerSize == 0)
        {
            containerSize = SiteConfiguration.MediaHelper.GridSize;
        }
        parameters["containerSize"] = (containerSize * parentContainerSize) / SiteConfiguration.MediaHelper.GridSize;
        parameters["entity"] = cp;
        parameters["area"] = mvcData.ControllerAreaName;
        foreach (var key in mvcData.RouteValues.Keys)
        {
            parameters[key] = mvcData.RouteValues[key];
        }
        MvcHtmlString result = helper.Action(mvcData.ActionName, mvcData.ControllerName, parameters);
        if (WebRequestContext.IsPreview)
        {
            result = new MvcHtmlString(TridionMarkup.ParseEntity(result.ToString()));
        }
        return result;
    }
    return null;
}


Test the Reference Implementation

1.       Test with an Apple Device


2.       Test with a Non Apple Device