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