Tuesday, September 1, 2009

How to update a SharePoint item while retaining the modified by and date information

In my current project I need to move documents from one document library to another with version history and last modified infomation etc. This works great using SPImport and SPExport.

When the document is moved to the new location, I have a further process of modifiying the values of one of the fields. I needed to do this without modifying the Modified or Modified By fields.

Here is how I did it:

foreach (SPListItem listItem in list.Items)
 {
     var modifiedBy = listItem[SPBuiltInFieldId.Modified_x0020_By];
     var modifiedDate = listItem[SPBuiltInFieldId.Modified];
 
     listItem[_documentTypeFieldName] = GetDocumentType();
     listItem[SPBuiltInFieldId.Modified_x0020_By] = modifiedBy;
     listItem[SPBuiltInFieldId.Modified] = modifiedDate;
     listItem.Update();
 }

BTW. Using Update() creates a new version here. You can not create a new version by using UpdateOverwriteVersion().

Friday, August 21, 2009

Useful code for modifying a Site

I am currently working on a project that needs to support the clients SharePoint implementation. They do everything directly in production, so I have created a site definition to reproduce there site structure. One feature I created to help me with this is basically a web scoped feature that modifies the site with custom changes I want in code (I am a programmer after all!!).

The following code does some interesting stuff:
  • Creates a document library in code
  • Sets the document library title, enables versioning and allows content types in the document library
  • Finds a site content type and associates it with the document library
  • Changes the content type order so it contains just my content type
  • Sets the default page of the site to the default view of the document library
  • Adds some of my content type fields to the default view


public class ProjectSiteReceiver : SPFeatureReceiver
{
    private string _documentLibraryTitle = "Project Documents";
 
    public override void FeatureActivated(SPFeatureReceiverProperties properties)
    {
        using (SPWeb web = (SPWeb)properties.Feature.Parent)
        {
            SPListTemplateType templateType = SPListTemplateType.DocumentLibrary;
            Guid listId = web.Lists.Add(_documentLibraryTitle, null, templateType);
 
            SPList list = web.Lists[listId];
            list.Title = _documentLibraryTitle;
            list.EnableVersioning = true;
            list.ContentTypesEnabled = true;
            list.Update();          
 
            SPContentType contentType = web.GetContentType("Project Document");
            SPContentType listContentType = list.ContentTypes.Add(contentType);
            listContentType.Update();
 
            List<SPContentType> contentTypeList = new List<SPContentType>();
            contentTypeList.Add(listContentType);
            list.RootFolder.UniqueContentTypeOrder = contentTypeList;
            list.RootFolder.Update();
 
            SPFolder webFolder = web.RootFolder;
            webFolder.WelcomePage = list.DefaultView.Url;
            webFolder.Update();
 
            SPView view = list.DefaultView;
            view.ViewFields.Add("Project ID");
            view.ViewFields.Add("Project Type");
            view.Update();
        }
    }
 
    public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
    {
        using (SPWeb web = (SPWeb)properties.Feature.Parent)
        {
            SPList list = web.GetListByName(_documentLibraryTitle);
            list.Delete();
        }
    }
 
    public override void FeatureInstalled(SPFeatureReceiverProperties properties)
    {
    }
 
    public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
    {
    }
}




PS: Sorry GetContentType and GetListByName are my own custom extension methods. There isn't much to them really, just better error handling then using the default SharePoint API methods with square brackets.

PPS: You also need to set the welcome page back to the default in the FeatureDeactivating method:

SPFolder webFolder = web.RootFolder;
webFolder.WelcomePage = "default.aspx";
webFolder.Update();

Opening SPFile from full file URL

I had to implement a custom search to find documents matching certain criteria. I could easily find a document and retrieve its "Path" (full url). From this URL, I needed to get the associated SPFile object. This took longer then I thought it warranted.

Here is the solution:

string fileUrl = dt.Rows[0]["Path"].ToString();
 
using (SPSite site = new SPSite(fileUrl))
{
    using (SPWeb web = site.OpenWeb())
    {
        SPFile file = web.GetFile(fileUrl);
 
        if (!file.Exists)
            throw new ApplicationException("Could not find document");
 
        return file;
    }
}

Wednesday, August 12, 2009

Sharepoint Assembly Platform

I recently started a project which consisted of a set of assemblies deployed to the GAC. While integrating with a larger application, the assemblies build configuration were modified to build on x86 Platform (32-bit) explicitly. This was done believing that if they were built as 32-bit, both 32-bit and 64-bit applications could use them.

This worked fine in a 32-bit environment. However, when deploying to a 64-bit environment I was getting "file not found" errors. After some looking into the C:\windows\assembly\gac_msil directory on the server I noticed there where multiple directories there:

  • gac_msil
  • gac32
  • gac64


After some research I discovered that on a 64-bit server, assemblies built as:

Any CPU: go into the gac_msil directory and can be used by either 32-bit or 64-bit processes.

x86: go into the gac32 directory and can only seen and used by 32-bit processes.

x64: go into the gac64 directory and can only be seen and used by 64-bit processes.

My conclusion: leave the build configuration as "Any CPU" !!!!

Monday, August 10, 2009

Unable to move web part

I had an error with not being able to move webparts from in a page. Something like this:



After a bit of research I found the the problem was related to web part zones having a relative position in the CSS. See http://www.sharepointblogs.com/tmt/archive/2007/11/01/CSS-causes-JavaScript-error-while-moving-Web-Parts-in-edit-mode.aspx .

There are a few ways mooted online to fix this problem, but the best solution for me was to set the position of body css element to relative also:

body
{
    position:relative;
}

Problem solved...

Friday, July 17, 2009

Item Adding Event firing on a move

I have an ItemAdding event handler that generates a unique id for a document across document libraries and sites. The event handler is attached to the base content type.

All was going well until I started moving documents. A move fires the ItemAdding event, which recreates a new number for a document.

As the FileMoving and FileMoved event handlers are useless, I needed away to tell if the ItemAdding event is a new document or a moved document.

I eventually worked out a way...In the SPItemEventProperties object is a property named ListItemId. This will be 0 for new items, and a number greater then 0 for an existing item (it is in fact the ID of the record in the existing document library).

Monday, March 16, 2009

A Feature to Setup Site Auditing

Some of our SharePoint sites need auditing turned on to monitor changes to content. This is a fairly simple thing to do through the user interface (http://siteurl/_layouts/AuditSettings.aspx).

When using site definitions it is best to remove as many manual steps as you can. The following is a handy feature to automatically setup the Audit Settings.

The feature:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

using Microsoft.SharePoint;

 

using MaskedSharePointer.Logging;

 

namespace MaskedSharePointer.Features.Receivers

{

    public class SiteAuditingReceiver : SPFeatureReceiver

    {

        public override void FeatureActivated(SPFeatureReceiverProperties properties)

        {

            if (!(properties.Feature.Parent is SPSite))

                throw new ApplicationException("Site Auditing Receiver is a site feature.");

 

            SPSite site = properties.Feature.Parent as SPSite;

 

            int auditFlags = 0;

 

            foreach (SPFeatureProperty featureProperty in properties.Feature.Properties)

            {

                try

                {

                    auditFlags += Convert.ToInt32(Enum.Parse(typeof(SPAuditMaskType), featureProperty.Value, true));

                }

                catch (Exception ex)

                {

                    Logger.LogException(ex);

                }

            }

 

            site.Audit.AuditFlags = (SPAuditMaskType)Enum.ToObject(typeof(SPAuditMaskType), auditFlags);

            site.Audit.Update();

        }

 

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)

        {

            if (!(properties.Feature.Parent is SPSite))

                throw new ApplicationException("Site Auditing Receiver is a site feature.");

 

            SPSite site = properties.Feature.Parent as SPSite;

            site.Audit.AuditFlags = SPAuditMaskType.None;

            site.Audit.Update();

        }

 

        public override void FeatureInstalled(SPFeatureReceiverProperties properties)

        {

        }

 

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)

        {

        }

    }

}



And the feature to activate the audit settings. Its customisable by changing the properties to the appropriate level of auditing:

<?xml version="1.0" encoding="utf-8" ?>

<Feature

    Title="Site Auditing"

    Description="Sets up site collection audit settings"

    Id="{GUID}"

    Scope="Site"

    Version="1.0.0.0"

    Hidden="FALSE"

    xmlns="http://schemas.microsoft.com/sharepoint/"

    ReceiverAssembly="MaskedSharePointer.Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx"

    ReceiverClass="MaskedSharePointer.Features.Receivers.SiteAuditingReceiver">

    <Properties>

        <!-- One of these -->

        <Property Key="All" Value="All"/>

        <Property Key="None" Value="None"/>

 

        <!-- OR a combination of any of the following-->

        <Property Key="View" Value="View"/>

        <Property Key="CheckIn" Value="CheckIn"/>

        <Property Key="CheckOut" Value="CheckOut"/>

        <Property Key="ChildDelete" Value="ChildDelete"/>

        <Property Key="Copy" Value="Copy"/>

        <Property Key="Delete" Value="Delete"/>

        <Property Key="Move" Value="Move"/>

        <Property Key="ProfileChange" Value="ProfileChange"/>

        <Property Key="SchemaChange" Value="SchemaChange"/>

        <Property Key="SecurityChange" Value="SecurityChange"/>

        <Property Key="Undelete" Value="Undelete"/>

        <Property Key="Update" Value="Update"/>

        <Property Key="Workflow" Value="Workflow"/>

        <Property Key="Search" Value="Search"/>

    </Properties>

</Feature>

Setting up Timer Job to Cleanup Audit Log

I had a requirement to cleanup the SharePoint audit log after a certain amount of time (in our case a year). This is a simple operation using the SPAudit class and the DeleteEntries() method.

The timer job definition:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

using Microsoft.SharePoint;

using Microsoft.SharePoint.Administration;

using Microsoft.SharePoint.Utilities;

 

using MaskedSharePointer.Logging;

 

namespace MaskedSharePointer.TimerJobs

{

    public class AuditLogManagerJobDefinition : SPJobDefinition

    {

        private const string siteIdPropertyName = "SiteCollectionId";

        private const string daysToRetainPropertyName = "DaysToRetain";

 

        private const string dateFormat = "dd/MM/yyyy HH:mm:ss";

 

        public AuditLogManagerJobDefinition()

        {

        }

 

        public AuditLogManagerJobDefinition(string jobName, SPWebApplication webApp, Guid siteId, int daysToRetain): base(jobName, webApp, null, SPJobLockType.ContentDatabase)

        {

            this.Title = jobName;

            this.Properties.Add(siteIdPropertyName, siteId);

            this.Properties.Add(daysToRetainPropertyName, daysToRetain);

        }

 

        public override void Execute(Guid targetInstanceId)

        {

            try

            {

                Guid siteId = (Guid)this.Properties[siteIdPropertyName];

                int daysToRetain = (int)this.Properties[daysToRetainPropertyName];

 

                DateTime deleteEndDate = DateTime.Now.AddDays(-daysToRetain);

 

                using (SPSite site = new SPSite(siteId))

                {

                    SPAudit audit = site.Audit;                   

                    int recordsDeleted = audit.DeleteEntries(deleteEndDate);

 

                    Logger.LogInformation(string.Format("{3} Timer Job completed at {0}.  It removed all records created before {1} ({2} audit log records were deleted).", DateTime.Now.ToString(dateFormat), deleteEndDate.ToString(dateFormat), recordsDeleted, Title));

                }

            }

            catch (Exception ex)

            {

                Logger.LogException(ex);

            }

        }

    }

}



The feature to activate the timer job:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

 

using Microsoft.SharePoint;

using Microsoft.SharePoint.Administration;

 

using MaskedSharePointer.TimerJobs;

 

namespace MaskedSharePointer.Receivers

{

    public class AuditLogManager : SPFeatureReceiver

    {

        private readonly string _scheduleKey = "Schedule";

        private readonly string _daysToRetainKey = "DaysToRetain";

 

        private const int _defaultDaysToRetain = 365;

 

        public override void FeatureActivated(SPFeatureReceiverProperties properties)

        {

            string jobName = properties.Definition.DisplayName;

 

            SPSite site = properties.Feature.Parent as SPSite;

 

            RemoveExistingTimerJob(site, jobName);

 

            int daysToRetain = GetDaysToRetain(properties);

 

            AuditLogManagerJobDefinition auditLogManagerJobDefinition = new AuditLogManagerJobDefinition(jobName, site.WebApplication, site.ID, daysToRetain);

            auditLogManagerJobDefinition.Schedule = GetSchedule(properties);

            auditLogManagerJobDefinition.Update();

        }

 

 

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)

        {

            string jobName = properties.Definition.DisplayName;

 

            SPSite site = properties.Feature.Parent as SPSite;

            RemoveExistingTimerJob(site, jobName);

        }

 

        public override void FeatureInstalled(SPFeatureReceiverProperties properties)

        {

        }

 

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)

        {

        }

 

        private static void RemoveExistingTimerJob(SPSite site, string jobName)

        {

            foreach (SPJobDefinition job in site.WebApplication.JobDefinitions)

            {

                if (job.Name == jobName)

                {

                    job.Delete();

                    break;

                }

            }

        }

 

        private SPSchedule GetSchedule(SPFeatureReceiverProperties properties)

        {

            try

            {

                if (properties.Feature.Properties[_scheduleKey] != null)

                    return SPSchedule.FromString(properties.Feature.Properties[_scheduleKey].Value);

                else

                    return new SPWeeklySchedule();

            }

            catch

            {

                return new SPWeeklySchedule();

            }

        }

 

        private int GetDaysToRetain(SPFeatureReceiverProperties properties)

        {

            try

            {

                if (properties.Feature.Properties[_daysToRetainKey] != null)

                    return Convert.ToInt32(properties.Feature.Properties[_daysToRetainKey].Value);

                else

                    return _defaultDaysToRetain;

            }

            catch

            {

                return _defaultDaysToRetain;

            }

        }

    }

}



And the xml of the feature to setup how often to run the timer job. I run it daily as the call to DeleteEntries() does a simple SQL delete directly on the content database:

<?xml version="1.0" encoding="utf-8" ?>

<Feature

    Id="{GUID}"

    Title="Audit Log Manager Timer Job"

    Description="Timer job that clears the audit log after a certain amount of time."

    Scope="Site"

    Version="1.0.0.0"

    Hidden="FALSE"

    xmlns="http://schemas.microsoft.com/sharepoint/"

    ReceiverAssembly="MaskedSharePointer.Demo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=xxxxxxxxxxxxxxxx"

    ReceiverClass="MaskedSharePointer.Receivers.AuditLogManager ">

    <Properties>

        <Property Key="DaysToRetain" Value="365" />

 

        <!-- Choose one of the following and modify as necassary -->

        <Property Key="Schedule" Value="every 20 minutes"/>

        <Property Key="Schedule" Value="hourly between 8 and 18"/>

        <Property Key="Schedule" Value="daily at 09:00:00"/>

        <Property Key="Schedule" Value="weekly at mon 09:00:00"/>

        <Property Key="Schedule" Value="monthly at 1 09:00:00"/>

        <Property Key="Schedule" Value="yearly at jan 1 09:00:00"/>

    </Properties>

</Feature>

Saving Sort Order to Column in Links List

The following code copies the sort order (using "Change Order") for a links list into a list column.

The sort order for a list item in a links list is stored in the Xml property, in the attribute ows_Order. This however is not migrated with content deployment, hence the need for this event handler.

This ItemEventReceiver is activated as a web scoped feature, on all links lists by using the following in the elements.xml file:

<Receivers ListTemplateId="103">



The event handler code is here:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Xml.Linq;

using System.Text;

 

using Microsoft.SharePoint;

 

namespace MaskedSharePointer.Features.Receivers

{

    public class LinksListReceiver : SPItemEventReceiver

    {

        private string _rankFieldName = "Rank";

        private string _xmlAttributeName = "ows_Order";

 

        public override void ItemAdded(SPItemEventProperties properties)

        {

            base.ItemAdded(properties);

            SaveSortOrder(properties);

        }

 

        public override void ItemUpdated(SPItemEventProperties properties)

        {

            base.ItemUpdated(properties);

            SaveSortOrder(properties);

        }

 

        private void SaveSortOrder(SPItemEventProperties properties)

        {

            this.DisableEventFiring();

            try

            {

                using (SPWeb web = properties.OpenWeb())

                {

                    SPList list = web.Lists[properties.ListTitle];

 

                    if (!list.Fields.ContainsField(_rankFieldName))

                    {

                        SPField newField = list.Fields.CreateNewField(SPFieldType.Number.ToString(), _rankFieldName);

                        newField.ReadOnlyField = true;

                        list.Fields.Add(newField);

                    }

 

                    foreach (SPListItem listItem in list.Items)

                    {

                        XElement element = XElement.Parse(listItem.Xml);

                        XAttribute attribute = element.Attribute(_xmlAttributeName);

 

                        decimal newRank = Convert.ToDecimal(attribute.Value);

 

                        if (newRank != Convert.ToDecimal(listItem[_rankFieldName]))

                        {

                            listItem[_rankFieldName] = newRank;

                            listItem.Update();

                        }

                    }

                }

            }

            finally

            {

                this.EnableEventFiring();

            }

        }

    }

}