Link Attributes Extension: Final Touch Ups

Welcome to the third installment (and hopefully final) work on our Link Attributes GUI Extension. In our first post, we discovered how to customize one of the Tridion default popup dialogs (the Link popup for our extension). The post gave birth to the GUI Extension piece of our extension and added special hash parameters to the link’s href attribute to represent the attributes that needed to be created. In our second post, we created a Template Building Block that would be responsible for converting the hash parameters from the GUI Extension into actual attributes on the link.

Our updated Link Attributes dialog!

Our updated Link Attributes dialog!

If you’d rather just skip the reading and download the code, you can find it here.

Today we’ll be adding some finishing touches and enhancements to the work. The biggest piece missing was the ability to add multiple link attributes onto our hyperlink. There was also an issue if the url contained a hash string that wasn’t in a key = value format (ie www.example.com#noKeyValue). For our multiple fields, we want to have a delete button that will either delete the field completely if there are more than one field available, or just clear the field if its the only one remaining. Clicking the add button will insert an additional empty field set.

HashCollection.js

Our first update is to our HashCollection.js file. Our update here is strictly to allow existing hash strings in the url that is not in the key equals value format (ie “#someValue” vs “#someValue=blah”).

Type.registerNamespace("ContentBloom.Extensions");

/**
 * HashCollection is a collection of styles on a link's hash parameters.  This class is responsible
 * for getting, setting, and clearing link attributes.
 *
 * @constructor
 */
ContentBloom.Extensions.HashCollection = function (url) {
    var hashString,
        params,
        param,
        i;

    /**
     * The link attribute prefix.  This prefix is applied to any hash params that are for link attributes only.
     * @type {string}
     * @private
     */
    this._linkAttributePrefix = "linkAttr-";

    /**
     * Whether or not the hash collection contains link attributes. 
     * @type {boolean}
     */
    hasLinkAttributes: false;

    /**
     * The original url passed into the HashCollection instance.
     * @type {string}
     */
    this.url = url;

    /**
     * The params on the url
     * @type {object[]}
     */
    this.params = [];

    // only populate the params if the url contains a hashstring
    if (url.indexOf("#") !== -1) {
        hashString = url.substring(url.indexOf("#") + 1);

        params = hashString.split('&');
        for (i = 0; i < params.length; i++) {
            param = params[i].split('=');
            this.params[this.params.length] = { property: param[0], value: param[1] };
            if (!this.hasLinkAttributes && param[0].indexOf(this._linkAttributePrefix) === 0) {
                this.hasLinkAttributes = true;
            }
        }
    }
};

// StyleCollection prototype members
ContentBloom.Extensions.HashCollection.prototype = {

    /**
     * Clears all params from the collection that are specific to Link Attributes.
     */
    clearLinkAttributes: function () {
        var length = this.params.length,
            param,
            i;

        while (length--) {
            param = this.params[length];
            if (param.property.indexOf(this._linkAttributePrefix) === 0) {
                this.params.splice(length, 1);
            }
        }
        this.hasLinkAttributes = false;
    },

    /**
     * Gets the hash string (starting with '#') only of the wrapped url. Supplies link attribute hash params at the end of the string.
     *
     * @returns {string}
     */
    getHashString: function () {
        var hashString = "#",
            linkAttributes = "",
            length = this.params.length,
            param,
            i;

        if (length === 0) {
            return "";
        }

        for (i = 0; i < length; i++) {
            param = this.params[i];
            if (param.property.indexOf(this._linkAttributePrefix) === 0) {
                if (i > 0) {
                    linkAttributes += "&";
                }
                linkAttributes += "{0}={1}".format(param.property, param.value);
            } else {
                if (i > 0) {
                    hashString += "&";
                }
                if (param.value) {
                    hashString += "{0}={1}".format(param.property, param.value);
                } else {
                    hashString += param.property;
                }
            }
        }


        return hashString + linkAttributes;
    },

    /**
     * If a link attribute property is supplied, strips the prefix and returns just the key. Else just reutrns the key.
     *
     * @returns {string}
     */
    getKey: function (key) {
        if (key.indexOf(this._linkAttributePrefix) === 0) {
            return key.substring(9);
        }
        return key;
    },

    /**
     * Gets a link attribute param by its key.
     *
     * @param {string} key - the key to retrieve (excluding the link attribute prefix)
     * @returns {Object|null} null if no attribute is found with given key.
     */
    getLinkAttribute: function (key) {
        var length = this.params.length,
            param,
            i;

        for (i = 0; i < length; i++) {
            param = this.params[i];
            if (param.property === this._linkAttributePrefix + key) {
                return param;
            }
        }
        return null;
    },

    /**
     * Gets only the parameters that are link attribute parameters.
     */
    getLinkAttributes: function () {
        var linkAttributes = [],
            length = this.params.length,
            param,
            i;

        if (!this.hasLinkAttributes) {
            return linkAttributes;
        }

        for (i = 0; i < length; i++) {
            param = this.params[i];
            if (param.property.indexOf(this._linkAttributePrefix) === 0) {
                linkAttributes[linkAttributes.length] = param;
            }
        }

        return linkAttributes;
    },

    /**
     * Returns a new url including the hash string generated by any parameters.
     *
     * @returns {string}
     */
    getHashedUrl: function () {
        var url;

        if (this.url.indexOf("#") !== -1) {
            url = this.url.substring(0, this.url.indexOf("#"));
        } else {
            url = this.url;
        }

        return url + this.getHashString();
    },

    /**
     * Sets a link attribute param.  If attribute already exists, updates it.
     *
     * @param {string} key - The key of the link attribute (exluding link attribute prefix).
     * @param {string} value - the value of the link attribute.
     */
    setLinkAttribute: function (key, value) {
        var param = this.getLinkAttribute(key);
        if (param !== null) {
            param.value = value;
        } else {
            this.params[this.params.length] = { property: this._linkAttributePrefix + key, value: value };
        }
        this.hasLinkAttributes = true;
    }

}

LinkAttributes.js

Our next update is to the LinkAttributes.js file, where we are adding the ability to add new rows, delete existing rows, and some extra markup for the button controls. We’ve also had to add a way to allow the popup window to dynamically alter its height based off the adding/removing of rows.

Type.registerNamespace("ContentBloom.Extensions");

/**
 * Represents functionality for adding custom attributes onto links from the Link Popup dialog.
 */
ContentBloom.Extensions.LinkAttributes = {

    /**
     * The table row in the popup containing the key value pairs of links.
     * @type {JQuery}
     */
    attributeContainer: null,

    /**
     * The url input field.
     * @type {JQuery}
     */
    urlField: null,

    /**
     * Initializes the LinkAttributes object.
     */
    init: function () {
        var self = this;

        this.customizeLinkObject();
        $jq('#rowTarget').parent().append('<tr id="customAttributes" valign="top"><td>Attributes:</td><td id="attributeContainer" colspan="2"></td><td colspan="2" valign="bottom"><div class="addLinkAttributesRow"></div></td></tr>');

        this.attributeContainer = $jq('#attributeContainer');
        this.urlField = $jq("#FieldUrl");
        
        this.insertKeyValuePair();
        this.initValues();

        // add new rows
        $jq("#customAttributes .addLinkAttributesRow").click(function () {
            self.insertKeyValuePair();
        });

        // delete rows, or clear input fields
        this.attributeContainer.on("click", ".deleteLinkAttributesRow", function () {
            var row =$jq(this).parent();

            if ($jq("div.linkAttributeSet").length > 1) {
                // remove the row if there are more than one fieldset...
                row.remove();
                window.resizeBy(0, -$jq("#attributeContainer div").height());
            } else {
                // clear the fields if there's only one row
                $jq('input.key, input.value', this.attributeContainer).val('');
            }
        });
    },

    /**
     * Initializes input field values on the gui (url, link attributes...).
     */
    initValues: function () {
        var hashParams,
            linkAttributes,
            attribute,
            length,
            i,
            oldLink = window.dialogArguments && window.dialogArguments.link ? window.dialogArguments.link : {};


        if (oldLink.href) {
            hashParams = new ContentBloom.Extensions.HashCollection(oldLink.href);
            linkAttributes = hashParams.getLinkAttributes();
            length = linkAttributes.length;

            if (hashParams.hasLinkAttributes) {
                for (i = 0; i < length; i++) {
                    attribute = linkAttributes[i];
                    if (i === 0) {
                        $jq('.key', this.attributeContainer).val(hashParams.getKey(attribute.property));
                        $jq('.value', this.attributeContainer).val(attribute.value);
                    } else {
                        this.insertKeyValuePair(hashParams.getKey(attribute.property), attribute.value);
                    }
                }
                hashParams.clearLinkAttributes();
                this.urlField.val(hashParams.getHashedUrl());
            }
        }
    },

    /**
     * Gets an array of custom attributes based on text fields in the gui.
     *
     * @returns {object[]}
     */
    getCustomAttributes: function () {
        var attributes = [];

        $jq('div.linkAttributeSet', this.attributeContainer).each(function(index, element) {
            var key = $jq('.key', element).val(),
                value = $jq('.value', element).val();

            if (key) {
                attributes[attributes.length] = { key: key, value: value };
            }
        });

        return attributes;
    },

    /**
     * Inserts a new key value pair of text fields into the attribute container.
     *
     * @param {string=} key - The optional key to set the key input field to.
     * @param {string=} value - The optional value to set the value input field to.
     */
    insertKeyValuePair: function (key, value) {
        if (key == undefined) {
            key = "";
        }
        if (value == undefined) {
            value = "";
        }
        this.attributeContainer
            .append('<div class="linkAttributeSet"><label>Key</label><input class="key" type="text" value="' + key + 
                '" /> <label>Value</label><input class="value" type="text" value="' + value + '" /><div class="deleteLinkAttributesRow"></div></div>');
        window.resizeBy(0, $jq("#attributeContainer div").height());
    },

    /**
     * Modifies the Link._buildNewLinkHtml method to also apply the custom link attributes. Monkey patches, so existing method is still used and not replaced.
     */
    customizeLinkObject: function () {
        var self = this,
            originalFn = Tridion.Cme.Views.Link.prototype._buildNewLinkHtml;
            

        Tridion.Cme.Views.Link.prototype._buildNewLinkHtml = function Link$_buildNewCustomLinkHtml() {
            var link,
                hashParams,
                attributes = self.getCustomAttributes(),
                attribute,
                i;

            originalFn.apply(this);

            hashParams = new ContentBloom.Extensions.HashCollection(this.properties.NewLink.href);
            hashParams.clearLinkAttributes();

            for (i = 0; i < attributes.length; i++) {
                attribute = attributes[i];
                hashParams.setLinkAttribute(attribute.key, attribute.value);
            }

            this.properties.NewLink.href = hashParams.getHashedUrl();
        };
    }
};

// We call init only after document has loaded.
$jq(function () {
    ContentBloom.Extensions.LinkAttributes.init();
});

LinkAttributes.css

And finally, our stylesheet needs to be updated to be updated to support our newly added elements to our customization. We’re reusing some of the icons that are exists in Tridion for our delete and add buttons.

#attributeContainer {
    border: solid 1px #a1a9b2;
    border-bottom-width: 0;
}

    #attributeContainer label {
        font-weight: bold;
        margin-right: 6px;
    }

    #attributeContainer input {
        width: 30%;
    }

.addLinkAttributesRow {
    height: 22px;
    width: 22px;
    background-image: url("/WebUI/Editors/CME/Themes/Carbon/Images/Icons/add.16x16_v6.1.0.55920.93_.png");
    background-position: center;
    background-repeat: no-repeat;
    cursor: pointer;
    position: relative;
    bottom: 8px;
}

.deleteLinkAttributesRow {
    height: 22px;
    width: 22px;
    background-image: url("/WebUI/Editors/CME/Themes/Carbon/Images/Icons/delete.16x16_v6.1.0.55920.93_.png");
    background-position: center;
    background-repeat: no-repeat;
    cursor: pointer;
    display: inline-block;
    position: relative;
    top: 6px;
    left: 5px;
}

And that’s it… our Editor.config file requires no change and our Template Building Block from our previous post and our TBB was already setup to handle the multiple hash parameters.

If you are interested in using this extension and don’t feel like piecing everything together from this and the past two articles, you can download the extension here. A quick installation documentation is provided to help you get it setup.

Enjoy and stay coding my friends!

Searching and Modifying Output In Tridion Template Building Blocks With HtmlAgilityPack

In your Tridion career, you’ve probably written countless of C# Template Building Blocks. You’ve probably rolled your own custom Link Resolver, your own “Add or Extract Binaries From…”, your own cleanup templates… dozens of Building Blocks where the goal was to search for specific html patterns and attributes or modify the output in some way. And, if you’re like me, you’ve probably had to write numerous regex expressions to accomplish your tasks. I’ll admit that I’m no regex ninja or anything, its usually through trial and error that I get my regular expressions working correctly. Recently however, I attempted to write an expression that handled nested elements that could go any number of levels deep, with possibility to have several different variations of the pattern that I was looking for. My regex skills was just not good enough, and I thought for sure there must be a better way to parse the html output string of these patterns.

The search was short, but I found exactly what I was looking for: Html Agility Pack

Using HtmlAgilityPack in your Tridion Template Building Block is simple… just reference the DLL, and make sure you install the DLL into the GAC on the CMS and Publishing servers.

Now that we are set up and ready to run, let’s go ahead and write some code that will take the output from the package, and load it into an HtmlDocument.  Your code should look something like the following:

using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Tridion.ContentManager.Templating;
using Tridion.ContentManager.Templating.Assembly;
using HtmlAgilityPack;

namespace CodedWeapon.Samples
{
    [TcmTemplateTitle("HtmlAgilityPack Tester")]
    public class HtmlAgilityPackTester : ITemplate
    {
        private TemplatingLogger _logger = null;

	protected TemplatingLogger Logger
	{
		get
		{
			if (_logger == null) 
                            _logger = TemplatingLogger.GetLogger(this.GetType());

			return _logger;
		}
	}

        public override void Transform(Engine engine, Package package)
        {
            Item outputItem = package.GetByName(Package.OutputName);
            string outputString = outputItem.GetAsString();

            HtmlDocument doc = new HtmlDocument();
            doc.LoadHtml(outputString);
        }
    }
}

If you’re familiar with XmlDocument, then HtmlDocument should not look so strange to you. All we are doing in the above is grabbing the output, and passing the output (as a string) into our HtmlDocument instance. Simple enough right? And now for the fun… seeing how simple it is to use this library to get the exact elements that we are looking for. Since a lot of the time we work with links, I’ll show some examples of grabbing some anchor tags.

Grabbing Every Anchor

Lets say we want to grab every anchor element present on the output:

HtmlNodeCollection nodes = doc.DocumentNode.Descendants("a");

You can loop over each of the nodes and perform any necessary operations that you want:

foreach (var node in nodes)
{
    Logger.Debug("Found node with url: " + anchor.Attributes["href"].Value); 
}

Just like with XmlDocument, you can also use XPath:

HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//a");

Querying for Specific Elements

You can use XPath to search for the specific elements that you are looking for. For example, if we wanted to match all links with a specific url:

HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//a[@href=\"http://www.example.com\"]");

Or what if we wanted to grab every link that actually contains a title attribute:

HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//a[@title]");

Or better yet… what if we want check any node that does not contain the title attribute at all?

HtmlNodeCollection nodes = doc.DocumentNode.SelectNodes("//a[not(@title)]")

If you’re not a fan of XPath, then you’ll be happy to know that you can also use LINQ like in this following example where we are looking for any link that has an href attribute of “#”:

var nodes = doc.DocumentNode.Descendants()
    .Where(n => n.Attributes["href"] != null && n.Attributes["href"].Value.Equals("#"));

Modifying Output

Normally when we’re searching for elements it’s because we may need to modify the markup in someway, either by adding attributes, removing elements, etc. In the following example, we are adding a custom attribute onto our anchors that contain a value of “#” in the href attribute:

foreach (var node in doc.DocumentNode.SelectNodes("//a[@href=\"#\"]"))
{
    node.Attributes.Add("data-my-custom-attribute", "true");
}

But what if we have a more complicated requirement where we need to strip a <tcdl:ComponentPresentation> tag (but ensure that we keep the inside markup). One may be tempted to try the following:

foreach (var node in doc.DocumentNode.Descendants("tcdl:ComponentPresentation"))
{
    node.ParentNode.RemoveChild(node, true); // this strips the tcdl tag while preserving the children... but its modifying the collection...
}

package.Remove(outputItem);
outputItem.SetAsString(doc.DocumentNode.OuterHtml);
package.PushItem(Package.OutputName, outputItem);

However, while running the above, you’ll get an error that says Collection was modified; enumeration operation may not execute. This is because you cannot modify the collection (add/remove) while enumerating it. But, we can keep a record of the nodes and operate on them after:

List nodesToChange = new List();
foreach (var node in doc.DocumentNode.Descendants("tcdl:ComponentPresentation"))
{
    nodesToChange.Add(node);
}

// Now we strip the wrapping tags...
foreach (var node in nodesToChange)
{
    node.ParentNode.RemoveChild(node, true);
}

package.Remove(outputItem);
outputItem.SetAsString(doc.DocumentNode.OuterHtml);
package.PushItem(Package.OutputName, outputItem);

Hopefully this will end the regex blues out there for anyone who may be struggling!

Changing Components’ Schemas With Core Service

Besides making new extensions and applications to extend the features of Tridion, sometimes the Core Service API is a great tool for scripting something that would otherwise take a long time to do manually. A colleague of mine is in need of such a script, and has never worked with the Core Service API before, so I thought I would write this tutorial to help him get on the right track.

The project he’s in has the need to change the schema for a bunch of multi-media components. The GUI does not provide a way to change the schema, as changing schemas for components could lead to data loss. For example, if your metadata values have different names than the schema that you are switching to, data in those fields would be gone. Fortunately for my colleague, there is no metadata on the old schemas.

As my colleague is somewhat new to the world of .NET as well, this tutorial may explain some of the basics of .NET and Visual Studio as well.

The Project Setup

To start, create a new .NET Console Application project in Visual Studio (for this tutorial, I am using Visual Studio 2010 Professional Edition). Lets call it “Examples.ComponentSchemaChanger”. Visual Studio will create a new project with a Program class that contains a static void Main(string[] args) method. This Main method is the entry point for your console application. The args parameter is arguments that you can pass to your application from the command line.

Next, create a folder in Windows in the Solution folder that was created called “Resources”. Here we’ll include all referenced assemblies that our project will use. Since this is a fairly simple script, only one will be needed.

On a Tridion server (or VM), from the /bin/client/ directory of the location where Tridion is installed. Copy “Tridion.ContentManager.CoreService.Client.dll.config” and “Tridion.ContentManager.CoreService.Client.dll” from this location to the Resources folder you created in the previous step. The DLL contains the Core Service API that you will use for this script, and the config contains the configuration bindings that your application will use to connect to Tridion.

Next, lets add the reference to the DLL. Right click the References folder in your Visual Studio project, and select “Add Reference”. Select the “Browse” tab, and then browse to your Resources folder. Select “Tridion.ContentManager.CoreService.Client.dll” and click “Ok”.

You’ll also need to add several of other references. Right click and add references, but this time select the “.NET” tab and add references to “System.Configuration”, “System.ServiceModel” and “System.Runtime.Serialization”.

Now that we have referenced the Core Service library, lets modify the configuration. Right click the project, and select “Add -> New Item”. Under the General section, select “Application Configuration File”. Next copy the contents of “Tridion.ContentManager.CoreService.Client.dll.config” to your “App.config” folder. Right underneath the <configuration> element in your App.config file, add the following:

  <appSettings>
    <add key="CoreServiceEndpoint" value="netTcp_2011" />
    <add key="TridionUsername" value="YOUR TRIDION USERNAME" />
    <add key="TridionPassword" value="YOUR TRIDION PASSWORD"/>
  </appSettings>

The above will let you set the username and password that your script will use to authenticate with the Core Service, as well as which endpoint name to use (more on that later).

The SchemaChanger Class

Let’s add a new class to the project called “SchemaChanger”. Right click the project, click “Add -> Class” and then give it the name “SchemaChanger”.

Now add the following field to your new class:

private SessionAwareCoreServiceClient _client;

You’ll see an error squiggly line and a message of “The type or namespace name ‘SessionAwareCoreServiceClient’ could not be found (are you missing a using directive or an assembly reference?)”. This is because we have not referenced the namespace in a using statement. Let’s let Visual Studio do this automatically for us. Click the squiggly lined “SessionAwareCoreServiceClient”, and when the down arrow appear, click that, and then “using Tridion.ContentManager.CoreService.Client;“. You’ll see the using statement automatically added to the top, and your error message has gone away. For the rest of this tutorial, do the same to any similar errors you encounter to automatically add the the namespaces with the using directive.

Next, let’s add a property to our class that will use a getter accessor for getting our client.

public SessionAwareCoreServiceClient Client
{
    get
    {
        if (_client == null)
        {
            string endpointName = ConfigurationManager.AppSettings["CoreServiceEndpoint"];
            if (String.IsNullOrEmpty(endpointName))
            {
                throw new ConfigurationErrorsException("CoreServiceEndpoint missing from appSettings");
            }

            _client = new SessionAwareCoreServiceClient(endpointName);

            string username = ConfigurationManager.AppSettings["TridionUsername"];
            string password = ConfigurationManager.AppSettings["TridionPassword"];

            if (!String.IsNullOrEmpty(username) && !String.IsNullOrEmpty(password))
            {
                _client.ClientCredentials.Windows.ClientCredential = new NetworkCredential(username, password);
            }
        }
        return _client;
    }
}

What the above is doing is, if our _client variable is null, to create a new instance of SessionAwareCoreSerivceClient using the endpoint that we supply in our appSettings. If _client has already been instanciated, it’ll return it without creating a new instance. We’ll also be using the TridionUsername and TridionPassword appSettings that we set up earlier. If the appConfig does not have these settings, or if they are empty, the application will attempt to use the user that will be running the script. It also checks to make sure the CoreServiceEndpoint appSetting exists, and throws an error if its missing.

Next, lets add a method that’s going to be doing the work for us. Let’s call this method “ChangeSchemasForComponentsInFolder”, and allow it to take 4 arguments: one for the TcmUri of the folder we want to search in, one to allow the operation to happen recursively, the third one for the TcmUri of the schema that we want to change from, and the forth one for the TcmUri of the schema that we want to change the components to.

public void ChangeSchemasForComponentsInFolder(string folderUri, bool recursive, string fromSchemaUri, string schemaUriToChangeTo)
{

}

We can only modify components that aren’t already checked out, so lets create a variable that will keep track of components that we are not able to edit.

    List<ComponentData> failedItems = new List<ComponentData>();

Next we’ll want to open the folder that we are going to search in. We can use the CoreService client’s Read method for this.

    FolderData folder = Client.Read(folderUri, null) as FolderData;

We’ll also want to grab the Schema that we’ll be switching to, and the namespace of that schema.

    SchemaData schema = Client.Read(schemaUriToChangeTo, null) as SchemaData;
    XNamespace ns = schema.NamespaceUri;

Now we’ll create a filter to actually query for multimedia items. We’ll want to make sure to only grab Components, and to only grab components of a Multimedia type. We’ll also want to check for components recursively if we’ve supplied to do so in the passed argument.

    OrganizationalItemItemsFilterData filter = new OrganizationalItemItemsFilterData();
    filter.ItemTypes = new ItemType[] { ItemType.Component };
    filter.ComponentTypes = new ComponentType[] { ComponentType.Multimedia };
    filter.Recursive = recursive;

And finally the actual work of switching the schema of the component. We’ll open up the component in read mode first, and only check it out to modify if the component’s current schema ID matches the schema we want to change from. If the component doesn’t match, we’ll attempt to check it out. If we are successful on checking it out, we’ll change its schema, save, and check back in. If we weren’t successful on checking the item out, we’ll make note of that item to report at the end.

    XElement items = Client.GetListXml(folder.Id, filter);
    foreach (XElement item in items.Elements())
    {
        ComponentData component = Client.Read(item.Attribute("ID").Value, null) as ComponentData;

        if (!component.Schema.IdRef.Equals(fromSchemaUri))
        {
            // If the component is not of the schmea that we want to change from, do nothing...
            continue;
        }

        component = Client.TryCheckOut(component.Id, new ReadOptions()) as ComponentData;

        if (component.IsEditable.Value)
        {
            component.Schema.IdRef = schemaUriToChangeTo;
            component.Metadata = new XElement(ns + "Metadata").ToString();
            Client.Save(component, null);
            Client.CheckIn(component.Id, null);
            Console.WriteLine(String.Format(" - changed schema for {0} ({1}) ", component.Title, component.Id));
        }
        else
        {
            failedItems.Add(component);
        }
    }

This is where the namespace of the schema comes into play with the component.Metadata = new XElement(ns + "Metadata").ToString() Here we are setting up an empty metadata section using the namespace from the Schema that we are switching to. Without this line, you might see an error like “Root element must be in namespace”.

What about that failedItems variable that we were keeping track of? Good catch. Let’s go ahead and report the items that we weren’t able to change by adding the following to the end of our method.

    if (failedItems.Count > 0)
    {
        Console.WriteLine();
        Console.WriteLine("The following items could not be converted. Please have them checked in and try again.");
        foreach (ComponentData component in failedItems)
        {
            Console.WriteLine(component.Id + " :" + component.LocationInfo.WebDavUrl);
        }
    }

Our SchemaChanger class is now complete, and should look something like the following:

using System;
using System.Collections.Generic;
using System.Configuration;
using System.Net;
using System.Xml.Linq;
using Tridion.ContentManager.CoreService.Client;

namespace Examples.ComponentSchemaChanger
{
    /// <summary>
    /// Script example of changing schemas for multi media components.
    /// </summary>
    class SchemaChanger
    {
        /// <summary>
        /// The session aware core service client.
        /// </summary>
        private SessionAwareCoreServiceClient _client;

        /// <summary>
        /// Gets the SessionAware Core Service client.
        /// </summary>
        public SessionAwareCoreServiceClient Client
        {
            get
            {
                if (_client == null)
                {
                    string endpointName = ConfigurationManager.AppSettings["CoreServiceEndpoint"];
                    if (String.IsNullOrEmpty(endpointName))
                    {
                        throw new ConfigurationErrorsException("CoreServiceEndpoint missing from appSettings");
                    }

                    _client = new SessionAwareCoreServiceClient(endpointName);

                    string username = ConfigurationManager.AppSettings["TridionUsername"];
                    string password = ConfigurationManager.AppSettings["TridionPassword"];

                    if (!String.IsNullOrEmpty(username) && !String.IsNullOrEmpty(password))
                    {
                        _client.ClientCredentials.Windows.ClientCredential = new NetworkCredential(username, password);
                    }
                }
                return _client;
            }
        }

        /// <summary>
        /// Changes schemas for multimedia components located in a specific folder.
        /// </summary>
        /// <param name="folderUri">The tcm uri of the folder to change the items in.</param>
        /// <param name="recursive">Whether or not to perform this recursively on child folders.</param>
        /// <param name="fromSchemaUri">The tcm uri of the schema that we are changing from.</param>
        /// <param name="schemaUriToChangeTo">The tcm uri of the schema to change the components to.</param>
        public void ChangeSchemasForComponentsInFolder(string folderUri, bool recursive, string fromSchemaUri, string schemaUriToChangeTo)
        {
            // Let's keep track of items that couldn't be checked out and report at the end.
            List<ComponentData> failedItems = new List<ComponentData>();

            // Open the folder to check
            FolderData folder = Client.Read(folderUri, null) as FolderData;

            // Open up the schema that we will be changing to.
            SchemaData schema = Client.Read(schemaUriToChangeTo, null) as SchemaData;
            XNamespace ns = schema.NamespaceUri;

            // Create a filter to get all multi-media components.
            OrganizationalItemItemsFilterData filter = new OrganizationalItemItemsFilterData();
            filter.ItemTypes = new ItemType[] { ItemType.Component };
            filter.ComponentTypes = new ComponentType[] { ComponentType.Multimedia };
            filter.Recursive = recursive;

            XElement items = Client.GetListXml(folder.Id, filter);
            foreach (XElement item in items.Elements())
            {
                ComponentData component = Client.Read(item.Attribute("ID").Value, null) as ComponentData;

                if (!component.Schema.IdRef.Equals(fromSchemaUri))
                {
                    // If the component is not of the schmea that we want to change from, do nothing...
                    continue;
                }

                if (component.Schema.IdRef.Equals(schema.Id))
                {
                    // If the component already has this schema, don't do anything.
                    continue;
                }

                component = Client.TryCheckOut(component.Id, new ReadOptions()) as ComponentData;

                if (component.IsEditable.Value)
                {
                    component.Schema.IdRef = schemaUriToChangeTo;
                    component.Metadata = new XElement(ns + "Metadata").ToString();
                    Client.Save(component, null);
                    Client.CheckIn(component.Id, null);
                    Console.WriteLine(String.Format(" - changed schema for {0} ({1}) ", component.Title, component.Id));
                }
                else
                {
                    failedItems.Add(component);
                }
            }

            if (failedItems.Count > 0)
            {
                Console.WriteLine();
                Console.WriteLine("The following items could not be converted. Please have them checked in and try again.");
                foreach (ComponentData component in failedItems)
                {
                    Console.WriteLine(component.Id + " :" + component.LocationInfo.WebDavUrl);
                }
            }
        }
    }
}

The Program Class

Now lets go back to the Program class’ Main method and put our code to work. We’ll allow the user to pass into the command line the arguments for the folder’s tcm uri, whether or not to perform this recursively, the schema uri of the components we want to change from, and the schema uri of the schema to change the components to. We’ll also display some a usage message when an incorrect # of arguments is supplied, a message showing the error if one pops up, and finally a message letting the user know that the script has completed.

static void Main(string[] args)
{
    if (args.Length < 4)
    {
        Console.WriteLine("Usage: Examples.ComponentSchemaChanger  <y/n for recursive>  ");
        Console.WriteLine("Example:");
        Console.WriteLine("Examples.ComponentSchemaChanger tcm:100-12345 y tcm:100-987-8 tcm:100-1234-8");
    }
    else
    {
        string folderUri = args[0];
        string recursive = args[1].ToLower();
        string schemaUriFrom = args[2];
        string schemaUriTo = args[3];

        try
        {
            SchemaChanger changer = new SchemaChanger();
            changer.ChangeSchemasForComponentsInFolder(folderUri, recursive.Equals("y"), schemaUriFrom, schemaUriTo);
        }
        catch (Exception ex)
        {
            Console.WriteLine("There was an error:");
            Console.WriteLine(ex);
        }
    }

    Console.WriteLine();
    Console.WriteLine(" press <ENTER> to continue");
    Console.ReadLine();
}

Building and Deploying

Right click the project and select “Build”. This will compile and build the project for you, putting the files you need in %Project Directory%\bin\Debug\ (or %Project Directory%\bin\Release\ if you are targeting Release). The 3 files that you’ll need are Examples.ComponentSchemaChanger.exe, Examples.ComponentSchemaChanger.exe.config (this is the file containing your app settings), and Tridion.ContentManager.CoreService.Client.dll.

Executing Our Script

Our script is setup to work either on the CMS Server or remotely with a simple configuration change. Remember that CoreServiceEndpoint application config setting that we added? When this setting is “wsHttp_2011″, you’ll be able to run your script remotely (as long as you have access to contact the CMS server from your location), and when this setting is “netTcp_2011″, you’ll be able to run locally on the CMS Server. You can actually run the wsHttp_2011 binding from the CMS Server, but the netTcp binding will perform faster for you.

Whether deployed locally or remotely, open up the command prompt, navigate to the folder you deployed to, and enter the following command:
Examples.ComponentSchemaChanger tcm:12-3456-2 y tcm:12-1000-8 tcm:12-1234

Sit back, and watch magic happen. :)

Again a Warning

Remember as mentioned, changing a component’s schema can lead you to lose data from fields if the field definitions are different. Before trying out the following code, make sure the components you are changing doesn’t contain any such fields that will be lost.

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!

Razor Mediator: Including Code From An External DLL

The Razor Mediator allows you to easily add other libraries and namespaces to your Razor templates.  Due to an issue found with version one, you will want to get the latest installer from the Google Code site (or just download the latest version 1.0.1 installer here).  If you have version 1.0.1 or later you should be in the clear and hopefully problem free.

For this tutorial we’re just going to create a really simple class in a separate project and then allow our Razor templates to use that new class. After this tutorial you should have a pretty good understanding of how to use the namespaces and assemblies sections of the razor.mediator configuration.

First, create a new project in Visual Studio.  We’ll name it “RazorSample.Test”.

Next, create the following class in your new project.

namespace RazorSample.Test
{
    public class MyClass
    {
        public static string Hello()
        {
            return "Hello World!";
        }

        public static string Hello(string name)
        {
            return "Hello " + name + "!";
        }
    }
}

I did mention that it would be a simple class! We’ve got two methods and I think it can be safe to assume that I don’t need to explain what they do. Compile the project, and copy the newly created DLL to your CMS server. For this tutorial, I’ve put mine at C:MyLibraryRazorSample.Test.dll.

Next you’ll want to modify the Tridion.ContentManager.config file located at %Tridion Install Path%config on your Tridion CMS server. Search for the razor.mediator section, then update the assemblies section to add your newly created DLL.

<assemblies>
      <add assembly="C:MyLibraryRazorSample.Test.dll" />
</assemblies>

Go ahead and restart the Tridion COM+ package.

Now go ahead and create a new Template Building Block in Tridion called “Razor Test” (or whatever you wish to name it). From the Source tab, make sure to select “RazorTemplate” for the Template Type, and then add the following:

<div>@RazorSample.Test.MyClass.Hello()</div>
<div>@RazorSample.Test.MyClass.Hello(Component.Title)</div>

Save and close. Now go ahead and open up Template Builder and create a new Compound Component Template. Add the “Razor Test” building block from the previous step, and then Run the template using any Component. You should see both of your newly created method returns in the output:

<div>Hello World!</div>
<div>A Test Component!</div>

Congratulations, you’ve just executed code from another assembly in your Razor Template! That’s great and all, but what if you want to load an assembly that’s in the GAC instead of on the file system? In order to get our sample assembly into the GAC, we’ll have to give it a strong name key. If you are unfamiliar with this, just proceed with the following steps.

  1. Right click the RazorSample.Test project in the Solution Explorer of Visual Studio and click Properties.
  2. In the left column, select the Signing tab.
  3. Check the “Sign the assembly” check box.
  4. In the dropdown underneath the “Choose a strong name key file:” label, select “”.
  5. In the Create Strong Name Key dialog, enter “RazorSample.Test” for the Key file name and uncheck the “Protect my key file with a password” option.
  6. Click OK.
  7. Save the project and recompile.

Once you have your RazorSample.Test project recompiled with a Strong Name Key, deploy it to the GAC on the Tridion server. For simplicity, I made the RazorSample.Test project target .NET 3.5 so that I could just drag and drop the DLL to C:Windowsassembly. Once your DLL is deployed to the GAC, edit the assemblies section in the Tridion.ContentManager.config file to look like the following.

<assemblies>
    <add assembly="RazorSample.Test, Version=1.0.1.0, Culture=neutral, PublicKeyToken=60ad7434f03dfcdc" />
</assemblies>

You’ll notice that the only difference in the config between loading it from the GAC and from the file system is that the file system is an absolute file path while the GAC is the fully qualified name.  Remember, your Public Key Token will be different than the one you see above.  If you are unsure how to get the Public Key Token, find your DLL at C:Windowsassembly (assuming that DLL is .NET 3.5 or less).  You’ll see the info you need in the Public Key Token column, or by right clicking your DLL and selecting Properties.  Once the configuration is saved, restart the Tridion COM+ package again. Running the template in Template Builder again should result in the same output.

Now what about the namespaces section? Well unless you don’t mind using the full namespace to your classes, this is where that section will come in hand. Reopen the Tridion.ContentManager.config file once again and modify to look like the following.

<namespaces>
  <add namespace="RazorSample.Test" />
</namespaces>
<assemblies>
  <add assembly="RazorSample.Test, Version=1.0.1.0, Culture=neutral, PublicKeyToken=60ad7434f03dfcdc" />
</assemblies>

After restarting the Tridion COM+ package, reopen your “Razor Test” Razor Template and modify the razor to look like:

<div>@MyClass.Hello()</div>
<div>@MyClass.Hello(Component.Title)</div>

Save and close. Restart the Tridion COM+ package. Retest your template. And enjoy the bliss of adding your own useful functionality to your Razor templates.

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…