Showing posts with label content delivery. Show all posts
Showing posts with label content delivery. Show all posts

Tuesday, December 30, 2014

SDL Tridion integration with Portal Servers

One very common use case during enterprise implementations is integrating SDL Tridion with Portal servers. There are several approaches out there, this article will show 2 options that can be use perform such integration.
Integrating with Portal servers is not an straight forward exercise since Portals control the overall page rendering as well as page life cycle, in that sense SDL Tridion becomes to be a data (content) repository that is consumed in certain manner. Portals can consume formatted or unformatted data from Tridion in the format of XML, JSON or HTML chunks, selecting the content format is very important since it will determine design and styling responsibilities.

Thinking as a Software Architect I prefer to specialize responsibilities instead of sharing responsibilities between different systems. Having said that I prefer sending unformatted data to the Portal Server and passing the Content formatting and styling to the Portal server itself.

Based on different approaches I’ve seen so far I have determined two different options

Option 1: Integrated through the Content Delivery Web Service


click to see original image

  • Data (Content) is consumed via the Content Delivery Web Service
  • Binaries are consumed via the Content Delivery Web Service or via a Network Shared location (NAS)
  • Content is sent unformatted using JSON or XML  (no styling)


Option 2: Building Meta Files


click to see original image


  • A deployer extension to be developed in order to build a Meta Page File containing Content Positioning as well as relevant metadata is generated during publishing
  • The Portal Server will consume the Page Meta File by mapping the Portal Page name with the Page Meta File name File Name
  • The Meta File Name will contain unformatted data in XML or JSON format (No Styling)
  • Binaries are consumed from a Network Shared  location (NAS)


The two options shown above are not intended to be considered final solution but to be options to be evaluated during Portal Integrations. Below my considerations to take while deciding the Integration strategy

  • Styling to be controlled in only 1 system (SDL Tridion or the Portal Server). I would suggest to control Styling in the Portal Server so that the HTML Design Team will work with only 1 paradigm
  • Page  Life Cycle to be controlled by the Portal Server
  • SDL Tridion to control editorial content as well as any relevant metadata
  • SDL Tridion to control Binaries
  • SDL Tridion page templates to produce unformatted data that can be consumed by Portals (Option 2)
  • Component Templates to produce unformatted data that can be consumed by Portals (Option 1 and 2)

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


Friday, October 4, 2013

Improving Tridion .Net Controls - Component Presentation

In several .Net implementations I've noticed certain deficiencies related to the Component Presentation .Net Web Control. Basically this controls allow us to retrieve content from either the Tridion Broker database or file system. Additionally it will execute any server side code in the form of ASP .NET inline code / code blocks or REL tags. In the next sections in this post we will explore some of the deficiencies.

.NET server side code must be located in the File System.

A restriction is that if the developer wants to publish component presentations that includes .NET server side code to the database the standard Component Presentation web control won't execute this code. This is issue is related to the design behind the Component Presentation Assembler class, this class is intended to be technology agnostic so that it could be reason why it is not specialized to execute neither .NET or JAVA code from the database.

In a previous post I have written about how to solve this issue using a Virtual Path provider. This approach will instruct ASP .NET to consider Component Presentations stored in a database as ASCX files.


Child Controls are not initialized properly.

The standard Component Presentation Web Control renders its content by overriding the Render method in the following way.

protected override void Render(HtmlTextWriter writer)
{
    if(HttpContext.Current != null && HttpContext.Current.Application != null)
    {
        ComponentPresentationAssembler assembler = new ComponentPresentationAssembler(pageUri, this.Page);
        writer.Write(assembler.GetContent(componentUri, templateUri));

        this.RenderChildren(writer);
    }
}

This approach will work fine for common things like having <tridion:ComponentLink> or <tridion:ComponentPresentation> as child controls. However let's consider an scenario where we need more complex controls initialization (let's consider having fully functional ASP .NET Form in the form of a component presentation) like <asp:RegulerExpressionValidator> or <asp:RequiredFieldValidator> in those case the standard Component Presentation Web Control won't be able to initialize them.

The main problem resides on the fact that Render is called too late in the ASP .Net page life cycle, it will lead to child controls not to be initialized.

I order to solve it, I create a new version of the Component Presentation, this version will use the CreateChildControls method instead of Render.

The following code sample is a refactored and improved version of code I presented before in the  Virtual Path provider post.

protected override void CreateChildControls() {
    if (HttpContext.Current != null && HttpContext.Current.Application != null) {
        ComponentPresentationMeta meta = new ComponentPresentationMetaFactory(ComponentUri).GetMeta(ComponentUri, TemplateUri);
        if (meta != null) {
            string contentType = meta.ContentType;
            if (contentType.StartsWith("ASCX")) {
                using (ComponentPresentationFactory factory = new ComponentPresentationFactory(ComponentUri)) {
                    Tridion.ContentDelivery.DynamicContent.ComponentPresentation componentPresentation =
                        factory.GetComponentPresentation(ComponentUri, TemplateUri);

                    TcmUri componentId = new TcmUri(ComponentUri);
                    TcmUri templateId = new TcmUri(TemplateUri);

                    ComponentMetaFactory metaFactory = new ComponentMetaFactory(componentId.PublicationId);
                    IComponentMeta componentMeta = metaFactory.GetMeta(componentId.ItemId);

                    string virtualPath = string.Format("dcp_{0}_{1}.ascx", componentId.ItemId, templateId.ItemId);
                    CacheInvalidation(virtualPath, componentMeta);

                    Control control = Page.LoadControl(virtualPath);
                    this.Controls.Add(control);
                }
            }
            else {
                Tridion.ContentDelivery.Web.UI.ComponentPresentation componentPresentation = new Tridion.ContentDelivery.Web.UI.ComponentPresentation();
                componentPresentation.Page = this.Page;
                componentPresentation.PageUri = PageUri;
                componentPresentation.ComponentUri = ComponentUri;
                componentPresentation.TemplateUri = TemplateUri;
                this.Controls.Add(componentPresentation);
            }
        }
    }
}

The source code above will execute server side code from the database and it will also initialize child controls properly. This code can be improved to recognize if there is a Virtual Path provided present if not, it can retrieve the component presentation from the file system as before.

Child Controls events are not properly attached and initialized.

This is another issue related to complex scenarios where we have complex child controls like the ones present in an ASP .NET web form. Basically even if we use CreateChildControls instead of Render event handlers are still not attached properly. In order to solve it our Component Presentation Web Control should inherit from CompositeControls instead from Web Control.

Here a sample of how the class declaration should look like.

[DefaultProperty("ComponentUri"),
ToolboxData("<{0}:ComponentPresentation runat=server></{0}:ComponentPresentation>"),
ParseChildrenAttribute(ChildrenAsProperties = false)]
public class ComponentPresentation : CompositeControl {
...
}

Here how the whole class should look like.

[DefaultProperty("ComponentUri"),
ToolboxData("<{0}:ComponentPresentation runat=server></{0}:ComponentPresentation>"),
ParseChildrenAttribute(ChildrenAsProperties = false)]
public class ComponentPresentation : CompositeControl {

    [Bindable(true), Category("Appearance"), DefaultValue("")]
    public string ComponentUri { get; set; }
    [Bindable(true), Category("Appearance"), DefaultValue("")]
    public string TemplateUri { get; set; }
    [Bindable(true), Category("Appearance"), DefaultValue("")]
    public string PageUri { get; set; }

    protected override void CreateChildControls() {
        if (HttpContext.Current != null && HttpContext.Current.Application != null) {
            ComponentPresentationMeta meta = new ComponentPresentationMetaFactory(ComponentUri).GetMeta(ComponentUri, TemplateUri);
            if (meta != null) {
                string contentType = meta.ContentType;
                if (contentType.StartsWith("ASCX")) {
                    using (ComponentPresentationFactory factory = new ComponentPresentationFactory(ComponentUri)) {
                        Tridion.ContentDelivery.DynamicContent.ComponentPresentation componentPresentation =
                            factory.GetComponentPresentation(ComponentUri, TemplateUri);

                        TcmUri componentId = new TcmUri(ComponentUri);
                        TcmUri templateId = new TcmUri(TemplateUri);

                        ComponentMetaFactory metaFactory = new ComponentMetaFactory(componentId.PublicationId);
                        IComponentMeta componentMeta = metaFactory.GetMeta(componentId.ItemId);

                        string virtualPath = string.Format("dcp_{0}_{1}.ascx", componentId.ItemId, templateId.ItemId);
                        CacheInvalidation(virtualPath, componentMeta);

                        Control control = Page.LoadControl(virtualPath);
                        this.Controls.Add(control);
                    }
                }
                else {
                    Tridion.ContentDelivery.Web.UI.ComponentPresentation componentPresentation = new Tridion.ContentDelivery.Web.UI.ComponentPresentation();
                    componentPresentation.Page = this.Page;
                    componentPresentation.PageUri = PageUri;
                    componentPresentation.ComponentUri = ComponentUri;
                    componentPresentation.TemplateUri = TemplateUri;
                    this.Controls.Add(componentPresentation);
                    componentPresentation.Dispose();
                }
            }
        }

    }

    private void CacheInvalidation(string virtualPath, IComponentMeta componentMeta) {
        if (HttpContext.Current.Cache[virtualPath] == null) {
            HttpContext.Current.Cache[virtualPath] = componentMeta.LastPublicationDate;
        }
        else {
            DateTime lastPublishedDate = (DateTime)HttpContext.Current.Cache[virtualPath];
            if (lastPublishedDate < componentMeta.LastPublicationDate) {
                HttpContext.Current.Cache.Remove(virtualPath);
                HttpContext.Current.Cache[virtualPath] = componentMeta.LastPublicationDate;
            }
        }
    }
}