Tridion Event System: Automated Page Creation After Creating A Component

It’s been a great week that I had visiting upstate New York and parading my son around for all the family to see. With that said, I think its due time for another post. Today I will be following up with yet another Tridion Event System tutorial, again using the power of Tridion’s API to add a bit more automation to your environment.  We’ll be automatically creating Pages for our newly created Article components.  As Alvin mentioned in my other post, when you are adding functionality that changes the behavior of Tridion (such as moving your content author’s content for them or creating pages for them), make sure that they are fully aware of these customizations, else they will be in for a world of confusion.  The reverse is also nice… if there’s a new member that comes in, let them know that these are customizations and not out of the box features.  I’ve been on projects where one of the authors had prior Tridion experience and swore a special feature existed, yet not knowing that the feature was custom developed for their project.  If you haven’t done so already, you should also first read the other Event System tutorial.  After today’s tutorial, you should be a little more comfortable with Tridion 2011′s Event System and working with the TOM.NET API to create new Pages, create Structure Groups, and perform a Where Using search.

Although we could easily combine this new functionality to the code we created before, we’ll go ahead and set up a new class with the following using statements and unique ID for the extension.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Tridion.ContentManager;
using Tridion.ContentManager.CommunicationManagement;
using Tridion.ContentManager.ContentManagement;
using Tridion.ContentManager.ContentManagement.Fields;
using Tridion.ContentManager.Extensibility;
using Tridion.ContentManager.Extensibility.Events;

namespace Tridion.Samples
{
    [TcmExtension("AutomatedPageEventHandlerExtension")]
    public class AutomatedPageEventHandler : TcmExtension
    {

    }
}

We will only want to create new Pages for our Articles that have not yet been added to a page. For this, we’ll perform a Where Using check to see if our component is being used on a page or not.

private bool DoesNotHavePage(Component component)
{
    UsingItemsFilter filter = new UsingItemsFilter(component.Session);
    filter.ItemTypes = new List<ItemType> { ItemType.Page };
    filter.IncludedVersions = VersionCondition.OnlyLatestVersions;

    return component.GetUsingItems(filter).Count() == 0;
}

We’ll want to use the Component’s title to generate the title for the page. But what about the filename? Lets go ahead and add a quick slugging algorithm to just use the title and make it filename friendly (thanks to John Roland at http://predicatet.blogspot.com/2009/04/improved-c-slug-generator-or-how-to.html for posting this script).

private string CreateSlugTitle(string title)
{
    byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(title);
    title = System.Text.Encoding.ASCII.GetString(bytes).ToLower();

    title = Regex.Replace(title, @"[^a-z0-9\s-]", ""); // invalid chars
    title = Regex.Replace(title, @"\s+", " ")
        .Trim() // convert multiple spaces into one space
        .Substring(0, title.Length <= 45 ? title.Length : 45)
        .Trim(); // cut and trim it
    title = Regex.Replace(title, @"\s", "-"); // hyphens   

    return title;
}

Just like we auto-organized our Components in the previous Event System tutorial, lets write a couple of methods that will do the same for our generated pages. Remember, Titles and Directories for StructureGroup’s are required fields.

private StructureGroup GetStructureGroup(string title, string directory, StructureGroup parentSG, OrganizationalItemItemsFilter filter)
{
    StructureGroup sg = parentSG
        .GetItems(filter)
        .Where(f => f.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase))
        .FirstOrDefault() as StructureGroup;

    if (sg == null)
    {
        sg = CreateStructureGroup(title, directory, parentSG);
    }

    return sg;
}

private StructureGroup CreateStructureGroup(string title, string directory, StructureGroup parentSG)
{
    StructureGroup newSG = parentSG.GetNewObject<StructureGroup>();
    newSG.Title = title;
    newSG.Directory = directory;
    newSG.Save();

    return newSG;
}

Next is the bread and butter where we put it all together. We’ll use a WebDav URL to open up items that we need from Tridion (and as mentioned before, I would recommend an extra step and keeping these in some configuration file). Pay attention to how we create the Page using the TOM.NET API as well as add ComponentPresentations to it. One gotcha that you might run into is (real easy to forget), if you had tried adding the Component that is passed via the Event Handler argument, you’ll get an error. This is because the Page is expecting the Component to exist in the same Publication as it. That’s why we open the component in the Page’s publication, and then add that newly opened component to the Page.

private void CreatePage(Component component)
{
    Session session = component.Session;

    ItemFields metadata = new ItemFields(component.Metadata, component.MetadataSchema);
    DateTime articleDate = ((DateField)metadata["ArticleDate"]).Value;

    string year = articleDate.Year.ToString();
    string month = articleDate.ToString("MM MMMM");
    string monthDirectory = articleDate.ToString("MM");

    StructureGroup baseStructureGroup = session.GetObject("/webdav/040 Website Master/Root/000 Articles") as StructureGroup;
    if (baseStructureGroup == null)
        throw new Exception("CreatePage - Unable to get baseStructureGroup");

    ComponentTemplate componentTemplate = session.GetObject("/webdav/040 Website Master/Building Blocks/System/Templates/Component Templates/Article CT.tctcmp") as ComponentTemplate;
    if (componentTemplate == null)
        throw new Exception("CreatePage - Unable to get componentTemplate");

    PageTemplate pageTemplate = session.GetObject("/webdav/040 Website Master/Building Blocks/System/Templates/Page Templates/Article PT.tptcmp") as PageTemplate;
    if (pageTemplate == null)
        throw new Exception("CreatePage - Unable to get pageTemplate");

    OrganizationalItemItemsFilter filter = new OrganizationalItemItemsFilter(session);
    filter.ItemTypes = new List<ItemType> { ItemType.StructureGroup };

    StructureGroup yearSG = GetStructureGroup(year, year, baseStructureGroup, filter);
    StructureGroup monthSG = GetStructureGroup(month, monthDirectory, yearSG, filter);

    Page newPage = monthSG.GetNewObject<Page>();
    newPage.PageTemplate = pageTemplate;
    newPage.Title = component.Title;
    newPage.FileName = CreateSlugTitle(component.Title);

    Component localComponent = session.GetObject(new TcmUri(component.Id.ItemId, ItemType.Component, baseStructureGroup.Id.PublicationId)) as Component;
    ComponentPresentation cp = new ComponentPresentation(localComponent, componentTemplate);
    newPage.ComponentPresentations.Add(cp);

    newPage.Save(true);
}

Finally we add our Constructor where we will subscribe our event and the handler method itself. In the handler method, we’ll make sure that only our Articles will trigger the Page Generation, and only Articles that don’t already exist on a page.

public AutomatedPageEventHandler()
{
    EventSystem.Subscribe<Component, CheckInEventArgs>(OnComponentCheckedInPost, EventPhases.TransactionCommitted);
}

private void OnComponentCheckedInPost(Component component, CheckInEventArgs args, EventPhases phase)
{
    if (component.Schema.Title.Equals("Article") && DoesNotHavePage(component))
    {
        CreatePage(component);
    }
}

Your class should now look like the following.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Tridion.ContentManager;
using Tridion.ContentManager.CommunicationManagement;
using Tridion.ContentManager.ContentManagement;
using Tridion.ContentManager.ContentManagement.Fields;
using Tridion.ContentManager.Extensibility;
using Tridion.ContentManager.Extensibility.Events;

namespace Tridion.Samples
{
    /// <summary>
    /// Component Event Handler
    /// </summary>
    [TcmExtension("AutomatedPageEventHandlerExtension")]
    public class AutomatedPageEventHandler : TcmExtension
    {
        /// <summary>
        /// Constructor - Subscribe the component events to handle.
        /// </summary>
    public AutomatedPageEventHandler()
    {
        EventSystem.Subscribe<Component, CheckInEventArgs>(OnComponentCheckedInPost, EventPhases.TransactionCommitted);
    }

        /// <summary>
        /// On Component Checked In Transaction Committed events.
        /// </summary>
        /// <param name="component">The component checked in.</param>
        /// <param name="args">The CheckInEventArgs instance.</param>
        /// <param name="phase">The EventPhase enum.</param>
        private void OnComponentCheckedInPost(Component component, CheckInEventArgs args, EventPhases phase)
        {
            if (component.Schema.Title.Equals("Article") && DoesNotHavePage(component))
            {
                CreatePage(component);
            }
        }

        /// <summary>
        /// Creates a page and adds the component to it.
        /// </summary>
        /// <param name="component">The component to create the page for.</param>
        private void CreatePage(Component component)
        {
            Session session = component.Session;

            ItemFields metadata = new ItemFields(component.Metadata, component.MetadataSchema);
            DateTime articleDate = ((DateField)metadata["ArticleDate"]).Value;

            string year = articleDate.Year.ToString();
            string month = articleDate.ToString("MM MMMM");
            string monthDirectory = articleDate.ToString("MM");

            StructureGroup baseStructureGroup = session.GetObject("/webdav/040 Website Master/Root/000 Articles") as StructureGroup;
            if (baseStructureGroup == null)
                throw new Exception("CreatePage - Unable to get baseStructureGroup");

            ComponentTemplate componentTemplate = session.GetObject("/webdav/040 Website Master/Building Blocks/System/Templates/Component Templates/Article CT.tctcmp") as ComponentTemplate;
            if (componentTemplate == null)
                throw new Exception("CreatePage - Unable to get componentTemplate");

            PageTemplate pageTemplate = session.GetObject("/webdav/040 Website Master/Building Blocks/System/Templates/Page Templates/Article PT.tptcmp") as PageTemplate;
            if (pageTemplate == null)
                throw new Exception("CreatePage - Unable to get pageTemplate");

            OrganizationalItemItemsFilter filter = new OrganizationalItemItemsFilter(session);
            filter.ItemTypes = new List<ItemType> { ItemType.StructureGroup };

            StructureGroup yearSG = GetStructureGroup(year, year, baseStructureGroup, filter);
            StructureGroup monthSG = GetStructureGroup(month, monthDirectory, yearSG, filter);

            Page newPage = monthSG.GetNewObject<Page>();
            newPage.PageTemplate = pageTemplate;
            newPage.Title = component.Title;
            newPage.FileName = CreateSlugTitle(component.Title);

            Component localComponent = session.GetObject(new TcmUri(component.Id.ItemId, ItemType.Component, baseStructureGroup.Id.PublicationId)) as Component;
            ComponentPresentation cp = new ComponentPresentation(localComponent, componentTemplate);
            newPage.ComponentPresentations.Add(cp);

            newPage.Save(true);
        }

        /// <summary>
        /// Creates a url friendly slug title.
        /// </summary>
        /// <param name="title">The title to slug.</param>
        /// <returns>A sluggified title.</returns>
        /// <remarks>http://predicatet.blogspot.com/2009/04/improved-c-slug-generator-or-how-to.html</remarks>
        private string CreateSlugTitle(string title)
        {
            byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(title);
            title = System.Text.Encoding.ASCII.GetString(bytes).ToLower();

            title = Regex.Replace(title, @"[^a-z0-9\s-]", ""); // invalid chars
            title = Regex.Replace(title, @"\s+", " ")
                .Trim() // convert multiple spaces into one space
                .Substring(0, title.Length <= 45 ? title.Length : 45)
                .Trim(); // cut and trim it
            title = Regex.Replace(title, @"\s", "-"); // hyphens   

            return title;
        }

        /// <summary>
        /// Check whether or not the component exists on a page.
        /// </summary>
        /// <param name="component">The component to check.</param>
        /// <returns>True if no page is found.</returns>
        private bool DoesNotHavePage(Component component)
        {
            UsingItemsFilter filter = new UsingItemsFilter(component.Session);
            filter.ItemTypes = new List<ItemType> { ItemType.Page };
            filter.IncludedVersions = VersionCondition.OnlyLatestVersions;

            return component.GetUsingItems(filter).Count() == 0;
        }

        /// <summary>
        /// Gets a child StructureGroup with the correct title.
        /// </summary>
        /// <param name="title">The title of the StructureGroup to search for.</param>
        /// <param name="directory">The name of the SG's directory (if one needs to be created).</param>
        /// <param name="parentSG">The parent Structure Group.</param>
        /// <returns>The retrieved Structure Group.</returns>
        private StructureGroup GetStructureGroup(string title, string directory, StructureGroup parentSG, OrganizationalItemItemsFilter filter)
        {
            StructureGroup sg = parentSG
                .GetItems(filter)
                .Where(f => f.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase))
                .FirstOrDefault() as StructureGroup;

            if (sg == null)
            {
                sg = CreateStructureGroup(title, directory, parentSG);
            }

            return sg;
        }

        /// <summary>
        /// Creates a StructureGroup using a given title.
        /// </summary>
        /// <param name="title">The title to give the StructureGroup.</param>
        /// <param name="directory">The directory for the StructureGroup.</param>
        /// <param name="parentSG">The SG to create the new folder in.</param>
        /// <returns>The newly created SG.</returns>
        private StructureGroup CreateStructureGroup(string title, string directory, StructureGroup parentSG)
        {
            StructureGroup newSG = parentSG.GetNewObject<StructureGroup>();
            newSG.Title = title;
            newSG.Directory = directory;
            newSG.Save();

            return newSG;
        }
    }
}

Go ahead and deploy your new code (instructions at bottom of previous tutorial) and enjoy!

2 thoughts on “Tridion Event System: Automated Page Creation After Creating A Component

  1. Great description, Alexander. Comments and clean naming convention really helped me follow.

    And a new term for me: slug. Much easier than SEO-optimized-URL!

    I wonder if we should start a “hey, that’s not really Tridion” extensions list. I think “republishing from the queue” might be one of the them.

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>