Event System and Workflow Developer's Guide

Event System and Workflow Developer's Guide

Contents:

1                     Introduction  

2                     Workflow Scripting  

2.1                 Exposed objects and functions  

2.2                 Problems and solutions  

2.2.1             Working with users and groups  

2.3                 Retrieving Updated Component details within a Workflow  

2.3.1             Retrieving items when activity is "Assigned" 

2.3.2             Retrieving items when activity is "Started" 

2.3.3             Retrieving items when in an Automatic activity  

2.3.4             Publishing from within WF  

2.3.5             Publishing dynamic items  

2.3.6             Publishing static items  

2.3.7             Working with a newly-created item in WF  

2.3.8             Address format required for "Tridion Content Manager Instance" on the Start Activity in Visio  

2.3.9             Other issues  

3                     Using LDAP in Event Systems/Workflow   

3.1                 Authenticating LDAP Users with the Tridion Content Manager. 

3.2                 What is an LDAP Impersonation User?  

3.3                 When do I impersonate in my code?  

4                     Debugging in Events Systems & Workflow   

4.1                 Debugging the Events System   

4.2                 Debugging a Workflow Script 

4.3                 Debugging a Visual Basic application (DLL) that is called by a Workflow Script 

5                     Tridion Event System and Workflow Best Practices  

5.1                 Configuration files  

5.2                 Keep CM_EV.dll clean - Use Multiple DLLs  

5.3                 Minimizing Concurrency Issues  

5.4                 Stress/Performance Testing your Event System   

5.5                 Workflow Naming Best Practices  

5.6                 Error Handling in Event System Code  

5.6.1             Displaying events errors in the GUI to the user 

6                     Helpful Code Examples  

6.1                 Determine if a Workflow User belongs to a certain group. 

6.2                 Retrieve path to Tridion folder 

6.3                 Read-In and retrieve values from a Configuration File. 

6.4                 Send an Email from an Event/Workflow. 

6.5                 Impersonating in Events Code  

6.6                 Display all content of a Component in an email (text only) 

6.7                 Autopublishing: A full typical example. 

6.8                 Autopublishing: Convert Page Title to FileName  

6.9                 Automated Decision Activity: Get next activity ID  

6.10               Carry over Finish Message from Activity before an Automated Activity.

1        Introduction 

 

There are elements of any Events System or automated Workflow that you as a developer will encounter regularly. This document attempts to shed light on difficult or unclear areas of Events Systems and Workflow including use of LDAP, Impersonation and others. This document also provides some best practices and some code examples.

 

2        Workflow Scripting

 

This document contains some tips and tricks for developing Tridion Workflow Automatic Activity Scripts. 

 

2.1      Exposed objects and functions

 

The following objects are available in workflow scripts:

 

CurrentWorkItem

This is an instance of the class WorkItem, that contains the current work item. It is usually used to access the encapsulated object (component, page or template) using the getItem method. It can also be used to access the activity instance using the ActivityInstance property.

 

TDSE

           An instance of the TDSE object.

 

WFE

An instance of the WFE object, which can be used to retrieve information about all workflow definitions, processes and activities. 

 

The following functions are available in the workflow scripts:

 

FinishActivity

Synonymous to WorkItem.ActivityInstance.FinishActivity.

 

SelectNextActivity

Synonymous to WorkItem.ActivityInstance.SelectNextActivity.

 

SuspendActivity

Synonymous to WorkItem.ActivityInstance.SuspendActivity.

 

Sleep (milliseconds as long)

This setting indicates the time (milliseconds) for which the automated activity will sleep. This method can be used when the automated script wants to check if information is available. If information is not available, the thread can sleep for the specified amount of time and does not use processing power. Typically, this method can be used if the automated activity is Snoozed.

 

Snooze

This setting indicates that the automated activity is long running. The priority of the thread on which the automated activity is running is decreased. The thread is not counted when the thread limit is calculated.

 

WakeUp

This setting indicates that an automated activity is no longer long-running. The priority of the thread on which the automated activity is running is changed back to normal. The tread counts when the thread limit is calculated.

 

 

 

 

2.2      Problems and solutions

 

2.2.1   Working with users and groups

There are use cases when a work item should be returned back to the Assignment list of the performer of a previous activity - for example if the content is not approved by the Approval authority and must be adapted to meet certain requirements etc. 

 

Normally getting the performer URI should be achieved by writing script like:

 

      strPerformerURI = oActiveInstance.Performer.ID

 

but there is a case when oActivityInstance does not have Performer property set. If the ActivityInstance for which we are checking the performer was never started but for example a user with Workflow Management rights finished that Activity Instance from Process Instances list then Performer and Started Date properties for this Activity Instance will be invalid. This means that the Performer property should be checked:

 

      If Not oActiveInstance.Performer Is Nothing Then

            strPerformerURI = oActiveInstance.Performer.ID

      End If

 

Another detail that should be paid attention (although the API suggests the right solution) is that the Assignee of an Activity Definition must be of type Object as both Users and Groups could be set up as Assignees. By reading the ID property (as both Tridion ItemTypes support it) in a TDSSystem.tdsURI object and later checking the ItemType property of the tdsURI object, the workflow script can determine if the Activity Definition is assigned to a User or to a Group.

 

2.3      Retrieving Updated Component details within a Workflow

 

Often you may need to retrieve up-to-the-minute details of a component in Workflow (i.e. a component that is currently in an activity. 

 

If your component is currently in an activity, it is either in the Assignment List of people who must work with this component (and the activity is "Assigned") or it is in the Work List of a particular user (and the activity is "Started"). The only exception to this are automated activities (see below).

 

2.3.1   Retrieving items when activity is "Assigned"

 

If the item is in the Assignment List, to retrieve current details of the component, you must impersonate as someone in the group that the activity is assigned to. Only those assigned to that activity will have rights to view that component (remember this is ONLY when the component is "Assigned"). To do this, use:

 

  TDSE.GetObject(theComponentURI, OpenModeViewInAssignmentList)

 

2.3.2   Retrieving items when activity is "Started"

 

If the activity is already "Started" then the component is currently in the Work List of a particular user. Therefore, to retrieve the current details, you must impersonate as that particular user and open it in Edit mode.

 

TDSE.GetObject(theComponentURI, OpenModeEdit)

 

Most often, you won't be needing component details in a specific manual activity but in an automated activity that follows a manual (e.g. "edit content") activity. 

 

Generally, the best way of retrieving latest component details in this example is to get the Performer of the previous activity (e.g. ActivityInstance.Position -1) and impersonating as this user. 

 

2.3.3   Retrieving items when in an Automatic activity

 

Automated activities do not have assignees. The user performing automated activities (e.g. if you call a DLL from an automatic workflow script) will always be the user running the workflow service. By default, this user is LocalSystem in Windows 2000 and Windows 2003. 

 

The only way to retrieve the updated component is to be logged in as the user running the Workflow Service. Although it is impossible to log in as LocalSystem (this is a built-in windows account), it IS possible to change the user running the Workflow Service to another user (such as Administrator). Once the Workflow Service is running under Administrator (for example), the following call will correctly retrieve the latest updated values within your component:

 

TDSE.GetObject(theComponentURI, OpenModeEdit)

 

2.3.4   Publishing from within WF

 

Besides bringing what is expected as Workflow functionality (for more info you can check http://www.wfmc.org/- the Workflow Management Coalition site), Tridion Content Manager provides the option of publishing the latest checked in version of the item (the "static" version) or the checked out version of the item (the "dynamic" version). Publishing items that are not in Workflow will always publish the last checked in version. This is the default behavior for publishing items in TCM.

In the most common Tridion Workflow use case, the items should be published to a Staging web site (usually Intranet) before being approved for publishing on the company Live web site (the Internet). To help implementing this use case, the Approval Statuses are introduced in TCM. Each workflow activity has an Approval Status assigned to it. When an Activity instance finishes, the Tridion item for which the current workflow process is started will obtain the Activity Definition Approval Status as its own. On the other side Publication targets have the Minimal Approval Status property that helps filtering that only Tridion items having equal or higher Approval status could be published to them. If the Approval status of an item in workflow is lower than the Approval status required by a Publication target, the last checked in version of that item (if exists) will be published to that publication target. This way publishing unapproved items (items that hasn't obtained the required approval status yet) to live sites is prevented.

 

2.3.5   Publishing dynamic items

To publish the Dynamic version to the Staging web site so that it could be previewed by the approving authority is possible using the following script:

 

' Script for Automatic Activity Content Manager Workflow

call CurrentWorkItem.GetItem(1).Publish(strTargetTypeID, False, True, False)

FinishActivity "Automatic Activity ""Publish to Staging"" Finished"

 

In this script, the checked out (the Dynamic) version of the Tridion Item is retrieved from the CurrentWorkItem object (the "1" provided as a parameter to GetItem call for TDSDefines.OpenModeView) but this is not important. Using the default value "4" (TDSDefines.OpenModeViewOrEdit) or "2" (for TDSDefines.OpenModeEdit) will deliver the same result. What really important is is the third parameter of the Publish API. Specifying "True" will activate the check if the Published item meets the conditions for its checked out version to be published. It should be mentioned that ONLY user for which the current item is checked out in workflow (the Performer of the current WF activity instance) can Publish the dynamic version of the item.

 

2.3.6   Publishing static items

Use the following code:

 

' Script for Automatic Activity Content Manager Workflow

call CurrentWorkItem.GetItem(1).Publish(strTargetTypeID, False, False, False)

FinishActivity "Automatic Activity ""Publish to Live"" Finished"

 

 

Again it is not important which version of the item will be used. Important is that the third parameter of the Publish API call should be "False". Using this code, the last checked in version of the item will be published. This code is logical only in the case when the item already has a checked in version. As during the whole workflow process the item is checked out, the code cold be used more as learning example than as real code implementation case.

 

Very often the Workflow process finishes with publishing of the just updated items to the Live site (in its last Activity Instance). In this case the first example of publishing the Dynamic version of an item is the most logical implementation, but in fact it could lead to problems during the publishing process. The problem is that the publishing is executed in parallel of the activities executed by the Workflow Script Execution service. With the following sequence:

 

Call CurrentWorkItem.GetItem(1).Publish(strTargetTypeID, False, True, False)

FinishActivity "Automatic Activity ""Publish to Live"" Finished"

 

the Publish functionality will be initiated for a checked out item but meantime it is possible (and in most of the cases it fill happen for sure)   that the item will be checked in by the FinishActivity API (internally finishing the process instance) call. This will lead to errors in the windows event log that the item can not be read.

 

The recommended implementation is:

 

Dim strItemURI

strItemURI = CurrentWorkItem.GetItem(2).ID

 

FinishActivity "Automatic Activity ""Publish to Live"" Finished"

 

Dim oItem

Set oItem = TDSE.GetObject(strItemURI, 1)

call oItem.Publish(strTargetTypeID, False, False, False)

Set oItem = Nothing

 

This way the implementation does not depend on any external or undocumented features and will always work.

1        Introduction 

 

There are elements of any Events System or automated Workflow that you as a developer will encounter regularly. This document attempts to shed light on difficult or unclear areas of Events Systems and Workflow including use of LDAP, Impersonation and others. This document also provides some best practices and some code examples.

 

2        Workflow Scripting

 

This document contains some tips and tricks for developing Tridion Workflow Automatic Activity Scripts. 

 

2.1      Exposed objects and functions

 

The following objects are available in workflow scripts:

 

CurrentWorkItem

This is an instance of the class WorkItem, that contains the current work item. It is usually used to access the encapsulated object (component, page or template) using the getItem method. It can also be used to access the activity instance using the ActivityInstance property.

 

TDSE

           An instance of the TDSE object.

 

WFE

An instance of the WFE object, which can be used to retrieve information about all workflow definitions, processes and activities. 

 

The following functions are available in the workflow scripts:

 

FinishActivity

Synonymous to WorkItem.ActivityInstance.FinishActivity.

 

SelectNextActivity

Synonymous to WorkItem.ActivityInstance.SelectNextActivity.

 

SuspendActivity

Synonymous to WorkItem.ActivityInstance.SuspendActivity.

 

Sleep (milliseconds as long)

This setting indicates the time (milliseconds) for which the automated activity will sleep. This method can be used when the automated script wants to check if information is available. If information is not available, the thread can sleep for the specified amount of time and does not use processing power. Typically, this method can be used if the automated activity is Snoozed.

 

Snooze

This setting indicates that the automated activity is long running. The priority of the thread on which the automated activity is running is decreased. The thread is not counted when the thread limit is calculated.

 

WakeUp

This setting indicates that an automated activity is no longer long-running. The priority of the thread on which the automated activity is running is changed back to normal. The tread counts when the thread limit is calculated.

 

 

 

 

2.2      Problems and solutions

 

2.2.1   Working with users and groups

There are use cases when a work item should be returned back to the Assignment list of the performer of a previous activity -  for example if the content is not approved by the Approval authority and must be adapted to meet certain requirements etc. 

 

Normally getting the performer URI should be achieved by writing script like:

 

      strPerformerURI = oActiveInstance.Performer.ID

 

but there is a case when oActivityInstance does not have Performer property set. If the ActivityInstance for which we are checking the performer was never started but for example a user with Workflow Management rights finished that Activity Instance from Process Instances list then Performer and Started Date properties for this Activity Instance will be invalid. This means that the Performer property should be checked:

 

      If Not oActiveInstance.Performer Is Nothing Then

            strPerformerURI = oActiveInstance.Performer.ID

      End If

 

Another detail that should be paid attention (although the API suggests the right solution) is that the Assignee of an Activity Definition must be of type Object as both Users and Groups could be set up as Assignees. By reading the ID property (as both Tridion ItemTypes support it) in a TDSSystem.tdsURI object and later checking the ItemType property of the tdsURI object, the workflow script can determine if the Activity Definition is assigned to a User or to a Group.

 

2.3      Retrieving Updated Component details within a Workflow

 

Often you may need to retrieve up-to-the-minute details of a component in Workflow (i.e. a component that is currently in an activity. 

 

If your component is currently in an activity, it is either in the Assignment List of people who must work with this component (and the activity is "Assigned") or it is in the Work List of a particular user (and the activity is "Started"). The only exception to this are automated activities (see below).

 

2.3.1   Retrieving items when activity is "Assigned"

 

If the item is in the Assignment List, to retrieve current details of the component, you must impersonate as someone in the group that the activity is assigned to. Only those assigned to that activity will have rights to view that component (remember this is ONLY when the component is "Assigned"). To do this, use:

 

  TDSE.GetObject(theComponentURI, OpenModeViewInAssignmentList)

 

2.3.2   Retrieving items when activity is "Started"

 

If the activity is already "Started" then the component is currently in the Work List of a particular user. Therefore, to retrieve the current details, you must impersonate as that particular user and open it in Edit mode.

 

TDSE.GetObject(theComponentURI, OpenModeEdit)

 

Most often, you won't be needing component details in a specific manual activity but in an automated activity that follows a manual (e.g. "edit content") activity. 

 

Generally, the best way of retrieving latest component details in this example is to get the Performer of the previous activity (e.g. ActivityInstance.Position -1) and impersonating as this user. 

 

2.3.3   Retrieving items when in an Automatic activity

 

Automated activities do not have assignees. The user performing automated activities (e.g. if you call a DLL from an automatic workflow script) will always be the user running the workflow service. By default, this user is LocalSystem in Windows 2000 and Windows 2003. 

 

The only way to retrieve the updated component is to be logged in as the user running the Workflow Service. Although it is impossible to log in as LocalSystem (this is a built-in windows account), it IS possible to change the user running the Workflow Service to another user (such as Administrator). Once the Workflow Service is running under Administrator (for example), the following call will correctly retrieve the latest updated values within your component:

 

TDSE.GetObject(theComponentURI, OpenModeEdit)

 

2.3.4   Publishing from within WF

 

Besides bringing what is expected as Workflow functionality (for more info you can check http://www.wfmc.org/- the Workflow Management Coalition site), Tridion Content Manager provides the option of publishing the latest checked in version of the item (the "static" version) or the checked out version of the item (the "dynamic" version). Publishing items that are not in Workflow will always publish the last checked in version. This is the default behavior for publishing items in TCM.

In the most common Tridion Workflow use case, the items should be published to a Staging web site (usually Intranet) before being approved for publishing on the company Live web site (the Internet). To help implementing this use case, the Approval Statuses are introduced in TCM. Each workflow activity has an Approval Status assigned to it. When an Activity instance finishes, the Tridion item for which the current workflow process is started will obtain the Activity Definition Approval Status as its own. On the other side Publication targets have the Minimal Approval Status property that helps filtering that only Tridion items having equal or higher Approval status could be published to them. If the Approval status of an item in workflow is lower than the Approval status required by a Publication target, the last checked in version of that item (if exists) will be published to that publication target. This way publishing unapproved items (items that hasn't obtained the required approval status yet) to live sites is prevented.

 

2.3.5   Publishing dynamic items

To publish the Dynamic version to the Staging web site so that it could be previewed by the approving authority is possible using the following script:

 

' Script for Automatic Activity Content Manager Workflow

call CurrentWorkItem.GetItem(1).Publish(strTargetTypeID, False, True, False)

FinishActivity "Automatic Activity ""Publish to Staging"" Finished"

 

In this script, the checked out (the Dynamic) version of the Tridion Item is retrieved from the CurrentWorkItem object (the "1" provided as a parameter to GetItem call for TDSDefines.OpenModeView) but this is not important. Using the default value "4" (TDSDefines.OpenModeViewOrEdit) or "2" (for TDSDefines.OpenModeEdit) will deliver the same result. What really important is is the third parameter of the Publish API. Specifying "True" will activate the check if the Published item meets the conditions for its checked out version to be published. It should be mentioned that ONLY user for which the current item is checked out in workflow (the Performer of the current WF activity instance) can Publish the dynamic version of the item.

 

2.3.6   Publishing static items

Use the following code:

 

' Script for Automatic Activity Content Manager Workflow

call CurrentWorkItem.GetItem(1).Publish(strTargetTypeID, False, False, False)

FinishActivity "Automatic Activity ""Publish to Live"" Finished"

 

 

Again it is not important which version of the item will be used. Important is that the third parameter of the Publish API call should be "False". Using this code, the last checked in version of the item will be published. This code is logical only in the case when the item already has a checked in version. As during the whole workflow process the item is checked out, the code cold be used more as learning example than as real code implementation case.

 

Very often the Workflow process finishes with publishing of the just updated items to the Live site (in its last Activity Instance). In this case the first example of publishing the Dynamic version of an item is the most logical implementation, but in fact it could lead to problems during the publishing process. The problem is that the publishing is executed in parallel of the activities executed by the Workflow Script Execution service. With the following sequence:

 

Call CurrentWorkItem.GetItem(1).Publish(strTargetTypeID, False, True, False)

FinishActivity "Automatic Activity ""Publish to Live"" Finished"

 

the Publish functionality will be initiated for a checked out item but meantime it is possible (and in most of the cases it fill happen for sure)   that the item will be checked in by the FinishActivity API (internally finishing the process instance) call. This will lead to errors in the windows event log that the item can not be read.

 

The recommended implementation is:

 

Dim strItemURI

strItemURI = CurrentWorkItem.GetItem(2).ID

 

FinishActivity "Automatic Activity ""Publish to Live"" Finished"

 

Dim oItem

Set oItem = TDSE.GetObject(strItemURI, 1)

call oItem.Publish(strTargetTypeID, False, False, False)

Set oItem = Nothing

  • The Automatic Activity definition Visual Basic script code is executed in the Microsoft Script control so it should be interpreted before the execution. Using precompiled COM object means that interpreting step won't be needed. 
  • Keeping different versions of the code (for example in MS source safe) is easier when the code is saved in different text files than in one Visio (*.vsd) file.
  • Code duplication will be avoided:
    • At the moment Process Definitions should be imported per publication. This means that similar code would exist separately for identical process definitions in different publications;
    • Different Automatic activities in the same process definitions could share the same functionality. In COM objects are not used then every Activity instance should have a copy of this code.
    • VB environment (where COM objects would be developed) has good code editing and verification features. The script code within the Activity definition will be checked for correct syntax at the moment when the code will be executed (so late in the development process) which is not convenient.   

This way the implementation does not depend on any external or undocumented features and will always work.

 

3        Using LDAP in Event Systems/Workflow

 

Often developers find themselves required to make a choice in their code, (typically events code or workflow code), on whether they should impersonate or not. To be able to answer this question and to know the implications, it's important to have some background about how exactly LDAP is integrated with Tridion. The following topics will be covered in this whitepaper:

 

  • Authenticating LDAP Users with the Tridion Content Manager.
  • What is an LDAP Impersonation User and why is it necessary.
  • When to impersonate in your code.   

 

Steps on installing and configuring LDAP will not be provided in this document. For this information, please consult the Tridion Publication Management Guide, the Administration Guide and the Installation Guide.

3.1      Authenticating LDAP Users with the Tridion Content Manager.

 

Firstly, it's important to have a little background on how LDAP works with Tridion particularly if you must make development decisions that may be impacted by its integration.

 

Without LDAP, the Tridion Content Manager is a website that runs under IIS using Integrated Windows Authentication which means that all authentication is encrypted.

 

For a business to use LDAP, they must first set up a replica of the TCM website under IIS with the exception of two things:

 

  • The site doesn't use Integrated Windows Authentication, it uses Basic Authentication (user/password is not encrypted i.e. clear text). Note that generally HTTPS is used for this website (so that user/passwords are indeed encrypted).
  • An ISAPI filter for LDAP is used. This is essentially a dll that takes in Tridion MMC-configured filter parameters such as the LDAP server name, port, where to search in the LDAP directory and importantly, the LDAP Impersonation User.   

 

An LDAP impersonation user then has to be configured (see "What is an LDAP Impersonation User?") and the LDAP ISAPI Filter in the Tridion MMC Snap-In must be correctly configured.

3.2      What is an LDAP Impersonation User?

 

An LDAP Impersonation User is a Windows-authenticated user (not an LDAP user) that provides a mapping between Windows and LDAP. It reconciles LDAP user authentication with Windows authentication via the LDAP ISAPI Filter set in the Tridion MMC Snap-In.

 

By default, Windows (and Tridion) cannot "see" LDAP Users in an LDAP directory. The LDAP Impersonation User (conventionally called LDAPImp) provides an access point in the Tridion Security Model that allows LDAP users to properly be authenticated with Tridion. 

 

It does this through the ISAPI Filter settings in the Tridion MMC snap-in and this filter is incorporated into the LDAP-enabled TCM website in IIS. This means that the LDAP-enabled website will correctly process any attempts at authenticating LDAP users. 

 

It should be noted, an LDAP Impersonation User should NEVER be "added" as a user in the Tridion Content Manager. Doing so could create a security loophole.

 

3.3      When do I impersonate in my code?

 

"Impersonating in code" refers to calling the Impersonate  method on a TDSE object and allows the developer to perform Tridion actions using the security permissions of a different Tridion-authenticated user.

 

If an LDAP user performs an action that causes an event to be fired (e.g. save a component), the event system will be run as that user. This means that after the component is saved and the user refreshes the GUI, the user will see that he/she was the last person to save that component.

 

Typically, you should impersonate in your events code when you need to take on the security permissions of another, usually a "master" user such as an Administrator. The following scenarios are typical for using impersonation:

  • When a user only has component management rights and saves a component that has an autopublish function associated with it. As the user does not have rights to publish pages, the developer must impersonate as a user that possesses page publish rights.
  • When you want to retrieve the up-to-date component contents of a component in the middle of a workflow process. See: Retrieving Component details within a Workflow.  

 

4        Debugging in Events Systems & Workflow4.1      Debugging the Events System

Events and Workflow developers often find the most difficult thing about Events code is debugging. By debugging, we mean being able to fire events such as OnComponentSavePre from the Content Manager GUI and trapping that thread in your Visual Basic 6 source code. 

The following checklist is intended to be a fail-safe, dummy-proof checklist for debugging the Events System. 

Setup and Assumptions:

 

1. In most Event Systems, you will have two or more DLLs: 

  • cm_ev.dll - the events DLL that for each activated event does nothing more than call another DLL (we'll call it cm_YourFunctionality.DLL) which handles all functionality. 
  • cm_YourFunctionality.DLL - handles all your functionality.   

 

We will assume for this event system you have this arrangement - two DLLs, where cm_ev.dll has a reference to cm_yourfunctionality.dll.

 

2. Set Binary Compatibility (Project -> Properties -> Component Tab) to the DLL file in Tridion\bin directory. If it is the first time you're creating your DLL, you will need to set it to Project Compatibility first, then after the DLL is made, you can then switch to Project Compatibility.

 

If you change the specifications of yourfunctionality.dll such as adding a new parameter to a Sub or Function and you re-make your dll, you will be prompted to choose to Preserve Compatibility or Break Compatibility - always choose Preserve.

 

3. Ensure your COM+ "Tridion Content Manager" application is running as Interactive User (i.e. the User who is currently logged in). To check this, go to COM+ "Tridion Content Manager" application, right click -> Properties -> Identity tab: tick Interactive User.

 

Checklist:

 

If you're having trouble debugging, start from scratch - it's always easier.

  • Backup your source code AND dlls.
  • Unregister cm_ev.dll
  • Unregister cm_yourfunctionality.dll
  • (Optional) Restart COM+
  • (Optional) Trigger your event from the GUI, you should get "Event System Object not found". This means cm_ev.dll is not registered. If you don't get this error, a registered Event System dll (cm_ev.dll) was found (probably in another directory). Find this dll and unregister it.
  • Open up cm_ev.dll and cm_yourfunctionality.dll source code.
  • Goto cm_ev.dll source code and goto Project -> References and remove the reference to yourfunctionality.dll.
  • Now goto cm_yourfunctionality.dll. Ensure Binary Compatibility is set (see Setup) with Tridion\bin\cm_yourfunctionality.dll and make a new DLL to that directory. If you get an error getting a lock on that file, you may have other source code open that has a reference to that DLL. If there is no other source code open, restart COM+ "Tridion Content Manager" application to release the lock and make your DLL.
  • Register Tridion\bin\cm_yourfunctionality.dll.
  • Goto your cm_ev.dll. Re-add cm_yourfunctionality.dll as a project reference (Project -> References). 
  • Ensure Binary Compatibility with Tridion\bin\cm_ev.dll is set. Re-make cm_ev.dll to Tridion\bin directory. Again, if you cannot get a file lock on it, simply restart COM+ "Tridion Content Manager" application.
  • Register Tridion\bin\cm_ev.dll. Both dll's should be correctly registered.
  • Switch your event on in the Tridion Snap-In. 
  • In your source code, set a breakpoint in your code that MUST get fired. Hit play.
  • Restart COM+ "Tridion Content Manager" application.
  • Fire your event from the GUI and you should be able to debug.   

 

If you ever encounter an Automation Error, this is usually because your events system dll (cm_ev.dll) is registered ok but yourfunctionality.dll is not. This regularly occurs if you make a change to yourfunctionality.dll's interface (e.g. add a new parameter to a Sub), remake that dll and re-register it but you don't re-add the reference to yourfunctionality.dll from cm_ev.dll's project references and re-register the event system.

If ever in doubt, use the above checklist as a fail-safe way of being able to debug.

 

It is worthy to note here that when debugging events code that involves publishing, when a user right click and publish a page the following should be known:

 

OnPagePublishPost/Pre corresponds to: Event triggered AFTER / BEFORE a page is queued for publishing NOT Event triggered AFTER / BEFORE a page is set to published or unpublished for a specific publication target.

 

The event that corresponds to Event triggered AFTER / BEFORE a page is set to published or unpublished for a specific publication target is: 

OnPageSetToPublishedPost/Pre.

 

You may, as a result, place your breakpoint at the incorrect event (or at an event that may not be switched on). This is a very common mistake.

4.2      Debugging a Workflow Script 

 

There is no way to debug actually in a script within Visio. As a Tridion best practice, please don't put any actual functionality in your workflow script code. In the script, simply make a call to a different dll that handles the functionality.

 

Most importantly, the workflow script is a VBScript script (NOT Visual Basic, thus you cannot Dim x As String, only Dim x). This means you cannot use On Error Goto yourlabelhere and handle errors. Therefore, Tridion-recommended best practice is to ALWAYS use On Error Resume Next in your workflow script in Visio. If you do not and an error is thrown, your workflow process will be "Suspended" and only system administrators can roll back the process.

 

Thus, use on error resume next and keep all functionality in a separate dll.

4.3      Debugging a Visual Basic application (DLL) that is called by a Workflow Script

 

As a Tridion best practice, ALWAYS call a separate DLL when using automated scripts in Visio for Workflow. The following are fail-safe steps to allow you to pick up a thread to the DLL that you call in your workflow script.

 

Setup:

 

1. It is assumed you have a workflow automatic activity or automatic decision that calls a dll (we'll refer to this dll as yourfunctionality.dll).

 

Checklist:

 

  • Goto the TRUSTEES table in Tridion_cm database in Oracle or MSSQL Server. Retrieve the ID of whatever user you are currently logged in.
  • Goto the Tridion Snap In -> Workflow -> Automatic Trustee - and set this value to the value to retrieved from the TRUSTEES table. Threads initiated via workflow will now operate as you.
  • Ensure the activity before the automated activity where you script runs is assigned to you (i.e. Everyone, a group your belong to, or specifically your user account).
  • Change Workflow Service to your user (Right click -> Log On tab -> This Account - and enter your details)
  • Restart the Workflow Service.
  • Ensure Binary compatibility in your DLL and that whatever method you might call from the workflow script that it, it's parameters and the Type of each parameter is consistent with your dll.
  • Unregister your DLL.
  • Re-Make your DLL.
  • Re-register your DLL
  • Set a breakpoint in your source code where you are absolutely positive it will fire (e.g. Class_initialize() if you have one is perfect as this will always fire when a new instance of this DLL is created). Hit play.
  • Restart COM+    

Kickoff your workflow and attempt to debug your code. 

 

Note: It may be useful for purposes of debugging to take out On Error Resume Next from your workflow script. This is because, your workflow script should only contain a call to instantiate an object of type yourfunctionality.dll and maybe a method on that object (e.g. yourObject.handleWorkflowActivity()) and if you remove error handling and you find that your workflow status goes to "Suspended", this immediately tells you yourfunctionality.dll cannot be found (probably was not registered correctly).

5        Tridion Event System and Workflow Best Practices5.1      Configuration files 

Event Systems must always have a configuration file (XML). This is because there are some parameters that must be made configurable (e.g. publication targets for development environment will differ from acceptance and production.

Configuration files allow system administrators to widen the possibilities of your event code and allow migration from environment to environment possible.

As a Tridion best practice, we recommend:

  • Putting all configuration files in the Tridion\config directory
  • Making config files XML. This is done for performance reasons: It is much more efficient reading in an XML file, loading it as a DOM object and manipulating its contents than any normal parsing methods. For more complicated or regularly updated configurations or for system administrators who may not have a basic grasp of XML, a Custom Page could be built that updated this file.
  • Giving configuration files functionally-relevant names (e.g. MailingSystemConfiguration.xml).
  • For Content Managers with functionality that is different from publication to publication and for Event Systems with multiple DLLs that require different configurations, we recommend to create separate configurations for each separate DLL / functional grouping.    =

An example of reading in a configuration file can be found in Helpful Code Examples.

5.2      Keep CM_EV.dll clean - Use Multiple DLLs

We strongly recommend keeping your cm_ev.dll completely empty of code except for calls to other DLLs that implement your events functionality. It is recommended to make a DLL per publication if each publication has discrete functionality.

By keeping your functionality in separate DLLs, you have a number of advantages:

 

  • This allows for more logical separation of code.
  • Better readability.
  • Less code may be executed as a result.
  • Modularity ''you may have "switch on" / "switch off" functionality.
  • Much easier to maintain and enhance.
  • As Event System code is synchronous, you can call other DLLs with performance-intensive or time-consuming functionality to function in the background while the events code continues. 
  • Developers can work on different parts at the same time.   

 

5.3      Minimizing Concurrency Issues

 

A common requirement in Content Managers is to maintain a system component that stores values. E.g. if on your website you wanted the last 10 published articles, at publish time you might add an XML node with link information to an XML list stored in a system component field. But what happens if two users attempt to get a lock on this component shortly after one another? 

 

Concurrency issues can occur only on versionable (checkout-able) items such as components. They can also occur if you try to write to a file on the file system and another user has a lock on that file.

 

Tridion best practice in dealing with this situation is this:

 

Using Metadata on a folder, structure group or publication (as they are non-versionable) may be a solution for you.

 

However this may not be appropriate. In your Events code, instead of writing directly to your list in a component or metadata, why not write your information to a uniquely named file. Then write a program (e.g. a VB application/DLL) and run the program as a service that every, say, 30 minutes, polls for these files and writes to the system component. This means there will never be concurrency issues as the only service obtaining a lock on the component is one service. This is the surest way of ensuring no concurrency issues will occur.

 

5.4      Stress/Performance Testing your Event System

 

Before deploying your Events System to an acceptance environment be absolutely sure that it is efficient code. The best way to determine this is by writing a test stub that replicates major parts of your code and by watching the CPU usage history graph (ctrl-alt-del -> task manager -> performance tab). 

 

For example, you may have an events system that onComponentSavePre loads metadata. To stress-test your events code, you would write a quick function that created 2000 dummy components and watch your CPU usage. 

 

You can tell if there are serious memory leaks this way. Memory leaks are caused by not destroying instantiated objects in your events code. Whenever you Set object = something, you must also Set object = Nothing once you are finished with it, otherwise it persists in memory.

 

A good way of minimizing memory usage is by having a Class_Terminate() method that will be called when a DLL thread is finished. In this method you can do any cleanup of global variables. 

5.5      Workflow Naming Best Practices

 

Workflow names as a best practice should be kept as short as is possible. On the popup where users finish activities, there is a field that displays the Workflow Name (start activity -> name of workflow process) and unfortunately if the name is greater than one line, the name gets truncated.

5.6      Error Handling in Event System Code

 

Visual Basic 6 error handling is on a per-sub/function basis (there is no global label). Consequently, as a best practice you must use "On Error Goto label" and attach an appropriate catch statement on your label such as writing to a log.

 

5.6.1   Displaying events errors in the GUI to the user

 

It is possible to bubble errors from the event system up to the GUI. The following code example demonstrates how.

 

Call Err.Raise(-10, "OnComponentSavePre", "Not a correct value for this field.")

 

Note that you can only use numbers below 0 and above 2000 - all numbers in between are reserved for Microsoft errors.

 

Appropriate use-cases for this functionality include Pre errors when validating component content input.

6        Helpful Code Examples

 

All Event Systems and Workflow code is similar. Often you'll find history repeating itself. In an effort to make this easier, here are some code examples that are very helpful in common situations.

6.1      Determine if a Workflow User belongs to a certain group.


Automatic Decision activities usually revolve around determining if the performer of the previous activity was a member of a particular group (e.g. was the previous Performer a Content Editor or the Chief Editor?).

 

This function is very helpful in that it returns true/false if a given user is a member of the particular group in question. It recursively calls itself to crawl the GroupMembership tree of a given user for the particular group in question.

 

'***********************************************************

'    hasMember(strGroup As String, oGroupMemberships As GroupMemberships, groupID As String)

'    strGroup - current group being searched

'    oGroupMemberships - group memberships strGroup's group memberships

'    groupID - the group ID that you are searching for (e.g. reviewer group ID, editor group ID etc).

'    Description: Recursively determines if group parameter exists in given group memberships and all sub memberships.

'

'***********************************************************

Private Function hasMember(strGroup As String, oGroupMemberships As GroupMemberships, groupID As String)

     On Error GoTo WFError

    

     Dim oGroup As Group

     Dim result As Boolean

     Dim oNextGroupMemberships As GroupMemberships

    

     ' For all groups that have remaining group memberships

     If oGroupMemberships.count <> 0 Then

        

         ' Iterate through each group within this level's group memberships

         For i = 1 To oGroupMemberships.count

        

             ' Get a reference to this particular group

             Set oGroup = oGroupMemberships.Item(i)

            

             ' Determine if at one level lower if the member exists

             If oGroupMemberships.Contains(groupID) Then

            

                 ' Member found at this level, return it immediately

                 hasMember = True

                 Exit Function

             Else

                 ' Member didn't exist at one level lower, so set reference to that level for next round.

                 Set oNextGroupMemberships = oGroup.GroupMemberships

                 

                  ' Perform the same operation but one level lower.

                 result = hasMember(strGroup, oNextGroupMemberships, groupID)

                

                 ' Upon exit, if member was found, we want to return this immediately.

                 If result = True Then

                     hasMember = True

                     Exit Function

                 End If

             End If

         Next

                

     ' We are at the bottom level (there are no deeper membership groups

     Else

         If Not oGroupMemberships.Contains(groupID) Then

            

             result = False

         Else

             result = True

         End If

     End If

    

     ' Return the result

     hasMember = result

    

     ' Empty Memory

     If IsObject(oGroup) Then Set oGroup = Nothing

     If IsObject(oGroupMemberships) Then Set oGroupMemberships = Nothing

     If IsObject(oNextGroupMemberships) Then Set oNextGroupMemberships = Nothing

    

     Exit Function

    

WFError:

     Call WriteEventToLog("Error in determining if this user belongs Group of interest.", Err.Description, "1")

    

End Function

 

 

6.2      Retrieve path to Tridion folder

 

Whenever reading in configuration files or working with files on the Content Manager (e.g. writing to a log), it's handy to always know the path to the Tridion folder. By having a reference to the Tridion folder, we can make references to config files in Tridion\config and write log files to Tridion\log (where they should be).

 

The following example retrieves the path to the Tridion folder (e.g. C:\Program Files\Tridion". 

 

Note: For this function to work, you MUST have your dlls running in some subdirectory of the Tridion directory (e.g. Tridion\bin). 

 

'***********************************************************

'    getPathToTridionFolder

'    Author: James English (Tridion PS)

'    Description: Assumes this dll exists in some subdirectory of Tridion. Based on the directory of this dll, determine and return the Tridion directory.

'    Return example: "C:\Program Files\Tridion"

'***********************************************************

Private Function getPathToTridionFolder() As String

    

     On Error GoTo TridionPathError

     Dim count As Integer

     Dim pathSplit() As String

     Dim pathToTridion As String

 

     count = 0

    

     ' Deconstruct the path to this dll into an array.

     pathSplit = Split(App.Path, "\")

    

     ' Use deconstructed path to reconstruct where we can find the Tridion directory (this is where all config files should be).

     While Not UCase(pathSplit(count)) = ("TRIDION")

    

         If count = 0 Then

             pathToTridion = pathSplit(0)

        

         Else

             pathToTridion = pathToTridion & "\" & pathSplit(count)

        

         End If

        

         count = count + 1

 

     Wend

       

     getPathToTridionFolder = pathToTridion & "\" & "Tridion"

    

     Exit Function

    

TridionPathError:

    

     Call WriteEventToLog("Error constructing path to Tridion home directory.", Err.Description, "1")

End Function    

 

6.3      Read-In and retrieve values from a Configuration File.

 

Almost every Event System or Workflow DLL must have configurable parameters that are stored in an XML configuration file. Here is a typical example of how to read in this file and retrieve values for use with your code.

 

 

'***********************************************************

'    getConfiguration

'

'    Description: Draws from configuration file found in the Tridion\config directory.

'

'***********************************************************

Private Sub getConfiguration()

 

     Dim objXml As DOMDocument40

     Dim objRootElement As IXMLDOMElement

     Dim booLoadConfig As Boolean

     Dim pathToConfigFile As String

    

     On Error GoTo ConfigError

     Set objXml = New DOMDocument40

    

     pathToConfigFile = getPathToTridionFolder() & "\config\" & yourConfigurationFileName

    

     booLoadConfig = objXml.Load(pathToConfigFile)

 

     If Not booLoadConfig Then

        

         ' The config file cannot be found, write to log and exit.

         Call WriteEventToLog("Configuration file cannot be found or its contents are not correct. Ensure " & yourConfigurationFileName & " is in the Tridion\config directory and that it is valid XML.", Err.Description, "1")

         Exit Sub

        

     End If

    

     ' <configuration> root node.

     Set objRootElement = objXml.firstChild

    

     ' Retrieve

     Set oPublications = objRootElement.selectNodes("/configuration/publication")

     C_CONFIGURED_LOG_PATH = objRootElement.selectSingleNode("/configuration/log/@path").nodeTypedValue

     C_EVENT_LOG_LEVEL = objRootElement.selectSingleNode("/configuration/log/@level").nodeTypedValue

 

     ' Empty Memory

     If IsObject(objXml) Then Set objXml = Nothing

     If IsObject(objRootElement) Then Set objRootElement = Nothing

 

     Exit Sub

    

ConfigError:

 

     Call WriteEventToLog("Configuration Error", Err.Description, "1")

     Exit Sub

End Sub

 

 

 

6.4      Send an Email from an Event/Workflow.

 

Often emails are sent from Workflow during Translation. Here is an example of sending a non-HTML email. To make an HTML email, refer to http://msdn.microsoft.com/libraryand search for CDO.Message.

 

 

'***********************************************************

'    sendEmail(smtpServer As String, smtpConnectionTimeout As Integer, toAddress As String, fromAddress As String, subject As String, nonHTMLBody As String, HTMLBody As String)

'

'    Description: Sends an email with configured parameters.

'

'***********************************************************

Public Sub sendEmail(smtpServer As String, smtpConnectionTimeout As String, toAddresses As String, ccAddresses As String, fromAddress As String, subject As String, nonHTMLBody As String)

 

     On Error GoTo EmailError

    

     Dim iMSG As CDO.Message

     Dim iConf As CDO.Configuration

     Dim Flds As Object

 

     Set iMSG = CreateObject("CDO.Message")

     Set iConf = CreateObject("CDO.Configuration")

 

     Set Flds = iConf.fields

 

     ' Set the CDOSYS configuration fields to use the SMTP server (port 25)

    

     With Flds

         .Item("http://schemas.microsoft.com/cdo/configuration/sendusing") = cdoSendUsingPort

         .Item("http://schemas.microsoft.com/cdo/configuration/smtpserver") = smtpServer

         .Item("http://schemas.microsoft.com/cdo/configuration/smtpconnectiontimeout") = smtpConnectionTimeout

         .Item("http://schemas.microsoft.com/cdo/configuration/smtpauthenticate") = cdoBasic

         .Update

     End With

    

     ' Apply the settings to the message, fill the message fields, add the 'download' file, and actually send the message

     With iMSG

         Set .Configuration = iConf

         .To = toAddresses

         .Cc = ccAddresses

         .From = fromAddress

         .subject = subject

         .TextBody = emailBody & defaultInfo

         .Send

     End With

    

  

     ' Empty Memory

     If IsObject(iMSG) Then Set iMSG = Nothing

     If IsObject(iConf) Then Set iConf = Nothing

     If IsObject(Flds) Then Set Flds = Nothing

    

     Exit Sub

    

EmailError:

 

     Call WriteEventToLog("Email Error", Err.Description, C_EVENT_LOG_LEVEL)

     Exit Sub

    

End Sub

 

6.5      Impersonating in Events Code

 

To perform impersonation, apply the code in the Event System (file: Event.cls). The impersonation is part of an imagined routine called ITCMEvents_OnItemDoSomethingPrePostPost, which takes an item of undefined type as a parameter. 

 

Dim sActiveUser As String

Private Property Let ITCMEvents_identity(ByVal identity As String)

 

             sActiveUser = identity

 

             Call LogEvent("Identity of current user: " & identity, TDSDefines.severityInfo)

 

End Property

 

 

 

Private Sub ITCMEvents_OnItemDoSomethingPrePostPost(ByVal item As .)

 

             Dim oTDSE As TDS.TDSE

 

             Set oTDSE = New TDS.TDSE

 

             Call oTDSE.Impersonate (sActiveUser)

 

             Call oTDSE.Initialize

 

End Sub

                                

 

6.6      Display all content of a Component in an email (text only)

 

Regularly, you are required to create components within a workflow and send emails to Reviewers and other Editors with current components that were updated in workflow. 

 

Here are some functions that put together a general summary in a string.

 

Notes: 

  • For rich text fields, you will need to apply an XSLT to remove x/html tags.
  • For component and multimedia component links, only it's title and name are given. You could easily extend this code to feed the linked component into this function recursively.   

 

'***********************************************************

'    Private Function getComponentDetails(oWorkItem As WorkItem) As String

'

'    Description: Retrieves up-to-date workflow component details.

'

'***********************************************************

Private Function getComponentDetails(oComponent As Component) As String

    

     On Error GoTo compDetailsError

    

     Dim oFields As ItemFields

     Dim result

    

     result = vbLf & vbLf & "Component Title: " & oComponent.Title & vbLf & "Component ID: " & oComponent.id & vbLf & vbLf

    

     If Not oComponent.fields Is Nothing And Not oComponent.fields.count = 0 Then

        

         Set oFields = oComponent.fields

         result = result & DisplayComponentFields(oComponent, oFields)

        

     End If

    

     getComponentDetails = result

    

     Exit Function

    

compDetailsError:

 

     Call WriteEventToLog("Configuration Error retrieving Component details.", Err.Description, "1")

     Exit Function

End Function

 

'***********************************************************

'    DisplayComponentFields(ByVal iComp, ByRef fields) As String

'    Author: Tridion w/ additions from James English (Tridion PS)

'    Description: Displays all fields of a component (caters for all field types). Text Only output (no HTML)

'***********************************************************

Private Function DisplayComponentFields(ByVal iComp, ByRef fields) As String

     Dim lLngFieldNum, lField, lValue

     Dim result As String

    

     On Error GoTo displayError

    

     result = ""

 

     lLngFieldNum = 1

 

     For Each lField In fields

    

         If Not lField.Value Is Nothing Then

             If Not lField.Value.count = 0 Then

                 result = result & lField.Description & ":" & vbLf

        

                 Select Case lField.FieldType

                 ' Multi Line Text field

                 Case 0

                     result = result & ConcatenateValues(lField) & vbLf & vbLf

        

                 ' Number/Date/Single Line Text/Keyword field

                 Case 1, 2, 7, 9

                     result = result & ConcatenateValues(lField) & vbLf & vbLf

        

                 ' Schema Embed field

                 Case 3

                     If Not lField.Value.count = 0 Then

                         result = result & "Embedded Schema:" & vbLf

                         For Each lValue In lField.Value

                             result = result & DisplayComponentFields(iComp, lValue) & vbLf & vbLf

                         Next

                         result = result & vbLf

                     End If

        

                 ' Component Link field

                 Case 4

                     If Not lField.Value.count = 0 Then

                         For Each lValue In lField.Value

                             If Not lValue Is Nothing Then

                                 result = result & "Linked Component:" & vbLf

                                 result = result & "Component Title: " & lValue.Title & vbLf

                                 result = result & "Component ID: " & lValue.id & vbLf

                             End If

                             result = result & vbLf

                         Next

                         result = result & vbLf

                     End If

                    

                 ' Multimedia link field

                 Case 5

                     If Not lField.Value.count = 0 Then

                         For Each lValue In lField.Value

                             If Not lValue Is Nothing Then

                                 result = result & "Multimedia Component:" & vbLf

                                 result = result & DisplayComponent(lValue) & vbLf

                                

                             End If

                          Next

                     End If

                    

                 ' Format text field

                 Case 6

                     For Each lValue In lField.Value

                         result = result & lValue & vbLf & vbLf

                     Next

                    

        

                 ' External link field

                 Case 8

                     If Not lField.Value.count = 0 Then

                         For Each lValue In lField.Value

                             result = result & lValue & vbLf

                         Next

                         result = result & vbLf

                     End If

                    

                 End Select

        

                

                 lLngFieldNum = lLngFieldNum + 1

             End If

         End If

        

     Next

    

     DisplayComponentFields = result

    

 

     Exit Function

    

displayError:

     Call WriteEventToLog("Error in writing out component details for email.", Err.Description, "1")

        

End Function

'***********************************************************

'    DisplayComponent(ByVal iComp) As String

'    Author: Tridion w/ additions from James English (Tridion PS)

'    Description: Displays all fields of a component (caters for all field types). Text Only output (no HTML)

'***********************************************************

Private Function DisplayComponent(ByVal iComp) As String

     Dim result

    

     result = ""

    

     result = result & iComp.Title & ", based on Schema " & iComp.schema.Title & vbLf & vbLf

    

    

     If iComp.IsMultimediaComponent Then

         If Not iComp.MetadataFields Is Nothing Then

             If Not iComp.MetadataFields.count = 0 Then

                 'Multimedia components only use Metadata fields

                 result = result & "Metadata Fields:" & vbLf

                 result = result & DisplayComponentFields(iComp, iComp.MetadataFields) & vbLf & vbLf

             End If

         End If

     Else

         result = result & "Component Fields:" & vbLf

            

         If iComp.IsBasedOnTridionWebSchema = True Then

             result = result & DisplayComponentFields(iComp, iComp.fields) & vbLf & vbLf

         End If

        

         If Not iComp.MetadataFields Is Nothing Then

             If Not iComp.MetadataFields.count = 0 Then

            

                 result = result & "Metadata Fields:" & vbLf

                 result = result & DisplayComponentFields(iComp, iComp.MetadataFields) & vbLf & vbLf

             End If

         End If

     End If

 

    

     DisplayComponent = result

    

End Function

 

'***********************************************************

'    ConcatenateValues(ByRef field)

'    Author: Tridion w/ additions from James English (Tridion PS)

'    Description: Concatenates values of multivalue fields and appends them to a string.

'***********************************************************

Function ConcatenateValues(ByRef field)

     Dim lValue, lStrFirst, lStrOutput, lStrValue

 

     lStrValue = ""

     lStrOutput = ""

     lStrFirst = True

 

     For Each lValue In field.Value

         If lStrFirst Then

             lStrFirst = False

         Else

             lStrOutput = lStrOutput & vbLf

         End If

 

         lStrValue = lValue

         lStrOutput = lStrOutput & lStrValue

     Next

 

     ConcatenateValues = lStrOutput

End Function

6.7      Autopublishing: A full typical example.

 

The auto page creation and publish functionality will make a page based on a component you save. The functionality is configurable via a metadata schema, which should be attached to a folder. The metadata values are checked each time a component is saved in the event system.

 

The functionality works in the following way: If the metadata values are filled with correct values the event system will check if a page already exists. If the page doesn't yet exist, it will create the page in a specific structuregroup. The structuregroup uri is one of the values that need to be given in the metadata fields. If the page already exists, the page is republished or not, depending on the meta data. An additional index page can be republished also.

 

                                      ' The TDS constants

Const ItemTypeNull = 0

 

Const ItemTypePublication = 1

Const ItemTypeFolder = 2

Const ItemTypeStructureGroup = 4

Const ItemTypeSchema = 8

Const ItemTypeComponent = 16

Const ItemTypeComponentTemplate = 32

Const ItemTypePage = 64

Const ItemTypePageTemplate = 128

Const ItemTypeTargetGroup = 256

Const ItemTypeCategory = 512

Const ItemTypeKeyword = 1024

Const ItemTypeTemplateBuildingBlock = 2048

 

Const ItemTypePublicationTarget = 65537

Const ItemTypeTargetType = 65538

Const ItemTypeTargetDestination = 65540

Const ItemTypeMultimediaType = 65544

Const ItemTypeUser = 65552

Const ItemTypeGroup = 65568

Const ItemTypeScheduleItem = 65600

Const ItemTypeDirectoryGroupMapping = 65792

Const ItemTypeDirectoryService = 65664

Const ItemTypeMultipleOperations = 66048

Const ItemTypeApprovalStatus = 131073

 

Const ItemTypeWFManagers = 13

Const ItemTypeWFParticipants = 240

Const ItemTypeWFProcessDefinition = 131074

Const ItemTypeWFProcessInstance = 131076

Const ItemTypeWFProcessHistory = 131080

Const ItemTypeWFActivityDefinition = 131088

Const ItemTypeWFActivityInstance = 131104

Const ItemTypeWFActivityHistory = 131136

Const ItemTypeWFWorkItem = 131200

Const ItemTypeWFTypes = 131326

 

Const ItemTypeClassMask = 196608

Const ItemTypeAll = 262143

 

 

Const OpenModeView = 1

Const OpenModeEdit = 2

Const OpenModeEditWithFallback = 3

Const OpenModeViewOrEdit = 4

Const OpenModeNewItem = 5

 

XMLReadNull = 0

 

 

Sub OnComponentSavePost(ByVal component, ByVal doneEditing)

 

     Dim objStructGroup

     Dim objFolder

     Dim objPublication: Set objPublication = Nothing

     Dim objPage

     Dim objCT

     Dim objTDSE: set objTDSE = Nothing

     Dim objFields

     Dim colTargetURI

     Dim colPageToPublishURI

     Dim blnPublish

     Dim blnPage

     Dim lItem

     Dim arrTargetURIs()

     Dim strCTuri

     Dim strSGuri

     Dim strPageURI

     Dim varPageToPublishURI

     Dim i

     Dim lID

     Dim strTitle

     Dim strFolderTitle

 

     ' First check if we are done editing

     If doneEditing Then

 

         ' Load the necessary objects

         Set objFolder = component.OrganizationalItem

         Set objFields = objFolder.MetadataFields

        

         ' Set the collections to nothing

         Set colPageToPublishURI = Nothing

         Set colTargetURI = Nothing

        

         ' Check the values

         If Not objFields.item("componenttemplateuri") Is Nothing Then

             strCTuri = objFields.item("componenttemplateuri").Value(1)

         End If

         If Not objFields.item("structuregroupuri") Is Nothing Then

             strSGuri = objFields.item("structuregroupuri").Value(1)

         End If

         If Not objFields.item("targeturi") Is Nothing Then

             Set colTargetURI = objFields.item("targeturi").Value

             ReDim arrTargetURIs(colTargetURI.Count - 1)

             For i = 0 To UBound(arrTargetURIs)

                arrTargetURIs(i) = colTargetURI.item(i + 1)

             Next

             blnPublish = True

         End If

         If Not objFields.item("pageuri") Is Nothing Then

             Set colPageToPublishURI = objFields.item("pageuri").Value

             blnPage = True

         End If

        

        

         ' Code for automatic page creation

         If strCTuri <> "" And strSGuri <> "" Then

        

             Set objPublication = component.publication

        

             If Not component.Info.HasUsingItems(component.publication, ItemTypePage) Then

            

                 Set objTDSE = CreateObject("TDS.TDSE")

                 objTDSE.Initialize

        

                 strFolderTitle = objFolder.Title

                 strTitle = component.Title

            

                 Set objStructGroup = objTDSE.GetObject(strSGuri, OpenModeView, objPublication)

                

                 Set objPage = objTDSE.GetNewObject(ItemTypePage, objStructGroup, objPublication)

                

                 objPage.FileName = TDSEvents.ConvertTitleToFileName(strTitle)

                 objPage.Title = strTitle

                

                 objPage.ComponentPresentations.Add component, strCTuri

                

                 objPage.Save (True)

                

                 If blnPublish = True Then

                     Call objPage.Publish(arrTargetURIs(0), False, False, False)

                 End If

    

                 strPageURI = objPage.ID

                

                 Call TDSEvents.WriteEventToLog("OnComponentSavePost", "Sucessfully created new page" & strTitle & " with ID = " & strPageURI)

        

             Else

                 If blnPublish = True Then

                     component.Publish arrTargetURIs(0), False, False, False

                 End If

             End If

         End If

        

        

         If blnPage = True Then

             ' Publish the rest of the pages

 

             ' It could have already be initialized

             If ((objPublication Is Nothing) Or (objTDSE Is Nothing)) Then

                 Set objTDSE = CreateObject("TDS.TDSE")

                 objTDSE.Initialize

                 Set objPublication = component.publication

             End If

            

             For Each varPageToPublishURI In colPageToPublishURI

                 If varPageToPublishURI <> "" Then

    

                     Set objPage = objTDSE.GetObject(varPageToPublishURI, OpenModeView, objPublication, XMLReadNull)

    

                     If blnPublish = True Then

                         objPage.Publish arrTargetURIs(0), False, False, False

                     End If

    

                 End If

             Next

         End If

     End If

    

     Set objPage = Nothing

     Set objCT = Nothing

     Set objFolder = Nothing

     Set objStructGroup = Nothing

     Set objPublication = Nothing

     Set objTDSE = Nothing

     Set objFields = Nothing

     Erase arrTargetURIs

 

End sub

 

 

Here is the metadata schema for this autopublish functionality.

 

                                     <xsd:schema targetNamespace="uuid:C6E960F2-B8DA-4334-A59A-870CA1C00C44" elementFormDefault="qualified" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:tcmi="http://www.tridion.com/ContentManager/5.0/Instance" xmlns="uuid:C6E960F2-B8DA-4334-A59A-870CA1C00C44">

      <xsd:annotation>

            <xsd:appinfo>

                 <tcm:Labels xmlns:tcm="http://www.tridion.com/ContentManager/5.0">

                      <tcm:Label ElementName="componenttemplateuri" Metadata="true">Component template URI</tcm:Label>

                      <tcm:Label ElementName="structuregroupuri" Metadata="true">Structuregroup URI</tcm:Label>

                      <tcm:Label ElementName="targeturi" Metadata="true">Target URIs</tcm:Label>

                      <tcm:Label ElementName="pageuri" Metadata="true">Page URI</tcm:Label>

                 </tcm:Labels>

            </xsd:appinfo>

      </xsd:annotation>

      <xsd:import namespace="http://www.tridion.com/ContentManager/5.0/Instance"/>

      <xsd:element name="Metadata">

            <xsd:complexType>

                 <xsd:sequence>

                      <xsd:element name="componenttemplateuri" minOccurs="1" maxOccurs="1" type="xsd:normalizedString">

                            <xsd:annotation>

                                 <xsd:appinfo/>

                            </xsd:annotation>

                      </xsd:element>

                      <xsd:element name="structuregroupuri" minOccurs="1" maxOccurs="1" type="xsd:normalizedString">

                            <xsd:annotation>

                                 <xsd:appinfo/>

                            </xsd:annotation>

                      </xsd:element>

                      <xsd:element name="targeturi" minOccurs="1" maxOccurs="unbounded" type="xsd:normalizedString">

                            <xsd:annotation>

                                 <xsd:appinfo/>

                            </xsd:annotation>

                      </xsd:element>

                      <xsd:element name="pageuri" minOccurs="1" maxOccurs="unbounded" type="xsd:normalizedString">

                            <xsd:annotation>

                                 <xsd:appinfo/>

                            </xsd:annotation>

                      </xsd:element>

                 </xsd:sequence>

            </xsd:complexType>

      </xsd:element>

</xsd:schema>

                                                                

6.8      Autopublishing: Convert Page Title to FileName

 

When autopublishing components onto pages, you will always need this function. When you create a page, it usually inherits the title of the component you've just created (e.g. as part of workflow). Most titles don't make suitable a suitable filename for a page. This function runs a regular expression over the Title and returns a filename that has "offending" characters (e.g. whitespaces and anything else) replaced by underlines.

 

 

'***********************************************************

'    TitleToName(strWhat)

'    Author: Quirijn Slings (Tridion PS)

'    Description:

'***********************************************************

Private Function TitleToName(strWhat)

     Dim RE As RegExp

    

     Set RE = New RegExp

    

     RE.Pattern = "[^a-zA-Z0-9\-_.]"

    

     RE.Global = True

    

     TitleToName = RE.Replace(strWhat, "_")

 

End Function

6.9      Automated Decision Activity: Get next activity ID

 

In every automated decision, at some point you must decide programmatically which activity to go to next. This is done by ActivityInstance.FinishActivity(finishMessage, ID of next activity).

 

The following function retrieves the second parameter to this call. The function requires the next activity's name in the workflow diagram and it is strongly recommended that this be configurable should the workflow process definition change in the future.

 

'***********************************************************

'    getNextActivityID

'    Description: Takes the current activity instance, the configured name of the intended next activity and returns the URI of that activity.

'

'***********************************************************

Private Function getNextActivityID(currentActivityInstance As ActivityInstance, nextActivityName As String)

    

     On Error GoTo WFError

 

     Dim activityDefinitions As activityDefinitions   ' ActivityDefinitions based on the ProcessDefinition for this workflow.

     Dim activityDefinition As activityDefinition     ' ActivityDefinition for the above activity definitions collection.

    

     ' Get a reference to all the activities defined for this workflow

     Set activityDefinitions = currentActivityInstance.ProcessInstance.ProcessDefinition.activityDefinitions

    

     ' Iterate through each activitydefinition and match the configured name for the intended next activity (the next activity)

     For Each activityDefinition In activityDefinitions

         If activityDefinition.Title = nextActivityName Then

            

             getNextActivityID = activityDefinition.id

            

             ' Empty Memory

             If IsObject(activityDefinitions) Then Set activityDefinitions = Nothing

             If IsObject(activityDefinition) Then Set activityDefinition = Nothing

            

             Exit Function

         End If

     Next

    

     ' Empty Memory

     If IsObject(activityDefinitions) Then Set activityDefinitions = Nothing

     If IsObject(activityDefinition) Then Set activityDefinition = Nothing

    

     ' No activities in this workflow definition match the name given in the configuration file. Report the error and exit.

     GoTo WFError

     Exit Function

    

WFError:

     Call WriteEventToLog("Workflow Error in Automated Decision Activity.", Err.Description, "1")

End Function

6.10Carry over Finish Message from Activity before an Automated Activity.

 

For each automated activity by default, you will find that it will FinishActivity "Automated activity has finished" by default. If an editor creates a component and passes it to an automated activity then passes it again to a reviewer for that component, the initial finish message from the creator is lost.

 

In most Workflows, we get around this problem by passing the initial finish message on as the finish message of the automated activity. That way, reviewers can still see the message given to them by the content creators.

 

'***********************************************************

'    getPreviousFinishMsg(currentActivityID As String) As String

'

'    Description: Retrieves finish message of the activity that came before the given activity.

'    Usage Note: IMPORTANT - Only use this function if you are absolutely positive there is a valid finished activity before the activity you provide.

'

'***********************************************************

Private Function getPreviousFinishMsg(oActivityInstance As ActivityInstance) As String

 

     Dim finishMessage As String

    

     finishMessage = oActivityInstance.ProcessInstance.ActivityInstances(oActivityInstance.Position - 1).finishMessage

    

     getPreviousFinishMsg = finishMessage

 

End Function