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


Wednesday, September 24, 2014

Developing a Custom Transport Handler

In this blog post I wanted to show a more advanced feature available in SDL Tridion, this feature gives developers a more deeper control on the Transport Package. Unfortunately this nice feature is neither documented nor supported but I think it is good enough to show how to use it in a Blog Post.

Developing a Transport Handler is very similar to developing other Tridion extension points like Resolvers or Event Systems. So, let’s provide a list of steps in order to develop one. In this sample I am writing a Transport Handler that overrides the existing TaxonomyTransportHandler in order to add the user that published the Category in the Instructions document.

1.       Implement a class that inherits ITransportHandler interface. This interface is not available in the DLLs available in /bin/client, you have to reference Tridion.ContentManager.Publishing.Transporting.dll that is in the GAC

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
using Tridion.ContentManager.Publishing.Rendering;
using Tridion.ContentManager.Publishing.Resolving;
using Tridion.ContentManager.Publishing.Transporting;

namespace TransportHandlers {
    public class CustomTaxonomyTransportHandler : ITransportPackageHandler {

        private TaxonomyTransportPackageHandler _defaultTaxonomyHandler;
        private XmlDocument _defaultInstructionsDocument;
        private string _user;

        public CustomTaxonomyTransportHandler(TransportPackage package) {
            _defaultTaxonomyHandler = new TaxonomyTransportPackageHandler(package);
            _defaultInstructionsDocument = package.InstructionsDocument;
            _user = package.PublishTransaction.Creator.Title;
        }

        public void HandleItemForPublishing(object item, XmlElement parentElement) {
            _defaultTaxonomyHandler.HandleItemForPublishing(item, parentElement);
        }

        public XmlElement HandleRenderedItemForPublishing(RenderedItem renderedItem, XmlElement parentElement) {
            XmlElement customCategoryElement = _defaultInstructionsDocument.CreateElement("CustomCategoryInfo");
            customCategoryElement.InnerXml = string.Format("<Category>{0}</Category><User>{1}</User>", renderedItem.ResolvedItem.Item.Id, _user);
            _defaultInstructionsDocument.DocumentElement.AppendChild(customCategoryElement);

            return _defaultTaxonomyHandler.HandleRenderedItemForPublishing(renderedItem, parentElement);
        }

        public void HandleResolvedItemForUnPublishing(ResolvedItem resolvedItem) {
            _defaultTaxonomyHandler.HandleResolvedItemForUnPublishing(resolvedItem);
        }
    }
}


2.       The class above should be compiled in an Strong Name Assembly and registered in the GAC
3.       Register it in the Tridion.ContentManager.config configuration file

<transporting rootStorageFolder="c:\Temp">
  <mappings>
    .
    .
    .
    <add itemType="Tridion.ContentManager.ContentManagement.Category">
      <handler type="TransportHandlers.CustomTaxonomyTransportHandler" assembly="TransportHandlers, Version=1.0.0.0, Culture=neutral, PublicKeyToken=3b4b2f6e0f11afc8" />
    </add>
    .
    .
    .
  </mappings>
</transporting>

4.       Restart the TcmPublisher Windows Service
5.       Publish any Publishable Category, as a Result you will see a custom Xml Element appended in the Transport Package Instructions document

<ProcessorInstructions version="7.1.0.1290">
  <Publication Id="tcm:0-3-1" Title="400 Example Site" Key="400 Example Site" PublicationPath="\" PublicationUrl="/" MultimediaPath="\media" MultimediaUrl="/media/" />
  <Action>Deploy</Action>
  <Target Type="6" />
  <CustomCategoryInfo>
    <Category>tcm:3-29-512</Category>
    <User>T2013SP1\Administrator</User>
  </CustomCategoryInfo>
  <Section Type="Taxonomies" Name="Taxonomies">
    <Taxonomy Id="tcm:3-29-512" Name="tcm_3-29-512.xml" UseForIdentification="False" />
  </Section>
</ProcessorInstructions>

Monday, September 22, 2014

ILMerge and SDL Tridion Development

SDL Tridion comes with a nice feature that allows developers to upload DLLs as managed Assemblies (Template Building Blocks, Workflow Activities) or to reference DLLs in the file system (Event System, Resolvers). While implementing SDL Tridion we normally have to re-use projects or external assemblies across different areas in our implementation which is normal in modular software development.

The fact of re-usability is mandatory and key in any software implementation. .NET code is executed in boundaries called Application Domains, at execution time the .NET Framework creates an Application Domain which will contain the executing assembly and any other dependency (Assemblies and Resources). The .NET Framework looks for dependencies in the following places.

1.     Checks for assemblies referenced in Configuration Files (<runtime> element)
2.     Checks for assemblies in the same Folder
3.     Check for assemblies in the Global Assembly Cache

However the way SDL Tridion executes code brings some issues. .NET TBBs and Workflow Activities are dynamically loaded from records in the Content Manager database. It means we cannot reference assemblies in the same folders or dependencies in configuration files (because there is no folder or configuration file involved), it leaves GAC as the only option

So, the question is, What are my alternatives when I want to use a shared library or project between different areas in our implementation?, for instance I have developed a Logging Framework or XML Parsing libraries that are used by Event System and TBBs as well as Workflow Activities. In that scenario the only option is to register those shared libraries in the GAC (Global Assembly Cache) which makes deployment process more difficult and managing dependencies even more complicated.

ILMerge allows to merge different assemblies (libraries, projects) into a single assembly which in fact will make all the code and functionality available in different libraries and projects in the same Application Domain. With ILMerge we can have a logging library or project references in Workflow Activities as well as referenced in TBBs. ILMerge will generate a single assembly that contains the result of merging different assemblies (executing assemblies and dependencies) in the same DLL so we don’t need to worry about registering all the shared dependencies in the GAC.

DD4T and SDL Reference Implementation are using that approach in order to make compilation and deployment easier.


Here a sample on how DD4T templates are compiled using ILMerge. This sample shows the Post Build Event configured in the DD4T Template Project

Command Segment
Description
"C:\Development\dynamic-delivery-4-tridion-master\dotnet\Dynamic Delivery Publishing Solution Items\ilmerge"
ILMerge location
/lib:"C:\Windows\Microsoft.NET\Framework\v4.0.30319"
/lib:"C:\Program Files (x86)\Tridion\bin\client"
Use a /lib parameter in order to specify paths where ILMerge will look for dependencies that should be merged in the resulting assembly
/t:dll
Target Format (DLL, EXE)
/targetplatform:v4,C:\Windows\Microsoft.NET\Framework\v4.0.30319
Specifies that the DLL is intended for the .NET Framework 4 and higher
/out:"C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.Templates.merged.dll"
Resulting merged DLL path
"C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.ContentModel.Contracts.dll" "C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.ContentModel.dll" "C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.Templates.Base.dll" "C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.ContentModel.XmlSerializers.dll" "C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.Templates.dll"
The list of assemblies that should be merged

"C:\Development\dynamic-delivery-4-tridion-master\dotnet\Dynamic Delivery Publishing Solution Items\ilmerge"
/lib:"C:\Windows\Microsoft.NET\Framework\v4.0.30319"
/lib:"C:\Program Files (x86)\Tridion\bin\client"
/t:dll
/targetplatform:v4,C:\Windows\Microsoft.NET\Framework\v4.0.30319
/out:"C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.Templates.merged.dll"
"C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.ContentModel.Contracts.dll" "C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.ContentModel.dll" "C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.Templates.Base.dll" "C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.ContentModel.XmlSerializers.dll" "C:\Development\dynamic-delivery-4-tridion-master\build\DD4T.Templates.dll"