Link Attributes Extension: Template Building Block

Earlier we talked about customizing an existing default popup in Tridion, and the post gave birth to the start of our Link Attributes GUI Extension.  As a recap, we were creating an extension that would allow us to add custom link attributes onto our hyperlinks added by the Hyperlink button.  We decided that the GUI Extension would add hash parameters to the link’s href attribute, and that’s pretty much where the post left off.  Today we will be adding a Template Building Block that will be responsible for stripping out the hash parameters that were added by the GUI Extension and converting them to attributes on the link element itself.

In another post we mentioned using HtmlAgilityPack in our Template Building Blocks, so although the regex for this task would be fairly simple, I’ll be using that library in our TBB.  If you are going to be using this extension and this TBB yourself, you’ll have to add the HtmlAgilityPack DLL to the GAC on your Tridion servers as well as reference it in your TBB project.

Next you will want to create a new class in your TBB project and add the following code:

using System;
using System;
using HtmlAgilityPack;
using Tridion.ContentManager.Templating;
using Tridion.ContentManager.Templating.Assembly;

namespace ContentBlooom.TemplateBuildingBlocks
{
    /// <summary>
    /// This TBB is responsible for converting the linkAttr- hash params created by the GUI Extension  into actual
    /// attributes on the link element that they've been placed.
    /// </summary>
    [TcmTemplateTitle("Link Attributes Converter")]
    public class LinkAttributesConverter : ITemplate
    {
        public void Transform(Engine engine, Package package)
        {
            bool outputModified = false;
            Item outputItem = package.GetByName(Package.OutputName);
            string outputString = outputItem.GetAsString();

            HtmlDocument doc = new HtmlDocument();
            doc.LoadHtml(outputString);
            doc.OptionOutputOriginalCase = true;

            var linksWithAttributes = doc.DocumentNode.SelectNodes("//a[contains(@href, 'linkAttr-')]");
            if (linksWithAttributes == null)
            {
                return;
            }
            
            foreach (var link in linksWithAttributes)
            {
                string url = link.Attributes["href"].Value;
                string hashString = url.Substring(url.IndexOf("#") + 1);
                string[] hashParams = hashString.Replace("&amp;", "&").Split('&');
                
                bool hasLinkAttributes = false;

                url = url.Substring(0, url.IndexOf("#"));
                hashString = String.Empty;

                foreach (string hashParam in hashParams)
                {
                    if (hashParam.StartsWith("linkAttr-"))
                    {
                        // If its a link attribute, add it as an attribute and then remove it from the hash string
                        hasLinkAttributes = true;
                        outputModified = true;

                        string[] rule = hashParam.Split('=');
                        string attributeKey = rule[0];
                        string attributeValue = rule[1];

                        link.Attributes.Add(attributeKey.Replace("linkAttr-", String.Empty), attributeValue);
                        url = url.Replace(hashParam, String.Empty);
                    }
                    else
                    {
                        // Keep any existing hash info there...
                        hashString += hashString.Length == 0 ? "#" : "&amp;";
                        hashString += hashParam;
                    }
                }
                if (hasLinkAttributes)
                {
                    link.Attributes["href"].Value = url + hashString;
                }
            }

            if (outputModified)
            {
                package.Remove(outputItem);
                outputItem.SetAsString(doc.DocumentNode.OuterHtml);
                package.PushItem(Package.OutputName, outputItem);
            }
        }
    }
}

Simple, no? You’ll want to put this TBB directly after your Building Block that provides the output (your DWT, Razor… whatever). Or even possibly as the first item in your Default Finish Actions. Make sure to create a component that has some links created with our modified Link popup from the earlier article, and run and execute.

Output prior to Link Attribute Converter:

<article>
    <p>Testing link attributes and what not and other stuff.</p>  
    <p>Should include a <a href="tcm:14-103941#linkAttr-newLink=test" title="a title">component link</a> as well as a normal <a href="http://www.example.com#linkAttr-custom=check" title="blah">http type of link</a>.</p>
    <p>We also need to see how it plays with <a href="http://www.test.com#blah=meh&amp;linkAttr-t=v">links with hashes</a> already.</p>
    <p><a href="mailto:e@mail.com#a=b&amp;c=d&amp;linkAttr-mailed=them" title="title">Multi hashed Link</a></p>
 </article>

And our output after our TBB:

 <article>
     <p>Testing link attributes and what not and other stuff.</p>  
     <p>Should include a <a href="tcm:14-103941" title="a title" newlink="test">component link</a> as well as a normal <a href="http://www.example.com" title="blah" custom="check">http type of link</a>.</p>
     <p>We also need to see how it plays with <a href="http://www.test.com#blah=meh" t="v">links with hashes</a> already.</p>
     <p><a href="mailto:e@mail.com#a=b&amp;c=d" title="title" mailed="them">Multi hashed Link</a></p>
 </article>

Not Quite Done

Our extension is not quite done just yet. Stay tuned once more as we add some enhancements and fixes to our little extension, such as the ability to add more than just one link attribute!

Searching and Modifying Output In Tridion Template Building Blocks With HtmlAgilityPack

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

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

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

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

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

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

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

			return _logger;
		}
	}

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

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

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

Grabbing Every Anchor

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

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

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

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

Just like with XmlDocument, you can also use XPath:

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

Querying for Specific Elements

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

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

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

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

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

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

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

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

Modifying Output

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

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

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

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

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

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

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

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

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

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

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!

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.

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

Inherited Page Workflow Process Settings On Structure Groups

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

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

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

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

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

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

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

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

    ProcessDefinition pageProcess = sg.PageProcess;

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

    SetChildrenStructureGroups(sg, filter, pageProcess);
}

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

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

        SetChildrenStructureGroups(structureGroup, sgFilter, pageProcess);
    }
}

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

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

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

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

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

            ProcessDefinition pageProcess = sg.PageProcess;

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

            SetChildrenStructureGroups(sg, filter, pageProcess);
        }

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

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

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

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

Razor Mediator 4 Tridion Version 1.2 Released

Hi readers! My apologies for the lack of blog writing over the past month, but things have been extremely busy on the development front. After much feedback and help with testing from colleagues, I am pleased to announce the release of the Razor Mediator Version 1.2. I put it up for submission to SDL Tridion World yesterday, but if you can’t wait, you can also grab it at its Google Code Project Site. There are a lot of fixes, updates, and new features added to this version, so you can probably expect some more posts coming soon with even more examples and tutorials than what is in the new updated documentation. So, what are some of these new updates and features you ask?

Ability to Import!

That’s right, you heard correctly. You can now import other Razor Template Building Blocks or even external Razor Templates stored in text files on the CMS Server to your templates. You can do this globally via the configuration to allow you to import templates to all of your Razor Templates (or even to all of your Razor Templates in one publication), or at the Template level via the @importRazor(“PathToYourTemplate”) command.

Updated GetComponentPresentationsByTemplate and GetComponentPresentationsBySchema

These methods have been updated to accept multiple parameters. So, that means you can now do something like:

@foreach (var cp in GetComponentPresentationsByTemplate("Template One Name", "Template Two Name", "Template Three Name")) {

}

RenderComponentPresentations and RenderComponentPresentationsByTemplate

Two new utility methods have been added to the base template that allows you to just render templates quickly. RenderComponentPresentations() will render all ComponentPresentations on the Page, while RenderComponentPresentationsByTemplate(param string[] templateNames) will only render the ComponentPresentations given the passed template names.

<div id="allTemplates">
    @RenderComponentPresentations()
</div>
<div id="someTemplates">
    @RenderComponentPresentationsByTemplate("Template One Name", "Template Two Name")
</div>

Better Compile Error Message

When you receive a compile error upon saving your Razor Template, you will now be shown a more informative message that also displays the line of code in question. Remember though, that the line of code that is shown is the generated C# code, and not your actual Razor code. This means that “<span>@blasadsfdsaf</span>” would show the line as being “Write(blasadsfdsaf);”. Also, the error displayed to the user in Tridion will no longer include Warning messages.

Index, IsFirst, and IsLast Properties

To further help you write cleaner code, the properties “Index”, “IsFirst” and “IsLast” has been added to the ComponentPresentationModel, ComponentModel, KeywordModel, and DynamicItemFields classes. For ComponentPresentationModels, these properties are automatically set when accessing the ComponentPresentations via the ComponentPresentations property of the base template, or by either of the GetComponentPresentationsByTemplate() or GetComponentPresentationsBySchema() methods.

@foreach (var cp in ComponentPresentations) {
    @if (cp.IsFirst) {
        <div>@cp.Component.Title is the first item.
    } else if (cp.IsLast) {
        <div>@cp.Component.Title is the last item.
    }
}

For ComponentModel and KeywordModel, these properties are automatically set when accessing them via the DynamicItemFields (that is, when they are set as a multi-valued field of course).

@foreach (var kw in Component.Fields.SomeKeywords) {
    <div class="@(kw.Index % 2 == 0 ? "alt1" : "alt2")">@kw.Title</div>
}
@foreach (var comp in Fields.SomeComponents) {
    @if (comp.IsLast) {
        <span>We only wanted the last ComponentLink item!</span>
    }
}

These properties are automatically set for DynamicItemFields when it is accessed via the DynamicItemFields as a multi-valued EmbeddedSchemaField.

@foreach (var embeddedFields in Fields.SomeEmbeddedFields) {
    <div class="@(embeddedFields.IsLast ? "last" : String.Empty)">@embeddedFields.Address (@embeddedFields.ZipCode)</div>
}

Quick Access to Debug Writing

Pre version 1.2, the Razor Mediator documentation said that you could write logging statements like @Log.Debug(“Your Message”). This was actually incorrect and would have thrown an error… you would of had to write them as @{ Log.Debug(“Your Message”); }. As of version 1.2, you can now do @Debug(“Your Debug”), @Info(“Your Info”), @Warning(“Your Warning”), and @Error(“Your Error”).

Of Fixes and More

There are a couple other minor goodies that have been added, as well as some critical fixes and updates that include caching and thread safety (for publishing) fixes. If you are already using a previous version of the Razor Mediator, I would definitely recommend updating to version 1.2. You can view the Change Log on the Google Code project for a full list of all the updates and fixes made in this release.

Version 1.3?

Yes, a Version 1.3 is now in the works, with even more helpers/utilities to make your templating life a bit less difficult, and some more features to help empower your abilities. And of course any more bugs or issues that you report to me. Much thanks to you for your feedback!

Tridion PublishEngine – Of Transactions and Publish Information

Today I thought I would give some examples of querying publish transactions (publishing queue) as well as getting publish information from specific items, like seeing which Publication Targets a page has been published to and when, or just seeing if an item has been published in general. This post is inspired by a recent question asked in the forums, but I have noticed it come up from time to time. Luckily this task is easy using the TOM.NET API and the static PublishEngine class.

IsPublished

One task you might need to attempt in your Tridion development day to day activities is to check to see if a given item has been published, or published to a particular Publication Target.  PublishEngine contains the following overloaded methods.

bool IsPublished(IdentifiableObject item)
bool IsPublished(IdentifiableObject item, PublicationTarget publicationTarget)
bool IsPublished(IdentifiableObject item, PublicationTarget publicationTarget, bool isPublishedInContext)

The first IsPublished method is useful if you just want to see if an item is published, regardless of which Publication Target it has been published to.

if (PublishEngine.IsPublished(page))
{
    // This page has been published. Do cool stuff here.
}

The second IsPublished method will return true only if the item has been published to the Publication Target passed in the 2nd argument. If the 2nd argument is null, then it’ll act just like the first method and return true if the item has been published to any Publication Target.

if (PublishEngine.IsPublished(page, pubTarget))
{
    // This page has been published to a specific publication target. Do cool stuff here.
}

The third method allows for even finer control. I haven’t personally played with this one as of yet, but the documentation for the isPublishedInContext argument states “Indicates if state should be returned regardless of the context Publication. true only check if item is published in the context Publication; otherwise, false.” I’m assuming this means that, if this argument is set to true, the method will only pass if the item is published to a particular Publication Target, and only if the item is published in it’s own context Publication.

GetPublishInfo

What if you need to get more information about an item? Like, what if you not only wanted to see what Publication Targets it was published to, but at what time too? That’s where the following method comes in hand.

foreach (PublishInfo info in PublishEngine.GetPublishInfo(page))
{
    Console.WriteLine("Published To: " + info.PublicationTarget.Title);
    Console.WriteLine("Published At: " + info.PublishedAt); // The time the page was published
    Console.WriteLine("Published By: " + info.PublishedBy.Title); // The user who published
    Console.WriteLine("Rendered With: " + info.RenderedWith.Title); // The title of the template used
}

The above will loop through each Publication Target that the item has been published to and give you some useful information about the publishing.

GetPublishTransactions

And what if you actually need to check the publishing queue to see if an item has recently been published in the past hour? The PublishEngine allows you to also query the publish transactions.

XmlElement GetListPublishTransactions(PublishTransactionsFilter filter)
IEnumerable<PublishTransaction> GetPublishTransactions(PublishTransactionsFilter filter)

Note that the above methods will throw an AccessDeniedException if the user is not a System Administrator or have PublishManagement rights in any publication. That means if your code relies on always needing to be able to check the transactions, regardless of rights, you’ll need to impersonate.

Session session = new Session("DOMAIN\username"); // Impersonate System Admin or at minimum user with PublishManagement rights.
 
PublishTransactionsFilter filter = new PublishTransactionsFilter(session);
filter.StartDate = DateTime.Now.AddHours(-1); // Add some criteria for when the publishing was started.
filter.PublishTransactionState = PublishTransactionState.Success;

IEnumerable<PublishTransaction> transactions = PublishEngine.GetPublishTransactions(filter);

foreach (PublishTransaction transaction in transactions)
{
    // Check cool stuff with the transaction, like the transaction.Items property.
}

The above will loop through all the successful transactions that has happened in the past hour. Notice the PublishTransactionState property… it allows you to only filter based on one state. But what if you need to grab anything that’s not Success or Failed perhaps, or any other combination of states? You’ll have to grab the items and filter programatically.

PublishTransactionsFilter filter = new PublishTransactionsFilter(session);
filter.StateDate = DateTime.Now.AddHours(-1);

IEnumerable<PublishTransaction> transactions = PublishEngine.GetPublishTransactions(filter)
    .Where(t => t.State != PublishTransactionState.Success && t.State != PublishTransactionState.Failed);

foreach (PublishTransaction transaction in transactions)
{
    // Do cool stuff with transactions that are not Success or Failed
}

And with that I leave. Happy developing everyone!

Tridion Event System: Automated Page Creation After Creating A Component

It’s been a great week that I had visiting upstate New York and parading my son around for all the family to see. With that said, I think its due time for another post. Today I will be following up with yet another Tridion Event System tutorial, again using the power of Tridion’s API to add a bit more automation to your environment.  We’ll be automatically creating Pages for our newly created Article components.  As Alvin mentioned in my other post, when you are adding functionality that changes the behavior of Tridion (such as moving your content author’s content for them or creating pages for them), make sure that they are fully aware of these customizations, else they will be in for a world of confusion.  The reverse is also nice… if there’s a new member that comes in, let them know that these are customizations and not out of the box features.  I’ve been on projects where one of the authors had prior Tridion experience and swore a special feature existed, yet not knowing that the feature was custom developed for their project.  If you haven’t done so already, you should also first read the other Event System tutorial.  After today’s tutorial, you should be a little more comfortable with Tridion 2011′s Event System and working with the TOM.NET API to create new Pages, create Structure Groups, and perform a Where Using search.

Although we could easily combine this new functionality to the code we created before, we’ll go ahead and set up a new class with the following using statements and unique ID for the extension.

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

namespace Tridion.Samples
{
    [TcmExtension("AutomatedPageEventHandlerExtension")]
    public class AutomatedPageEventHandler : TcmExtension
    {

    }
}

We will only want to create new Pages for our Articles that have not yet been added to a page. For this, we’ll perform a Where Using check to see if our component is being used on a page or not.

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

    return component.GetUsingItems(filter).Count() == 0;
}

We’ll want to use the Component’s title to generate the title for the page. But what about the filename? Lets go ahead and add a quick slugging algorithm to just use the title and make it filename friendly (thanks to John Roland at http://predicatet.blogspot.com/2009/04/improved-c-slug-generator-or-how-to.html for posting this script).

private string CreateSlugTitle(string title)
{
    byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(title);
    title = System.Text.Encoding.ASCII.GetString(bytes).ToLower();

    title = Regex.Replace(title, @"[^a-z0-9\s-]", ""); // invalid chars
    title = Regex.Replace(title, @"\s+", " ")
        .Trim() // convert multiple spaces into one space
        .Substring(0, title.Length <= 45 ? title.Length : 45)
        .Trim(); // cut and trim it
    title = Regex.Replace(title, @"\s", "-"); // hyphens   

    return title;
}

Just like we auto-organized our Components in the previous Event System tutorial, lets write a couple of methods that will do the same for our generated pages. Remember, Titles and Directories for StructureGroup’s are required fields.

private StructureGroup GetStructureGroup(string title, string directory, StructureGroup parentSG, OrganizationalItemItemsFilter filter)
{
    StructureGroup sg = parentSG
        .GetItems(filter)
        .Where(f => f.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase))
        .FirstOrDefault() as StructureGroup;

    if (sg == null)
    {
        sg = CreateStructureGroup(title, directory, parentSG);
    }

    return sg;
}

private StructureGroup CreateStructureGroup(string title, string directory, StructureGroup parentSG)
{
    StructureGroup newSG = parentSG.GetNewObject<StructureGroup>();
    newSG.Title = title;
    newSG.Directory = directory;
    newSG.Save();

    return newSG;
}

Next is the bread and butter where we put it all together. We’ll use a WebDav URL to open up items that we need from Tridion (and as mentioned before, I would recommend an extra step and keeping these in some configuration file). Pay attention to how we create the Page using the TOM.NET API as well as add ComponentPresentations to it. One gotcha that you might run into is (real easy to forget), if you had tried adding the Component that is passed via the Event Handler argument, you’ll get an error. This is because the Page is expecting the Component to exist in the same Publication as it. That’s why we open the component in the Page’s publication, and then add that newly opened component to the Page.

private void CreatePage(Component component)
{
    Session session = component.Session;

    ItemFields metadata = new ItemFields(component.Metadata, component.MetadataSchema);
    DateTime articleDate = ((DateField)metadata["ArticleDate"]).Value;

    string year = articleDate.Year.ToString();
    string month = articleDate.ToString("MM MMMM");
    string monthDirectory = articleDate.ToString("MM");

    StructureGroup baseStructureGroup = session.GetObject("/webdav/040 Website Master/Root/000 Articles") as StructureGroup;
    if (baseStructureGroup == null)
        throw new Exception("CreatePage - Unable to get baseStructureGroup");

    ComponentTemplate componentTemplate = session.GetObject("/webdav/040 Website Master/Building Blocks/System/Templates/Component Templates/Article CT.tctcmp") as ComponentTemplate;
    if (componentTemplate == null)
        throw new Exception("CreatePage - Unable to get componentTemplate");

    PageTemplate pageTemplate = session.GetObject("/webdav/040 Website Master/Building Blocks/System/Templates/Page Templates/Article PT.tptcmp") as PageTemplate;
    if (pageTemplate == null)
        throw new Exception("CreatePage - Unable to get pageTemplate");

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

    StructureGroup yearSG = GetStructureGroup(year, year, baseStructureGroup, filter);
    StructureGroup monthSG = GetStructureGroup(month, monthDirectory, yearSG, filter);

    Page newPage = monthSG.GetNewObject<Page>();
    newPage.PageTemplate = pageTemplate;
    newPage.Title = component.Title;
    newPage.FileName = CreateSlugTitle(component.Title);

    Component localComponent = session.GetObject(new TcmUri(component.Id.ItemId, ItemType.Component, baseStructureGroup.Id.PublicationId)) as Component;
    ComponentPresentation cp = new ComponentPresentation(localComponent, componentTemplate);
    newPage.ComponentPresentations.Add(cp);

    newPage.Save(true);
}

Finally we add our Constructor where we will subscribe our event and the handler method itself. In the handler method, we’ll make sure that only our Articles will trigger the Page Generation, and only Articles that don’t already exist on a page.

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

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

Your class should now look like the following.

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

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

        /// <summary>
        /// On Component Checked In Transaction Committed events.
        /// </summary>
        /// <param name="component">The component checked in.</param>
        /// <param name="args">The CheckInEventArgs instance.</param>
        /// <param name="phase">The EventPhase enum.</param>
        private void OnComponentCheckedInPost(Component component, CheckInEventArgs args, EventPhases phase)
        {
            if (component.Schema.Title.Equals("Article") && DoesNotHavePage(component))
            {
                CreatePage(component);
            }
        }

        /// <summary>
        /// Creates a page and adds the component to it.
        /// </summary>
        /// <param name="component">The component to create the page for.</param>
        private void CreatePage(Component component)
        {
            Session session = component.Session;

            ItemFields metadata = new ItemFields(component.Metadata, component.MetadataSchema);
            DateTime articleDate = ((DateField)metadata["ArticleDate"]).Value;

            string year = articleDate.Year.ToString();
            string month = articleDate.ToString("MM MMMM");
            string monthDirectory = articleDate.ToString("MM");

            StructureGroup baseStructureGroup = session.GetObject("/webdav/040 Website Master/Root/000 Articles") as StructureGroup;
            if (baseStructureGroup == null)
                throw new Exception("CreatePage - Unable to get baseStructureGroup");

            ComponentTemplate componentTemplate = session.GetObject("/webdav/040 Website Master/Building Blocks/System/Templates/Component Templates/Article CT.tctcmp") as ComponentTemplate;
            if (componentTemplate == null)
                throw new Exception("CreatePage - Unable to get componentTemplate");

            PageTemplate pageTemplate = session.GetObject("/webdav/040 Website Master/Building Blocks/System/Templates/Page Templates/Article PT.tptcmp") as PageTemplate;
            if (pageTemplate == null)
                throw new Exception("CreatePage - Unable to get pageTemplate");

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

            StructureGroup yearSG = GetStructureGroup(year, year, baseStructureGroup, filter);
            StructureGroup monthSG = GetStructureGroup(month, monthDirectory, yearSG, filter);

            Page newPage = monthSG.GetNewObject<Page>();
            newPage.PageTemplate = pageTemplate;
            newPage.Title = component.Title;
            newPage.FileName = CreateSlugTitle(component.Title);

            Component localComponent = session.GetObject(new TcmUri(component.Id.ItemId, ItemType.Component, baseStructureGroup.Id.PublicationId)) as Component;
            ComponentPresentation cp = new ComponentPresentation(localComponent, componentTemplate);
            newPage.ComponentPresentations.Add(cp);

            newPage.Save(true);
        }

        /// <summary>
        /// Creates a url friendly slug title.
        /// </summary>
        /// <param name="title">The title to slug.</param>
        /// <returns>A sluggified title.</returns>
        /// <remarks>http://predicatet.blogspot.com/2009/04/improved-c-slug-generator-or-how-to.html</remarks>
        private string CreateSlugTitle(string title)
        {
            byte[] bytes = System.Text.Encoding.GetEncoding("Cyrillic").GetBytes(title);
            title = System.Text.Encoding.ASCII.GetString(bytes).ToLower();

            title = Regex.Replace(title, @"[^a-z0-9\s-]", ""); // invalid chars
            title = Regex.Replace(title, @"\s+", " ")
                .Trim() // convert multiple spaces into one space
                .Substring(0, title.Length <= 45 ? title.Length : 45)
                .Trim(); // cut and trim it
            title = Regex.Replace(title, @"\s", "-"); // hyphens   

            return title;
        }

        /// <summary>
        /// Check whether or not the component exists on a page.
        /// </summary>
        /// <param name="component">The component to check.</param>
        /// <returns>True if no page is found.</returns>
        private bool DoesNotHavePage(Component component)
        {
            UsingItemsFilter filter = new UsingItemsFilter(component.Session);
            filter.ItemTypes = new List<ItemType> { ItemType.Page };
            filter.IncludedVersions = VersionCondition.OnlyLatestVersions;

            return component.GetUsingItems(filter).Count() == 0;
        }

        /// <summary>
        /// Gets a child StructureGroup with the correct title.
        /// </summary>
        /// <param name="title">The title of the StructureGroup to search for.</param>
        /// <param name="directory">The name of the SG's directory (if one needs to be created).</param>
        /// <param name="parentSG">The parent Structure Group.</param>
        /// <returns>The retrieved Structure Group.</returns>
        private StructureGroup GetStructureGroup(string title, string directory, StructureGroup parentSG, OrganizationalItemItemsFilter filter)
        {
            StructureGroup sg = parentSG
                .GetItems(filter)
                .Where(f => f.Title.Equals(title, StringComparison.InvariantCultureIgnoreCase))
                .FirstOrDefault() as StructureGroup;

            if (sg == null)
            {
                sg = CreateStructureGroup(title, directory, parentSG);
            }

            return sg;
        }

        /// <summary>
        /// Creates a StructureGroup using a given title.
        /// </summary>
        /// <param name="title">The title to give the StructureGroup.</param>
        /// <param name="directory">The directory for the StructureGroup.</param>
        /// <param name="parentSG">The SG to create the new folder in.</param>
        /// <returns>The newly created SG.</returns>
        private StructureGroup CreateStructureGroup(string title, string directory, StructureGroup parentSG)
        {
            StructureGroup newSG = parentSG.GetNewObject<StructureGroup>();
            newSG.Title = title;
            newSG.Directory = directory;
            newSG.Save();

            return newSG;
        }
    }
}

Go ahead and deploy your new code (instructions at bottom of previous tutorial) and enjoy!

Redeploying a Tridion 2011 Event System DLL

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

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

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