Customizing An Existing Tridion Popup Dialog

Ever had a need or an itch to completely modify and customize one of those default Tridion popup dialogs? Perhaps you even had a desire to overload an existing JavaScript function from one of those dialogs, but didn’t want to just copy/paste or replace the existing one entirely? Today I’ll be giving you a walk through of adding some custom fields and functionality to the Link popup. Before I begin, I want to give a special thanks to Frank Taylor as we worked on this kind of functionality together not too long ago. And by working together, I mean that I merely made some suggestions and he did all the work so that one day I could steal it as a reference point and write the post you are reading today. So if you find anything on this page useful, feel free to also give Mr. Frank a shout out! And respectively, if you think this article is complete crap and the code here somehow murders your beloved cat, feel free to leave a burning bag of poo on his front door step. Just ping me and I’ll give you his personal home address.

If you’re not interested in reading and just want the code that was done in this post, you can find it here.

The Mission

Our mission today is to add the ability for content authors to add extra custom attributes onto their links in rich text fields via the Hyperlink button in the ribbon bar.  For the sake of simplicity, we’ll only add a single set of a key value pair today, and save this enhanced feature for a post for another day.  Also, today’s post will only focus on the GUI Extension piece of this puzzle as the point of today’s blog is more focused on the “how do I customize an existing dialog?”

Analysis

At first thought, we could apply our custom link attribute directly to the link elements from the rich text field.  However, these would just get stripped away in an out of the box Tridion setup due to being invalid attributes.  Although we could configure Tridion to prevent this from happening (for information on how to do this, check Nick’s post here), I wanted to take a more generic approach where we could add attributes regardless of this configuration.  So what we’ll be doing instead is adding these special link attributes as hash parameters to the href attribute.  We’ll give them a special prefix of “linkAttr-” to distinguish our hash parameters from any that may already be a part of the url supplied.  This means that we’ll also need to create a Template Building Block that will search for these specially marked hash params, turn them into attributes on the link element, and remove them from the hash string.  As mentioned earlier, since we’ll only be focusing on the GUI Extension piece today, we’ll leave that for the next blog post.

Assumptions

I’m going to assume that you are already somewhat familiar with developing Tridion GUI Extensions, and will only be going over code and configuration that is specific to this extension. If you are looking for a tutorial that teaches you how to create a Hello World from scratch, there’s already quite a few good ones out there. We’re also going to assume that we’ll be using jQuery and that one doesn’t already exist. We’ll be adding jQuery to a global $jq variable.

The JavaScript

We’re going to start by creating a utility class, “HashCollection”.  This class will be responsible for extracting the hash parameters from a given url, as well as maintaining, adding and clearing of our link attributes.

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 = [];

    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].trim(), value: param[1].trim() };
            if (!this.hasLinkAttributes && param[0].trim().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 += "&";
                }
                hashString += "{0}={1}".format(param.property, param.value);
            }
        }


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

}

Now that we have our utility class, let’s write the code that’s going to be doing the real work.

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 () {
        this.customizeLinkObject();
        $jq('#rowTarget').parent().append('<tr id="customAttributes"><td>Attributes:</td><td id="attributeContainer" colspan="4"></td></tr>');

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

    /**
     * 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) {
                this.attributeContainer.html('');
                for (i = 0; i < length; i++) {
                    attribute = linkAttributes[i];
                    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', 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><label>Key</label><input class="key" type="text" value="' + key + 
                '" /> <label>Value</label><input class="value" type="text" value="' + value + '" /></div>');
    },

    /**
     * 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();
});

On Load

It’s important to notice that at the end of this script, we are making a call to our ContentBloom.Extensions.LinkAttributes.init() method only after the document loaded. If we were to call the init method immediately, we’d get some unexpected results.

initValues

You’ll notice here that we check if this is an existing hyperlink that has already been given link attributes. If the hash string contains our specially marked values, we’ll populate or key/value fields appropriately.

Future Proofing

You’ll also notice that although we are only displaying a single set of a key/value pair, our code is already setup to handle multiple sets.

Monkey Patching _buildNewLinkHtml

Here’s another cool trick that we are doing… we are adding functionality to the existing Tridion.Cme.Views.Link.prototype._buildNewLinkHtml method without actually modifying any of the existing code (which would be bad). We’re using a little closure magic here in our customizeLinkObject method to create a copy of the existing function, overwrite the existing function, and ensure that the original function still gets called prior to our custom code (while ensuring that the context of “this” remains for the Link instance).

The Stylesheet

Not too much going on here at this stage of our extension. We’re just making sure out labels stand out… depending on the browser you are on, this may display nicely or get wrapped onto a second line. We’ll come back in another post to pretty-ify it up a lil more though.

/**
 * Yep, not a too lot going on in here just yet...
 */
#LinkPopup #customAttributes label {
    font-weight: bold; // tempted to throw an !important in there just for you Frank...
}

Configuration

Here’s where everything comes into play that allows us to load our newly created JS and CSS files onto the existing Link Popup page. What we want to do here is create an Extension Group. An Extension Group allows us to extend an already existing Group and add additional files to it. In this case, we want to extend the “Tridion.Web.UI.Editors.CME.Views.Popups.Link” group. Since the configuration of a GUI Extension seems to be the biggest gotcha, I’ll go over the process for this extension piece by piece.

...
<?xml version="1.0"?>
<Configuration xmlns="http://www.sdltridion.com/2009/GUI/Configuration/Merge"
               xmlns:cfg="http://www.sdltridion.com/2009/GUI/Configuration"
               xmlns:ext="http://www.sdltridion.com/2009/GUI/extensions"
               xmlns:cmenu="http://www.sdltridion.com/2009/GUI/extensions/ContextMenu">

  <resources cache="true">
    <cfg:filters />
    <cfg:extensiongroups>
      <cfg:extensiongroup name="ContentBloom.ExtensionGroups.Link">
        <cfg:extension target="Tridion.Web.UI.Editors.CME.Views.Popups.Link">
          <cfg:insertafter>ContentBloom.Resources.LinkAttributes</cfg:insertafter>
        </cfg:extension>
      </cfg:extensiongroup>
    </cfg:extensiongroups>
    <cfg:groups>
     <cfg:group name="ContentBloom.Resources.LinkAttributes">
        <cfg:fileset>
          <cfg:file type="script">/js/libs/jquery.js</cfg:file>
          <cfg:file type="script">/js/HashCollection.js</cfg:file>
          <cfg:file type="script">/js/LinkAttributes.js</cfg:file>
          <cfg:file type="style">/css/LinkAttributes.css</cfg:file>
        </cfg:fileset>
      </cfg:group>
    </cfg:groups>
  </resources>
  <definitionfiles />
  <extensions>
      <ext:editorextensions/>
      <ext:dataextenders/>
  </extensions>
  <commands />
  <contextmenus />
  <localization />
  <settings>
    <defaultpage />
    <navigatorurl />
    <editurls/>
    <listdefinitions/>
    <itemicons/>
    <theme>
      <path/>
      <resourcegroup />
    </theme>
    <resourceextensions>
        <resourceextension>ContentBloom.ExtensionGroups.Link</resourceextension>
    </resourceextensions>
    <customconfiguration/>
  </settings>
</Configuration>

Creating the Resource Group

First we create our <cfg:group> element within the <cfg:groups> element. Our resource group is telling Tridion which resource files to include into our little extension… in our case we are loading our JS and CSS files.

Creating the Extension Group

This is where we inject our resources into an already existing resource group. We’ll need to create a <cfg:extensiongroup /> element within the <cfg:extensionsgroups /> element with a unique name attribute.  Within the <cfg:extensiongroup /> element, you’ll want to create a <cfg:extension /> element that has a target attribute that contains a value of the existing resource group that we want to extend (here we are extending “Tridion.Web.UI.Editors.CME.Views.Popups.Link”).  And within that <cfg:extension /> element, you’ll want to create a <cfg:insertafter /> element that contains the name of the resource group that we created in the above step.

Don’t Forget the Resource Extension!

One final piece to this configuration… in the settings element, we want to create a <resourceextensions> element that contains a <resourceextension> element. The value of this element should be that unique name that we gave to our extensiongroup that we created above.

Deploy!

And there you have the starts of the Link Attributes extension! You will have to deploy the extension as normal… a.) virtual directory creation… b.) Add the editor configuration in System.config… c.) Increment the modification attribute. Open up a component with a rich text field, create a Hyperlink using the Hyperlink button, and you should be in business! You may not yet be able to create actual link attributes, but at least you’ll be able to see your customizations as well as modified href attribute on the links!

Our extension of version 1 in action!

Our extension of version 1 in action!

Conclusions

Although our work for this Link Attributes Extension is not yet complete, you should have now be well armed in case you ever have to customize any of the default popup dialogs.

And for those of you who are actually interested in the completion of this extension, stay tuned for future posts where we’ll work on the TBB, as well as add the ability to add multiple link attributes (and fix up some issues that you may have noticed).

You can find the code to what we’ve done today here.

Stay coding my friends!

Updates

8/21/2014 – Follow up post for building out the Template Building Block

Razor Mediator Version 1.3.2 Released

A new minor version of the Razor Mediator has been released and can be found at the Google Code site. For anyone who is interested in installing Razor Mediator on Tridion 2013, you will find this release especially important to you. Prior to this version, the Installer will throw an error and the installation will fail.

Besides being able to install without an error, this version also changes how the configuration is done slightly. Prior to 1.3.2, the template ID for Razor Mediator was generated during installation by selecting the highest free available ID. With an out of the box Tridion setup, this would normally have resulted in a template type of “8″. Tridion 2013 comes with a new XSLT Mediator, but they have left the ID’s of 8 and 9 empty. So, installation for 1.3.2 will attempt to use 8 if its available, otherwise it’ll pick the ID just like it use to do.

An important thing to note is that you may have to manually modify this ID if you are porting from another system that used a different ID for the Mediator. If the ID’s don’t match up, you will get an error during the content porter process.

Thanks to Nicholas Vander Ende, Frank Taylor, and Piti Itharat for reporting and troubleshooting the installation error in 2013. A special thanks to Nicholas for actually finding the fix to the problem as well.

ComponentPresentations and ComponentTemplateModel

Thanks to Chris Curry for spotting that ComponentPresentationModel’s Template property was not returning a ComponentTemplateModel type, but rather just the Tridion’s ComponentTemplate type. This means you would have to grab the ItemFields in order to fetch fields. This version fixes the CompoenntPresentationModel’s Template property.

@foreach (var cp in ComponentPresentations) {
    <div>cp.Template.Fields.FieldName</div>
}

Get Version From Template

Although you can get the Razor Mediator version by looking at the Tridion.ContentManager.config file, sometimes you may not have access to the server and need another quick way to get the version. 1.3.2 comes with a “Version” property in the base template that you can output to check the version.

    <div>Version: @Version</div>

Models.GetComponentTemplate

The ModelUtilities class now comes with a GetComponentTemplate method to easily pull out ComponentTemplateModels.

@{
    var ct = Models.GetComponentTemplate("tcm:1-2345-32");
}

Thanks again to everyone for your feedback and suggestions!

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.

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.

Checking If An Item Is In Workflow With Core Service

During your quest of making custom applications using Tridion’s Core Service API, you may have come across the need to check whether or not an item is currently in a Workflow Process. Luckily, unlike some other tasks you may need to do with the Core Service API, this task is easily done by checking the WorkflowInfo property that is included in the PageData and ComponentData classes.

// Open a page in Core Service.
PageData page = client.Read(yourPageUri, new ReadOptions()) as PageData;

if (page.WorkflowInfo != null)
{
    // Ladies and gentlemen, WorkflowInfo property was not null, this page is in workflow
}
else
{
    // WorkflowInfo property is null, the page is not in workflow
}

Great, but now that you know that your item is in workflow, you’ll probably be wanting to grab some Workflow related items. The good news is, the WorkflowInfo class contains most of the data you’ll need to interact with the Workflow for that item. Here are some common things you’ll probably do with this property:

// Get the ProcessInstanceData from the WorkflowInfo property
ProcessInstanceData processInstance = 
    (ProcessInstanceData)client.Read(page.WorkflowInfo.ProcessInstance.IdRef, null);

// Get the ProcessDefinitionData from the process instnace
ProcessDefinitionData processDefinition =
    (ProcessDefinitionData)client.Read(processInstance.ProcessDefinition.IdRef, null);

// Getting the ActivityInstanceData from the WorkflowInfo property...
ActivityInstanceData activityInstance = 
    (ActivityInstanceData)client.Read(page.WorkflowInfo.ActivityInstance.IdRef, null);

// Get the ActivityDefinitionData from the activity instance
ActivityDefinitionData activityDefinition =
    (ActivityDefinitionData)client.Read(activityInstance.ActivityDefinition.IdRef, null);

// Note that if you only wanted to get the ActivityDefinition's Description, you can get that from the WorkflowInfo's ActivityDefinitionDescription property.
if (page.WorkflowInfo.ActivityDefinitionDescription.Equals(activityDefinition.Description))
{
    // true of course...
}

// Check how long its been since the activity was started...
if (page.WorkflowInfo.StartDate.HasValue)
{
    TimeSpan timeSinceStart = DateTime.Now - page.WorkflowInfo.StartDate.Value;
}

Other Useful Properties of WorkflowInfo

Here are the other properties of WorkflowInfo that wasn’t included in the samples above:

ActivityState: The enum value of the activity’s current state.
Assignee: The Link<TrusteeData> of the assignee.
CreationDate: The Nullable<DateTime> that the activity was created.
FinishDate: The Nullable<DateTime> that the previsou activity instance was finished.
Performer: The Link<UserData> of the activity’s performer.
PreviousMessage: The FinishMessage of the previous Activity.

Razor Mediator Version 1.3.1 Released

The next version of the Razor Mediator, version 1.3.1, is now up and ready to be downloaded at its Google Code site, and its updated documentation can be found here. This update includes mainly fixes for issues reported, though it does include a couple of new (or features that were missing) features as well. If after upgrading to version 1.3 and you have been receiving errors while attempting to do imports from the configuration, this may be the release for you!

Fixes for Imports

As mentioned, version 1.3 caused some issues when trying to use the new Where Used functionality along with global imports from the configuration. Errors included messages similar to “tcm:0-234-2048 does not exist”.

Fixes to Documentation

Thanks to Robert Curlette for supplying documentation that removed the smart quotes from the code samples in the documentation. The new version of the documentation and forward will be based off of his ascii documentation.

Non-Cache DynamicPackage

Thanks to Dominic Cronin for supplying a patch for the DynamicPackage to make it not cache the package’s values. This will ensure you don’t run into issues if the context of those package items gets changed during the razor’s scope.

Indexes for DynamicItemFields and DynamicPackage

You can now access fields for ItemFields and for the Package using indexers. This can help when creating generic templates, or when working with package items that contain dots in the item names.

@Fields["FieldName"]
@Package["SomeName"]
@Package["Some.Name.With.Dots"]

GetFields and GetFieldNames

Also to assist with the creation of making generic templates, DynamicItemFields now has a GetFields() and a GetFieldNames() method. GetFieldNames() returns an array of strings containing the field names, while GetFields() returns the underlying Dictionary<string, object> that represents the ItemFields.

@foreach (string name in @Fields.GetFieldNames()) {
    <span><strong>@name</strong>: @Fields[name]</span>
}

// var field is of type KeyValuePair<string, object>
@foreach (var field in @Fields.GetFields()) {
    <span><strong>@field.Key</strong>: @field.Value</span>
}

Fix to IsSiteEditEnabled

The IsSiteEditEnabled property would throw an error when working with a Publication Target that never had its Site Edit enabled or disabled yet. This is now fixed.

ParentKeywords and RelatedKeywords

These properties have been added to the KeywordModel, and both return a List of KeywordModel’s.

@foreach (var kw in @Fields.SomeKeyword.ParentKeywords) {
    <span>@kw.Title</span>
}

Thanks again to everyone who has been posting suggestions, issues, and fixes. Special thanks to Robert Curlette who has been the guinea pig for most of these updates!

Getting Using and Used Items With Core Services

A while back I posted about how to get Using and Used Items from Tridion using the TOM.NET API. Today I thought I would share again those same examples, only this time using the Core Service API. For those of you who may not know, the TOM.NET API is read only when used in Template Building Blocks, and read/write when used in the Tridion Event System. But, when you want to create custom applications or processes, you’ll want to create them using the Core Service API.

One of the major differences if you’re comparing this article with the TOM.NET one, is that the Core Service API doesn’t actually have a GetWhereUsed() or GetWhereUsing() method on object classes. Instead, we’ll be retrieving XML using the Core Service Client’s GetListXml(string identifier, SubjectRelatedListFileterData filter) method.

You may notice there’s a GetList(id, filter) method too and may be tempted to bypass working with the XML returned from GetListXml… however, as of Tridion 2011 SP1, if you look at the API’s documentation, currently the only filter supported by this method is the OrganizationalItemAncestorsFilterData, so trying to use that method with our UsingItemsFilterData or UsedItemsFilterData from the examples below will fail, and you’ll probably get an error stating “Unexpected List Type”.

Get Using Items (UsingItemsFilterData)

To get using items with Core Services, you just need to pass the GetListXml(id, filter) a filter of type UsingItemsFilterData, and the tcm uri of the item that you want to get the using items for.

GETTING ALL PAGES USED BY A COMPONENT:

SessionAwareCoreServiceClient client = new SessionAwareCoreServiceClient("netTcp_2011");

UsingItemsFilterData filter = new UsingItemsFilterData();
filter.ItemTypes = new [] { ItemType.Page };
filter.IncludedVersions = VersionCondition.OnlyLatestVersions;

XElement pages = client.GetListXml("tcm:12-3456", filter); // Pass in a Component ID

XML Sample Returned By GetListXml:

<tcm:ListUsingItems xmlns:tcm="http://www.tridion.com/ContentManager/5.0">
    <tcm:Item ID="tcm:17-384-64" Title="Some Page Title" Type="64" OrgItemID="tcm:17-107-4" Path="\040 Web Publication\Root\030 - Work" Icon="T64L1P0" Publication="040 Web Publication"></tcm:Item>
    <tcm:Item ID="tcm:31-871-64" Title="Another Page Title" Type="64" OrgItemID="tcm:31-206-4" Path="\050 Another Web\Root\030 - Work" Icon="T64L1P1" Publication="050 Another Web"></tcm:Item>
</tcm:ListUsingItems>

GETTING ALL COMPONENTS USED BY A SCHEMA:

SessionAwareCoreServiceClient client = new SessionAwareCoreServiceClient("netTcp_2011");
                
UsingItemsFilterData filter = new UsingItemsFilterData();
filter.ItemTypes = new ItemType[] { ItemType.Component };
filter.IncludedVersions = VersionCondition.OnlyLatestVersions;
filter.InRepository = new LinkToRepositoryData { IdRef = "tcm:0-31-1" };

XElement components = client.GetListXml("tcm:2-572-8", filter); // Pass in a Schema ID

GETTING ALL SCHEMAS THAT USED A GIVEN EMBEDDED SCHEMA:

UsingItemsFilterData filter = new UsingItemsFilterData
{
    ItemTypes = new ItemType[] { ItemType.Schema },
    IncludedVersions = VersionCondition.OnlyLatestVersions
};

XElement schemas = client.GetListXml("tcm:2-345-8", filter); // Pass in a Schema ID

GETTING ALL COMPONENTS USING A GIVEN KEYWORD:

UsingItemsFilterData filter = new UsingItemsFilterData
{
    ItemTypes = new ItemType[] { ItemType.Component },
    IncludedVersions = VersionCondition.OnlyLatestVersions
};

XElement components = client.GetListXml("tcm:2-213-1024", filter); // Pass in a Keyword ID

Note that the above could have just been retrieved using ClassifiedItemsFilterData.

XElement components = client.GetListXml("tcm:2-213-1024", new ClassifiedItemsFilterData());

Get Used Items (UsedItemsDataFilter)

Getting Used items from Core Service works in the same manner as getting Using items, except that you pass in a UsedItemsDataFilter as the filter instance.

GET THE EMBEDDED SCHEMAS USED IN A GIVEN SCHEMA:

SessionAwareCoreServiceClient client = new SessionAwareCoreServiceClient("netTcp_2011");
XElement schemas = client.GetListXml("tcm:2-184-8", new UsedItemsFilterData { ItemTypes = new[] { ItemType.Schema } });

GET COMPONENTS WITHIN COMPONENTLINKS OF A GIVEN COMPONENT:

UsedItemsFilterData filter = new UsedItemsFilterData
{
    ItemTypes = new ItemType[] { ItemType.Component }
};

XElement components = client.GetListXml("tcm:21-543", filter);

Happy coding everyone!

Getting Using and Used Items With TOM.NET

Periodically I will receive an e-mail from someone in the Tridion community asking questions like “How do I get the pages where a component is used?” or “How do I get all the components using a particular schema?”. Although there is already some good examples out there on retrieving Where Using and Where Used, I figured some more Tridion samples out there could never hurt.

GetUsingItems

The Tridion.ContentManager.IdentifiableObject (the base class that most of the other items in Tridion inherits) has a method called “GetUsingItems” that allows you to get all items using this item. Note that these are the items you see in the “Used in” tab when you use the “Where Used” functionality in the GUI. The UsingItemsFilter argument allows you to refine your query of using items even further.


IdentifiableObject.GetUsingItems()
IdentifiableObject.GetUsingItems(UsingItemsFilter filter)

Getting all pages used by a component:

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

IEnumerable<IdentifiableObject> pages = component.GetUsingItems(filter);

Getting all components used by a schema:

UsingItemsFilter filter = new UsingItemsFilter(schema.Session);
filter.ItemTypes = new List<ItemType> { ItemType.Component };
filter.IncludedVersions = VersionCondition.OnlyLatestVersions;
filter.InRepository = (Repository)publication; // assuming variable publication is of type Tridion.ContentManager.CommunicationManagement.Publication and the only publication you want the results to return components for

IEnumerable<IdentifiableObject> components = schema.GetUsingItems(filter);

Getting all schemas that used a given embedded schema:

UsingItemsFilter filter = new UsingItemsFilter(embeddedSchema.Session);
filter.ItemTypes = new ItemType[] { ItemType.Schema }; // Note can be an array too
filter.IncludedVersions = VersionCondition.OnlyLatestVersions;

IEnumerable<Schema> schemas = (IEnumerable<Schema>)embeddedSchema.GetUsingItems(filter);

Getting all components using a gien keyword:

UsingItemsFilter filter = new UsingItemsFilter(keyword.Session);

filter.ItemTypes = new List<ItemType> { ItemType.Component };
filter.IncludedVersions = VersionCondition.OnlyLatestVersions;

IEnumerable<IdentifiableObject> components = keyword.GetUsingItems(filter);

Note that the above could have just been retrieved using the keyword’s GetClassifiedItems() method:

IEnumerable<RepositoryLocalObject> components = keyword.GetClassifiedItems();

GetUsedItems

The “GetUsedItems” method of IdentifiableObject works very similarly to the “GetUsingItems” method, but in reverse: it’ll grab items that this item uses. This is the same functionality as the “Uses” tab in the “Where Used” functionality of the GUI.


IdentifiableObject.GetUsedItems();
IdentifiableObject.GetUsedItems(UsedItemsFilter filter);

Get the embedded schemas used in a given schema:

UsedItemsFilter filter = new UsedItemsFilter(schema.Session);
filter.ItemTypes = new ItemType[] { ItemType.Schema };

IEnumerable<IdentifiableObject> embeddedSchemas = schema.GetUsedItems(filter);

Get Components Within ComponentLinks of a given component:

UsedItemsFilter filter = new UsedItemsFilter(component.Session);
filter.ItemTypes = new ItemType[] { ItemType.Component };

var compLinks = component.GetUsedItems(filter);

Getting the List as XML

When working with a pretty large set of items that may get returned, you may want to work with an XML of the used items instead for performance reasons. The following methods return a type of XmlElement:


IdentifiableObject.GetListUsedItems();
IdentifiableObject.GetListUsedItems(UsedItemsFilter filter);
IdentifiableObject.GetListUsingItems();
IdentifiableObject.GetListUsingItems(UsingItemsFilter filter);

Anguilla Framework: Adding Messages With MessageCenter and $messages

The Anguilla Framework comes with a built in message center that allows you to display different types of messages and notifications to the user. You’ve probably seen these messages already – they appear at the top of the window when you do almost any action in Tridion. They can even appear as a modal, display a question that requests a user response, or display a progress message that lets the user know when a certain action is done. When building your custom GUI Extensions, instead of rolling your own status or update notification system, you can easily take advantage of the one that’s already in place (and make your extension feel like its an actual part of Tridion). Messages added with MessageCenter are even archived, so your users can go back and view them if needed (by clicking the flag like symbol in the upper left of the screen). In this post, we’ll cover how to add notifications, warnings, errors, questions, goals and progresses.

Notifications

Notifications get displayed with the “i” icon (as with the image at the top of this post). You can use these for simple updates or alerts. Notifications will disappear from the upper window after around ten seconds or so.

$messages.registerNotification(title, description, local, modal)

title – The title of the notification. This is the message that gets displayed when shown at the top of the screen.
description – (optional) A better description of the notification. Can be seen when user double clicks the message to get further details.
local – (optional) You’ve probably noticed that most of the messages get displayed across all of your open Tridion windows. Setting ‘local’ to true will only display your message at the top of the window you are working with. Note that it still goes to the MessageCenter, so you can still view it from the other windows via the Message Center icon.
modal – (optional) True or false, if true your notification will get displayed as a modal popup.

Goals


Goals are like notifications, except they are displayed with a check mark icon (and a different colored background!).

$messages.registerGoal(title, description, local, modal)

Warnings


Warnings are also similar to notifications as well and are displayed with a warning icon. Unlike Notifications and Goals, the warning message will not disappear on its own (you’ll have to click the little ‘x’).

$messages.registerWarning(title, description, local, modal)

Errors


Errors, like Warnings, do not disappear on their own, and they are displayed with an error icon. They allow for an extra area for specifics of your error.
Error message with Details.

$messages.registerError(title, description, details, local, modal)

details – Details are for displaying the details of your error. For example, this area would be a lovely place to display the stack trace of an error.

Questions

Questions allow you to supply a simple “yes/no” type of question to the user. You can add events to your message to perform an action upon confirming or canceling. Messages are great to use with the modal feature.

Question with modal option turned on

$messages.registerQuestion(title, description, local, modal, buttonLabels)

buttonLabels – (optional) Additional settings to control the labels of the button, which by default is “Yes” and “No”. { cancel: "Cancel Label", confirm: "Confirm Label" }

Question Code Samples:

var question = $messages.registerQuestion("Do you love GUI Extensions?", null, true, true, { cancel: "Hate Them!", confirm: "Love Them!" });
question.addEventListener("cancel", function (event) {
    $messages.registerNotification("Haters want to hate!");
});
question.addEventListener("confirm", function (event) {
    $messages.registerNotification("TridionLove++");
});

Progress


Progress messages let you display the progress of an action, for example, “Saving…”. Like Questions, they are a bit more interactive than just displaying a message to the user. You can set messages that will get displayed upon success of your action using setOnSuccessMessage(title, description). You can even have a cancel button appear and allow the user to cancel your action, and set a message to display upon cancellation using setOnCancelMessage(title, description). Note that the Success and Cancel messages will be displayed as Goals.

$messages.registerProgress(title, description, canCancel, local, modal)

canCancel – (Optional) True or false, when set to true will display a cancel button

Progress Code Samples:

var progress = $messages.registerProgress("Waiting...", null, true);

progress.setOnCancelMessage("You've canceled!");
progress.setOnSuccessMessage("Done waiting!!!");

progress.addEventListener("cancel", function (event) { 
    console.log("Do stuff when canceled..."); 
});

// ... somewhere later in your code, in a callback or some form of dark arts... 
progress.finish({ success: true }); // Will remove progress message and display your onSuccess message.
progress.finish(); // Will remove progress message, but not display your onSuccess message.

// You can even cancel programatically by... clicking the cancel button or calling this method directly will display the cancel message.
progress.cancel();

You can also set it to automatically call the .finish() method upon the event of another item via the .addFinishEvent(itemID, event, isSuccessEvent) method. The follow example will show a loading status, and will automatically fire our success message upon completion of our item being loaded.

var item = $tcm.getItem("tcm:1-234"),
    progress = $messages.registerProgress("Loading our item...");

progress.setOnSuccessMessage("Item Finished Loading!");
progress.addFinishEvent("tcm:1-234", "load", true);

item.load();

Playing around in a console…

If you want a quick way to experiment with MessageCenter, feel free to try the $messages api inside of the console window!

Update 08/01/2014

I’ve posted a follow up on this article on retrieving, archiving, and disposing of messages. You can find it here!

Anguilla Framework: Getting Items From Tridion With $tcm.getItem() And Load Events

For anyone wondering if you can get items from Tridion using the Anguilla Framework in your GUI Extensions, the answer is yes, yes you can.

var yourPage = $tcm.getItem("tcm:10-1234-64");

Simple, no? Unfortunately your work is not done here. The getItem() method loads data from a cached state. That means, if the properties that you are looking for hasn’t been loaded already, they will return undefined.

var templateID = yourPage.getPageTemplateId();
// templateID is "undefined".

To load the data, you can call the load() method. You’ll also want to create an event handler for when the event has been triggered.

var yourPage = $tcm.getItem("tcm:10-1234-64");
$evt.addEventHandler(yourPage, "load", function (event) {
    doSomethingWithYourPage(yourPage); // event.source also contains the page instance.
});
yourPage.load();

Remember to take advantage of how Tridion handles the caching. The above script is inefficient, because you’ll always load the item regardless of whether or not it is in the cache. To check if the items has already been loaded, you could do the following:

var yourPage = $tcm.getItem("tcm:10-1234-64");
if (yourPage.isLoaded()) {
    doSomethingWithYourPage(yourPage);
} else {
    $evt.addEventHandler(yourPage, "load", function (event) {
        doSomethingWithYourPage(yourPage); // event.source also contains the page instance.
    });
    yourPage.load();
}

Another possible gotcha is that .load() doesn’t load everything. For example, the WebDav URL. After the .load() call, the following will still not return anything.

var webDavUrl = yourPage.getWebDavUrl();

To get the data you need in the above example, you would have to do something along the lines of:

var yourPage = $tcm.getItem("tcm:10-1234-64"),
    webDavUrl = yourPage.getWebDavUrl();

if (!webDavUrl) {
    $evt.addEventHandler(yourPage, "loadwebdavurl", function (event) {
        webDavUrl = yourPage.getWebDavUrl(); // also could do event.source.getWebDavUrl()
    });
    yourPage.loadWebDavUrl();
}

There are other load functions that load different types of data, each with their own events. Here is a list of them with their associated load and fail event names.

Function Name Load Event Load Fail Event
load() load loadfailed
loadBlueprintInfo() loadblueprintinfo loadblueprintinfofailed
loadItemCompareInfo() compareinfoload compareinfoloadfailed
loadRollBackComment() rollbackcommentload rollbackcommentloadfailed
loadWebDavUrl() loadwebdavurl loadwebdavurlfailed
staticLoad() staticload staticloadfailed