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();

            }

        }

    }

}

Tuesday, December 23, 2008

Creating Timer Job Definitions

At my current workplace, developers do not have the full access we require at times to run console applications on production servers. To get around this problem I have started creating my batch processes as SharePoint Timer Jobs.

Firstly, the code that does the batch processing is contained in its own class. This class has one static method Execute with a parameter of type SPWeb. I contain it in its own class so I can call if from anywhere (timer job, feature, console app, web part etc).

eg.

    public class SynchronizeGroupsManager

    {

        public static void Execute(SPWeb web)

        {

            //Do something here...

        }

    }



From there I create a SharePoint job definition. This class is a subclass of SPJobDefinition. This class implements an override of the Execute method. In this method, I create an SPWeb instance and pass this onto the static method I created early. This is the method that gets executed when the timer job fires.

eg.

    public class SynchronizeGroupsJob : SPJobDefinition

    {

        private const string siteIdPropertyName = "SiteCollectionId";

 

        public SynchronizeGroupsJob() : base()

        {

        }

 

        public SynchronizeGroupsJob(string jobName, SPService service, SPServer server, SPJobLockType targetType) : base(jobName, service, server, targetType)

        {

        }

 

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

        {

            this.Title = JobName.SynchronizeGroups;

            this.Properties.Add(siteIdPropertyName, siteId);

        }

 

        public override void Execute(Guid targetInstanceId)

        {

            HandleEventFiring handleEventFiring = new HandleEventFiring();

 

            handleEventFiring.CustomDisableEventFiring();

            try

            {

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

 

                using (SPSite site = new SPSite(siteId))

                {

                    using (SPWeb web = site.OpenWeb())

                    {

                        try

                        {

                            SynchronizeGroupsManager.Execute(web);

                        }

                        catch (Exception ex)

                        {

                            ErrorLogger.WriteErrorToTraceLog(web, ex);

                        }

                    }

                }

            }

            finally

            {

                handleEventFiring.CustomEnableEventFiring();

            }

        }

    }



The final step is to create a feature to install the timer job (and to do an initial execution if needed). I like to do the initial execution because I can deactivate and reactivate the feature at any time and perform the batch processing immediately.

You have plenty of options to create different schedules for your timer job. The following example is a daily schedule. As I have not specified any times, the following timer job will fire at 12:00am by default.

eg.

    public class InstallSynchronizeGroupsJob : SPFeatureReceiver

    {

        public override void FeatureActivated(SPFeatureReceiverProperties properties)

        {

            SPSite site = properties.Feature.Parent as SPSite;

 

            RemoveExistingTimerJob(site);

 

            SPDailySchedule schedule = new SPDailySchedule();

 

            SynchronizeGroupsJob synchronizeGroupsJob = new SynchronizeGroupsJob(JobName.SynchronizeGroups, site.WebApplication, site.ID);

            synchronizeGroupsJob.Schedule = schedule;

            synchronizeGroupsJob.Update();

 

            InitialSynchronization(site);

        }

 

        public override void FeatureDeactivating(SPFeatureReceiverProperties properties)

        {

            SPSite site = properties.Feature.Parent as SPSite;

            RemoveExistingTimerJob(site);

        }

 

        public override void FeatureInstalled(SPFeatureReceiverProperties properties)

        {

        }

 

        public override void FeatureUninstalling(SPFeatureReceiverProperties properties)

        {

        }

 

        private static void RemoveExistingTimerJob(SPSite site)

        {

            foreach (SPJobDefinition job in site.WebApplication.JobDefinitions)

            {

                if (job.Name == JobName.SynchronizeGroups)

                {

                    job.Delete();

                    break;

                }

            }

        }

 

        private static void InitialSynchronization(SPSite site)

        {

            using (SPWeb web = site.OpenWeb())

            {

                SynchronizeGroupsManager.Execute(web);

            }

        }

    }