Razor Mediator Version 1.3 Released

Its taken much longer than I had originally anticipated, but the next version of the Razor Mediator has just been submitted to the SDL team. As usual, you can find the installer for this package here and the updated documentation for this version here. So what exactly is new in this package?

Working Where Used Functionality

Have a lot of Razor Imports and sad because its hard to track down where exactly they are used? Or have the blues from Content Porting since you have to make sure you CP your import files first before the rest of your Razor Templates? The great news is that with Version 1.3, including razor imports either through importRazor(“path”) statements or via the razor.mediator configuration setting will now finally be set as a reference so that these imports show up as Where Used! Just in case for some odd reason you absolutely do not like this features, there is also a new element in the config called “importSettings”, which allows you to turn on/off this feature. You can even turn on/off imports from importRazor statements and imports from the configuration separately.

<importSettings includeConfigWhereUsed="true" includeImportWhereUsed="true" replaceRelativePaths="false" />

The Where Used won’t kick in automatically once you upgrade to this version… you’ll have to Save your Razor Templates for the references to take place.

Relative WebDav URLs Supported!

Thanks’s to Will Price for suggesting this one! Import statements in version 1.2 only accepted TcmUri’s and full lengthy WebDav URL’s. As of Version 1.3, you can now supply relative WebDav URL paths! These paths to the imports are relative to the Razor Template that’s calling them.

@importRazor("Same Level Helper Functions.cshtml")
@importRazor("./Also Same Level Helper Functions.cshtml")
@importRazor("My Helper Functions/Global Helper Functions.cshtml")
@importRazor("My Helper Functions/Nav Functions/Main Navigation Functions.cshtml")
@importRazor("../Previous Level Functions.cshtml")
@importRazor("../../Even More Previous Level Functions.cshtml")

You probably noticed that “replaceRelativePaths” attribute in the previous importSettings config example? When set to “false” (out of the box setting), your relative import paths will stay relative. But if for some reason you would like these to automatically be transformed into the full WebDav URL, set this to true and upon saving your templates, the paths will be turned into the full paths.

Site Edit Enabled!

Razor Mediator 1.3 now includes a property named “IsSiteEditEnabled”. As the name may suggest, this property will check the Publication Target you are publishing to to see whether or not SiteEdit (inline editing) is enabled. This is useful for when you want to render specific HTML (like regions!) only for your editable staging site. This only works for Tridion UI 2012, and not previous versions of SiteEdit.

@if (IsSiteEditEnabled) {
    <strong>This page is SiteEdit Enabled!</strong>
}

Also added by default to version 1.3 is an enhancement request that came in for the RenderComponentField methods. Prior to Version 1.3, these methods would throw an error if the field was empty. Now these methods will spit out an empty tcdl tag. You can also have it spit out an empty string by using the new last argument to this field. The following is assuming that “Fields.FieldName” is empty.

@RenderComponentField("Fields.FieldName", 0)
@RenderComponentField("Fields.FieldName", 0, false)

In the first example, an empty tcdl tag like <tcdl:ComponentField name=”Fields.FieldName” index=”0″></tcdl:ComponentField> will get output so that you still have a section on your staging page for adding text to this area. The second example will just output an empty string and no tcdl tag.

Template Models

Version 1.3 now includes Template Models for the quick and easy access to template metadata that you have grown use to! @ComponentTemplate (accessible only from Component Templates), @PageTemplate and @Page.PageTemplate (accessible only when there’s a page that’s accessible), and @RazorTemplate (the Razor Template Building Block itself).

@if (IsComponentTemplate) {
    <span>@ComponentTemplate.Metadata.FieldName</span>
}

@if (Page != null) {
    <text>The following can be accessed from both Page Templates and Component Templates (only when a Page object is available though)</text>
    <span>@PageTemplate.Metadata.FieldName or @Page.PageTemplate.Metadata.FieldName</span>
}

@if (IsPageTemplate) {
    <text>While this example will only work when its a Page Template.</text>
    <span>@PageTemplate.Metadata.FieldName or @Page.PageTemplate.Metadata.FieldName</span>
}

<span>@RazorTemplate.Metadata.FieldName</span>

Other Changes and Fixes

For a full list of updates and fixes that were made in this version, please check out the Change Log.

Thanks To You

And I just wanted to personally thank everyone who has supported this project through kind comments, reporting issues, bugs and suggestions, and for testing the many features. This project definitely would not have come this far without you guys. :)

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.

Working With Tridion Workflow Automatic Activities and VBScript

My preference for working with Tridion Workflows is to use the Event System and the Tridion API (TOM API with 5.3 and 2009, and TOM.NET API with Tridion 2011).  This is probably due to my favoritism to .NET over using VBScript (who wouldn’t favor that?).  But knowing how to use the TOM API in the VBScript sections of Automatic Activities does come in handy, and just in case anyone is working with these Automatic Activities, I thought I’d put together some quick samples of how to do some basic things. When working with the VBScrit from “Edit Script…” button, remember to reference the TOM API documentation (not the TOM.NET API docs!).

Getting the Component or Page of the workflow work item.

Dim obtItem
Set objItem = CurrentWorkItem.GetItem()

Remember that VBScript works dynamically here, and the variable Item above will be a Page or a Component based on which WorkItem it is.

Dim strMetaInfo, strPageUrl
If Not objItem.MetadataSchema Is Nothing Then
    strMetaInfo = "Metadata Schema: " + objItem.MetadataSchema.Title
    If Not objItem.MetadataFields.Item("Keywords") Is Nothing Then
        strMetaInfo = strMetaInfo + "Keywords: " + objItem.MetadataFields.Item("Keywords").Value(1)
    End If
End If
strPageUrl =  objItem.Info.PublishLocationUrl

When working with Components, you can access the component fields in a similar way to metadata fields.

strSomeField = objItem.Fields.Item("SomeField").Value(1)

When working with Workflows, you’ll probably want to deal with the various workflow objects like ProcessInstance, ProcessDefinition, ActivityInstance and ActivityDefinition.

' Get the current Process Instance object
Dim objProcessInstance
Set objProcessInstance = CurrentWorkItem.ActivityInstance.ProcessInstance

' Get all Activity Instances that has happened thus far in this Process Instance
Dim objActivityInstances
Set objActivityInstances = objProcessInstance.ActivityInstances

' Get the previous Activity Instance (usually the manual activity that led to this automatic activity)
Dim objLastActivityInstance
Set objLastActivityInstance = objActivityInstances(objActivityInstances.Count - 1)

' Get the Finish Message that was input from the previous Activity Instance
Dim strFinishMessage
strFinishMessage = objLastActivityInstance.FinishMessage

' Get the performer who finished the last activity instance
Dim objLastPerformer, strLastPerformerName
Set objLastPerformer = objLastActivityInstance.Performer
' User object's use "Name" instead of "Title" (and same with groups)
strLastPerformerName = objLastPerformer.Name

' Get the first Activity Instance
Dim objOrigActivityInstance
Set objOrigActivityInstance = objActivityInstances(1)

' Get a specific ActivityDefinition in the Process matching a specific title
Dim objAct, objActivityDefinition
For Each objAct In objProcessInstance.ProcessDefinition.ActivityDefinitions
    If objAct.Title = "Some Specific Title" Then
        Set objActivityDefinition = objAct
    End If
Next

' Get a comma separated list of user IDs from an assigned group in an Activity Definition.
Dim objTrustee, objMembersXml, strMembers
strMembers = ""
Set objMembersXml = CreateObject("MSXML2.DOMDocument.4.0")
Call objMembersXml.LoadXml(objActivityDefinition.Assignee.GetMembersList)
For Each objTrustee In objMembersXml.documentElement.childNodes
    If Len(strMembers) > 0 Then
        strMembers = strMembers + ","
    End if
    strMembers = strMembers & objTrustee.getAttribute("xlink:title")
Next

Finally another important topic of working with Workflows… automatic publishing!

Call objItem.Publish("tcm:0-1-65538", True, True, True)

You’ll want to pay attention to that third argument, especially when you want to publish a work item that hasn’t completed a workflow process yet. You’ll notice that typically, only the last COMPLETED version gets published when you put something in the queue. Setting this third argument to True ensures that the version in the work list gets published. The full method definition for Publish is as follows:

Public Function Publish( ByVal targets As Variant, ByVal activateBlueprinting As Boolean, ByVal activateWorkflow As Boolean, ByVal rollbackOnFailure As Boolean, Optional ByVal publishTime As Date = 0, Optional ByVal unpublishTime As Date = 0, Optional ByVal deployTime As Date = 0, Optional ByVal resolveComponentLinks As Boolean = True, Optional ByVal priority As TDSDefines.EnumPublishPriority = Normal, Optional ByVal ignoreRenderFailures As Boolean = False, Optional ByVal maximumRenderFailures As Long = 0 ) As String

The arguments are as follows:

targets – Specifies to/from which target(s) to (un-/re-)publish. Can be one of the following:
A TargetType object or URI
An array of TargetType URIs
A TargetTypes collection object
A PublicationTarget object or URI
An array of PublicationTarget URIs
A PublicationTarget collection object
activateBlueprinting – Indicates whether the item should also be (un-/re-)published in child publications.
activateWorkflow – Indicates whether the item is being (un-/re-)published from the user’s work list.
rollbackOnFailure – Indicates if the entire publish session should be rolled back if a failure occurs while deploying
publishTime – If specified, the item is published (rendered) at the given date/time.
unpublishTime – If specified, the item is un-published at the given date/time. Should be later than publishTime (if specified)
deployTime – If specified, the item is deployed at the given date/time. Should be later than publishTime and earlier than unpublishTime (if specified). If not specified, the item will be deployed on publishTime (i.e. immediately after rendering).
resolveComponentLinks If specified, it resolves the component links. Default is set to true.
priority If specified, it gives a priority on the publish action. Default is set to normal.
ignoreRenderFailures – If specified, it gives the possibility to continue a publish action when there are render failures. Default is set to false.
maximumRenderFailures – If specified, it sets the limit on the number of render failures and ignoreRenderFailures must be set to true.

To further extend your functionality that you can do in Automatic Workflows, you can also create your own classes and functions that are COM visible, and call those methods and objects from your VBScript.  From your .NET code, you can use the Core Services API or even the old TOM API using the interop DLLs.  Remember, although the TOM.NET API has workflow objects, using it in this manner is not supported, and you should stick with one of the other two APIs.

And We Are Back

Hi readers!  I just wanted to apologize for this site being down for so long.  I’ve finally got my butt in gear and moved Coded Weapon to its own hosting site rather than trying to be cheap and hosting it on my personal sandbox development server (which has long ran out of disk space, hence the site going down!)