Vor kurzem wurde ich mit einem Sharepoint-Problem eines Kunden konfrontiert, für das ich nur einen selbstdefinierten TimerJob als sinnvolle Lösungsmöglichkeit sah. Da ich mich aber bisher noch nicht mit SharePoint TimerJobs auseinandergesetzt habe, war dies ein willkommene Gelegenheit, dies nachzuholen. Zwar findet man im Internet einige interessante Posts über SharePoint TimerJobs, aber ich wollte mir zunächst selbst die Basics erarbeiten.
Und genau über diese Basics möchte ich hier berichten:
Ein SharePoint TimerJob besteht aus einer Klasse, die von der Basisklasse SPJobDefinition abgeleitet ist. In der Ableitung wird im Wesentlichen die Methode Execute() mit der gewünschten Funktionalität überschrieben und der eigentliche TimerJob ist bereits fertig. Zusätzlich kann man auch noch die Konstruktoren überschreiben, um z.B. dem eigenen TimerJob einen aussagekräftigen Titel zu geben.
Ein TimerJob kann dann z.B. folgendermaßen aussehen:
using System;
using Microsoft.SharePoint.Administration;
namespace TimerJobNotifyExpiredPassword
{
public class NotifyExpiredPasswordTimerJob : SPJobDefinition
{
public NotifyExpiredPasswordTimerJob()
: base()
{
}
public NotifyExpiredPasswordTimerJob(string strJobName, SPService oService, SPServer oServer, SPJobLockType oTargetType)
: base(strJobName, oService, oServer, oTargetType)
{
}
public NotifyExpiredPasswordTimerJob(string strJobName, SPWebApplication oWebApp)
: base(strJobName, oWebApp, null, SPJobLockType.ContentDatabase)
{
this.Title = "NotifyExpiredPasswordTimerJob";
}
public override void Execute(Guid oContentDBId)
{
// Hier den auszuführenden Timer-Code einfügen
}
}
}
An der grün markierten Stelle würde man den eigenen Code einfügen, der vom SharePoint TimerDienst zyklisch aufgerufen werden soll. OK, nun haben wir bereits die Klasse, die vom SharePoint TimerDienst aufgerufen werden könnte - wenn es uns nun noch gelingen würde, die obige Klasse als SharePoint TimerJob auf einem SharePoint-Server zu installieren. Dies ist leider nur mit ein wenig zusätzlicher Programmierarbeit möglich. Das Verfahren ist ähnlich, wie es z.B. auch bei einem benutzerdefiniertem EventHandler angewendet werden kann. Wir werden unseren TimerJob über ein SharePoint Feature installieren und uns an den Event hängen, der beim Installieren bzw. beim Deinstallieren eines Features ausgelöst wird. Beim Installieren fügen wir unsere neue TimerJob-Klasse als neuen TimerJob der entsprechenden SharePoint-Liste hinzu, beim Deinstallieren entfernen wir unsere Klasse aus der Liste der SharePoint-TimerJobs. Im Folgenden möchte ich zeigen, wie man das genau macht:
Zuerst benötigen wir eine zweite Klasse, die aber diesmal von der Basisklasse SPFeatureReceiver abgeleitet wird. Wir erstellen uns also einen eigenen EventReceiver und überschreiben hier z.B. die Methoden FeatureActivated() und FeatureDeactivating(). Beim Aktivieren unseres Features wird unsere Methode FeatureActivated() aufgerufen und hier fügen wir nun den Code ein, um unseren TimerJob zu installieren. Beim Deaktivieren unseres Features wird dann die Methode FeatureDeactivating() aufgerufen und wir fügen hier den Code ein, um unseren TimerJob wieder zu deinstallieren.
Dies kann dann z.B. folgendermaßen aussehen:
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
namespace TimerJobNotifyExpiredPassword
{
class FeatureReceiver : SPFeatureReceiver
{
public override void FeatureActivated(SPFeatureReceiverProperties oProperties)
{
SPSite oSite = oProperties.Feature.Parent as SPSite;
// Falls ältere Instanz vorhanden, diese zuvor löschen
foreach (SPJobDefinition oJob in oSite.WebApplication.JobDefinitions)
{
if (oJob.Name == "TimerJobNotifyExpiredPassword")
{
oJob.Delete();
}
}
// Timer-Job installieren
TimerJobNotifyExpiredPassword.NotifyExpiredPasswordTimerJob oTimerJob = new TimerJobNotifyExpiredPassword.NotifyExpiredPasswordTimerJob("TimerJobNotifyExpiredPassword", oSite.WebApplication);
SPMinuteSchedule oSchedule = new SPMinuteSchedule();
oSchedule.BeginSecond = 0;
oSchedule.EndSecond = 59;
oSchedule.Interval = 1; // <- diesen Job jede Minute starten
oTimerJob.Schedule = oSchedule;
oTimerJob.Update();
}
public override void FeatureDeactivating(SPFeatureReceiverProperties oProperties)
{
SPSite oSite = oProperties.Feature.Parent as SPSite;
// Job suchen und löschen
foreach (SPJobDefinition oJob in oSite.WebApplication.JobDefinitions)
{
if (oJob.Name == "TimerJobNotifyExpiredPassword")
{
oJob.Delete();
}
}
}
public override void FeatureInstalled(SPFeatureReceiverProperties oProperties)
{
}
public override void FeatureUninstalling(SPFeatureReceiverProperties oProperties)
{
}
}
}
Das Ganze - also unsere beiden Klassen habe ich in einem einfachen VisualStudio 2008 Projekt für eine Klassenbibliothek verpackt und übersetzt. Um dies jetzt als SharePoint Feature installieren zu können, benötigen wir nur noch eine einzige Datei. Diese Datei trägt den Namen feature.xml und enthält eine XML-Beschreibung unsers Features. Für mein einfaches Beispiel reicht diese feature.xml völlig aus:
<?xml version="1.0" encoding="utf-8" ?>
<Feature xmlns="http://schemas.microsoft.com/sharepoint/"
Id="04496CA0-316D-11DD-BD11-0800200C9A66"
Title="TimerJobNotifyExpiredPassword"
Description="TimerJobNotifyExpiredPassword"
Version="1.0.0.0"
Scope="Site"
ReceiverAssembly="TimerJobNotifyExpiredPassword, Version=1.0.0.0, Culture=neutral, PublicKeyToken=639ff2c7cd9696b7"
ReceiverClass="TimerJobNotifyExpiredPassword.FeatureReceiver">
</Feature>
So einfach diese Datei auch aussieht, es gibt hier einiges, was man beachten muss:
-
unser Feature muss über eine eindeutige GUID identifizierbar sein. Die GUID in der Zeile "Id=" muss deswegen ausgetauscht werden
-
gleiches gilt für die Zeilen Title und Description. Hier gibt man dem Feature einen Namen und eine Beschreibung
-
ausgetauscht bzw. angepasst werden müssen auch die letzten beiden Zeilen, die die Assembly und die Klasse angeben, die als Receiver für die oben beschreibenen Feature-Events verwendet werden soll. Übrigens: den PublicKeyToken bekommt man recht einfach mit dem .NET Reflector von Lutz Roeder heraus.
-
wichtig ist auch die Zeile "Scope=". Hier gibt man den Gültigkeitsbereich des Features an. Wichtig zu beachten: der hier eingetragene Scope muss mit dem Scope übereinstimmen, den wir beim Installieren bzw. Deinstallieren unseres TimerJobs verwendet haben!
So - das waren alle Dateien, die man unbedingt benötigt, um einen eigenen TimerJob auf einem SharePoint-Server zu installieren. Stellt sich die Frage: wie geht es nun weiter?
Zuerst übersetzten wir unsere beiden Klassen. Sofern beide in einem Projekt zusammengefasst wurden, sollte dabei eine DLL erstellt werden. Diese DLL kopieren wird in den Global Assembly Cache. Als nächstes erzeugen wird im sogenannten 12-Hive unter ../TEMPLATE/FEATUES ein neues Verzeichnis mit dem Namen, dem wir dem Feature in der Datei feature.xml gegeben haben und kopieren die Datei feature.xml dort hin. Jetzt sind wir bereit, unser Feature zu installieren.
Dies geschieht mit folgendem Auruf:
stsadm -o installfeature -name TimerJobNotifyExpiredPassword
Nun müssen wir das Feature nur noch Aktivieren, damit unser EventReceiver aufgerufen wird und unseren TimerJob installieren kann:
stsadm -o activatefeature -name TimerJobNotifyExpiredPassword -url http://mossbasis
Die URL muss natürlich durch die URL des jeweiligen Ziels ersetzt werden.
Beide Aufrufe kann man auch in einer kleinen Batch-Datei zusammenfassen. Für meine Testinstallationen verwende ich gern diese Batch-Datei:
REM TO BE SAFE FIRST DEACTIVATE and UNINSTALL FEATURE
stsadm -o deactivatefeature -name TimerJobNotifyExpiredPassword -url http://mossbasis
stsadm -o uninstallfeature -name TimerJobNotifyExpiredPassword -force
PAUSE
REM NOW INSTALL FEATURE
stsadm -o installfeature -name TimerJobNotifyExpiredPassword
PAUSE
REM FINALLY ACTIVATE FEATURE
stsadm -o activatefeature -name TimerJobNotifyExpiredPassword -url http://mossbasis
Der beispielhafte TimerJob führt zwar keine Aktion aus, dennoch können wir mit dem Debugger schnell überprüfen, ob alles wie gewünscht funktioniert. Dazu setzten wir einen Breakpoint auf die Methode Execute() und verbinden uns mit dem Prozess OWSTIMER.EXE. Es kann notwendig sein, einen Haken neben "Prozesse aller Benutzer anzeigen" im Fenster "An den Prozess anhängen" des Debuggers zu setzen. Nach maximal einer Minute sollte der Debugger beim Aufruf der Methode Execute() anhalten - sofern am obigen Aktivierungsintervall (SPMinuteSchedule) nichts geändert wurde!
Auf ähnliche Weise kann man übrigens auch Testen, ob unser Feature-EventReceiver funktioniert. Einfach je einen Breakpoint auf die überschriebenen Methoden setzen und diesmal an den Prozess W3WP.EXE anhängen. Möglicherweise gibt es den Prozess in mehreren Instanzen. Sofern man die richtige Instanz nicht kennt, kann man sich auch einfach -zu Testzwecken- an alle Instanzen hängen.
Unser Feature läßt sich natürlich auch über die Zentraladministration aktivieren bzw. deaktiviren - in der Websitesammlungsverwaltung klickt man auf Websiteauflistungsfeatures. In der Liste Websitesammlungs-Features sollte sich unser TimerJob-Feature finden.
Ich habe hier das grundsätzliche Vorgehen beim Erstellen und Installieren eines benutzerdefinierten SharePoint TimerJobs beschrieben. Auf diese Weise funktioniert es zwar, aber für das Deployment auf ein produktives System ist diese Vorgehensweise natürlich nicht geeignet. Hierfür bietet es sich an, aus unserem SharePoint-Feature eine SharePoint-Solution zu machen