Workflow Dreamin’

March 23rd, 2012 | Posted by Robert Curlette in Tridion - (0 Comments)

Workflow has been in the Tridion product since version 4 and is one of the most often requested and least often implemented or used features in the product.  The upcoming bundles feature in the product has sparked up some discussions this week.  In this article I will attempt to de-construct these old assumptions and provide my dreams for workflow.

Overview

Chris Summers and Dominic Cronin brought us great posts this week about Tridion Workflow and I agree with both Dominic and Chris’s points. However, they suggest we might be missing a method in the API or the business needs to think and plan more for the workflow. But, I disagree. Tridion has added API features since it was built but the concept has remained the same since v4. I would argue that the assumptions that workflow was built for in 2001 do not exist today. These ficticious organizations and ficticious Authors are not there and therefore the workflow system we have does not work. Current Tridion workflow assumptions are reviewed below:

Assumption: Authors need everything reviewed all the time.

Tridion workflow currently assumes that the we have some rogue users in our Tridion system that need every change they make reviewed and approved every time for all content using a particular schema, for example an Article, with no exceptions. These rogue users are believed to be dangerous and we need to watch everything they do otherwise our site will fall to ruins.
In reality, this is very far from the truth. Our Tridion users are regular Tridion users, most of them daily users. They are experts in the content and know it better than most people in the organization. They know when they need approvals or not – and want to do the right thing by requesting an approval for a completely new article. However, for correcting a typo in some content they will not need to have an approval since it is a small change. In other words – we need to empower our Authors and make it very easy for them to get approval when they need it or want it, and all other times stay out of the way.

Assumption: Content needs approval every time.

Currently Tridion workflow is assigned to a Schema and whenever any content using that Schema is modified, even a little bit, the content goes into workflow and needs approval. A lot of content updates are for fixing typos, updating an image, adding 1 line to the text. What we need an approval for is big content changes and new items. And, who knows when we need an approval? – the Author. Because we trust them to know their content.

Assumption: All content using this Schema should go into workflow.

Context matters. Today when workflow is assigned to a Schema, all content using that Schema goes into workflow when edited. Maybe we want to have all articles under the Press Release folder to go into workflow A and the articles in the News folder into Workflow B. Or maybe the articles in the News folder don’t go into workflow, because the Author is the person in the organization responsible for that content – and we trust them to make the right decisions. We don’t want to turn workflow off for this – we want to specify the context the workflow is turned on.

Assumption: Workflow will protect our content by allowing certain roles to make changes

If we want to restrict users – then use the excellent Tridion security system with roles and rights. Want to not allow an author to change content? Set the security rights. Limit publishing to Staging? No problem, restrict publishing. But, I argue, limiting what users can do is a matter of security, not workflow.

Solution – Dreams for a New Workflow- If This Then That

My proposal is to flip workflow on its’ head. Think of the app If This Then That. The idea is simple – If ‘This’ happens then do ‘That’. Here are some examples, http://ifttt.com/recipes, Imagine these Tridion recipes:

If ‘I’ create an ‘article’ AND
If the ‘article’ is in the folder ‘News’ AND
If the date is ‘today’ THEN
Mark it for ‘Needs Approval’ AND
Notify ‘John’ with ‘once per day’ by ‘Email’

Or, maybe a pull workflow created by ‘John’ the manager:

If ‘anyone’ creates an ‘article’ AND
If the ‘article’ is in the folder ‘News’ THEN
Notify ‘me’ ‘once per day’ by ‘Email’

Or, maybe a pull workflow created by ‘Sally’ the web products manager:

If ‘anyone’ publishes ‘anything’ to ‘live’ AND
If the ‘article’ is in the structure group ‘products’ THEN
Notify ‘me’ ‘each time’ by ‘workflow notification center’

The power of this is amazing. We can create flexible rules that follow our organic organizations. And, we can empower our users.

Summary

The core concept here is notifications – notify someone that something has happened and request an approval. That’s it.  My dream is to allow users to pull and push notifications for actions in the GUI. I believe Tridion users and not only editors, but are owners of content, and responsible for the content they publish online. I believe they want to do the right thing – and we should help them do that with a flexible rules system that includes notifications and approvals.

Thoughts? Please share.

If you’re like me and you work in a lot of legacy Tridion implementations then there’s a good chance you spend time each week hunting for that 1 line of code to update, burried somewhere in the thousands of lines of VBScript templates. Oh, you don’t have this problem, lucky you! 🙂

Show Template Source Custom Page

I created a very small custom page that loops through 1 folder and displays all the template code to the screen. My folder structure specifies 1 template type per folder and the custom page is setup that way, and it loops through child folders too.  Also – you can pass in any URI of a folder and get back the source. You need to use ‘View Source’ in your browser and then copy the source to your favorite text editor. One tip – wait until the whole page finishes loading – otherwise you’ll not see all the templates.

*Update:  Pages also supported – thanks to Mihai’s suggestion in the comments.

Code highlights

' Show Template
Response.Write template.Content

The Code is hosted on GitHub as a Gist, https://gist.github.com/2044332 and also embedded here:

Enjoy!

Working with Tridion 2011 is a breath of fresh air – and sometimes from unexpected places. Today I was working with the new Content Delivery API and had a refreshingly light feeling.  I was immediately comfortable with the Criteria objects and the new possibilities. It just feels right.

Tridion Broker Query – Get item based on Metadata from all Publications

In Tridion 5.3 I have a query that finds a product based on the SKU in the Metadata for 1 Publication.  Now I have a need to get all instances of the Component for all Publications.  Using Tridion 5.3 we solved this for 1 Publication with a SearchQuery like the one below, but I could not find a way in Tridion 5.3  to query the Broker and all Publications for 1 Component using a Metadata query.  Using Tridion 2011 for getting all published instances, regardless of Publication, is very easy.

Tridion 5.3 Broker Query:

<%@ page import="com.tridion.dcp.*"%>
<%@ page import="com.tridion.dcp.filters.*"%>
<%@ page import="com.tridion.dcp.filters.query.*"%>
<%@ page import="com.tridion.broker.components.meta.*"%>
<%@ page import="com.tridion.util.*"%>
<%@ page import="com.tridion.meta.*"%>
<%
SearchFilter filt = new SearchFilter(publicationURI);
Query q = new Query();
String[] results = null;
strCustomQuery = "(KEY_NAME = '" + key + "' AND KEY_STRING_VALUE = '" + value + "')");
results = filt.match(q.toString(), strCustomQuery, ComponentMetaHome.FIELD_CREATION_DATE + "=asc", 100);

ComponentPresentationFactory cpFactory = new ComponentPresentationFactory(publicationURI);
ComponentMetaFactory componentMetaFactory = new ComponentMetaFactory(publicationURI);
if (results != null) {
	int i = 0;
	for (String result : results) {
		tcmURI = new TCMURI(result);
		ComponentPresentation cp = cpFactory.getComponentPresentationWithLowestPriority(tcmURI.getItemId());
		out.println(cp.getContent());
	}
}
%>

Tridion 2011, All Publications:

<%@ page language="java" contentType="texthtml; charset=UTF-8"%>
<%@page import="com.tridion.broker.StorageException,
com.tridion.broker.querying.*,
com.tridion.broker.querying.criteria.*,
com.tridion.broker.querying.criteria.categorization.*,
com.tridion.broker.querying.criteria.content.*,
com.tridion.broker.querying.criteria.metadata.*,
com.tridion.broker.querying.criteria.operators.*,
com.tridion.broker.querying.criteria.taxonomy.*,
com.tridion.broker.querying.filter.LimitFilter,
com.tridion.broker.querying.sorting.SortParameter"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
     <head>
          <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
          <title></title>
     </head>
<%
String sku = "1EX0.031.03";
String fieldName = "article_number";

//Create query
Query myQuery = new Query();

Criteria myCriteria = null;
CustomMetaKeyCriteria metaField = new CustomMetaKeyCriteria(fieldName);
CustomMetaValueCriteria customMeta = new CustomMetaValueCriteria(sku);

// glue the metadata together
AndCriteria fieldCriteria = new AndCriteria(customMeta, metaField);
myCriteria = fieldCriteria;
myQuery.setCriteria(myCriteria);

// Sort it
SortParameter sortParameter = new SortParameter(SortParameter.ITEMS_TITLE, SortParameter.ASCENDING);
myQuery.addSorting(sortParameter);

// Get results
myQuery.setResultFilter(new LimitFilter(100));

// Display
String[] itemURIs = myQuery.executeQuery();
String strOutput = "";
for (int i = 0; i < itemURIs.length; i++) {
	strOutput += itemURIs[i] + ", ";
}
%>
<body>
	output = <%=strOutput%>
</body>
</html>

Tridion 2011, 1 Publication (Code borrowed from Tridion Live Documentation with slight modifications)

<%@ page language="java" contentType="texthtml; charset=UTF-8"%>
<%@page import="com.tridion.broker.StorageException,
com.tridion.broker.querying.*,
com.tridion.broker.querying.criteria.*,
com.tridion.broker.querying.criteria.categorization.*,
com.tridion.broker.querying.criteria.content.*,
com.tridion.broker.querying.criteria.metadata.*,
com.tridion.broker.querying.criteria.operators.*,
com.tridion.broker.querying.criteria.taxonomy.*,
com.tridion.broker.querying.filter.LimitFilter,
com.tridion.broker.querying.sorting.SortParameter"%>

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
     <head>
          <meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1">
          <title></title>
     </head>
<%
int iPublicationID = 129;
String sku = "1EX0.031.03";
String fieldName = "article_number";

//Create query
Query myQuery = new Query();

Criteria myCriteria = null;
PublicationCriteria pubCriteria = new PublicationCriteria(iPublicationID);
CustomMetaKeyCriteria metaField = new CustomMetaKeyCriteria(fieldName);
CustomMetaValueCriteria customMeta = new CustomMetaValueCriteria(sku);

// glue the metadata together
AndCriteria fieldCriteria = new AndCriteria(customMeta, metaField);
AndCriteria allCriteria = new AndCriteria(fieldCriteria, pubCriteria);
myCriteria = allCriteria;
myQuery.setCriteria(myCriteria);

// Sort it
SortParameter sortParameter = new SortParameter(SortParameter.ITEMS_TITLE, SortParameter.ASCENDING);
myQuery.addSorting(sortParameter);

// Get results
myQuery.setResultFilter(new LimitFilter(100));

// Display
String[] itemURIs = myQuery.executeQuery();
String strOutput = "";
for (int i = 0; i < itemURIs.length; i++) {
	strOutput += itemURIs[i] + ", ";
}%>
<body>
	output = <%=strOutput%>
</body>
</html>

Helpful links:

Summary:

Tridion has opened a can of awesome sauce and spread it liberally over the new Tridion 2011 Content Delivery API. Marvel at the amount of Criteria available and prepare for the new possibilities it provides. Definitely worth a serious look when you upgrade – don’t leave your SearchFilter queries in there because you can – do yourself a favor and migrate that code right now to use the new API.

The Tridion TOM.NET API and the Core Service introduced some new ItemTypes to Tridion, including the IdentifiableObject type.  While this is not a UFO, it might appear to you as an Unidentifable (Flying) Object.  In this post I hope to explain these other ItemTypes: IdentifiableObject, SystemWideObject, RepositoryLocalObject, and VersionedItem and are what I call the Tridion UFOs.

The Tridion Core Service methods return the IdentifiableObject method a lot and knowing more about this type and also how to cast it to a more specific type is very important to get a working solution.  Recently I struggled a bit with this and was fortunate to have the Tridion Community on StackOverflow help me with my question.

In the old TOM API we did not have any generic parents to the objects – we always got back a concrete instance of an object that matched 1-1 to something we could see and touch in the GUI. This is not the case with TOM.NET or the Core Service where we can get back base-objects that contain a subset of the methods and properties of our real objects. These new classes are super-handy in .NET where we can use Generics and create more re-usable methods. But, using them requires a little bit of knowledge as to which one is best for the given scenario.

The Tridion 2011 UFOs

IdentifiableObject

The opposite of a UFO, but the most prevalent ItemType is the IdentifiableObject (IO). The favorite return type of many methods in TOM.NET and the Core Service, the IdentifiableObject always leaves us wanting more. Which is why we usually cast it to a more concrete object as soon as possible – unless we really only need the Title or URI.

Methods and Properties available:  * means the property is mandatory.

  • Title*
  • ID
  • IsEditable
  • AllowedActions
  • ExtensionData

SystemWideObject

Items not within a Publication. Most of these can be found in the Administration section of the GUI. Examples are Users and PublicationTargets.

Methods and Properties available:

  • Same as IdentifiableObject

RepositoryLocalObject

Repository is another name for Publication (why couldn’t they call it PublicationLocalObject?) so this means all items within our Publication. This ItemType is a good generic one to use when dealing with most content types.

Methods and Properties available include all from IdentifiableObject plus:

  • BlueprintInfo
  • IsPublishedInContext
  • LocationInfo
  • Metadata

VersionedItem

Content items such as Pages and Components. Does not include non-versioned items such as Folders or Structure Groups.

Methods and Properties available include all from RepositoryLocalObject plus:

  • LocationInfoVersionInfo

Tridion Old School Objects

Page

The page object we all know and love. Contains a mandatory property ‘FileName’ that must be set when creating new items.

Methods and Properties available include all from VersionedItem plus:

  • ComponentPresentations
  • FileName*
  • IsPageTemplateInherited
  • PageTemplate
  • WorkflowInfo

Component

The classic object containing the actual content. Contains a mandatory property of Schema.

Methods and Properties available include all from VersionedItem plus:

  • ApprovalStatus
  • BinaryContent
  • ComponentType
  • Content
  • IsBasedOnMandatorySchema
  • IsBasedOnTridionWebSchema
  • Schema*
  • WorkflowInfo

Casting the Generic IdentifiableObject to a Page object

Recently I was working on porting a classic ASP custom page to use the Core Service and was excited to learn about the copy method on this StackOverflow post. By default it returns an IdentifiableObject:

// newItem is ItemType IdentifiableObject
var newItem = client.Copy(source.Id, orgItemUri, true, new ReadOptions());

If I want to access the FileName property I need to cast it to a Page object:

// newItem is ItemType Page
var newItem = client.Copy(source.Id, orgItemUri, true, new ReadOptions()) as Page;

However, in my code I wanted to copy any kind of item and cast it to a Page if I need to. With some help from the Tridion StackOverflow community I learned how to get the ItemType and then for only Pages set the FileName property.

PageData pageData = newItem as PageData;  // Cast IdentifiableObject to Page object

Notice the new UnknownByClient (UBC) ItemType – almost a UFO!

	private string CreateNewItemCopy(string title, RepositoryLocalObjectData source, string filename)
        {
            string newItemUri = "";
            try
            {
                ItemType tridionItemType = GetTridionItemType(source);
                string orgItemUri = source.LocationInfo.OrganizationalItem.IdRef;
                var newItem = client.Copy(source.Id, orgItemUri, true, new ReadOptions());
                newItem.Title = title;
                if (tridionItemType == ItemType.Page)
                {
                    PageData pageData = newItem as PageData;
                    pageData.FileName = filename;
                    client.Update(pageData, new ReadOptions());
                }
                else
                {
                    client.Update(newItem, new ReadOptions());
                }
                newItemUri = newItem.Id;
            }
            catch (Exception ex)
            {
                throw;
            }

            return newItemUri;
        }

	private ItemType GetTridionItemType(RepositoryLocalObjectData source)
	{
		string itemType = source.GetType().Name;

		switch (itemType)
		{
			case "ComponentData":
				return ItemType.Component;
			case "PageData":
				return ItemType.Page;
		}
		return ItemType.UnknownByClient;
	}

Tridion Object Casting Is Good

Don’t worry if the method returns an IdentifiableObject and you need a concrete object such as Page or Component. Cast it to the object type you need and you’ll be cooking with gas!

Summary

It is a lot of fun learning about the Core Service. However, sometimes it is difficult getting familiar with the new ItemTypes and limited properties they provide. Becoming familiar with them and casting is essential.  What they offer allows us to use the most generic object possible for our methods yet also the opportunity to cast the object down to reality when you need to access that oh-so-special property. I hope I’ve provided a good overview of these UFOs and wish you luck in your casting adventures.

Tridion 2011 provides us with great possibilities to improve the user experience for our Authors and Editors using GUI Extensions. We can add new items to the context-menu, new icons to the ribbon, or even new tabs in the edit screen of a Component. All of this is possible thanks to the new Anguilla framework introduced in Tridion 2011. There is a lot to understand before you can make your first usable GUI Extension and in this article I will explain what I learned while creating my first GUI Extension.  This is a 3-section tutorial covering all aspects to create a GUI Extension, including the Configuration, JavaScript, and Tridion API calls.

Architecture – Tridion Custom Page (ala R4 and R5)

In the old school way using Tridion R4 or R5 we would create a custom page in classic ASP and directly instantiate the Tridion API COM+ TDS.TDSE object.  This gave us direct access to the Tridion API from our ASP Script and worked well since we had our ASP page in the Tridion CMS Website. However, we combined our display HTML with our code logic, needed to postback to the same page, and use if-conditions to determine to process the form or not. In addition, it was hard to test and debug, often limited to writing out strings of text.  Here is what the old-school Tridion Custom Page architecture looked like:

Tridion Custom Page

Architecture – Tridion R6 (aka Tridion 2011) GUI Extension

Tridion GUI Extension Overview

The main idea is the client and server are separated, with the client using mostly JavaScript and a little HTML and the server using a Web Service, .NET, C#, and the Tridion API.  The _execute method in the GUI Extension JavaScript file is run when the user clicks the GUI Extension button.  Usually we display a GUI Extension Interface popup and that popup is what does the work of getting the user input (if you need more than the item URI) and then calling a web service to do the heavy lifting with the Tridion API.  This architecture applies a clear separation of concerns – from getting and showing feedback to the user via the HTML page and interacting with the Tridion API in the Web Service layer.

Concept – Who Did It?

The goal is to show the last person who modified the item.  I call it ‘WhoDidIt’ and is part of the Sherlock namespace. I am a big fan of the books and also the recent BBC series and felt this was an appropriate name for the feature but also for the deep investigation work into how GUI Extensions are created.  It is implemented as a right-click context menu item and does not include a ribbon extension.

Overview – 3 main steps to perform when building a Tridion 2011 GUI Extension

1. Get your button in the ribbon or your link in the context menu. This is death by config. If it doesn’t kill you, there is a good chance you will have your GUI extension working.  GUI Extension button-click calls _execute method in your .js file, referenced in the config.  _execute contains a JavaScript call to show a popup

2. Show a popup HTML page when someone clicks on #1.  Popup makes an AJAX call to a web service.

3. Do something using the Tridion API from the popup via an AJAX call to the Web Service that uses the Tridion Core Service to interact with Tridion.

Some personal choices I made:

– Keep the popup plain and simple HTML and JavaScript/ jQuery. I have seen others use an ASPX page with some server-side control references. Too complicated and difficult to debug – let’s keep it simple and use JavaScript frameworks here in the client.

– Use ServiceStack for the Web Service layer – super-simple, clean, JSON-friendly, easy to call from AJAX

Sections of this tutorial:

Installing the Extension

Files: Client

Server

Installing the GUI Extension Client

1. Copy files to Tridion CMS Server. Location: Tridion\web\webUI\Editors

2. Open IIS and create a new Virtual Directory

VDIR for Tridion GUI Extension

3. Edit Tridion GUI System.Config file and tell Tridion about the new Extension. Hint: If an extension is misbehaving and breaking your GUI, comment it out in this file. Location: Program Files (x86)\Tridion\web\WebUI\WebRoot\Configuration\System.config

    <editor name="WhoDidIt">
      <installpath>C:\Program Files (x86)\Tridion\web\WebUI\Editors\WhoDidIt\</installpath>
      <configuration>WhoDidIt.config</configuration>
      <vdir>WhoDidIt</vdir><!-- Must match IIS Virtual Dir name -->
    </editor>

4. Recycle IIS Application Pool. Empty browser cache. Update from Peter K:  “You don’t have to recycle the app pool when you update the configuration. Increment the “modification” attribute in the configuration file and your cache will automatically be invalidated for all clients (so no need to clear the browser cache)”.  Test.

Installing the GUI Extension Server – Create the MVC3 Website for ServiceStack

1. Open the Visual Studio Solution and add the Tridion references to the Core Service DLL – Tridion.ContentManager.CoreService.Client dll located on the Tridion server in the bin/client folder. Also, I suggest to do a find and replace for ‘TridionDev2011’ with the CMS URL of your server. You should have about 55 replacements. (thanks to WCF config for the high # of refs)

2.  Create a new folder under Inetpub\wwwroot for the Web Service and copy the Web Service files there. If you can map a drive to your root folder on the Tridion CMS then you can easily use the ‘Publish’ option in Visual Studio to deploy your project. Simply right-click onthe project, select publish, chose file copy, then put the full path (including mapped drive) to the IIS site and select publish.

3.  Create a new App Pool in IIS for the TridionServiceStack ASP.NET 4.0 site.

4.  Create a new Website in IIS and point to the files in step #1.  I usually put this on a different port, such as 8001.

5.  Open the web.config file and change the Tridion Core Service URL references from Dev2011Tridion to your CMS Server URL.

 

Tridion GUI Extension Configuration Explained – WhoDidIt.config

Tridion Configuration is the first major step you need to understand to get a working GUI Extension. This is where we add an option to the GUI and is part of our ‘client’. We do not do any Tridion API calls in the ‘client’ part – it is all JavaScript. In my experience it was by far the most difficult part of the process and is why I dedicate 30% of this tutorial on the configuration. I extensively used the 8 steps post by Yoav Niran and the Hello World post for starting with GUI Extensions. When the going got tough I went to the Tridion StackOverflow community. Thanks to Chris Summers for helping clarify CommandSet config and John Winter for help with the GUI Extension icon.

Configuration Groups:

This is a list of resources you want available in the _execute method after the user clicks your button / link in the GUI. Not in the popup you will open very shortly from the _execute method. So, for example, if you reference jQuery here, you will have it avail in your _execute method, which is very handy, but not in your popup.

Name your config. This is one of those names you’ll use later, and when wrong, nothing will work.

<cfg:group name="Sherlock.ConfigSet" merger="Tridion.Web.UI.Core.Configuration.Resources.CommandGroupProcessor" merge="always">

3 types of files you will often see in the config:

  • <cfg:file type=”script”> – js file containing _execute method
  • <cfg:file type=”style”> – css file containing icons for extension
  • <cfg:file type=”reference”> – name of the commandset attribute, <cfg:commandset id=”Sherlock.Interface”>

* This is one of the difficult dependencies that is not documented or really explained and almost drove me mad. When this is wrong you will see a message like ‘Sherlock.Command’ does not have a reference.

OK – these are the highlights of the config section, and it is here in it’s entirety:

<cfg:groups>
  <cfg:group name="Sherlock.ConfigSet" merger="Tridion.Web.UI.Core.Configuration.Resources.CommandGroupProcessor" merge="always">
    <cfg:fileset>
          <cfg:file type="script">/client/js/dependencies/jquery.js</cfg:file>
          <cfg:file type="script">/client/js/dependencies/infoMessage.js</cfg:file>
          <cfg:file type="script">/client/js/utils/utils.js</cfg:file>
          <cfg:file type="style">/client/css/WhoDidIt.css</cfg:file>
          <cfg:file type="script">/client/js/WhoDidItCmd.js</cfg:file>
          <cfg:file type="reference">Sherlock.Interface</cfg:file>
    </cfg:fileset>
    <cfg:dependencies>
      <cfg:dependency>Tridion.Web.UI.Editors.CME2010</cfg:dependency>
      <cfg:dependency>Tridion.Web.UI.Editors.CME2010.commands</cfg:dependency>
    </cfg:dependencies>
  </cfg:group>
</cfg:groups>

Extension definition

Next is the extension definition. Basically, the text of the context-menu item and where it goes. This was the least problematic for me and almost always worked as expected. Maybe the fact it has the least amount of dependencies helped.

The text of the context-menu item is here as the ‘name’. Why didn’t they make an attribute called displayText? I have no idea, and this is one of the places where a name doesn’t have a dependency. Also, the id is not used anywhere else. The command is used later in the Commands section.

<cmenu:ContextMenuItem id="cmExtWhoDidIt" name="Who did it?" command="SherlockCommand"/>
      • Name: Text shown in content-menu
      • Command: Name of command in Commands section (discussed later)
      • ID: Used to define icon image in the CSS file

Another important dependency is to our config section above:

<ext:dependencies>
  <cfg:dependency>Sherlock.ConfigSet</cfg:dependency>
</ext:dependencies>

OK, with that behind us, we are ready to move on. Note, I did not implement a ribbon button for this extension. Full code below:

<extensions>
  <ext:dataextenders/>
  <ext:editorextensions>
    <ext:editorextension target="CME">
      <ext:editurls/>
      <ext:listdefinitions/>
      <ext:taskbars/>
      <ext:commands/>
      <ext:commandextensions/>
      <ext:contextmenus>
        <ext:add>
          <ext:extension name="SherlockExtension" assignid="" insertbefore="cm_refresh">
            <ext:menudeclaration>
              <cmenu:ContextMenuItem id="cmExtWhoDidIt" name="Who did it?" command="SherlockCommand"/>
            </ext:menudeclaration>
            <ext:dependencies>
              <cfg:dependency>Sherlock.ConfigSet</cfg:dependency>
            </ext:dependencies>
            <ext:apply>
              <ext:view name="DashboardView"/>
            </ext:apply>
          </ext:extension>
        </ext:add>
      </ext:contextmenus>
      <ext:lists/>
      <ext:tabpages/>
      <ext:toolbars/>
      <ext:ribbontoolbars/>
    </ext:editorextension>
  </ext:editorextensions>
</extensions>

Commands Section – Your link to the JS Magic

Last comes the most critical part of all – the commands section that contains a link to our js file. While not so obvious, it is the magic that makes this sparkle.

First, we link to our reference made up in the config. First dependency.

<cfg:commandset id="Sherlock.Interface">

Next, we specify the Namespace of the js file with our _execute method in it. Super important. Also, note the name of the command is used in the extensions config above.

<cfg:command name="SherlockCommand" implementation="Extensions.WhoDidIt"/>

Finally, comes the last important part of the config, the dependency on the config section.

<cfg:dependencies>
  <cfg:dependency>Sherlock.ConfigSet</cfg:dependency>
</cfg:dependencies>

Phew – we made it. Full config file here on GitHub for your viewing pleasure.

OK – that was step 1. I am almost out of breath, but let’s pretend it was only a 3 line config and we didn’t waste too much of our time with it.

Do Something – Show the users a popup from the GUI Extension

Now the fun part begins. Let’s get the JavaScript in place.

To setup the main methods we need to enable the interface, tell the Tridion GUI when our extension is available and enabled, and then give it code to fire when we hit our button. I will simply show the boiler-plate code here and you can rename the functions to suit your needs. The IsEnabled method is interesting in that if more than 1 item is selected the button is disabled in the context menu. Cool! This is something from Jaime Santos’s HellowWorldCM post. Notice the alert in the execute method.

WhoDidIt.js

Type.registerNamespace("Extensions");

Extensions.WhoDidIt = function Extensions$WhoDidIt()
{
  Type.enableInterface(this, "Sherlock.Interface");
  this.addInterface("Tridion.Cme.Command", ["WhoDidIt"]);
};

Extensions.WhoDidIt.prototype.isAvailable = function WhoDidIt$isAvailable(selection, pipeline)
{
    // Only show option for versioned items
    var items = selection.getItems();
    if (items.length > 1)
        return false;

    if (items.length == 1) {
        var item = $models.getItem(selection.getItem(0));
        if (item.getItemType() == $const.ItemType.STRUCTURE_GROUP || item.getItemType() == $const.ItemType.FOLDER || item.getItemType() == $const.ItemType.PUBLICATION) {
            return false;
        }
        return true;
    }
};

Extensions.WhoDidIt.prototype.isEnabled = function WhoDidIt$isEnabled(selection, pipeline) {
  var items = selection.getItems();
  if (items.length == 1) {
    return true;
  }
  else {
    return false;
  }
};

Extensions.WhoDidIt.prototype._execute = function WhoDidIt$_execute(selection, pipeline) {
  alert('hello');
}

OK – now we are at an interesting place and if we have our config in good order, our VDIR setup in IIS, our extension added to system.config, and all our files deployed to IIS, we might see something. Time to test. I suggest to start with my files and naming, etc, and then change 1 by 1 to your naming scheme.

JavaScript for GUI Extension

Next, let’s do something a little more interesting, show a popup. Code borrowed from Powertools / other GUI Extension examples. Note the selectedId is the item URI and part of the querystring. This is very important and we use it later on.

Extensions.WhoDidIt.prototype._execute = function WhoDidIt$_execute(selection, pipeline) {
    // Comment line below once client is working and you want to test the server
    //alert('Excellent!');

    // UnComment - Show Popup that calls Web Service using AJAX
    var selectedID = selection.getItem(0);
    var host = window.location.protocol + "//" + window.location.host;
    var url = host + '/WebUI/Editors/WhoDidIt/client/html/popup.htm?Uri=' + selectedID;
    var popup = $popup.create(url, "toolbar=no,width=400px,height=200px,resizable=false,scrollbars=false", null);
    popup.open();
};

Popup.htm:

<html>
<head>
  <script src="js/third-party/jquery-1.5.1.js" type="text/javascript"></script>
  <script src="js/third-party/jquery.ba-bbq.min.js" type="text/javascript"></script>
  <script src="js/popup.js" type="text/javascript"></script>
</head>
<body>
  <div><span id="suspect"></span> did it!</div>
</body>
</html>

Do you notice the js files were not in our extension config? That is because we do not need them within the execute method. The BBQ jQuery function is to serialize our querystring params into a json object that we pass to our web service. The AJAX magic is in the popup.js file.

popup.js contains a jQuery AJAX call to the web service:

$(function () {
    dataString = jQuery.param.querystring(window.location.href);
    $.ajax({
        type: "GET",
        url: "http://TridionDev2011:8001/Tridion2011ServiceStack/api/tridionItem",
        data: dataString,
        dataType: "jsonp",
        success: function (data) {
            $(document).ready(function () {
                $("#suspect").text(data.lastModifiedBy);
            });
        },
        error: function (request, status, error) {
            alert(request.responseText);
        }
    });
});

Use the jQuery BBQ library and serialize the Uri param from the querystring.

dataString = jQuery.param.querystring(window.location.href);

Specify the URL of our ServiceStack web service

url: "http://TridionDev2011:8001/Tridion2011ServiceStack/api/tridionItem",

Use jsonp since I run the Web Service on a different port and the browser treats it like a different domain and will not post if using only json.

dataType: "jsonp",

Update the HTML <span/> element with id=suspect after the DOM is loaded:

$(document).ready(function () {
  $("#suspect").text(data.lastModifiedBy);
});

This code runs on Page Load since we want to find out who did it right away and not waste another moment! We only need the URI to find out who did it and don’t need any extra info from the user.

OK, we’re done here on the client side of things, and while it feels like a lot of work (and is) the fun part of working with the Core Service has not yet begun.

Create the Server with Tridion API References using ServiceStack and Tridion API

I am always impressed how easy and quick it is to work with ServiceStack. I host it within an MVC 3 application, so if you decide to follow this approach you’ll need to install MVC3 on your server. Using the WebPI installer from Microsoft it is fairly painless and 100% automated.

Create ServiceStack project

The ServiceStack Web Service is created to call the Tridion API via the Core Service. You can run the Web App in Debug mode on your client development machine and step through the Core API calls. This is a very fast dev cycle when using the Tridion API and takes out the step of copying files to the server. Make sure your URL references / login info in the web.config is correct and you’re all set.

1. Create an empty MVC 3 Web App

2. Using NuGet, add a reference to ServiceStack MVC app. It downloads a lot of references.  See my ServiceStack post for more info on getting started.

3. Create a Services and Repositories folder.

4. Create a new class in the Model folder and give it string properties for whatever you want to pass back. Note, ServiceStack will camelCase all your variables for you in the JSON response.

5. Create a new class in the Services folder. Implement the RestBaseService<Type> method (see the Todo example). Have 1 method for each response type – if not using then return null.

6. Create a new class in the Repositories folder. Implement the Get method, Store, etc. I copy the ToDo repository and remove methods not used.

7. Add the URI / Model class to the App_Start/AppHost.cs file. For example,

Routes.Add<TridionItem>("/tridionItem");

8. Register the repository in the same file:

container.Register(new TridionItemRepository());

9. Update Global.asax to have /api/ ignored by MVC and avoid annoying favicon errors.

routes.IgnoreRoute("api/{*pathInfo}");
routes.IgnoreRoute("favicon.ico");

Add Tridion code to ServiceStack GetByUri Repository method:

TridionItemRepository.cs Code to get Last Modified By (Revisor) of item:

  public class TridionItemRepository
    {
        public TridionItem GetByUri(string uri)
        {
            TridionItem tridionItem = new TridionItem();
            try
            {
                CoreServiceClient client = new CoreServiceClient();
                client.ClientCredentials.Windows.ClientCredential.UserName = ConfigurationManager.AppSettings["impersonationUser"].ToString(); // "administrator";
                client.ClientCredentials.Windows.ClientCredential.Password = ConfigurationManager.AppSettings["impersonationPassword"].ToString();
                client.ClientCredentials.Windows.ClientCredential.Domain = ConfigurationManager.AppSettings["impersonationDomain"].ToString();
                IdentifiableObjectData objectData = client.Read(uri, null) as IdentifiableObjectData;
                FullVersionInfo versionInfo = objectData.VersionInfo as FullVersionInfo;
                tridionItem.Title = objectData.Title;
                tridionItem.Uri = uri;
                tridionItem.LastModifiedBy = versionInfo.Revisor.Title;
            }
            catch (Exception ex)
            {
                tridionItem.Error = ex.Source + ", " + ex.Message + ", " + ex.ToString();
            }
            return tridionItem;
        }
    }

Steps to implement:

Core Service references. This info is also in the Tridion Live Documentation.

1. Make a reference in Visual Studio to the Tridion.ContentManager.CoreService.Client dll located on the Tridion server in the bin/client folder.

2. Make a web service reference to your server:

http://TridionDev2011/webservices/CoreService.svc

3. Update the config of the web service reference in web.config with the config in the Tridion Live documentation.

Create an instance of the core service, making sure to add user info. Thanks to Andrey’s blog post for the impersonation info.

CoreServiceClient client = new CoreServiceClient();
client.ClientCredentials.Windows.ClientCredential.UserName = ConfigurationManager.AppSettings["impersonationUser"].ToString(); // "administrator";
client.ClientCredentials.Windows.ClientCredential.Password = ConfigurationManager.AppSettings["impersonationPassword"].ToString();
client.ClientCredentials.Windows.ClientCredential.Domain = ConfigurationManager.AppSettings["impersonationDomain"].ToString();

Get a Tridion Object

 

TryCheckOut and Read are the new GetObject. Why Tridion R&D decided to take a well known method name like GetObject and replace it with Read and TryCheckOut was a big surprise to me and cost time to figure out where it went. I was lucky enough to find an example from Ryan Durkin on Uploading Images using the Core Service.

IdentifiableObjectData objectData = client.Read(uri, null) as IdentifiableObjectData;

Version Info

VersionInfo comes in 2 flavors, FullVersionInfo and BasicVersionInfo. At first I tried with BasicInfo and it did not contain the Revisor property.  It was in the FullVersionInfo  and I discovered this while debugging using the Visual Studio debugger and locals window. Also, a good time to point out that I wrote and debugged all the Core Service code on my desktop, not on the server. Very nice feature of working with the Core Service!

Model Class containing properties

namespace Tridion2011ServiceStack.Models
{
    public class TridionItem
    {
        public string Title { get; set; }
        public string Uri { get; set; }
        public string LastModifiedBy { get; set; }
        public string Error { get; set; }
    }
}

ServiceStack works with POCOs (Plan ‘Ol CLR Objects) to define DTOs (Data Transfer Objects) that contain the properties to serialize for the JavaScript client.  Populate the DTO with Tridion info and return it.  ServiceStack does the serialization behind the scenes and we don’t have to worry about it. Note, these names will be changed to camelCase and therefore look like this in the client:

        • title
        • uri
        • lastModifiedBy
tridionItem.Title = componentData.Title;
tridionItem.Uri = uri;
tridionItem.LastModifiedBy = versionInfo.Revisor.Title;

Test

The Web Service can be tested using the HTML page in the Visual Studio Solution named GetTridionItem.htm.  Use the FireBug debugger and the network tab to see the response sent back from the Web Service.  Make sure to change the URL of the web service to the yours.  The URL should be the local URL from your debugging session – something like this http://localhost:61860/api/tridionItem .

Testing the GUI Extension client is best done with a js alert.  Once this is working then you should test calling the Web Service.  Again, make sure the URL to the web service is correct.  Use firebug to test this.  If you do more extensive coding in your _execute method then see my tips for debugging the GUI Extension js.

Check the web.config and make sure your Tridion User Info is there as well as the URL pointing to your CMS.

If you are running locally to test your Web Service Tridion API calls, change the URL in the TridionGetItem.htm page to something like:

url: "http://localhost:61860/api/tridionItem"

Set the breakpoint in the Repository class or even the js of your htm page and have fun debugging!

Summary

It is a real good feeling becoming familiar with the GUI extension framework and having so much potential at our fingertips. Once everything is setup and configured it is much easier to add new features and iterate on a working example. I hope I have helped demystify the elements in a GUI extension and given you a good place to start working with it. This article could not have been possible without all the GUI Extension and Core Service examples already created in the community as well as the official Tridion documentation. I hope we can continue improving the architecture of the config file and setup to make it even easier for developers to get started with writing extensions. Please give feedback. Thanks!

Download

Get the source code on GitHub.

While entering into the world of GUI Extension programming I have found it is another world where we need new tools and approaches to supplement our classic web development view.  For me it was an entirely different way of working, something I am still getting used to.  Today I discovered 2 things that I hope will help you get more comfortable in the new Tridion 2011 GUI Extension world.

Debugging JavaScript in Tridion 2011 GUI
Debugging has always been an art and the art is elevated with Tridion GUI Extensions, where everything starts with JavaScript.  I prefer to use Chrome for Tridion 2011 – it has the fastest JavaScript engine at the moment and this gives the fastest Tridion performance.  The developer tools included in Chrome are very good and using them is the key to debugging your new GUI Extension JavaScript.  I am using Tridion 2011 SP1.

1.  Open the Tridion 2011 GUI in Google Chrome browser
2.  Open Developer Tools (F12)
3.  Scripts tab
4.  In the file listbox, choose ‘Dashboard_v6.1.0.55920.10_aspx?mode=js’ located in WebUI/Editors/CME/Views/Dashboard/
5.  Use the search box (top-right of Dev Tools) to find some text from your _execute method in the js GUI Extension code.  For me it starts at around line 85702.
6.  Set a breakpoint by right-clicking the line # and add breakpoint
7.  Refresh the GUI, hit the button for your extension, and the breakpoint will be highlighted in the bottom debug window.
8.  On the right side of the debug window are the Watch Expressions, Call Stack, Scope Variables, etc.  I prefer to minimize Call Stack and expand Scope Variables.
9.  Step through the code by using the down-arrow on the right side.

Debugging Breaking Javascript Errors
Let’s say you forget to end a line with a ;.  That is bad js and will not compile.  If your JavaScript has an error then the Tridion GUI will be blank.  See below.  Tridion tries to load the js in your GUI Extension when it loads the browser, and as we can see from above it ends up in the main JavaScript of the GUI itself.  If you have breaking code (or even just write ‘something’ text in your js) it will break the GUI.  Good JavaScript programming practices is a must here!

Tridion GUI Extension js error

Solution
Have no fear, the Chrome debugger is here! Go to Console, see the red X with the error, click on the link on the right side with the Dashboard filename.  Your breaking error is shown.

Getting a popup URL
OK – now for a programming tip.  Once you get into the _execute statement then you might want to show a popup to get input from the user before performing actions.  This example uses the excellent HellowWorldCM tutorial from Jaime.  Here is 1 way to show a popup:

Extensions.HW.prototype._execute = function HW$_execute(selection, pipeline) {
var host = window.location.protocol + "//" + window.location.host;
var url = host + '/WebUI/Editors/HelloWorldCM/client/html/popup.htm#popup=UID_355';
var popup = $popup.create(url, "toolbar=no,width=600px,height=300px,resizable=false,scrollbars=false", null);
popup.open();

Also, if you are using Jaime’s example, comment out the message center code because in Tridion 2011 SP1 there are changes in the message center that are not compatible with this code.

//        var msg = $messages.createMessage("Tridion.Cme.Model.InfoMessage", "HELLO WORLD EXTENSIONS", ...
...
//        $messages.registerMessage(msg);

Setting up the IIS Virtual Directory
If you have any issues with the IIS Virtual Directory you will see the Tridion Minimalist view of the GUI.  This is the new cool thing – not very functional, but that is not the point.  😉

Update:  You will also see this view if any file paths in the  <cfg:fileset> of your GUI Extension cannot be found!

Tridion GUI Extension IIS VDIR Error

Likely the IIS Virtual Directory is not there or the name of the Virtual Directory does not match the name of the VDIR paramatere in the Extension config inside the system.config file.  It looks like:

<vdir>HelloWorldCM</vdir>

.  Get this in sync and you can again enjoy the full splendor of Tridion 2011.

Happy debugging!

Tridion provides excellent versioning on many items, including Components, Pages, Keywords and Templates. This weeks bUtil is bRollback, giving the ability to rollback an item and its’ Blueprint children to the previous version. This is very handy when you update Components via an external script and then realize there was a mistake and want to rollback that Component. The script operates on a Component, but can easily be modified to work on a Folder. Also, sometimes the editor updates 1 item and all of its’ localized copies but then needs to rollback 1 version because the business has changed their minds or the deadline is postponed. This might also happen with translations if there was a mistake in the original source content going out and only notice it later and need to rollback 1 version.

The Tridion GUI allows us to rollback versioned items using the context menu, select Versioning, History and all the previous versions of the item appear. When we rollback to a previous version and have 2 options – first to select to make a copy of the previous version and save it as the new version (safest) or to go back to that version and remove all versions after it (losing all changes after the rollback-to version). We can also compare our current version to a previous version from this screen.  This takes about 2 minutes per item – with 30 localized items it would take around 1 hour to rollback all of them to the previous version.

The Tridion API provides an easy way for us to access the versions of an items and to rollback to a previous version. I created an ASP Custom Page to do just this – rollback an item, and all of its’ localized items, to the previsou version, or in other words, current version – 1. There is also a ModifiedDate checkbox that will only show the last modified date and not rollback anytrhing. My assumption is that most of the time you use this tool the items are updated via a script or an author AND around the same time. I would not want to run the rollback tool if some items were updated much more recently than what I expected.

The script itself is a take on my 2 previous scripts, BCopy and BDelete. These are Blueprint-aware Custom Pages where the action starts on the Blueprint parent and also takes effect on the Blueprint child. Currently the only action in the GUI that provides this type of functionality is the Publish action, where we can choose to publish child items.

The Code to rollback an item in Tridion 2009 and before, with TOM API is:

Item.Rollback(toVersion, deleteVersions)

toVersion – Version# to rollback to
deleteVersion – Create copy of old version (safe) or Go back to old version and delete newer versions (unsafe))

The script uses the safe option creates a new version.

The Tridion 2011+ Core Service code is:

client.Rollback(uri, false, “rollback from script”, new ReadOptions());

 

The last modified date is found with:
Item.Info.RevisionDate

View the source on GitHub at https://github.com/rcurlette/TridionBRollback

The Blueprinting feature of Tridion is really great and keeps all of our items ‘connected’ to the parent – and won’t let us delete an item if it has localized children. This is a problem when we want to clean up our system and remove old items – or if you’ve been using bCopy and accidentally copied an item you didn’t want to.   

BDelete is the inverse of BCopy – UnLocalizing all children and then after this deleting the parent.  This script does this for Components, Pages, Structure Groups, or Folders.

Scenarios – When you need to do pre-cleanup work

Delete Page:  Pages need to be unpublished first.  The tool does not unpublish pages.
Delete Component:  Components need to be removed from Pages, removed from Component Link fields in other Components, and unpublished.  The tool does not do this and it will fail.
Delete Folder:  Will work every time, no dependencies.
Delete Structure Group:  Will work every time, no dependencies.

Code

Code is on GitHub at https://github.com/rcurlette/TridionBDelete/blob/master/bDelete.asp

Tridion provides great Blueprint functionality allowing us to share and localize content across many locales and Publications.  Sometimes we want to make a copy of an item – the whole item with all of its’ Blueprint-children, and then maybe change some metadata with the new item and nothing else. However, the copy button in Tridion only copies the current item –  and not its’ associated Blueprint-children.

Tridion BCopy – When A copy is not enough

Tridion BCopy copies the item (Component, Page, Structure Group, or Folder) and all of its’ Blueprint-children, including associated metadata, etc.  When copying a Folder or Structure Group it does not copy the contents of it – only the item itself.

Solution

Copy item with Blueprint children
The code is written in classic ASP – yes, I hear you moaning or cheering, but rest assured this approach works on all versions of Tridion up to Tridion 2011 and is quick to deploy and start using right away.  I tested the code on Tridion version 5.3 and it works great.

Getting Localized Items

The most interesting part of the code is getting the localized items.  This approach uses the GetListUsingItems and a special RowFilter condition.  You might want to use the same code to process localized items for updates, etc, and I have used it many times.

Function GetLocalizedItemNodes(itemUri)
    Dim tridionItem : set tridionItem = tdse.GetObject(itemUri,1)
    Dim rowFilter : set rowFilter = tdse.CreateListRowFilter()
    call rowFilter.SetCondition("ItemType", GetItemType(itemUri))
    call rowFilter.SetCondition("InclLocalCopies", true)
    Dim usingItemsXml : usingItemsXml = tridionItem.Info.GetListUsingItems(1919, rowFilter)
    Dim domDoc : set domDoc = GetNewDOMDocument()
    domDoc.LoadXml(usingItemsXml)
    Dim nodeList : set nodeList = domDoc.SelectNodes("/tcm:ListUsingItems/tcm:Item[@CommentToken='LocalCopy']")
    set tridionItem = nothing
    set domDoc = nothing
    set GetLocalizedItemNodes = nodeList
End Function

Updating content

We use the GetXML method to get the contents of the item.

GetLocalizedXml = localizedItem.GetXml(1919)

When updating, we use the UpdateXml method of an item to save the original contents to the copy. This is the best and fastest way to update Tridion content.

newItem.UpdateXml(xml)

Layout

I use Twitter Bootstrap version 1.0 for all custom pages.  If you have not checked this out – you need to.  No need to write your own CSS for custom pages anymore!  Use the Bootstrap HTML / CSS and have nice looking custom pages in no time.

Installing

Get the code
View the full code on GitHub at TridionBCopy or download the zip.

Upload to server
Copy to the Tridion/Web/CustomPages folder on your server or wherever you put your asp custom pages.

Use it
Create a link to it in the Custom Pages tree menu and save time with it!

Tridion 2011 provides some obvious benefits and many hidden benefits for upgrading from 2009 to 2011.  Overall it feels faster and for a very good reason-  Tridion R&D has upgraded every architecture piece in the puzzle – taking us from 2008 to 2011 in 1 upgrade. For example, we now have .NET 4.0, WCF, OData, NHibernate, Logback, Java 6, and Razor.

1.  World Server connector – If your organization uses SDL World Server for translations then the new SDL Tridion connector is for you.  Simply send the item for translation and it gets translated and automatically updated back in Tridion, triggering a possible workflow, and removing 90% of the effort of translations.  It works similar to previous SDL connectors such as SVN or Perforce.

2.  Faster and Cross-platform GUI – Better tree controls, faster GUI, and a thousand other small GUI improvements that make using Tridion 2011 a much better experience.  Use Chrome, Firefox, Safari, or IE to access the GUI.  The faster the js engine, the faster Tridion will respond.  Also supports Mac!

3.  GUI Extensions – Easier to extend and customize the GUI to improve the Editors  use of Tridion.  Create new tabs on a Component Edit screen, add a new column to the main list view, add a button to the toolbar or even a whole new tab in the toolbar.  Anything is possible in the new Anguilla GUI framework – and it is all supported by SDL Tridion.  Many blog posts are written about creating a GUI extension.

4.  .NET 4.0 – TOM.NET is Read/Write from native .NET.  This means our code is faster, but more importantly, we can use language features from .NET 4.0 to simplify our code and make it more maintainable

5.  Razor Template Mediator – Not available in 2009 because it uses Microsoft’s Razor engine, only available with .NET 4.0. Download the Razor Mediator and experience the power of .NET with the simplicity of VBScript.  A major advantage when upgrading VBScript templates.

6.  Re-invented Event System – The Event System is rewritten from ground up.  Now it is possible to fire events asynchronously, during 5 phases (instead of 2), and be written in .NET 4.0.  Overall a huge improvement.  See my article about creating a first 2011 Event System.

7.  Solr Search Engine – Verity is out and Solr is in. Solr is an open-source, http-based, faceted search engine from the Apache project that is much faster and handles large indexes much better. XML.com has an intro as well as IBM having a tutorial.

8.  New Business Connector (aka Core Service) – A WCF 3.5 Web Service called the Core Service can be used to access Tridion objects from any SOAP client.  An excellent article by Bart Koopman provides some insight into the new architecture.

9.  OData Web Service – Content delivery gets a new OData web service that can be used by external data consumers for querying the Broker database.  Ryan Durkin has a nice post walking through creating an OData client.

10.  Better multimedia publishing – Choose to store multimedia files in different storage places.  Julian has a great article explaining how to do this.

Do you have another benefit for upgrading?