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;
            }
        }
    }
}

4 comments:

  1. Excellent point on the ASP.NET lifecycle--I've mistakenly assumed forms would work fine with a regular .NET dynamic component presentation. So in our CMS designs, we can still consider "config" or "widget" type setups, but I should probably be aware of how the controls render in delivery (and when).

    ReplyDelete
  2. The function CreateChildControls function is by far badly written:
    protected override void CreateChildControls() {
    // Don't need to check this here. Why you check the current context here? If you are only using it in the CacheInvalidation function.
    // If there is no context, you are just not doing anything when you could in fact render the component w/o complications, but don't update cache.

    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")) {

    // Why do you create the factory? Not using the componentPresentation object for anything.
    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();
    }
    }
    }
    }

    The fixed code should be something like:
    protected override void CreateChildControls()
    {
    var meta = new ComponentPresentationMetaFactory(ComponentUri).GetMeta(ComponentUri, TemplateUri);
    if (meta == null)
    return;

    var contentType = meta.ContentType;
    if (contentType.StartsWith("ASCX"))
    {
    var componentId = new TcmUri(ComponentUri);
    var templateId = new TcmUri(TemplateUri);

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

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

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

    ReplyDelete
  3. private static void UpdateCache(string virtualPath, IItem componentMeta)
    {
    if (HttpContext.Current == null)
    return;

    if (HttpContext.Current.Cache[virtualPath] == null)
    {
    HttpContext.Current.Cache[virtualPath] = componentMeta.LastPublicationDate;
    return;
    }

    var lastPublishedDate = (DateTime) HttpContext.Current.Cache[virtualPath];
    if (lastPublishedDate >= componentMeta.LastPublicationDate)
    return;

    HttpContext.Current.Cache[virtualPath] = componentMeta.LastPublicationDate;
    }

    ReplyDelete