Creating Custom Timer Jobs in Windows SharePoint Services 3.0

Summary: Learn about building, deploying, and debugging custom timer jobs in Windows SharePoint Services 3.0, and examine the various configuration options that are available to developers. (17 printed pages)

Andrew Connell, Critical Path Training, LLC (Microsoft MVP)

April 2008

Applies to: Windows SharePoint Services 3.0, Microsoft Office SharePoint Server 2007

Contents

  • Introduction to Windows SharePoint Services Timer Jobs

  • Creating Custom Timer Jobs

  • Configuration Options

  • Deploying Custom Timer Jobs

  • Debugging Custom Timer Jobs

  • Conclusion

  • Additional Resources

Introduction to Windows SharePoint Services Timer Jobs

Many different types of applications require some variation of a scheduled process to run. These processes are used for complex calculations, notifications, and data validation checks, among many other tasks. Windows SharePoint Services is no exception. To return relevant and timely results to users' search queries, the content within a server farm must be indexed ahead of time. This indexing is performed at scheduled intervals. Search is only one example; another example might be sending nightly or weekly e-mail messages to users who want to be notified when changes occur in a SharePoint list. These scheduled tasks are handled by the SharePoint Timer service, a Windows service that is set up as part of the installation process.

The SharePoint Timer service is similar to tasks that you can create in any version of Windows by using the Task Scheduler application. The major benefits of using the SharePoint Timer service compared with Windows Task Scheduler jobs is that the timer service knows the topology of the server farm, and you can load balance the jobs across all the servers in the farm or tie them to specific servers that run particular services.

Although Windows SharePoint Services has included a timer service for some time, it has not been easy (or possible) for developers to take advantage of this service to create and register their own scheduled processes. Windows SharePoint Services 3.0 changed this and made the scheduled service much easier to use. First, farm administrators can now see all the registered and timer jobs in a server farm in Central Administration. To do this, on the Operations page, under the Global Configuration section, select Timer Jobs Definitions. In addition to the registered jobs, the Timer Job Status page contains a list of all the jobs and the status and outcome of the last execution of each job.

You define timer jobs by using a single class that inherits from the Microsoft.SharePoint.Administration.SPJobDefinition class. You must deploy the assembly that contains this class to the global assembly cache (GAC). Then you must deploy, or install, the timer job into the server farm. This article describes how to create a custom timer job and deploy it into a server farm that is running Windows SharePoint Services 3.0. In addition, just like many other applications, timer jobs may require some external configuration data in order to function correctly. You have several different options for where to store this configuration data.

The example timer job in this article is not very complex. The purpose of the custom timer job is to act as a replacement for the Windows SharePoint Services warmup scripts. Because Windows SharePoint Services is an ASP.NET 2.0 application, pages are compiled from the generic MSIL to native code upon first use. This is known as just-in-time (JIT) compilation. If not performed beforehand, it can cause pages to load slower the first time they are requested. For the Windows SharePoint Services 3.0 beta release, Microsoft provided a script, known as the warmup script, that issues HTTP requests to a list of Windows SharePoint Services URLs to force the JIT compilation. Although it is not widely used in a production environment, the warmup script removes the initial load time of a requested page. This is useful for Windows SharePoint Services demonstrations and development. The timer job that is demonstrated in this article serves to replace the warmup script.

Creating Custom Timer Jobs

The first step in creating a custom timer job is to create a class that inherits from SPJobDefinition. The SPJobDefinition class contains three overridden constructors. According to the Windows SharePoint Services 3.0 SDK, the constructor that has no parameters is reserved for internal use. The other two constructors set the timer job's name, the parent Web application or service (such as the search indexer), the server, and the job lock type. The lock type helps prevent multiple jobs from running at the same time.

There are three different types of locking, as defined by the SPJobLockType enumeration:

  • SPJobLockType.ContentDatabase   Locks the content database. A timer job runs one time for each content database that is associated with the Web application.

  • SPJobLockType.Job   Locks the timer job so that it runs on only one machine in the farm.

  • SPJobLockType.None   No locks. The timer job runs on every machine on which the parent service is provisioned.

For the warm-up job, you do not have to run the same job simultaneously on the same server, so the constructor can set the locking type on the job itself. In addition, it is much easier to set the title of the timer job within the constructor so that it can be retrieved by name later.

using System;
using System.Net;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;

namespace MSDN.SharePoint.Samples {
  public class SharePointWarmupJob : SPJobDefinition {

    public SharePointWarmupJob () : base() { }

    public SharePointWarmupJob (SPWebApplication webApp)
      : base(Globals.JobName, webApp, null, SPJobLockType.ContentDatabase) {
      this.Title = Globals.JobName;
    }
  }
}

Aside from the requirement of inheriting from the SPJobDefinition class, the only other requirement is that the class override the Execute virtual method defined in the SPJobDefinition class. This is the method that Windows SharePoint Services calls when the job is executed. It receives a single parameter, the ID of the content database that is being processed by the job. With the custom timer job in this article, it returns the value of Guid.Empty because the target type of the job is not a content database. The Execute method in the warmup job issues a request for the home page of each site collection in the specified Web application, as shown in this example.

public override void Execute (Guid targetInstanceId) {
  foreach (SPSite siteCollection in this.WebApplication.Sites) {
    WarmUpSiteCollection(siteCollection);

    siteCollection.RootWeb.Dispose();
    siteCollection.Dispose();
  }
}

private void WarmUpSiteCollection (SPSite siteCollection) {
  WebRequest request = WebRequest.Create(siteCollection.Url);
  request.Credentials = CredentialCache.DefaultCredentials;
  request.Method = "GET";

  WebResponse response = request.GetResponse();
  response.Close();
}

When the timer job is registered or deployed, it is associated with a particular Web application. This is why the Execute method can assume that the WebApplication property is not null. The next step is to obtain some information from an external configuration store. The next section explores the various options for storing configuration information.

Configuration Options

Many applications require some type of external configuration information. Configuration information could include things like the URLs to Web services or database connection information. Windows Forms applications can use special XML-based configuration files such as application.config. ASP.NET 2.0 applications use a Web.config file. Timer jobs are executed by using the Windows SharePoint Services Timer service (Owstimer.exe). One option is to create a configuration file for the SharePoint Timer service executable such as Owstimer.exe.config. This is not recommended for several reasons. First, you would have to modify the configuration file on each server in the farm. Second, you would have to make this change from the console instead of using the administration interface or through Windows SharePoint Services solution files (*.WSPs).

You have several other options when a custom timer job requires some type of external configuration data. These options include using the property bag on various Windows SharePoint Services objects, reading and parsing external files (such as a special XML configuration file), storing configuration data within SharePoint lists, or using the hierarchical storage.

External Files

Using external files for configuration data is very similar to creating an XML-based Web.config or *.exe.config file in common ASP.NET 2.0 applications and applications that are based on the Microsoft .NET Framework. The primary difference is that you must write the code to consume the data within the file. One approach is to create a class that is serializable to make reading and writing to the configuration file as simple as possible. For example, consider if a mailing address had to be consumed by the timer job. First, create a new class Address with the common fields for North American-based mailing addresses, as shown in this example.

public class Address {
  public string Address1;
  public string Address2;
  public string City;
  public string State;
  public string Zip;
}

The XML that stores the values of the address object would look like the following markup.

<?xml version="1.0" encoding="utf-8"?>
<Address>
  <Address1>One Microsoft Way</Address1>
  <City>Redmond</City>
  <State>WA</State>
  <Zip>98052</Zip>
</Address>

To read the data, use the XmlSerializer class to deserialize the XML into the Address object.

Address _address = new Address();
XmlSerializer serializer = new XmlSerializer(typeof(Address));
FileStream file = new FileStream(@"c:\address.xml", FileMode.Open);
_address = serializer.Deserialize(file) as Address;

To serialize the Address object back to the XML file, do the exact opposite of reading the file.

Address _address = new Address();
XmlSerializer serializer = new XmlSerializer(typeof(Address));
TextWriter writer = new StreamWriter(@"c:\address.xml");
serializer.Serialize(writer, _address);
writer.Close();

With the data stored in an external file, it can be managed by any application written to read and write to the file, or by an administrator who has console access to the server.

SharePoint Object Property Bag

In Windows SharePoint Services 3.0, Microsoft added a generic collection of properties to the following commonly used Windows SharePoint Services objects:

The SPWeb.Properties and SPListItem.Properties properties return an object of type Microsoft.SharePoint.Utilities.SPPropertyBag, which overrides the System.Collections.Specialized.StringDictionaryobject. The only difference between the two is that the SPPropertyBag object adds an Update method that commits changes to the property bag to the appropriate Windows SharePoint Services content database. Whenever you make changes to the property bag, you must call the Update method to save all the changes.

Working with the property bag is straightforward. This code demonstrates adding a new entry in the property bag of a site followed by reading the value back out and deleting it from the collection.

using (SPSite siteCollection = new SPSite("http://litware"){
  using (SPWeb site = siteCollection.RootWeb){

    // Add new key/value pair to the property bag.
    site.Properties.Add("SomeKey", "SomeValue");
    site.Properties.Update();

    // Read the value of some key.
    string keyValue = site.Properties["SomeKey"];

    // Delete the key/value pair.
    site.Properties.Remove["SomeKey"];
    site.Properties.Update();
  }
}

Using this technique, you can create application pages, site pages, Web Parts, or anything that can write to the current site's property bag.

SharePoint Lists

A third option available to you is to use SharePoint lists. SharePoint lists are appropriate to use when the timer job is associated with a specific site collection. You can create a special list (or lists) in the top level site of the site collection that the timer job interacts with. Working with SharePoint lists in timer jobs is no different from doing the same thing in a Web Part.

using (SPSite siteCollection = new SPSite("http://litwareinc.com"){
  using (SPWeb site = siteCollection.RootWeb){
    SPList taskList = site.Lists["TimerJobData"];
    foreach(SPListItem item = taskList.Items){

      // Consume list item data here.
    }
  }
} 

Hierarchical Object Storage

The Windows SharePoint Services 3.0 Software Development Kit (SDK) mentions a new addition to Windows SharePoint Services that enables you to create and interact with administrative data by using a common framework to interact with the hierarchical object store. This storage construct lets you keep the configuration data for timer jobs in Windows SharePoint Services, instead of in some external file and not tied so closely with a particular SharePoint site as the list or property bag options do.

To add items to the object store, first create a class that will contain the data for the data to store. This class must inherit from the Microsoft.SharePoint.Administration.SPPersistedObject object and must be serializable. To be serializable, it must have a default constructor that takes zero parameters. All properties that should be persisted must be implemented as public fields and decorated with the Microsoft.SharePoint.Administration.PersistedAttribute attribute. The following class is used in the SharePointWarmupJob timer job.

using System;
using System.Collections.Generic;
using Microsoft.SharePoint.Administration;

namespace MSDN.SharePoint.Samples {
  public class WarmupJobSettings : SPPersistedObject {

    [Persisted]
    public List<Guid> SiteCollectionsEnabled = new List<Guid>();

    public WarmupJobSettings () { }

    public WarmupJobSettings (string name, SPPersistedObject parent, Guid id)
      : base(name, parent, id) {
    }
  }
}

The WarmupJobSettings object contains a single public field SiteCollectionsEnabled that stores a list of IDs of each site collection that should be requested as part of the SharePointWarmupJob timer job requests.

Notice the second constructor, which sets the name, parent object, and a unique ID for the persisted object. The parent property associates the object with a particular object in the SharePoint farm. The parent object must be derived from SPPersistedObject. For a list of objects that are derived from the SPPersistedObject class, see SPPersistedObject Hierarchy in the Windows SharePoint Services 3.0 SDK.

The hierarchical object store is used to retain a list of site collections in each Web application that should be hit as part of the SharePointWarmupJob timer job. Get an instance of the parent object and use the GetChild method to read settings data back from the object store. Modify the SharePointWarmupJob.Execute method to retrieve an object back from the object store.

namespace MSDN.SharePoint.Samples {
  internal static class Globals {

    internal static string SharePointWarmupJobSettingsId {
      get { return "SharePointWarmupJobs"; }
    }

    internal static string JobName {
      get { return "SharePoint Warmup Job"; }
    }
  }
}
public class SharePointWarmupJob : SPJobDefinition {

  // Omitted...

  public override void Execute (Guid targetInstanceId) {

    // Get settings for the warmup job.
    WarmupJobSettings settings = this.WebApplication.GetChild<WarmupJobSettings>(Globals.SharePointWarmupJobSettingsId);      
    foreach (SPSite siteCollection in this.WebApplication.Sites) {
      if (settings.SiteCollectionsEnabled.Contains(siteCollection.ID))
        WarmUpSiteCollection(siteCollection);

      siteCollection.RootWeb.Dispose();
      siteCollection.Dispose();
    }
  } 
}

At this point, the SharePointWarmupJob timer job is finished to the point of being able to issue HTTP requests to site collections within a Web application. It can determine which site collections to hit based on the settings that are stored within the hierarchical object store. The next step is to deploy and install the timer job into the Windows SharePoint Services farm. In addition, the deployment process should provide a way to manage the data within the hierarchical object store.

Deploying Custom Timer Jobs

The SharePointWarmupJob class is compiled into a signed assembly to generate a strong name. You must install this assembly into the GAC. With the assembly deployed to the GAC, you can now deploy, or install, the timer job to the Windows SharePoint Services farm. The Windows SharePoint Services administrative interface does not provide a way to install the timer job, so you must write a little custom code. Although this may seem like a limitation, it does mean that there is virtually a limitless number of ways to deploy a timer job. The following sections describe three such ways. The last option, using a custom application, is the one demonstrated to install the SharePointWarmupJob timer job because it is likely one of the more complex options.

Feature Receiver

Windows SharePoint Services 3.0 added the Feature framework, which provides SharePoint site developers and owners a way to inject functionality into existing sites and deploy custom solutions, such as workflow templates that are developed in Microsoft Visual Studio. Features can also be used for timer job deployment. Although the Feature schema does not provide a way to do this, the Feature Activated and Deactivating events can be caught with custom code to handle the installation and uninstallation of a timer job.

The Feature that handles the installation and uninstallation of a timer job should be a hidden Feature so that activation is only possible by using Stsadm.exe through the console. This is because when Features are activated through the Web interface, the application pool's identity is used to execute the code in the Feature receiver. This account typically does not have permissions to install the timer job. Instead, use an account that is part of the farm administrators group to activate the Feature using Stsadm.exe. To set this, in Central Administration, click Operations. Under Security Configuration, click Update farm administrator's group. Stsadm.exe assumes the identity of the current logged-in user.

The following code installs a timer job by using a Feature receiver, specifically the FeatureActivated method, in a site collection–scoped Feature.

public override void FeatureActivated (SPFeatureReceiverProperties properties) {
  SPSite site = properties.Feature.Parent as SPSite;

  // Make sure the job isn't already registered.
  foreach (SPJobDefinition job in site.WebApplication.JobDefinitions) {
    if (job.Name == Globals.SharePointWarmupJobSettingsId)
      job.Delete();
  }

  // Install the job.
  SharePointWarmupJob SharePointWarmupJob = new SharePointWarmupJob(site.WebApplication);

  SPMinuteSchedule schedule = new SPMinuteSchedule();
  schedule.BeginSecond = 0;
  schedule.EndSecond = 59;
  schedule.Interval = 2;
  SharePointWarmupJob.Schedule = schedule;

  SharePointWarmupJob.Update();
}

Uninstalling the timer job is handled by using the FeatureDeactivated method within the Feature receiver.

public override void FeatureDeactivating (SPFeatureReceiverProperties properties) {
  SPSite site = properties.Feature.Parent as SPSite;

  // Delete the job.
  foreach (SPJobDefinition job in site.WebApplication.JobDefinitions) {
    if (job.Name == Globals.SharePointWarmupJobSettingsId)
      job.Delete();
  }
}

These two code examples demonstrate the installation and uninstallation using a site collection–scoped Feature. You can also do this same process by using a Web application–scoped Feature. You can also use a farm-scoped Feature for the timer job installation and uninstallation when the job is associated with the farm as a service, such as the Microsoft Office SharePoint Server Search service, instead of a specific Web application.

Custom Stsadm.exe Command

The Stsadm.exe command-line–based administration interface in Windows SharePoint Services does not include options to install, uninstall, or manage timer jobs. However, Stsadm.exe is an extensible application because you can create custom operations and register them for use within Stsadm.exe. It can provide farm administrators an easy to way to manage timer jobs or even script operations using standard command-line–based batch files.

Creating a custom command for Stsadm.exe involves two steps:

  1. Create a class that implements the Microsoft.SharePoint.StsAdmin.ISPStadmCommand interface, and deploy the strong-named assembly that contains the class to the GAC of all servers in the SharePoint farm.

  2. Create an XML file named stsadmcommands.*.xml that contains a list of the commands, the class that contains the command, and the strong name of the assembly that contains the class. This XML file is then deployed to the [..]\12\CONFIG folder on all servers in the SharePoint farm.

The challenge with using this approach is the command must be either written to install and uninstall a specific job or dynamic enough to install and uninstall any timer job. The latter is a much more complicated approach considering each timer job may have any number of arguments on the constructors. For more information about creating custom commands for STSADM.EXE, see the Additional Resources section.

Custom Applications

Another option for installing and uninstalling custom timer jobs is to create a custom application. This is the approach that is implemented when you install and uninstall the SharePointWarmupJob timer job. This is because not only does the job have to be installed and uninstalled, but the job also requires configuration data to execute correctly. The custom application for the SharePointWarmupJob consists of a single administration page that is added to the server farm's Central Administration site. A new Manage SharePoint Warmup Jobs link is added to the Application Management page in Central Administration. You do this by using a Feature, as follows.

<?xml version="1.0" encoding="utf-8"?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
         Id="5B16233D-4396-4241-B320-276EF3FA42FE"
         Title="MSDN Site Collection Warmup Job"
         Scope="Farm"
         Hidden="FALSE"
         ActivateOnDefault="TRUE"
         Version="1.0.0.0">
  <ElementManifests>
    <ElementManifest Location="centralAdmin.xml"/>
  </ElementManifests>
</Feature>

The element manifest file centralAdmin.xml does the work of adding a new link to the Application Management page in Central Administration.

<?xml version="1.0" encoding="utf-8" ?>
<Elements xmlns="http://schemas.microsoft.com/sharepoint/">
  <CustomAction Id="B2EA3A10-701C-4B7A-A1D5-68EFD00D9957"
                GroupId="SiteManagement"
                Location="Microsoft.SharePoint.Administration.ApplicationManagement"
                Sequence="100"
                Title="Manage SharePoint Warmup Jobs"
                Description="Manage the warmup jobs for existing SharePoint Web applications.">
    <UrlAction Url="_admin/MSDN/WarmupJobManager.aspx" />
  </CustomAction>
</Elements>

The next step is to create the administration page. This page enables you to do the following:

  • Select the Windows SharePoint Services–extended Web application that you want.

  • Display a list of site collections within the selected Web application. The user should be able to choose specific site collections that the SharePointWarmupJob should hit when it runs on the set schedule.

  • Specify how often the SharePointWarmupJob should run.

The WarmupJobManager.aspx page reuses a few Windows SharePoint Services user interface controls that Microsoft developed for Central Administration. Specifically it uses the InputFormSelector, ButtonSection, and WebApplicationSelector controls to give the user a similar experience to that of other pages within Central Administration. The content of WarmupJobManager.aspx is as follows.

<%@ Page Debug="true" Language="C#" MasterPageFile="~/_admin/admin.master" 
  Inherits="MSDN.SharePoint.Samples.WarmupJobManager, MSDN.SharePoint.Samples.SharePointWarmupJob, 
  Version=1.0.0.0, Culture=neutral, PublicKeyToken=7fd1d26c5854f031" %>

<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" 
  Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>

<%@ Register TagPrefix="wssuc" TagName="InputFormSection" src="~/_controltemplates/InputFormSection.ascx" %>

<%@ Register TagPrefix="wssuc" TagName="ButtonSection" src="~/_controltemplates/ButtonSection.ascx" %>

<asp:Content ID="Content1" ContentPlaceHolderID="PlaceHolderPageTitleInTitlearea" >
  Web Application Warmup Job Manager
</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="PlaceHolderPageDescription" >

  This page allows you to manage a warmup job that will simply issue requests to trigger 
  a Just In Time (JIT) compilation. Forcing a JIT compilation can speed up the initial request 
  for a page which is helpful on development and sites that do not incur a high volume of traffic.

</asp:Content>

<asp:Content ID="Content3" ContentPlaceHolderID="PlaceHolderMain" >
  <table width="100%" class="propertysheet" cellpadding="0" cellspacing="0" border="0">
    <tr><td class="ms-error"><asp:Literal ID="ErrorMessageLiteral"  EnableViewState="false" /> </td></tr>
  </table>
  
  <table border="0" cellspacing="0" cellpadding="0" width="100%">
    <tr>
      <td>
        <!-- web application selector -->
        <wssuc:InputFormSection 
                    Title="<%$Resources:spadmin, multipages_webapplication_title%>"
                    Description="<%$Resources:spadmin, multipages_webapplication_desc%>" >
          <Template_InputFormControls>
            <tr>
              <td>
                <SharePoint:WebApplicationSelector id="WebAppSelector"  
                  OnContextChange="WebAppSelector_OnContextChange" />
              </td>
            </tr>
          </Template_InputFormControls>
        </wssuc:InputFormSection>
        
        <!-- site collection selector -->
        <wssuc:InputFormSection 
                                  Title="Site Collections"
                                  Description="Select the site collections to enable or disable 
                                  the warmup job. Only the top level site's homepage within the site collection is hit.">
          <Template_InputFormControls>
            <tr valign="top">
              <td>
                <asp:Repeater ID="SiteCollectionRepeater"  
                OnItemDataBound="SiteCollectionRepeater_OnItemDataBound">
                  <ItemTemplate>
                    <asp:TextBox  Visible="false" ID="SiteCollectionIdTextBox" />
                    <asp:Literal  ID="SiteCollectionLiteral" /><br />
                    <asp:RadioButtonList ID="JobStatusList"  RepeatDirection="Horizontal">
                      <asp:ListItem class="ms-descriptionText">Enabled</asp:ListItem>
                      <asp:ListItem class="ms-descriptionText">Disabled</asp:ListItem>
                    </asp:RadioButtonList>
                  </ItemTemplate>
                  <SeparatorTemplate><br /></SeparatorTemplate>
                </asp:Repeater>
              </td>
            </tr>
          </Template_InputFormControls>
        </wssuc:InputFormSection>
        
        <!-- job runtime schedule -->
        <wssuc:InputFormSection 
                                title="Scheduled Run Interval">
          <Template_Description>Select the interval for the warmup jobs. All site collections 
          for a particular web application will have the same schedule.</Template_Description>
          <Template_InputFormControls>
            Run warmup job every <asp:DropDownList id="IntervalMinutesDropDownList"  >
                                    <asp:ListItem>1</asp:ListItem>
                                    <asp:ListItem>2</asp:ListItem>
                                    <!-- omitted for readability -->
                                    <asp:ListItem>58</asp:ListItem>
                                    <asp:ListItem>59</asp:ListItem>
                                  </asp:DropDownList> minutes.
          </Template_InputFormControls>
        </wssuc:InputFormSection>
        
        <wssuc:ButtonSection >
          <template_buttons>
            <asp:Button id="SetTimerJobsButton"  class="ms-ButtonHeightWidth" Text="OK" 
            OnClick="SetTimerJobsButton_OnClick" />
          </template_buttons>
        </wssuc:ButtonSection>
      </td>
    </tr>
  </table>
</asp:Content>

When viewed in the browser, the SiteWarmUpJobManager.aspx page appears similar to Figure 1.

Figure 1. Web Application Warmup Job Manager

You should note several things in the source of SiteWarmUpJobManager.aspx. First, the Inherits attribute on the <%@ Page %> points to the fully qualified name of a class in a strongly named assembly in the global assembly cache that contains the code-behind for the page. This facilitates standard ASP.NET 2.0 code-behind techniques with the administration page. Second, the <SharePoint:WebApplicationSelector> contains an attribute OnContextChange that is set to WebAppSelector_OnContextChange. This lets you trap the event when the user changes the selected Web application. Two additional event handlers are trapped:

  • The SiteCollectionRepeater ASP.NET 2.0 Repeater OnItemDataBound event is registered with the SiteCollectionRepeater_OnItemDataBound method to facilitate binding data values in each site collection to specific Web controls.

  • The SetTimerJobsButton ASP.NET 2.0 Button OnClick event is registered with the SetTimerJobsButton_OnClick method to do the work of installing and uninstalling the timer job and setting the necessary configuration data.

With the page created, the next step is to build the code-behind file that contains the class for the page. However before moving onto the code-behind file, you must define two utility classes. The first is a static class that contains the name of the job and the unique identifier for the settings that are stored in the hierarchical object store (Globals.cs).

using System;

namespace MSDN.SharePoint.Samples {
  internal static class Globals {

    internal static string SharePointWarmupJobSettingsId {
      get { return "SharePointWarmupJobs"; }
    }

    internal static string JobName {
      get { return "SharePoint Warmup Job"; }
    }
  }
}

The second utility class contains the settings data that is saved to the hierarchical object store (WarmupJobSettings.cs).

using System;
using System.Collections.Generic;
using Microsoft.SharePoint.Administration;

namespace MSDN.SharePoint.Samples {
  public class WarmupJobSettings : SPPersistedObject {

    [Persisted]
    public List<Guid> SiteCollectionsEnabled = new List<Guid>();

    public WarmupJobSettings () { }

    public WarmupJobSettings (string name, SPPersistedObject parent, Guid id)
      : base(name, parent, id) {
    }
  }
}

Now it is time to create the code-behind file. The first part of this process is to create a class that inherits from the Microsoft.SharePoint.ApplicationPages.GlobalAdminPage base class. This class, found in the [..]\12\CONFIG\ADMINBIN\Microsoft.SharePoint.ApplicationPages.Administration.dll assembly, includes boilerplate code to help manage the security and redirection of requests within the Central Administration site. In addition, you should override the OnLoad method to enable the current site to be updated with data provided via an HTTP GET request (a requirement of the <SharePoint:WebApplicationSelector> control.

using System;
using System.Collections.Generic;
using System.Web.UI.WebControls;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
using Microsoft.SharePoint.ApplicationPages;
using Microsoft.SharePoint.WebControls;

namespace MSDN.SharePoint.Samples {
  public class WarmupJobManager : GlobalAdminPageBase {

    protected Literal ErrorMessageLiteral;
    protected WebApplicationSelector WebAppSelector;
    protected Repeater SiteCollectionRepeater;
    protected DropDownList IntervalMinutesDropDownList;

    protected override void OnLoad (EventArgs e) {
      SPContext.Current.Web.AllowUnsafeUpdates = true;
    }
}

Next, you must handle the OnContextChange event of the control, which sets the state for the rest of the page by loading the site collections within the selected Web application.

protected void WebAppSelector_OnContextChange (object sender, EventArgs e) {
  InitSiteCollectionSelector();

  // Try to get the settings out for the job and init the drop-down.
  foreach (SPJobDefinition job in this.WebAppSelector.CurrentItem.JobDefinitions) {
    if (job.Title == Globals.JobName)
      IntervalMinutesDropDownList.Items.FindByValue(((SPMinuteSchedule)job.Schedule).Interval.ToString()).Selected = true;
  }
}

private void InitSiteCollectionSelector () {
  if (this.WebAppSelector.CurrentItem.Sites.Count > 0) {
    this.SiteCollectionRepeater.DataSource = this.WebAppSelector.CurrentItem.Sites;
    this.SiteCollectionRepeater.DataBind();
  }
}

protected void SiteCollectionRepeater_OnItemDataBound (object sender, RepeaterItemEventArgs e) {
  if (e.Item.ItemType == ListItemType.Item || e.Item.ItemType == ListItemType.AlternatingItem) {
    SPSite siteCollection = e.Item.DataItem as SPSite;

    TextBox siteCollectionIdTextBox = e.Item.FindControl("SiteCollectionIdTextBox") as TextBox;
    siteCollectionIdTextBox.Text = siteCollection.ID.ToString();

    Literal siteCollectionName = e.Item.FindControl("SiteCollectionLiteral") as Literal;
    siteCollectionName.Text = siteCollection.Url;

    RadioButtonList jobStatusList = e.Item.FindControl("JobStatusList") as RadioButtonList;
    if (SiteCollectionHasWarmupJobConfigured(siteCollection))
      jobStatusList.SelectedValue = "Enabled";
    else
      jobStatusList.SelectedValue = "Disabled";
  }
}

private bool SiteCollectionHasWarmupJobConfigured (SPSite siteCollection) {
  try {
    WarmupJobSettings settings = this.WebAppSelector.CurrentItem.GetChild<WarmupJobSettings>(Globals.SharePointWarmupJobSettingsId);

    // If no settings previously created, create them now.
    if (settings == null) {
      SPPersistedObject parent = this.WebAppSelector.CurrentItem;
      settings = new WarmupJobSettings(Globals.SiteWarmupJobSettingsId, parent, Guid.NewGuid());
      settings.Update();
    }

    return settings.SiteCollectionsEnabled.Contains(siteCollection.ID);
  } catch (Exception ex) {
    ErrorMessageLiteral.Text = "A new storage location had to be created. Please go back to the Application Management 
      page and come back in before doing any work.";
    return false;
  }
}

The page should now load the site collections within the selected Web application. The last step is to implement the OnClick event handler of the Submit button to install and uninstall the custom timer job and set the necessary configuration data, as follows.

protected void SetTimerJobsButton_OnClick (object sender, EventArgs e) {
  WarmupJobSettings settings = this.WebAppSelector.CurrentItem.GetChild<WarmupJobSettings>(Globals.SiteWarmupJobSettingsId);

  // Delete the job for the current Web application.
  foreach (SPJobDefinition oldJob in this.WebAppSelector.CurrentItem.JobDefinitions) {
    if (oldJob.Title == Globals.JobName)
      oldJob.Delete();
  }
  // Purge the configuration data for the current Web application.
  settings.SiteCollectionsEnabled.Clear();
  settings.Update();

  // Get a list of all the site collections that were requested to be enabled.
  List<Guid> selectedSiteCollections = GetSelectedSiteCollections();
  if (selectedSiteCollections.Count > 0) {

    // Create a new instance of the job and schedule it.
    SPMinuteSchedule schedule = new SPMinuteSchedule();
    schedule.BeginSecond = 0;
    schedule.EndSecond = 59;
    schedule.Interval = Convert.ToInt32(this.IntervalMinutesDropDownList.SelectedValue);

    SharePointWarmupJob warmupJob = new SharePointWarmupJob(this.WebAppSelector.CurrentItem);
    warmupJob.Schedule = schedule;
    warmupJob.Update();

    // Add the settings.
    foreach (Guid siteCollectionID in selectedSiteCollections) {
      settings.SiteCollectionsEnabled.Add(siteCollectionID);
    }
    settings.Update();
  }
}

private List<Guid> GetSelectedSiteCollections () {
  List<Guid> selectedSiteCollections = new List<Guid>();

  TextBox siteCollectionIdTextBox;
  RadioButtonList siteCollectionList;
  foreach (RepeaterItem item in SiteCollectionRepeater.Items) {
    siteCollectionIdTextBox = item.FindControl("SiteCollectionIdTextBox") as TextBox;
    siteCollectionList = item.FindControl("JobStatusList") as RadioButtonList;
    if (siteCollectionList.SelectedValue == "Enabled")
      selectedSiteCollections.Add(new Guid(siteCollectionIdTextBox.Text));
  }

  return selectedSiteCollections;
}

Notice how the schedule is set for the timer job. The SPMinuteSchedule.BeginSecond property and the SPMinuteSchedule.EndSecond property specify a start window of execution. The SharePoint Timer service starts the timer job at a random time between the BeginSecond property and the EndSecond property. This aspect of the timer service is designed for expensive jobs that execute on all servers in the farm. If all the jobs started at the same time, it could place an unwanted heavy load on the farm. The randomization helps spread the load out across the farm.

The last step is to sign the assembly to generate a strong name, deploy the assembly to the GAC, and deploy the ASPX page to a new subfolder for the application within Central Administration located in the following directory: [..]\12\ADMIN\MSDN. When you have deployed all the necessary files and activated the Feature, the Manage SharePoint Warmup Jobs link appears on the Application Management page under the SharePoint Site Management section (see Figure 2).

Figure 2. Manage SharePoint Warmup Jobs link in Central Administration

An administrator can now browse to the new page through the familiar Central Administration interface and enable the SharePointWarmupJob timer job on specific site collections in the selected Web application. When it is enabled on at least one site collection in a Web application, the SharePoint Warmup Job appears on the Timer Job Definitions page in Central Administration. The job also appears on the Timer Job Status page after it has run at least one time.

Debugging Custom Timer Jobs

Inevitably at some phase during the development of an application, you must debug your custom code to isolate and troubleshoot a defect or to monitor the state of the application. Thankfully, you can debug timer jobs by using Visual Studio just as you would any other managed application. However, it is not as easy as pressing F5 or manually attaching to the W3WP.exe process that hosts application pools. Timer jobs are executed by a special Windows service that is set up on the server when you install Windows SharePoint Services: the Windows SharePoint Services Timer. This service triggers the executable Owstimer.exe. You must attach to this process to debug custom timer jobs.

It can be challenging at times to determine whether the application has attached to the process because the jobs may not fire for a few minutes at a time. You could be left with Visual Studio attached to the Owstimer.exe process with breakpoints set, not knowing if the job is running or if the breakpoints are not being hit because of an issue with loading the symbol (*.pdb) files. An easy way to determine whether the timer job is running is to add a single line of code into the SPJobDefinition.Execute method that displays a messagebox on the server and blocks execution of the timer job until you close the message box.

By adding the following code to the Execute method, you not only see your timer jobs running (as shown in Figure 3), but you also have time to manually attach the Visual Studio debugger to the Owstimer.exe process. After attaching the debugger to the process, click Ignore in the message box to enable the timer job to continue to run.

public override void Execute (Guid targetInstanceId) {

  // If in debug mode, trigger a false assertion to give time 
  // to attach the debugger to the OWSTIMER.EXE process.
#if (DEBUG)
      System.Diagnostics.Trace.Assert(false);
#endif

  // Continue with the rest of existing Execute() method.
}

Figure 3. Result of the Assert(false) statement

By placing the Assert(false) statement within the #if (DEBUG) compiler directive, the message box appears only when the project is build in Debug mode, and not in Release mode.

Note

The Owstimer.exe process probably will not be listed in the Visual Studio Attach To Process dialog box at first. In that case, make sure that the Show process from all users check box is selected. This results in a much longer list. This occurs because the initial view only shows processes that the current user started.

Conclusion

Microsoft introduced the concept of scheduled processes, known as timer jobs, in Windows SharePoint Services 3.0. The implementation is open to developers to build custom timer jobs in order to satisfy almost any project requirement.

Additional Resources

For more information, see the following resources: