Thursday, May 28, 2009

Performing Long Running Tasks in SharePoint

Occasionally you may have a custom action that takes a while to run. On one of my recent projects we had just this need. Basically we wanted to convert an ordinary out of the box document library into a 'Projects' Document library. This involved setting the properties of the library (e.g. versioning, content types, enforce checkout etc), adding new content types to the library, setting up workflows for the library and applying event handlers.
The way that we did this was to create an Application page (under layouts), which got called by the custom action. Within the OnLoad of this page, we had the following:

using (SPLongOperation operation = new SPLongOperation(this.Page))
{
try
{
operation.LeadingHTML = "Convert to Project Documents library";
operation.TrailingHTML = "Please wait while your document library is being prepared for project use.";
operation.Begin();

// your code goes here

operation.End(string.Format("{0}&ConversionStatus={1}", this.CurrentRequestUrlAndQuery, FormSubmitStatus.Success.ToString()));
}
catch (Exception ex)
{
// record the error
operation.End(string.Format("{0}&ConversionStatus={1}", this.CurrentRequestUrlAndQuery, FormSubmitStatus.Fail.ToString()));
}
}

How easy is that? SharePoint Rocks!

Creating SharePoint Timer Jobs

In my current project, I have been developing a bunch of functionality around project space provisioning. As part of this, I decided that as there were a lot of activities to be performed when creating and configuring the new site, developing a SharePoint Timer Job was the best option.

So lesson learnt #1 - You can't programmatically create a timer job from a site collection feature. The reason for this is that typically the application pool that looks after the site does not have permission to write to the configuration database. Creating a Timer Job requires you to be able to write the timer job definition into the configuration database, so if your application pool doesn't have the rights, then the creation of the job will fail.

So basically you have to create a Web Application level feature (i.e. activated through Application Management tab of Central Admin) with a feature receiver method that creates the timer job. E.g.

<?xml version="1.0" encoding="utf-8"?>
<Feature Id="2DEA0BA9-A8D1-4475-AD0B-8FE25EBF65A2"
Title="Project Spaces Provisioning Timer Job"
Description="Add timer job required for project space provisioning."
Version="12.0.0.0"
Hidden="FALSE"
Scope="WebApplication"
DefaultResourceFile="core"
ReceiverAssembly="Collaboration.Projects, Version=1.0.0.0, Culture=neutral, PublicKeyToken=d76123a549458435"
ReceiverClass="Collaboration.Projects.FeatureCode.ProjectProvisioningTimerJob"
xmlns="http://schemas.microsoft.com/sharepoint/">
<elementmanifests>
</feature>

Creating the timer job in code is actually quite simple. Below is the feature activation/deactivation code:

using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Collaboration.Projects.Provisioning;
namespace Collaboration.Projects.FeatureCode
{
///
/// Creates the timer job that runs the site creation process for confirmed requests in the provisioning request list
///

public class CollabProjectProvisioningTimerJob : SPFeatureReceiver
{
public override void FeatureActivated(SPFeatureReceiverProperties properties)
{
SPWebApplication application = properties.Feature.Parent as SPWebApplication;
DeleteProvisioningJob(application);
// install the job
ProjectSpaceCreationTimerJob provisioningJob = new ProjectSpaceCreationTimerJob(application);
SPMinuteSchedule schedule = new SPMinuteSchedule() { BeginSecond = 0, EndSecond = 59, Interval = 30 };
provisioningJob.Schedule = schedule;
provisioningJob.Update();
}
public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
{
DeleteProvisioningJob(properties.Feature.Parent as SPWebApplication);
}
public override void FeatureInstalled(SPFeatureReceiverProperties properties)
{
}
public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
{
}
///
/// Deletes the provisioning job if it exists
///

///
private void DeleteProvisioningJob(SPWebApplication application)
{
foreach (SPJobDefinition job in application.JobDefinitions)
{
if (job.Name == ProjectSpaceConstants.ProvisioningJobName)
job.Delete();
}
}
}
}


Now the ProjectSpaceCreationTimerJob class just inherits from SPJobDefinition. Below is a code snippet (note that the Execute method is the actually code that gets executed when the job runs).

using System;
using System.Net;
using System.Web;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Collaboration.Projects.Resources;
using Collaboration.Provisioning;
using Collaboration.Common;
using System.Text;

namespace Collaboration.Projects.Provisioning
{
  public class ProjectSpaceCreationTimerJob : SPJobDefinition
  {
    public ProjectSpaceCreationTimerJob() : base() { }
    public ProjectSpaceCreationTimerJob(SPWebApplication web)
: base(ProjectSpaceConstants.ProvisioningJobName, web, null, SPJobLockType.Job)
    {
      this.Title = ProjectSpaceConstants.ProvisioningJobName;
    }

    public override void Execute(Guid targetInstanceId)
    {
      // your code goes here
      base.Execute(targetInstanceId);
    }
  }
}

Hope this helps. I've written it down here so that I remember it in the future.