By Mats Halfvares, the Content Studio development team


Breaking changes between Content Studio version 5.1 and 5.2

Starting with version 5.2 the ContentStudio.EventActions.ICSEventHandler interface is no longer defined in the main CSServer5 assembly. It has been moved to a new assembly, CS5Interfaces, in order to address a version compatibility problem that occured when Content Studio is upgraded.

for more information see Breaking interface changes in version 5.2.html


Synchronous event handlers in Content Studio

Synchronous event handlers is a powerful functionality that executes when a user performs a certain action in Content Studio. They behave very much the same way as a regular event in Windows program or as a trigger in SQL Server. The following synchronous events are supported in Content Studio 5.3.

OnBeforeDocumentSave
This event gets excuted before a document has been saved or before any processing has been made. Use this event to check a document content before it has reached any of the Content Studio save or create methods.
New in version 5.2.
Supplies the content to be saved. This event can prevent a document from beeing saved or created but cannot roll back any change made in Content Studio.
New in version 5.3.
In this version a new interface for event handlers is supported if implemented by the handler. This makes it possible for a handler to change the content of the document that is about to be saved. The event handler can then supply the changed content back to Content Studio before it goes on with the save operation. This is only supported in the OnBeforeDocumentSave event.
OnDocumentPreview
Added in version 5.3.
The event handler receives limited data (similar to the OnBeforeDocumentSave save event) about the document and occurs when a document is being previewed from the Content Studio editor. When the event occurs all of the temporary files has been generated and this type of an event handler cannot prevent preview from accuring. The event handler can return a custom identifier and a custom url that can be used when previewing data in an external content store.
OnDocumentPreviewDispose
Added in version 5.3.
The event handler receives limited data (similar to the OnBeforeDocumentSave save event) about the document and occurs the Content Studio editor removes the temporary files after a preview operation. This event can be used to clean up temporary data in an external content store.
OnBeforeDocumentSynchronize
New in version 5.2
No content is supplied. This event can prevent a document from beeing synchronized but will not roll back any already processed document.
OnDocumentApprove
Starting with version 5.2, this event now supplies the just approved content of the document. In earlier versions the content was empty.
OnDocumentCheckIn
New in version 5.2
The draft content is supplied.
This event is fired when a document is checked in and is atomic.
OnDocumentCheckOut
New in version 5.2
The draft content is supplied.
This event is fired when a document is checked out and is atomic.
OnDocumentCreate
No content is supplied since this event is raised when the meta data is created before the actual content is processed. To access the content you must use the OnDocumentSave event that is raised just after this event. This event is is atomic.
OnDocumentDelete
This event is fired when a document is thrown into the recycling bin
If existing, the approved content is supplied; otherwise the draft content is passed. For corrupt documents where no content exists content is empty. This event is atomic.
OnDocumentDestroy
This event is fired when a document in the recycling bin is deleted.
No content is supplied since the document has already been deleted. This event is atomic.
OnDocumentMoveInHierarchy
New in version 5.2
Content is not supplied since this event can occur on a document not checked out and for this reason Content Studio cannot determine which type of content to read.
This event is fired when a document is moved within a document hierarchy (i.e its parent document is changed or its position is changed). This event is atomic.
OnDocumentReject
New in version 5.2
The draft content is supplied.
This event is fired when a document that are on versioning is rejected by an editor. This event is atomic.
OnDocumentRestore
New in version 5.2
The draft content is supplied.
This event is fired when a document is restored from the recycling bin. This event is atomic.
OnDocumentRevision
New in version 5.2
The draft content is supplied.
This event is fired when a document is sent on versioning so it can be approved by an editor. This event is atomic.
OnDocumentRevisionRestore
New in version 5.3
The draft content is supplied.
This event is fired when an older version of a document is restored and its content is copied into the draft content of the document. This event is atomic.
OnDocumentSave
The draft content is supplied. This event is atomic.
OnXmlIndexSave
New in version 5.2
Important
For more information on the content supplied in this event see the OnXmlIndexSave event data.html knowledgebase article.
This event is not atomic to its nature which means that it will NOT rollback the document save operation nor will it rollback the previous xml indexing change operation.
OnXmlIndexFieldDelete
New in version 5.2
Important
For more information on the content supplied in this event see the OnXmlIndexFieldDelete event data.html knowledgebase article.
This event is not atomic to its nature which means that it will NOT rollback the previous xml indexing delete operation.

A synchronous event in Content Studio is raised whenever any of the actions above occurs on a category were an event handler has been registered. The main advantage with these events is that they are atomic - i.e. they can roll back any change that the action made to Content Studio. If an event handler triggers when any document in the Test1 category is approved and the event handler is set up to synchronize data with an external system in an atomic way, it important that the event handler can undo the preceding approve procedure made in Content Studio. Since synchronous event handler has the capability to roll back events in Content Studio this type of event handler has the character of being OnBefore event handlers.

Important information to implementers!

Versions prior to 5.2 RC1

Never try perform any read or update operation on the document that triggered the event in a Content Studio synchronous event handler. This is true for all events that calls their event handlers from within an Sql transaction. This can cause serious problems and Content Studio ends up in a dead lock situation since the document affected is locked by Sql Server until the transaction is committed. When your event handler tries to read the locked document it will wait until the document is released by the lock holder, which will not happend since the caller is waiting for the event handler to finish.
The affected events are:

  • OnDocumentApprove
  • OnDocumentSave
  • OnDocumentDelete
  • OnDocumentDestroy
  • OnDocumentCreate

Versions 5.2 RC1 and later

Read operations on the triggering document are supported, write operations are not. When you read properties for the affected document you will get the values as they will appear after that the main transaction has commited the changes. Deleted documents will be deleted and approved documents will appear as beeing approved even though that the changes might be rolled back by your event handler implementation.

OnDocumentDestroy is a special case. Since the document appears as beeing deleted in the database and its underlying file was deleted by a preceeding delete operation there is no way of getting information of the document deleted other that the properties passed in to the event handler.

Recursive events

Remember that that it is possible to create recursive events that can effectively lock up Content Studio entirely until a StackOverflowException occurs. This will occur if, for example, a OnDocumentSave event handler creates a new or updates an existing document in the same category as itself. The first document's OnDocumentSave will trigger the same event handler on the second document which in its turn will trigger OnDocumentSave on a third document and so forth. In these cases you must handle these issues in you implementation to avoid uncontrolled recursions.

In your handler you can easily check for the number of recursions by using the fact that each recursive call to your handler will be executed on the same thread. You can use a static counter field in your handler marked with the ThreadStatic attribute. This will ensure that only instances running on the same thread will share the value.

public class OnDocumentSave : DocumentSaveSyncHandler
{  
    private readonly int MaxRecursions = 1;
    
    [ThreadStatic]
    private static int Recursions;
    
    protected override void Init()
    {
       base.Init();
       Recursions++;
    }
    protected override void DoWork()
    {
       //stop execution if the number of recursions has 
       //exceeded the limit.
       if (Recursions > MaxRecursions)
          return;
       //Working code follows
    }         
}   
            
Public Class OnDocumentSave 
    Inherits DocumentSaveSyncHandler
    
    Private Const MaxRecursions As Integer = 1
    
    <ThreadStatic>
    Private Shared Recursions As Integer
    
    Protected Overloads Overrides Sub Init()
       MyBase.Init()
       Recursions += 1
    End Sub
    Protected Overloads Overrides Sub DoWork()
       'stop execution if the number of recursions has 
       'exceeded the limit.
       If Recursions > MaxRecursions Then
           Return;
       'Working code follows
    End Sub         
End Class   
            

Creating an event handler in Content Studio version 5.2 and later

In earlier versions of Content Studio you had to create synchronous event handlers from scratch by implementing the ContentStudio.EventActions.ICSEventHandler interface. This is, of course, still possible in Content Studio 5.2 and later but requires a lot of tedious Xml parsing and type checking. Starting with Content Studio version 5.2 there is a complete library of base classes avaliable that makes writing a synchrous event handler far easier and fully object oriented. In addition to this, support for the IDisposable interface has been implemented. This means that Content Studio will call Dispose when implemented. The base class, SynchronousEventHandlerBaseis located in the assembly SyncEvtHand.dll that gets installed in the program files folder of Content Studio (by default C:\Program files\Teknikhuset\Content Studio 5\CSServer).

If you write your event handler in Visual Studio 2005 or later you must create a new class library project and set a reference to both the SyncEvtHand.dll and CS5Interfaces.dll assemblies. These libraries are not directly related to Content Studio and is therefor not subject to versioning problem that can occure between Content Studio product updates. In your class library, you can have as many event handlers as you like but each handler must be implemented in a public class of their own. Each class should extend (inherit from) one of the following abstract base classes.

Event handler base classes
Base class Usage
DocumentPreviewSyncHandler Represents an event handler that handles the OnDocumentPreview synchronous event in Content Studio 5.3 and later.
DocumentPreviewDispose Represents an event handler that handles the OnDocumentPreviewDispose synchronous event in Content Studio 5.3 and later.
BeforeDocumentSaveSyncHandler Represents an event handler that handles the OnBeforeDocumentSave synchronous event in Content Studio 5.2 and later.
BeforeDocumentSynchronizeSyncHandler Represents an event handler that handles the OnBeforeDocumentSynchronize synchronous event in Content Studio 5.2 and later.
DocumentApproveSyncHandler Represents an event handler that handles the OnDocumentApprove synchronous event in Content Studio 5.2 and later.
DocumentCheckInSyncEventHandler Represents an event handler that handles the OnDocumentCheckIn synchronous event in Content Studio 5.2 and later.
DocumentCheckOutSyncEventHandler Represents an event handler that handles the OnDocumentCheckOut synchronous event in Content Studio 5.2 and later.
DocumentCreateSyncHandler Represents an event handler that handles the OnDocumentCreate synchronous event in Content Studio 5.2 and later.
DocumentDeleteSyncHandler Represents an event handler that handles the OnDocumentDelete synchronous event in Content Studio 5.2 and later.
DocumentDestroySyncHandler Represents an event handler that handles the OnDocumentDestroy synchronous event in Content Studio 5.2 and later.
DocumentMoveInHierarchySyncEventHandler Represents an event handler that handles the OnDocumentMoveInHierarchy synchronous event in Content Studio 5.2 and later.
DocumentSaveSyncHandler Represents an event handler that handles the OnDocumentSave synchronous event in Content Studio 5.2 and later.
DocumentRejectSyncEventHandler Represents an event handler that handles the OnDocumentReject synchronous event in Content Studio 5.2 and later.
DocumentRestoreSyncEventHandler Represents an event handler that handles the OnDocumentRestore synchronous event in Content Studio 5.2 and later.
DocumentRevisionSyncEventHandler Represents an event handler that handles the OnDocumentRevision synchronous event in Content Studio 5.2 and later.
DocumentRevisionRestoreSyncEventHandler Represents an event handler that handles the OnDocumentRevisionRestore synchronous event in Content Studio 5.3 and later.
XmlIndexFieldDeleteSyncHandler Represents an event handler that handles the OnXmlIndexFieldDelete synchronous event in Content Studio 5.2 and later.
XmlIndexSaveSyncHandler Represents an event handler that handles the OnXmlIndexSave synchronous event in Content Studio 5.2 and later.

Each class supplies a set of properties representing the document and the calling user. There are also a number of methods you should or can override and the actual work for the developer is to supply his or her programming logic in these methods.

ParseInputXml
Parses the xml passed in to base class implementation of the EventHandler method and loads the properties.
Normally, there is no need to override the base class implementation.
ValidateEvent
Validates the passed in event and determines whether this implementation can handle this event. This method returns false if the wrong type of event invokes the handler which results in an InvalidOperationException.
If you choose to inherit from one of the classes above there is no need override this method since these classes already have taken care of the
validation for you.
ParseCustomData
Called during initialization and provides a possibility for the implementer to analyze and perhaps parse the custom data passed in. Custom data can be supplied by the person that subscribed to the event handler in Content Studio.
Override this method if you need to analyze or parse the custom data passed in.
Init
This method gets called when the base class has initialized all of its data, just before that the actual work is to be done.
Override this method when there is a need for your implementation to do some private initialization such as creating a logging utility or read data from the registry or a configuration file.
DoWork
This is the method in which the actual work is done.
Each event handler must override the method and supply the actual implementation of the event handler.
Finish
This method is called by the base class when the actual work has been done.
Override this method if there is a need to perform some code after that all work has been done. For example you might like to set your own value to the Status , StatusText and Cancel properties.
Note that if an exception is generated in the DoWork method this method is not called. For this reason you should not rely on this method for releasing any resources that needs to be freed, such as file handles. Instead you should override the Dispose method and release these resources there.

Improvements in version 5.3

Version 5.3 introduces a number of improvements to event actions and to the base classes that your handlers inherit from. A number of properties have been added and document content is now avaliable in a more structured way. When dealing with EPT-documents the content now comes as an ICSEptContent implementation where you can query each field by its name or just simply iterate through each one of the fields. In the OnBeforeDocumentSave event you can also change, remove or add any field to the fields collection before Content Studio saves the document. Thus it is now possible to change the content of a document on the server side before it is saved.

The possibility of changing document content that is about to be saved in the OnBeforeDocumentSave event opens up a number of possibilities for the programmer. For example you could combine two or more Ept-fields into another field to create a new view of the data. Before Content Studio 5.3 you had to do these kinds of operations on the client using Javascript code.

The following code shows an handler that combine data from two Ept fields, FirstName and LastName, into a third field, DisplayName. The field DisplayName must exist in the schema or it will be ignored by Content Studio

//Only works in an handler that handles the OnBeforeDocumentSave event
public class MyBeforeDocumentSaveHandler : BeforeDocumentSaveSyncHandler
{
  protected override void DoWork()
  {
     if (DocumentType == DocumentTypes.EPT_Document)
     {
        EptContent["DisplayName"] =
             String.Format("{0} {1}", EptContent["FirstName"], EptContent["LastName"]);
     }         
  } 
}
'Only works in an handler that handles the OnBeforeDocumentSave event
Public class MyBeforeDocumentSaveHandler : BeforeDocumentSaveSyncHandler

  Protected Overrides Sub DoWork()
    If DocumentType = DocumentTypes.EPT_Document Then
        EptContent("DisplayName") = _
            String.Format("{0} {1}", EptContent("FirstName"), EptContent("LastName"))
    End If         
  End Sub
  
End Class

There also has been a great number of properties added to the event handler base classes.

Properties added in version 5.3
Name Description
ApprovedByUser Gets an IPrincipalInfo that represents the user that approved the document that triggered the event. This value is null if the document is not approved yet.
ArchiveDate Gets the archive date of the document that triggered the event. This value can be null to indicate no limit.
CalledByUser Gets an IPrincipalInfo that represents the user that initiated the event.
CheckedOutByUser Gets an IPrincipalInfo that represents the user that has checked out the document that triggered the event. This value is null if the document is not checked out
ContentBinary

Important
This property is currently not implemented and always returns null.
Gets or sets the binary content passed in. This property can be null if no content was supplied from Content Studio or the content represents a regular document or ept document.

CreatedByUser Gets an IPrincipalInfo that triggered the event. This value is null if the document is not yet created (OnBeforeDocumentSave).
DeletedByUser Gets an IPrincipalInfo that represents the user that deleted the document that triggered the event. This value is null if the document is not deleted.
DocumentStatus Gets a IDocumentStatus that specifies a set of different status properties of the document that triggered the event.
For more information about this property see the section below.
DocumentType Gets a DocumentTypes value that specifies the type of document that triggered the event.
EptContent Gets the content of the ept document as an object as Content Studio's implementation of the ICSEptContent interface. For Ept documents only, for other document types this property is null.
ModifiedByUser Gets an IPrincipalInfo that represents the user that modified the document that triggered the event. This value is null if the document is not modified.
PublishDate Gets the publish date of the document that triggered the event.
RejectedByUser Gets an IPrincipalInfo that represents the user that rejected the document that triggered the event. This value is null if the document is not sent for revison and has not been rejected.
SaveOperationArguments Gets an ISaveOperationArguments that contains the argument that was passed in to the save operation operation that triggered the event. This property has a value only for the OnBeforeDocumentSave event, otherwise the value is null. For more information about this property see the section below.
SentForApprovalByUser Gets an IPrincipalInfo represents the user sent the document that triggered the event for revision. This value is null if the document is not sent for revision.

The DocumentStatus property can display status of the affected document.

Properties defined in the IDocumentStatus interface returned by the new DocumentStatus property.
Property name Description
CanBePublished. Gets a value indicating whether the document that triggered the event currently has the flag set that allows it to be published.
HasBeenReviewedInWorkflow. Gets a value indicating whether the document that triggered the event has been reviewed in a workflow controlled by Content Studio Workflow Server.
HasDraft. Gets a value indicating whether the document has a draft content.
HasMetaData. Gets a value indicating whether the document that triggered event the adds meta data content.
IsAuthoredInWorkflow. Gets a value indicating whether the document that triggered the event has been authored in Content Studio Workflow Server.
IsForApprovalInWorkflow. Gets a value indicating whether the document that triggered the event has been approved by Content Studio Workflow Server.
IsForAuthoringInWorkflow. Gets a value indicating whether the document that triggered the event is for authoring in Content Studio Workflow Server.
IsForReviewInWorkflow. Gets a value indicating whether the document that triggered the event is beeing reviewed in a workflow controlled by Content Studio Workflow Server.
IsInRecyclingBin. Gets a value indicating whether the document that triggered the event is in the recycling bin.
IsOnWorkflow. Gets a value indicating whether the document is on workflow.
IsProtected. Gets a value indicating whether the document that triggered the event is protected.
IsRejected. Gets a value indicating whether the document that triggered the event is rejected in workflow.
IsRejectedByWorkflowServer. Gets a value indicating whether the document that triggered the event is rejected by the Content Studio Workflow Server.

The new property SaveOperationArguments which returns as an ISaveOperationArguments interface makes it possible to change some meta data of the document and have Content Studio to save them on the document. This is possible in the OnBeforeDocumentSave event only.

Meta data properties defined in the ISaveOperationArguments interface.
Name Description
ArchiveDate Gets or sets the archive date of the document that triggered the event. This value can be null to indicate that no archive date should be applied.
The value of this property indicates the value that is about to be saved, not necessarily the current value of the document.
BodyProperties Set or gets a value that specifies the content of the BODY tag of the document that triggered the event.
The value of this property indicates the value that is about to be saved, not necessarily the current value of the document.
Introduction Set or gets a value that specifies the introduction content the document that triggered the event.
The value of this property indicates the value that is about to be saved, not necessarily the current value of the document.
Keywords Gets or sets the keywords of the document that triggered the event.
The value of this property indicates the value that is about to be saved, not necessarily the current value of the document.
Marking Set or gets a value that specifies a short description of the document that triggered the event.
The value of this property indicates the value that is about to be saved, not necessarily the current value of the document.
MenuData Set or gets a value that specifies the MenuData for the document that triggered the event. MenuData contains user defined data for menu items and is only applicable if the document acts as a meny node content.
The value of this property indicates the value that is about to be saved, not necessarily the current value of the document.
MenuTarget Gets or sets a value that represents the MenuTarget of the document that triggered the event. MenuTarget contains a user defined target data for menu items and is only applicable if the document acts as a meny node content.
The value of this property indicates the value that is about to be saved, not necessarily the current value of the document.
MenuUrl Set or gets a value that specifies the MenuUrl for the document that triggered the event. MenuUrl contains a user defined Url for menu items and is only applicable if the document acts as a meny node content.
The value of this property indicates the value that is about to be saved, not necessarily the current value of the document.
PublishDate Gets or sets the publish date of the document as passed in by the caller that triggered the event.
The value of this property indicates the value that is about to be saved, not necessarily the current value of the document.
Published Gets or sets a value indicating whether the document that triggered the event can be published.
The value of this property indicates the value that is about to be saved, not necessarily the current value of the document.

The following sample handler shows how to change the PublishDate and the Keywords of a document that is about to be saved.
This code only works in the OnBeforeDocumentSave synchronous event.

//Only works in an handler that handles the OnBeforeDocumentSave event
public class MyBeforeDocumentSaveHandler : BeforeDocumentSaveSyncHandler
{
  protected override void DoWork()
  {         
    //Do not forget to check for null!
    if (SaveOperationArguments != null)
    {
      SaveOperationArguments.PublishDate = DateTime.Today.AddDays(1);
      SaveOperationArguments.Keywords = "Content Studio CMS";
    }
  }
}
'Only works in an handler that handles the OnBeforeDocumentSave event
Public Class MyBeforeDocumentSaveHandler : BeforeDocumentSaveSyncHandler
   Protected Overrides Sub DoWork()
     'Do not forget to check for Nothing!
     If Not SaveOperationArguments Is Nothing Then
        SaveOperationArguments.PublishDate = DateTime.Today.AddDays(1)
        SaveOperationArguments.Keywords = "Content Studio CMS"
     End If
   End Sub
End Class
 

Implementing IDisposable

If you need to release some resources in your handler, you should override the Dispose method, free all the resources and eventually call the base class implementation of this method.
The code sample is taken from the official MSDN documentation provided by Microsoft.

protected override void Dispose(bool disposing) 
{
   if (disposing) 
   {
      // Release managed resources.
   }
   // Release unmanaged resources.
   // Set large fields to null.
   // Call Dispose on your base class.
   base.Dispose(disposing);
}
Protected Overloads Overrides Sub Dispose(disposing As Boolean) 
   If disposed = False Then
      If disposing Then 
        'Release managed resources.
      End If
   End If
   'Release unmanaged resources.
   'Set large fields to null.
   'Call Dispose on your base class.
   Mybase.Dispose(disposing)
End Sub

Handling exceptions in atomic events

All exceptions generated in your handler will bubble up to the caller and when the event is the current database transaction in Content Studio is rolled back to avoid data corruption. You can also cancel the event and rollback any previous uncommitted database changes by setting the Cancel property to true. If you do that you have two options depending on how you use the Status property. All exceptions generated in your handler will bubble up to the caller and when the event handler executes within the current Content Studio database transaction changes are rolled back to avoid data corruption. You can also cancel the event and rollback any previous uncommitted database changes by setting the Cancel property to true indicating to the caller that the event was cancelled by an event handler. This might be sufficient for some cases but in order to provide more detailed information to the caller you can set Status to -1 and provide a description of the error in the Status property. So, for example, if you would like to inform the caller that "No documents can be deleted in this category" this is should be the value of Status . On the web site the caller will see a CSException with this text.

Of course, you also have the possibility to throw other exceptions if you like and sometimes this is the best thing to do when an unexpected problem occurs in your event handler.

Exceptions in non-atomic events

Non-atomic events are those that is not executed in the transaction context of the corresponding Content Studio action. In these type of events the Cancel property is ignored and will not cause the Content Studio action to be rolled back. In addition to this an error in your event handler will not be displayed to the caller, instead errors will be logged to the Content Studio log. The following events are non-atomic.

Show code A sample event handler

The following code sample shows a simple implementation of a handler that implements auditing functionality when a document is successfully approved. It writes information about the approver and the document to a simple text file.

public class OnDocumentApprove : DocumentApproveSyncHandler
{
    private System.IO.StreamWriter writer;

    protected override void Init()
    {
        base.Init();
        //create a StreamWriter that writes to a simple text file
        try
        {
           writer = new System.IO.StreamWriter(@"C:\temp\logging\CSApprove.log", true);
           writer.WriteLine()
           writer.WriteLine("The job has been initialized")
        }
        catch(System.IO.IOException){}
    }
    protected override void DoWork()
    {
        //write the auditing message
        if(writer != null)
        {
           writer.WriteLine(@"{0}\t{1} (id {2}) has been approved by {3} ({4})",
                            DateTime.Now,
                            DocumentName,
                            DocumentId,
                            CallerName,
                            CallerLogOnName);
        }
    }
    protected override void Finish()
    {
        base.Finish();
        Status = 0;
        StatusText = "Success";
        Cancel = false;
        if(writer != null)
           writer.WriteLine("The job has been completed.");
    }

    //Handle disposing of the writer
    private bool disposed;
    protected override void Dispose(bool disposing)
    {
        if (disposing)
        {
            // Release managed resources.
            if (!disposed && writer != null)
            {
                writer.WriteLine("The writer is closing.");
                writer.Dispose();
            }
            disposed = true;
        } 
        base.Dispose(disposing);
    }
}
Public Class OnDocumentApprove 
	    Inherits DocumentApproveSyncHandler
    Private writer As System.IO.StreamWriter 

    Protected Overrides Sub Init()
        Mybase.Init()
        'create a StreamWriter that writes to a simple text file
        Try
            writer = New System.IO.StreamWriter("C:\temp\logging\CSApprove.log", True)
            writer.WriteLine()
            writer.WriteLine("The job has been initialized")
        Catch System.IO.IOException
        End Try
    End Sub

    Protected Overrides Sub DoWork()
        'write the auditing message
        If Not writer Is Nothing Then
           writer.WriteLine("{0}" & vbTab & "{1} (id {2}) has been approved by {3} ({4})", _
                            DateTime.Now, _
                            DocumentName, _
                            DocumentId, _
                            CallerName, _
                            CallerLogOnName)
        End If
    End Sub
    Protected Overrides Sub Finish()
        Mybase.Finish()
        Status = 0
        StatusText = "Success"
        Cancel = False
        If Not writer Is Nothing Then
            writer.WriteLine("The job has been completed.")
        End If
    End Sub

    'Handle disposing of the writer
    Private disposed As Boolean
    Protected Overloads Overrides Sub Dispose(disposing As Boolean)
        If disposing Then       
            'Release managed resources.
            If Not disposed And Not writer Is Nothing
                writer.WriteLine("The writer is closing.")
                writer.Dispose()
                disposed = True
            End If
        End If
        Mybase.Dispose(disposing)
    End Sub
End Class

This should output something like this to the log file.

The job has been initialized
2008-11-27 13:54:27	Test/Doc/Doc1 (id 2261) has been approved by John Doe (COORP\johdoe)
The job has been completed.
The writer is closing.
     

When you have built and tested your handler you are ready to install it in Content Studio. How this is done is described later in this article.

 

Validating, policy based event handlers

Policy based event handlers are available in version 5.2 SP1 and later.

In Content Studio 5.2 SP1 you can create validating event handlers that gets its validating rules through a set of policies. These policies are defined in an XML based format and supplied to the handler from the Command property in the Event Actions dialog.

The format of this XML is strictly controlled by an XML Schema and the following sample code gives an example of a policy set with one policy that limits the size of an uploaded file.

A sample policy set
   
<customData>
  <policies>
    <policy Enabled="true"
             Name="MaxUploadedFileSize"
             Value="23" 
             ValueClass="Mb" 
             Compare="FileSize"
             InterpretValueAs="Maximum"
             Description="Policy: Limits the size of an uploaded file." 
             ViolationMessage="A rule on this web site limits the size of an uploaded file, Please use a smaller file instead. Maximum file size is {0}" />
  </policies>
</customData>
       
The policy XML explained
XPath Description
customData/policies  Defines the start of the policy set. Only one policies element is allowed.
customData/policies/policy Defines a single policy in the set. There can be unlimited number of policies in the set.
customData/policies/policy/@Enabled Defines whether the policy is enabled or not. When the document is evaluated against the policy, only enabled policies are used. this attribute must be supplied and must have one of the acceptable values; true or false.
customData/policies/policy/@Name Defines the name of the policy. All policy declarations must have a name of at least one character and each policy must have a name that is unique in the set of policies.
customData/policies/policy/@Value Defines the value of the policy. This is the value that your event handler will use to compare against some property of the document. This can for example be the maximum allowed length of a file to upload. The attribute must exist but can be empty.
customData/policies/policy/@ValueClass Defines a value that tells your event handler how the policy value should be interpreted. This can be the unit used when determine the size of the file uploaded. In this case @ValueClass might be "" or "Kb", "Mb" or "Gb" to indicate size in Bytes, KiloBytes, MegaBytes or GigaBytes respective. The attribute is optional.
customData/policies/policy/@Compare This attribute provides a way for the caller to inform the handler which check operation to make. Compare is useful if you have more that one policy check to do and you need to inform the handler on what this policy should be checked against. The attribute optional.
customData/policies/policy/@InterpretValueAs

This value tells your event handler how the comparison should be made. This attribute is required and values accepted are defined in the ValueInterpretation enumeration.

Values accepted
Value Meaning
NotApplicable The value cannot be interpreted in any standard way.
Maximum The value in the policy is the maximum value allowed in the document comparison data.
Minimum The value in the policy is the minimum value allowed in the document comparison data.
Equal The value in the policy must be equal to the document comparison data.
NotEqual The value in the policy cannot be equal to the document comparison data.
Contains The value must exist in the document comparison data.
ContainsNot The value may not exist in the document comparison data.
customData/policies/policy/@Description A description of the policy. The attribute is required but can be empty.
customData/policies/policy/@ViolationMessage The message to use as message in the InvalidOperationException that is thrown when a policy is violated. If this message contains the format placeholder {0} this place holder will be replaced by the value that was violated. The attribute optional.

When you have written you policy definition you simply into the Command field in an Event Action definition.

The Event actions property dialog with a policy definition.

The Event actions property dialog with a policy definition.

When you implement your own policy based event handler you typically derive from one of the bases classes provided.

Policybased event handler base classes
Base class Usage
BeforeDocumentSavePolicySyncHandler Represents a policy based event handler that handles the OnBeforeDocumentSave synchronous event in Content Studio 5.2 SP1 and later.
BeforeDocumentSynchronizePolicySyncHandler Represents a policy based event handler that handles the OnBeforeDocumentSynchronize synchronous event in Content Studio 5.2 SP1 and later.
DocumentApprovePolicySyncHandler Represents a policy based event handler that handles the OnDocumentApprove synchronous event in Content Studio 5.2 SP1 and later.
DocumentCheckInPolicySyncEventHandler Represents a policy based event handler that handles the OnDocumentCheckIn synchronous event in Content Studio 5.2 SP1 and later.
DocumentCheckOutPolicySyncEventHandler Represents a policy based event handler that handles the OnDocumentCheckOut synchronous event in Content Studio 5.2 SP1 and later.
DocumentCreatePolicySyncHandler Represents a policy based event handler that handles the OnDocumentCreate synchronous event in Content Studio 5.2 SP1 and later.
DocumentDeletePolicySyncHandler Represents a policy based event handler that handles the OnDocumentDelete synchronous event in Content Studio 5.2 SP1 and later.
DocumentDestroyPolicySyncHandler Represents a policy based event handler that handles the OnDocumentDestroy synchronous event in Content Studio 5.2 SP1 and later.
DocumentMoveInHierarchyPolicySyncEventHandler Represents a policy based event handler that handles the OnDocumentMoveInHierarchy synchronous event in Content Studio 5.2 SP1 and later.
DocumentSavePolicySyncHandler Represents a policy based event handler that handles the OnDocumentSave synchronous event in Content Studio 5.2 SP1 and later.
DocumentRejectPolicySyncEventHandler Represents a policy based event handler that handles the OnDocumentReject synchronous event in Content Studio 5.2 SP1 and later.
DocumentRestorePolicySyncEventHandler Represents a policy based event handler that handles the OnDocumentRestore synchronous event in Content Studio 5.2 SP1 and later.
DocumentRevisionPolicySyncEventHandler Represents a policy based event handler that handles the OnDocumentRevision synchronous event in Content Studio 5.2 SP1 and later.
DocumentRevisionRestorePolicySyncEventHandler Represents a policy based event handler that handles the OnDocumentRevisionRestore synchronous event in Content Studio 5.3 and later.
XmlIndexFieldDeletePolicySyncHandler Represents a policy based event handler that handles the OnXmlIndexFieldDelete synchronous event in Content Studio 5.2 SP1 and later.
XmlIndexSavePolicySyncHandler Represents a policy based event handler that handles the OnXmlIndexSave synchronous event in Content Studio 5.2 SP1 and later.

There are at least two methods you must override in your event handler; ValidatePolicy which performs the individual validating operation and the DoWork method that can perform additional work, besides validation of document data, that your event handler might need to do. The PerformPolicyValidation method starts the actual validation process and gets called just before DoWork in the base class' implementation of the Init method. If you override Init it is important that your implementation calls the base class implementation as a part of your code. If not, PerformPolicyValidation will never be called an no validation will take place.

The actual part of the validation work takes place in your implementation of the ValidatePolicy method, which you must override. If you have more that one policy in the policy set this method gets called once for each enabled policy.

Show code A sample validating event handler

The follwing sample shows a complete implementation of a validating event handler that handles the OnBeforeDocumentSave event and uses a policy that was demonstrated earlier in this article. This class extends the BeforeDocumentSavePolicySyncHandler base class which makes it much simpler to write event handlers for this particular event.

using System;
using ContentStudio.EventActions.SynchronousEventHandlers;
using System.IO;

namespace SyncTestHandlers
{
    public class UploadFileSizeCheckingSyncHandler : BeforeDocumentSavePolicySyncHandler
    {
        protected override void Init()
        {
            //get the document type?
            var catReader = new ContentStudio.Document.CategoryReader();
            if (catReader.GetDocumentType(ConnectionId, CallerSessionId, CategoryId) != ContentStudio.Document.DocumentTypes.File)
                throw new InvalidOperationException("This event handler implementation can only be used on categories that contains uploaded files");
            //make sure that the base class init method gets executed properly
            base.Init();
        }
        protected override bool ValidatePolicy(Policy policy)
        {
            if (policy.Enabled == false)
                return true;
            //Validate the file size against the rule that checks the file size
            if (String.Equals("FileSize", policy.Compare, StringComparison.OrdinalIgnoreCase))
            {
                //determine whether the temporary, uploaded file exists on disc
                if (!File.Exists(ContentSourceFile))
                    throw new FileNotFoundException("Uploaded temporary file \"{0}\" could not be found.", ContentSourceFile);
                //get the size of the file stored on disc
                var fileSize = new FileInfo(ContentSourceFile).Length;
                //get the limiting value
                var limitFileSize = GetActualFileSize(policy.GetValue<long>(), policy.ValueClass);
                //This policy checks the size of the uploaded
                switch(policy.InterpretValueAs)
                {
                    //The file may not be larger than the limiting value 
                    case Policy.ValueInterpretation.Maximum:
                        if (fileSize > limitFileSize )
                            return false;
                        return true;
                    //The file may not be smaller than the limiting value 
                    case Policy.ValueInterpretation.Minimum:
                        if (fileSize < limitFileSize)
                            return false;
                        return true;
                    //The file must be equal to the limiting value
                    case Policy.ValueInterpretation.Equal:
                        if (fileSize != limitFileSize)
                            return false;
                        return true;
                    //The file cannot be equal to the limiting value
                    case Policy.ValueInterpretation.NotEqual:
                        if (fileSize == limitFileSize)
                            return false;
                        return true;
                    //there is no comparison for other types in this handler!
                    default:
                        throw new ArgumentException
                            ("policy", 
                              String.Format("The value {0} of the InterpretValueAs attribute is not valid for this handler!",
                                            policy.InterpretValueAs)
                            );
                }
            }
            return true;
        }
        protected override string InterpretPolicyValue(Policy policy)
        {
            if(policy == null)
                return String.Empty;

            var format = "{0} {1}";
            if (String.Equals("FileSize", policy.Compare, StringComparison.OrdinalIgnoreCase))
            {
                if(String.IsNullOrEmpty(policy.ValueClass))
                    return String.Format(format, policy.Value, "Bytes");
                if (policy.ValueClass.Equals("B", StringComparison.OrdinalIgnoreCase))
                    return String.Format(format, policy.Value, "Bytes");
                if (policy.ValueClass.Equals("KB", StringComparison.OrdinalIgnoreCase))
                    return String.Format(format, policy.Value, "Kb");
                if (policy.ValueClass.Equals("MB", StringComparison.OrdinalIgnoreCase))
                    return String.Format(format, policy.Value, "Mb");
                if (policy.ValueClass.Equals("GB", StringComparison.OrdinalIgnoreCase))
                    return String.Format(format, policy.Value, "Gb");
            }
            return base.InterpretPolicyValue(policy);
        }        
        private long GetActualFileSize(long size, string unit)
        {
            if(String.IsNullOrEmpty(unit))
                return size;
            if (unit.Equals("B", StringComparison.OrdinalIgnoreCase))
                return size;
            if (unit.Equals("KB", StringComparison.OrdinalIgnoreCase))
                return size * 1024;
            if (unit.Equals("MB", StringComparison.OrdinalIgnoreCase))
                return size * 1024 * 1024;
            if (unit.Equals("GB", StringComparison.OrdinalIgnoreCase))
                return size * 1024 * 1024 * 1024;
            return -1;
        }
        protected override void DoWork()
        {
            //nothing to do here this time
        }
    }
}   

When the policy is violated a user that tries to upload a too large file will experience the following message stating that the file is to large to be uploaded.

Validation error message

Validation error message

 

Creating the Event handler in earlier version or from scratch

Often there is very little use to implement the eventh handler interfaces directly, it is much preferred to inherit from any of the base classes above to radically facilitate the development task.

Any event handler of this type must implement the ICSEventHandler interface or the ICSEventHandler2 (Content Studio version 5.3 and later). During execution Content Studio will create an instance of your event handler and retrieves its implementation of the ICSEventHandler interface. It then calls the appropriate EventHandler method where the actual event handler code gets executed. These interfaces are defined in the assembly CS5Interfaces.dll (version 5.2 and later) or in CSServer5.dll (version 5.1 and earlier).

Content Studio introduced the ICSEventHandler2 which is a modified version of the standard ICSEventHandler interface. The reason to use this interface is to provide the caller with more functionality, most of all the possibility to edit the content that is about to be saved in the OnBeforeDocumentSave event. The old interface will still be supported in versions but Content Studio will prefer the new interface over the old one.

The EventHandler method of the ContentStudio.EventActions.ICSEventHandler interface has the following declaration:

C#
void EventHandler(string EventXMLArguments,
                  string CustomData,
                  object Content,
                  ICSCredentialsContainer Credentials,
                  ref bool Cancel,
                  out int Status,
                  out string StatusText)
    
Visual Basic.NET
Sub EventHandler(EventXMLArguments As String, _
                 CustomData As String, _
                 Content As Object, _
                 Credentials As ICSCredentialsContainer, _
                 ByRef Cancel As Boolean, _
                 <OutAttribute> ByRef Status As Integer, _
                 <OutAttribute> ByRef StatusText As String)
    

The EventHandler method of the ICSEventHandler2 interface used in Content Studio 5.3 or later has the following declaration:

C#
void EventHandler(
	string eventXmlArguments,
	string customData,
	ICSContentContainer content,
	ICSCredentialsContainer credentials,
	ref bool cancel,
	out int status,
	out string statusText
)       
        
Visual Basic.NET
Sub EventHandler ( _
	eventXmlArguments As String, _
	customData As String, _
	content As ICSContentContainer, _
	credentials As ICSCredentialsContainer, _
	ByRef cancel As Boolean, _
	<OutAttribute> ByRef status As Integer, _
	<OutAttribute> ByRef statusText As String _
)
        
Parameters
EventXMLArguments
String
An xml document sent to the implementer from Content Studio.
The exact content varies between different events. The sample is typical for a document event (ex. OnDocumentSave or OnDocumentApprove).
Xml
<event type="Integer value"
       event="String value"
       msgid="String value">
   <timestamp>Date value</timestamp>
   <connectionid>Integer value</connectionid>
   <callerinfo sid="String value" 
               logonname="String value" 
               email="String value" 
               fullname="String value" 
               userkey="String value" 
               sessionid="Integer value"/>
   <objectdata>
      <documentid>Integer value</documentid>
      <documentname>String value</documentname>
      <filename>String value</filename>
      <encoding>String value</encoding>
      <categoryid>Integer value</categoryid>
      <documenttitle>String value</documenttitle>
      <!-- In version 5.3, additional fields have been added -->
   </objectdata>
</event>
        
Event Xml explained
Element Attribute Description
event   This element acts as the root node of the document.
  type The numeric identifier of the event.
  event The name the event.
  msgid A unique identifier of the specific event message.
timestamp   The date and time of when the event was raised.
connectionid   An identifier of the Web site
callerinfo   This element has attributes that contain data about the caller.
  sid The caller's security identifier in the SDL format (ex. S-1-5-16).
  logonname The caller's logon name in the DOMAIN\USERNAME format.
  email The caller's email address, if registered in CS.
  fullname The caller's fullname, if registered in CS.
  userkey The caller's Content Studio userkey (ex. ABCD1).
  sessionid The caller's session identifier.
objectdata   This element act as root node for elements that contains data about the affected document.
documentid   The identifier of the document
documentname   The logical file name of the affected document (ex. Unit1/Category1/pic1.gif).
filename   The file name of the affected document as it appears on disc.
encoding   The encoding (ex. utf-8), if existing, of the affected document.
categoryid   An identifier of the category where the affected document is placed.
documenttitle   The name of the affected document.
CustomData
String
User defined static data that has been specified for the Event action definition that triggered this event. The data is supplied via the Command text field in the Event actions property window.
Content (in ICSEventHandler) only.
System.Object
The document content as it exists in Content Studio when the event was raised. Not all events supply content.
Content (in ICSEventHandler2) only.
ContentStudio.EventActions.ICSContentContainer
This interface is used to supply the affected document content from Content Studio to the handler and back from the handler to Content Studio. In addition to this you can pass a collection of property names and values back to Content Studio from the event handler. Currently Content Studio only uses returned content and property values in the OnBeforeDocumentSave event.
Credentials
ContentStudio.EventActions.ICSCredentialsContainer
A reference to Content Studio's implementation of the ICSCredentialsContainer interface. This is used to pass system defined credentials to the custom implementation. The event handler can use these credentials when ex. communicating with an external system such as a database or a mail server. This parameter is null (Nothing in Visual Basic) if no credentials has been specified.
Cancel
Boolean
A reference to a Boolean parameter. Event handlers sets this parameter to true to indicate that all changes in the Content Studio event that triggered the event should be rolled back. If the value remains false after the call, the event in Content Studio will not be rolled back, regardless of the outcome of the operation performed by the event handler.
Status
Int32
Implementations should set this parameter to zero to indicate success. All other values will throw an error in the Content Studio event that triggered the event causing data to be rolled back. This parameter is passed uninitialized.
StatusText
String
Implementation sets this parameter to the textual representation of the error indicated in the Status parameter. This parameter is passed uninitialized.

All your logic will be implemented in this method and depending on what you like to do you might not need all of the parameters. However, since both the Status and the StatusText parameters are output parameters, you must provide a value for them. They will inform Content Studio about the outcome of the call - Status = 0 indicates success and any other value is an error. The Cancel parameter is important since it controls whether Content Studio should roll back every change been made in Content Studio. By setting Cancel to true the event handler informs Content Studio that a rollback should be performed.

A small sample is shown below - it is intended to run OnDocumentDelete and effectively prevents any document in the category where it is applied, to be deleted. For demostration purpose the code uses the CustomData parameter to provide a user defined message to the caller. In this particular sample we do not use any of the Xml data passed in via the EventXMLArguments parameter but with this data you can get some information about the document and the user that triggered the event.

Finally the event handler is ready to be compiled and tested.

Show code A sample event handler

C#
using System;
using ContentStudio.EventActions;

namespace SyncEventHandler
{
    public class OnDocumentDeleteHandler : ContentStudio.EventActions.ICSEventHandler2
    {
        void ICSEventHandler.EventHandler(string EventXMLArguments, 
                                          string CustomData, 
                                          ICSContentContainer Content, 
                                          ICSCredentialsContainer Credentials, 
                                          ref bool Cancel, 
                                          out int Status, 
                                          out string StatusText)
        {
            Status = -1;
            //Any custom message passed in?
            if((CustomData != null) || (CustomData.Length == 0))
                StatusText = CustomData;
            else
                StatusText = "Delete is forbidden in this category";
            Cancel = true;
        }
    }
}
Visual Basic.NET
Imports System
Imports ContentStudio.EventActions

Namespace SyncEventHandler
    Public Class OnDocumentDeleteHandler 
                 Implements ContentStudio.EventActions.ICSEventHandler2
        Private Sub EventHandler(EventXMLArguments As String, _ 
                                 CustomData As String, _
                                 Content As ICSContentContainer, _
                                 Credentials As ICSCredentialsContainer, _
                                 ByRef Cancel As Boolean, _
                                 <OutAttribute> ByRef Status As Integer, _
                                 <OutAttribute> ByRef StatusText As String) _
                    Implements ICSEventHandler.EventHandler
        
            Status = -1
            'Handle the case when CustomData is Nothing
            If CustomData Is Nothing Then 
                CustomData = String.Empty
            End If
            'Any custom message passed in?
            If CustomData.Length = 0 Then
               StatusText = "Delete is forbidden in this category"
            Else
                StatusText = CustomData
            End If
            Cancel = True
        End Sub
    End Class
End Namespace

Installing the event handler

Compile the code as a class library (.dll file). In Visual Studio this is done automatically for you and outside Visual Studio you can use one of the compilers that is a part of the freely .NET Framework SDK. For more information about the command line compilers see the .NET Framework SDK documentation. You can name your .dll SyncEventHandler.dll, the name of the file is normally also the Assembly name and the assembly name is important when you use the event handler from Content Studio later.
NOTE
You must use at least version 2.0 of the .NET Framework SDK or Visual Studio 2005 or later to be able to build the event handler.

Now, locate the installation directory of the Content Studio Server service (CS5ServiceHost.exe) that normally gets installed in the C:\Program files\Teknikhuset\Content Studio 5\CSServer folder. Copy the dll into this directory and you are ready to go!

Use the event handler

When you have properly installed your event handler in Content Studio it is time to test it.
Select a category where you would like the event handler to be used. Be default only members of the administrators group has permissions to manage event handlers in Content Studio. This permission can be delegated on the site root level or on a single category but event actions permissions are not inherited to child categories.
On the category of choise, create a new Event handler. This will bring up the "Properties for Event actions" dialog.

Properties for an event actions event
The EventActions properties dialog
  • Name your event handler Prohibit document delete
  • Select the OnDocumentDelete event
  • Set the type to Synchronous Event handler object

The User data button opens up the User Credentials dialog that makes it possible to provide credentials that the event handler receives through the ICSCredentials interface but in this case there is no need for this functionality. Credentials, however, can be useful if you need to communicate with an external system and could contain login credentials for that system.

The ProgID field specifies the programmatic name of your event handler and can be written in serveral ways where the simplest form is

Namespace.Class, Assembly name

The complete format looks like:

Namespace.Class, Assembly name, Version=value, Culture=value, PublicKeyToken=value
Examples
The first example is as simple as it gets where only the full name and the assembly name are given. The assembly name is normally the same as the name of the dll file minus the .dll extension.
SyncEventHandler.OnDocumentDelete, SyncEventHandler
The second example is more specific an specifies that the event handler has been given a strong name and we also needs a specific version of the assembly to be loaded with a specific culture. This advanced type info is needed when your event handler assembly has been registered in the global assembly cache (GAC).
SyncEventHandler.OnDocumentDelete, SyncEventHandler, Culture=neutral, Version=1.0.0.0, PublicKeyToken=feea6d462bd740da

Finally, if everything is OK so far Content Studio will throw an error message when you delete any document located in the category in question.

No delete message
Message from the Event handler

Debugging the event handler

In order to be able to debug your event handler from Visual Studio you must have Content Studio installed locally on your machine and the services Content Studio server and Content Studio Service Manager must be running.

  • Change the output directory of your event handler's assembly to the same directory where the Content Studio services are installed. ex. C:\Program Files\Teknikhuset\Content Studio 5\CSServer, look for the file CS5ServiceHost.exe
  • Rebuild your project, with debugging information, at the new location.
  • Start the debugging session from Visual Studio by clicking the Debug - Attach to Process... menu alternative. A new dialog shows up and from that dialog you select the CS5ServiceHost.exe process and press the Attach button.
  • You now can set breakpoints in your code and the breakpoints should be hit whenever Content Studio raises a event that is set up to be handled by your event handler.

See Also

Creating an asynchronous event handler.