DCSIMG
January 2009 - Posts - David Birin's blog

January 2009 - Posts

SharePoint – Events are duplicated when using templates

I encountered a strange behavior in SharePoint, this phenomenon happens when you create an event handler and attach it to a list template (for example, a document library) using a web-site level feature. If you save the list as template, the event handler reference is saved inside the list template (STP file) and when you create an instance using the template it can cause the following problems:

  1. The event handler will be activated automatically on a web site where the feature is not enabled.
  2. If the event feature is enabled in the web site the event will pop twice (or more if a list with duplicated events will be saved as template…)

Now for proving this point I will attach some code samples which demonstrate the above, and a solution to the problem.

I created a sample Event Handler, which adds the text “From event handler [THE CURRENT TIME] to the Title field:

public class SampleEvent : SPItemEventReceiver
{
    public override void ItemAdded(SPItemEventProperties properties)
    {
        try
        {
            this.DisableEventFiring();
 
            //Get the context item
            using (SPWeb contextWeb = properties.OpenWeb())
            {
                SPList contextList = contextWeb.Lists[properties.ListId];
                SPListItem contextItem = contextList.GetItemById(properties.ListItemId);
                //Sample update for testing
                contextItem["Title"] += "From event handler " + DateTime.Now.ToShortTimeString();
                contextItem.SystemUpdate(false);
            }
        }
        finally
        {
            this.EnableEventFiring();
        }
    }
 
}

I registered it using the following feature:

Feature.xml:

<?xml version="1.0" encoding="utf-8" ?>
<Feature Id="227EEA0A-D42F-4ab7-84DF-E31406A98AFD" 
Title="Sample event handler feature"
Description="Used for testing eventviewer"
Version="1.0.0.0"
Scope="Web"
Hidden="False"
AlwaysForceInstall="TRUE"
ImageUrl=""          
xmlns="http://schemas.microsoft.com/sharepoint/">
    <ElementManifests>
        <ElementManifest Location="event.xml" />
    </ElementManifests>
</Feature>

event.xml:

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
    <Receivers ListTemplateId="101">
      <Receiver>
          <Name>SampleEvent</Name>
          <Type>ItemAdded</Type>
          <SequenceNumber>1000</SequenceNumber>
          <Assembly>SampleEvent, Version=1.0.0.0, Culture=neutral, PublicKeyToken=392ad9e36e9f08a3</Assembly>
          <Class>SampleEvent.SampleEvent</Class>
          <Data></Data>
      </Receiver>       
</Receivers>
</Elements>

I activated the feature (web-site level):

image

and uploaded a document to a document library in a site, as seen in the next screenshot the event did it’s job:

image

Now, I saved the document library as a template and created a new document library from this template, as seen in the screenshot bellow after adding an item to the new list, you can see that the event occurred twice:

image

And for those of you who are sceptic the following screenshot form SharePoint Manager 2007 will prove that I am not bluffing you…

image

Now that we are aware of the problem, how do we fix it?

So I wrote a new function named RemoveDuplicateEvents to remove the duplicate events:

/// <summary>
/// This function remove duplicate events that were created when
/// saving a list with an event receiver as a template and creating
/// instances from the template
/// </summary>
/// <param name="list">The list to check for duplicate events</param>
/// <param name="className">The class which contains the receiver function</param>
/// <param name="eventType">The event type (if more than one event reciver in class)</param>
/// <returns>True if duplicate events were found, False otherwise</returns>
private bool RemoveDuplicateEvents(SPList list, string className, SPEventReceiverType eventType)
{
    //List of pointers to the duplicate events
    List<int> evnetReciverPointers = new List<int>();
    bool rc = false;
    try
    {
        SPSecurity.RunWithElevatedPrivileges(delegate
        {
            if (list.EventReceivers.Count < 2)
            {
                rc = false;
                return;
            }
            //Find events from the same type and classs
            for (int i = 0; i < list.EventReceivers.Count; i++)
            {
                if ((list.EventReceivers[i].Class == className) && (list.EventReceivers[i].Type == eventType))
                {
                    evnetReciverPointers.Add(i);
                }
            }
            //We delete one at a time
            if (evnetReciverPointers.Count > 1)
            {
                list.EventReceivers[evnetReciverPointers[1]].Delete();
                list.Update();
                rc = true;
            }
        });
 
    }
    catch (Exception ex)
    {
        //Do some error handling
    }
    return rc;
}

If we want to call this function from the event receiver we need to be aware of two things:

  1. If the problematic list was saved as a template, and we create a list instance from this template there will be 3 event handlers (and so on… save as template… instance… 4 event receivers… and so on…)
  2. Let’s say that we have a list with duplicate ItemAdded event, if in the first call to the ItemAdded function even if we remove the duplication, all the duplicate events will pop for the item who triggered the event(the next one will work fine).

that’s why in the function above I delete one duplication at a time. In order to overcome these problems your event receiver code should look like this:

public override void ItemAdded(SPItemEventProperties properties)
{
    try
    {
        this.DisableEventFiring();
 
        //Get the context item
        using (SPWeb contextWeb = properties.OpenWeb())
        {
            //Remove duplicate events
            if (RemoveDuplicateEvents(contextWeb.Lists[properties.ListId], this.GetType().ToString(), SPEventReceiverType.ItemAdded))
            {
                //because we want the code bellow the run only once
                return;
            }
            SPList contextList = contextWeb.Lists[properties.ListId];
            SPListItem contextItem = contextList.GetItemById(properties.ListItemId);
            //Sample update for testing
            contextItem["Title"] += "From event handler " + DateTime.Now.ToShortTimeString();
            contextItem.SystemUpdate(false);
        }
    }
    finally
    {
        this.EnableEventFiring();
    }
}

Stay tuned for more SharePoint adventures :-)

David Birin

SharePoint – Add a SPField to all the content types in a SPList

I found out (another) strange behavior of SharePoint, I tried to add a field (a column) to a list which contain more than one content type using the following code (the new field name is “New Field”) :

using (SPSite currentSite = new SPSite("http://moss"))
{
    using (SPWeb currentWeb = currentSite.OpenWeb())
    {
        SPList currentList = currentWeb.Lists["DocLib"];
        SPField newField = currentList.Fields.CreateNewField(SPFieldType.Text.ToString(), "New Field");
        currentList.Fields.Add(newField);
        currentList.Update();
    }
}

I found out that the field was added only to the default content type as shown in the following screenshot:

image

This was pretty annoying because when a field is added using the GUI it’s added to all the content types. I had to do some digging using Reflector (old helpful friend), and I found out that this behavior is controlled by a Enum named SPAddFieldOptions, which can be sent as a parameter to the AddFieldAsXML function as seen in the code sample bellow (the new field name is “New Field2”):

using (SPSite currentSite = new SPSite("http://moss"))
{
    using (SPWeb currentWeb = currentSite.OpenWeb())
    {
        SPList currentList = currentWeb.Lists["DocLib"];
        SPField newField = currentList.Fields.CreateNewField(SPFieldType.Text.ToString(), "New Field2");
        currentList.Fields.AddFieldAsXml(newField.SchemaXml, true, SPAddFieldOptions.AddToAllContentTypes);
        currentList.Update();
    }
}

Which brings us to the requested result:

image

Note: This will not work on content types that will be added to the list later on (at least this behavior is consistent with the GUI)
MOSS – Open search results for editing (Office 2003/7)

I visited a customer which wanted the option to directly open a document from the search results for editing (in default when you click a document from the search results it opens as read-only). I found this blog article which describes how to do it by embedding some JavaScript into the search results XSL, I did exactly as described and it didn’t work, I realized that this happens because this customer uses Office 2003. I had to do some digging using the IE developer toolbar (soon to be an integral part of IE8) on the “Edit with Microsoft Word” context menu item, which brought me to the conclusion to use other JavaScript functions to open files as described bellow:

<div class="srch-Description">
    <a href="{$url}" onclick="return editDocumentWithProgID2('{$url}','','SharePoint.OpenDocuments')">Edit Document</a>
</div>

To use this, edit you results XSL, insert this code before the srch-Description span (in the “main body template” area – this span appears few more times in the XSL).

After adding it your search result should look like this:
 image

This code was tested and working both with Office 2003 and Office 2007.

I did some more editing on the XSL in order to display the “Edit Document” link only for office documents.

The version which supports 2003 documents (.doc, .xls, .ppt)

<xsl:if test="substring($url,string-length(normalize-space($url))-3,4) = '.doc'
or substring($url,string-length(normalize-space($url))-3,4) = '.xls'
or substring($url,string-length(normalize-space($url))-3,4) = '.ppt'">
 
<div class="srch-Description">
        <a href="{$url}" onclick="return editDocumentWithProgID2('{$url}','','SharePoint.OpenDocuments')">Edit Document</a>
    </div>
</xsl:if>

The version which supports 2003 & 2007 documents (.doc, .xls, .ppt, .docx, .xlsx, .pptx)

<xsl:if test="substring($url,string-length(normalize-space($url))-3,4) = '.doc'
or substring($url,string-length(normalize-space($url))-3,4) = '.xls'
or substring($url,string-length(normalize-space($url))-3,4) = '.ppt'
or substring($url,string-length(normalize-space($url))-4,5) = '.docx'
or substring($url,string-length(normalize-space($url))-4,5) = '.pptx'
or substring($url,string-length(normalize-space($url))-4,5) = '.xlsx'">
 
<div class="srch-Description">
        <a href="{$url}" onclick="return editDocumentWithProgID2('{$url}','','SharePoint.OpenDocuments')">Edit Document</a>
    </div>
</xsl:if>

David Birin