Checking If An Item Is In Workflow With Core Service

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

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

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

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

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

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

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

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

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

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

Other Useful Properties of WorkflowInfo

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

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

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.