Tridion Event System Tutorial: Automatic Content Organization

One of the great things about Tridion 2011 is the new and improved Event System and the revamped TOM.NET API.  Prior to this release, one would implement the Tridion Event System using the TOM API (old, slow, and clunky COM interop).  Any Tridion developer who has worked with the old Event System truly has a special appreciation for the new one. If you have done an upgrade, remember, the old Event System still exists for backwards compatibility and actually runs side by side with the new one, so don’t forget to remove the old events as you replace them.

I thought it would be good to demonstrate a sample of creating an Event Handler for the new Event System that would help automate some content organization.  For this tutorial, we’ll create some code that will automatically place Articles in folders organized by year and month, with the dates coming from the Article’s MetaData date field “ArticleDate”.  If these folders don’t exist, we’ll also create them.  Your content authors can create these Articles in any location that they want, and our code will make sure it gets filed away correctly.  We’ll assume that you already have a project set up that references the Tridion DLLs (Tridion.Common, Tridion.ContentManager, Tridion.ContentManager.Common, Tridion.ContentManager.Publishing, Tridion.ContentManager.Templating).

First, lets create a class and let it inherit Tridion.ContentManager.Extensibility.TcmExtension. Make sure to include the following using statements as well. You’ll also want to make sure to give your newly created class a TcmExtension attribute that includes a unique ID for your Event Handler.

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

namespace Tridion.Samples.EventSystem
{
    /// <summary>
    /// Component Event Handler
    /// </summary>
    [TcmExtension("ComponentEventHandlerExtension")]
    public class ComponentEventHandlers : TcmExtension
    {

    }
}

Next we’ll write a method that will create a new folder with a specified title.  We’ll want the method to return the newly created Folder instance.

private Folder CreateFolder(string title, Folder parentFolder)
{
    Folder newFolder = parentFolder.GetNewObject<Folder>();
    newFolder.Title = title;
    newFolder.Save();

    return newFolder;
}

Now that we have a way to create folders, we’ll create another method that will retrieve a folder that has a certain title (from another folder’s child items). If that folder can’t be found, we’ll create it using the method we wrote above. We’ll also pass the method an OrganizationalItemItemsFilter instance that’ll ensure that we only query for folders.

private Folder GetFolder(string title, Folder parentFolder, OrganizationalItemItemsFilter filter)
{
    Folder folder = parentFolder
        .GetItems(filter)
        .Where(f => f.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase))
        .FirstOrDefault() as Folder;

    if (folder == null)
    {
        folder = CreateFolder(title, parentFolder);
    }

    return folder;
}

If you are unfamiliar with the LINQ statement above, it is pretty much the equivalent to:

Folder folder = null;
foreach (Folder f in parentFolder.GetItems(filter))
{
    if (f.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase))
    {
        folder = f;
    }
}
if (folder == null)
{
    folder = CreateFolder(title, parentFolder);
}

return folder;

Next lets tie things together. This function will get the “ArticleDate” field. If the Component is not already in the correct folder, we’ll search for the correct folder to put it in, or create them if they don’t exist, using the methods we’ve written above. We’ll have a base folder for the Articles that we’ll use as the root for the searching/creating of these organizational folders. Notice in the sample below that we open the Folder using a WebDav URL. You can also use the Tridion TCM URI to open items. As mentioned in this article by Robert Curlette, it is generally better to use a WebDav URL over a TCM URI as TCM URIs will usually vary from environment to environment while your WebDav URLs will usually remain constant (unless of course you rename or move things around). Normally I will keep WebDav URLs as well as TCM URIs out of the code and in a managed configuration file, but for the sake of this tutorial we’ll just hard code it in. Also notice how we create the filter that will narrow our search for folders only. For the month folders, we’ll use the full name of the month, prefixed with the month’s two digit number so that they will appear sorted in the correct order.

private void MoveArticleToCorrectFolder(Component component)
{
    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");

    if (component.OrganizationalItem.Title.Equals(month)
            && component.OrganizationalItem.OrganizationalItem.Title.Equals(year))
    {
        // Content is already in correct folder, so do nothing.
        return;
    }

    Folder baseFolder = component.Session.GetObject("/webdav/020 Content Master/Building Blocks/Content/Articles") as Folder;
    if (baseFolder == null)
    {
        throw new Exception("MoveArticleToCorrectFolder - Unable to get the baseFolder");
    }

    OrganizationalItemItemsFilter filter = new OrganizationalItemItemsFilter(baseFolder.Session);
    filter.ItemTypes = new List<ItemType> { ItemType.Folder };

    Folder yearFolder = GetFolder(articleDate.Year.ToString(), baseFolder, filter);
    Folder monthFolder = GetFolder(articleDate.ToString("MM MMMM"), yearFolder, filter);

    component.Move(monthFolder);
}

Now we’ll go ahead and create our Constructor where we’ll subscribe our event, as well as the handler method itself. In our method we’ll make sure that we’ll only perform this automated organization on components that use a schema with the title “Article”. We are going to have this even take place AFTER a Component has been checked in by using the CheckInEventArgs and the TransactionCommitted event phase.  One might think that having the event take place after a component has been saved would be an optimum place for this code, but as this developer learned (the hard way with much hair pulling), the .Move(OrganizationalItem) method will throw an error if you attempt it in a Save event.

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

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

As a recap, your class should look something similar to the following.

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

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

        /// <summary>
        /// On Component CheckedIn 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"))
            {
                MoveArticleToCorrectFolder(component);
            }
        }

        /// <summary>
        /// Moves the article to a folder organized by date. Creates the folder(s) if they don't already exist.
        /// </summary>
        /// <param name="component">The component being moved.</param>
        /// <remarks>Interesting, the .Move(folder) method doesn't work in the Save Event, only for the Check In event.</remarks>
        private void MoveArticleToCorrectFolder(Component component)
        {
            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");

            if (component.OrganizationalItem.Title.Equals(month)
                    && component.OrganizationalItem.OrganizationalItem.Title.Equals(year))
            {
                // Content is already in correct folder, so do nothing.
                return;
            }

            Folder baseFolder = component.Session.GetObject("/webdav/020 Content Master/Building Blocks/Content/Articles") as Folder;
            if (baseFolder == null)
            {
                throw new Exception("MoveArticleToCorrectFolder - Unable to get the baseFolder");
            }

            OrganizationalItemItemsFilter filter = new OrganizationalItemItemsFilter(baseFolder.Session);
            filter.ItemTypes = new List<ItemType> { ItemType.Folder };

            Folder yearFolder = GetFolder(articleDate.Year.ToString(), baseFolder, filter);
            Folder monthFolder = GetFolder(articleDate.ToString("MM MMMM"), yearFolder, filter);

            component.Move(monthFolder);
        }

        /// <summary>
        /// Gets a child folder with the correct title.
        /// </summary>
        /// <param name="title">The title of the folder to search for.</param>
        /// <param name="parentFolder">The parent folder.</param>
        /// <returns>The retrieved folder.</returns>
        private Folder GetFolder(string title, Folder parentFolder, OrganizationalItemItemsFilter filter)
        {
            Folder folder = parentFolder
                .GetItems(filter)
                .Where(f => f.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase))
                .FirstOrDefault() as Folder;

            if (folder == null)
            {
                folder = CreateFolder(title, parentFolder);
            }

            return folder;
        }

        /// <summary>
        /// Creates a folder using a given title.
        /// </summary>
        /// <param name="title">The title to give the folder.</param>
        /// <param name="parentFolder">The folder to create the new folder in.</param>
        /// <returns>The newly created folder.</returns>
        private Folder CreateFolder(string title, Folder parentFolder)
        {
            Folder newFolder = parentFolder.GetNewObject<Folder>();
            newFolder.Title = title;
            newFolder.Save();

            return newFolder;
        }
    }
}

Now we can compile and deploy our new Event Handler. In case you haven’t already read up on how to deploy this code, you move the DLL to the Tridion CMS server (to any location you wish). Next you’ll want to edit the Tridion.ContentManager.config file. You’ll want to search for the <extensions /> element and add a node that points to the DLL you just deployed. One of the other beauties of this new Event System is that you are not tied to having your entire Event System in a single project/DLL.

  <extensions>
    <add assemblyFileName="C:Program Files (x86)TridionbinTridion.Samples.EventSystem.dll" />
  </extensions>

Finally, you’ll want to restart the SDL Tridion Content Manager COM+ package, the Tridion Content Manager Publisher service, and recycle the SDL Tridion IIS Application Pool. Now go ahead and create a new Article component, and you should see your code in action!

There’s still some room for improvement. Like, what if you try to create an Article that already has the same title of another Article in the location its trying to move to? I’ll leave it to you to experiment and find the little gotchas with this code. That, or leave room for another article for another day…

3 thoughts on “Tridion Event System Tutorial: Automatic Content Organization

  1. Nice walk-through, Alex.

    I’d only caution that authors might not expect an automatic move, but I understand this as an example starting point where we can work out the requirements and use cases.

    I’m thinking similar functionality would make for an awesome Power Tool (hint hint). ;-)

    • Agreed, if the authors weren’t aware of this custom functionality being added, I’m sure they would be pretty confused. Though… perhaps as a good April Fools joke…

      I’ve been meaning to hop on over to the new and improved Power Tools project!

  2. Hey Alex, I’m so glad I came across this. I just completed an upgrade to 2011 SP1 and was about to update my 5.3 event system. This is almost the exact same code you developed for us several years ago for our articles and press releases and you kindly updated it for 2011 – I can’t thank you enought!

    Regards, –Haniel

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>