Stupid Mistakes: Tridion Event System and Async Subscriptions

It’s easy to make one of those simple and stupid mistakes when developing with Tridion.  You know, one of those mistakes that make you lose an hour or more to debugging.  One of those mistakes that, after realizing what you did wrong, you are extremely happy you found the issue, but at the same time still just a bit embarrassed that you missed it in the first place.  I thought I’d share one that I made over the weekend in hopes that perhaps if someone else accidently went down the wrong path they’d quickly be able to correct themselves.

Over the weekend I was working on some Event System code that worked with Multimedia Components.  I’ll spare most of the details, but the code had to do two main things: 1.) Get the binary data out of the Multimedia Component and 2.) Set a Metadata Field to a value if the field was empty.  Pretty easy right?  We’ve all done these things before and this kind of functionality should be smooth sailing.  Until its not…

Coded. Deployed. Created a new Multimedia Component.  Save and Closed.  And… wth? The Event Log showed the following error.

A database error occurred while executing Stored Procedure "EDA_ITEMS.GETBINARYCONTENT".
ORA-01403: no data found
ORA-06512: at "TRIDION_CM.EDA_ITEMS", line 4100
ORA-06512: at line 1

Looks like my code didn’t like my call to the GetByteArray() method… weird. This portion of the Event System was using the Processed event phase. Interestingly, switching it to Initiated made the error go away. Shouldn’t be a solution, but hey, it was working now. I’ll move on and figure out the root issue later.

Next I added code to set the Metadata Field when it was null. Coded. Deployed. Saved and Closed the modified MM component. And… wth again? The Event Log now showed the following error.

The item tcm:123-4567-16-v0 does not exist.

Another piece of code I’ve done countless times… why is it failing now?  Tridion, why have you forsaken me!?!

The Issue

After much debugging and even coming up with a workaround to get my code working, I looked up at the piece that was subscribing my event and immediately face palmed.

EventSystem.SubscribeAsync<Component, SaveEventArgs>(OnComponentSaveInitiated, EventPhases.Initiated);

Can you see the issue above? Yep, I had subscribed my event asynchronously, and those weird issues I was seeing was the price I paid for doing it incorrectly.  For those of you who are not familiar with the Tridion Event System, you should only subscribe events asynchronously via the SubscribeAsync method with a TransactionCommitted phase.

What had happened was, when I started on this Event Handler, I had a different set of requirements. I was originally going to use a TrasnactionCommitted phase and a Check In event, and started coding it, but switched to the Initiated phase with a Save event once I got the updated requirements. Unfortunately, I forgot to change the subscription method to not subscribe asynchronously.

EventSystem.Subscribe<Component, SaveEventArgs>(OnComponentSaveInitiated, EventPhases.Initiated);

Ah, all better now…

Another Clue…

Another clue that I had used the wrong subscription method should have been the errors themselves. Normally when an Exception is thrown in the Initiated or Processed phase, that error prevents the save, and the error message is displayed to the user. With my code, the component was saving just fine, and the error message was only getting logged and not communicated to the user.

Inherited Page Workflow Process Settings On Structure Groups

If you’ve worked with Tridion Workflows, you know that the process of assigning Component workflows differs greatly from the process of assigning Page workflows.  With components, you just add the Component Process to the Schema (and of course make sure you check the “Enable Workflow Process Associations in Shared Schemas and Structure Groups” from your Content Publication’s properties’s Workflow tab).  However, Page Processes are attached to Structure Groups.  And unfortunately Structure Groups do not inherit this setting, so if you wanted every location where pages get created to be affected, you’ll have to add this setting to each and every Structure Group.

Being inspired by Nuno’s post on inheriting metadata schemas on folders, I thought I’d write up a quick Event System example of how to mock inheritance of Page Processes on Structure Groups.

First lets write an event handler that will set the Associated Page Process of new Structure Groups to that of their parent Structure Group.

public WorkflowStructureGroupHandler()
{
    EventSystem.Subscribe<StructureGroup, LoadEventArgs>(SetNewStructureGroupProcessDefinition, EventPhases.Processed);
}

private void SetNewStructureGroupProcessDefinition(StructureGroup sg, LoadEventArgs args, EventPhases phase)
{
    if (sg.Id.IsUriNull)
    {
        StructureGroup parentSG = sg.OrganizationalItem as StructureGroup;
        sg.PageProcess = parentSG.PageProcess;
    }
}

Fairly easy right? This takes care of the automatic setting of Page Processes for new Structure Groups, but what about existing Structure Groups? Normally by the time we start attaching process definitions, there could be a ton of existing Structure Groups already. What if we wanted to modify a parent Structure Group, and have all the children automatically updated? Lets add a Save event to the Structure Groups.

public WorkflowStructureGroupHandler()
{
    EventSystem.Subscribe<StructureGroup, LoadEventArgs>(SetNewStructureGroupProcessDefinition, EventPhases.Processed);
    EventSystem.Subscribe<StructureGroup, SaveEventArgs>(SetInheritedStructureGroupProcessDefinitions, EventPhases.TransactionCommitted);
}

private void SetInheritedStructureGroupProcessDefinitions(StructureGroup sg, SaveEventArgs args, EventPhases phase)
{
    if (args.EventStack.Count() > 1)
    {
        return;
    }

    ProcessDefinition pageProcess = sg.PageProcess;

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

    SetChildrenStructureGroups(sg, filter, pageProcess);
}

private void SetChildrenStructureGroups(StructureGroup sg, OrganizationalItemItemsFilter sgFilter, ProcessDefinition pageProcess)
{
    IEnumerable<RepositoryLocalObject> children = sg.GetItems(sgFilter);

    foreach (StructureGroup structureGroup in children)
    {
        if (structureGroup.PageProcess != pageProcess)
        {
            structureGroup.PageProcess = pageProcess;
            structureGroup.Save();
        }

        SetChildrenStructureGroups(structureGroup, sgFilter, pageProcess);
    }
}

And there you have it, modifying a parent Structure Group will not automatically set its children to have the same Associated Page Process, and creating a new SG will now automatically set its Page Process to that of its parent. The entire code will now look like:

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

namespace Tahzoo.EventSystem.Samples
{
    [TcmExtension("WorkflowStructureGroupHandlerExtension")]
    public class WorkflowStructureGroupHandler : TcmExtension
    {
        public WorkflowStructureGroupHandler()
        {
            EventSystem.Subscribe<StructureGroup, LoadEventArgs>(SetNewStructureGroupProcessDefinition, EventPhases.Processed);
            EventSystem.Subscribe<StructureGroup, SaveEventArgs>(SetInheritedStructureGroupProcessDefinitions, EventPhases.TransactionCommitted);
        }

        private void SetNewStructureGroupProcessDefinition(StructureGroup sg, LoadEventArgs args, EventPhases phase)
        {
            if (sg.Id.IsUriNull)
            {
                StructureGroup parentSG = sg.OrganizationalItem as StructureGroup;
                sg.PageProcess = parentSG.PageProcess;
            }
        }

        private void SetInheritedStructureGroupProcessDefinitions(StructureGroup sg, SaveEventArgs args, EventPhases phase)
        {
            if (args.EventStack.Count() > 1)
            {
                return;
            }

            ProcessDefinition pageProcess = sg.PageProcess;

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

            SetChildrenStructureGroups(sg, filter, pageProcess);
        }

        private void SetChildrenStructureGroups(StructureGroup sg, OrganizationalItemItemsFilter sgFilter, ProcessDefinition pageProcess)
        {
            IEnumerable<RepositoryLocalObject> children = sg.GetItems(sgFilter);

            foreach (StructureGroup structureGroup in children)
            {
                if (structureGroup.PageProcess != pageProcess)
                {
                    structureGroup.PageProcess = pageProcess;
                    structureGroup.Save();
                }

                SetChildrenStructureGroups(structureGroup, sgFilter, pageProcess);
            }
        }
    }
}

The solution above is not perfect and there’s still some work that would need to go into this before I’d put it into production (unless of course your project is a one process fits all and everything needs to be affected the same). For example, you’ve configured your Structure Groups that have a combination of different processes and even some without. Edit the root Structure Group, and say good-bye to all of your work! An “Inherit from Parent” eXtension would be perfect here, so you can then tell Structure Groups to not inherit (and thus not change when one of the parent Structure Groups gets saved!) Unfortunately this story is coming to a close, and the GUI eXtension’s story is a tale for another day.

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!

Redeploying a Tridion 2011 Event System DLL

Ever have a problem with redeploying your Tridion 2011 Event System DLLs and keep getting a “File In Use” error? Even after shutting down the Tridion COM package, recycling the SDL Tridion application pool, and shutting down the related Tridion services? Normally on a small development team (or working by yourself on the Tridion instance) you won’t run into this issue if you follow the normal steps outlined in the documentation. But if you are working on a large project (with lets say 50+ concurrent Tridion users), you might of run into this little frustrating situation of trying to hunt down all the processes that have your file locked.

Here’s a quick and easy trick you can do to get around this issue, and its easier than having to ensure that everything is shut down first.

  1. Rename the existing DLL on the server.  For example, rename Tridion.Sample.EventSystem.dll to Tridion.Sample.EventSystem.dll_OLD.  Even though it is in use, you still have the power to rename it.
  2. Copy your updated DLL to the server.
  3. Restart the Tridion COM+ package.
  4. Restart the Tridion Publisher service.
  5. Recycle the IIS SDL Tridion 2011 application pool.
  6. Delete the old renamed DLL when you get the chance.

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…