Custom Recurring Scheduled Tasks in Sitefinity with Cron Jobs
Learn how to create custom recurring scheduled tasks in Sitefinity with Cron Jobs and how to display the current state of the task with Kendo UI ProgressBar
Sometimes your Sitefinity web site needs to run a scheduled task on the server, and sometimes this scheduled task needs to run repeatedly, e.g. every day or every hour, or even every first Thursday of the month.
Goal
This blog post will show you how to do that and also provide you with a nice little UI to monitor the progress of the running task.
Implementation
Sitefinity provides an infrastructure for creating and managing custom scheduled tasks.
This is done by the Scheduling Service for starting and managing the task and the abstract ScheduledTask class which provides basic properties like ExecuteTime, IsRunning and the abstract ExecuteTask() method. Normally, implementing the ScheduledTask class will give you the opportunity to run your task once - at ExecuteTime.
So, how to make the task recurring?
Enter Cron Jobs - "'Cron' is a time-based job scheduler in Unix-like operating systems (Linux, FreeBSD, Mac OS etc...). And these jobs or tasks are referred to as "Cron Jobs"."
(Source).
Fortunately, Sitefinity support Cron jobs. To make a scheduled tab recurring, you need to:
- set its ScheduleSpecType property to "crontab"
- set its ScheduleSpec property to a cron expression. Cron expression determines how often and when the task is going to run.
It consists of five parts:
- minute
- hour
- day of month
- month
- day of week
Example:
50 11 * * * - means every day at 11:50AM
0 4 1,10 * * - means every 1st and 10th day of the month, at 4:00AM
20 4 * * SUN - means every Sunday at 4:20AM
A great tool to use for creating/testing cron expressions is https://crontab.guru
Now, let's have a look at the code.
First, we register our custom scheduled task in Global.asax.cs:
using
SitefinityWebApp.Custom.ScheduledTasks;
using
System;
using
Telerik.Sitefinity.Services;
namespace
SitefinityWebApp
{
public
class
Global : System.Web.HttpApplication
{
protected
void
Application_Start(
object
sender, EventArgs e)
{
SystemManager.ApplicationStart += SystemManager_ApplicationStart;
}
private
void
SystemManager_ApplicationStart(
object
sender, EventArgs e)
{
CustomScheduledTaskBase.RegisterCustomScheduledTasks();
}
}
}
What RegisterCustomScheduledTasks() method does, is to find all classes that implement the CustomScheduledTaskBase abstract class and add them if they do not already exist. This is useful if you have more than 1 implementation of the class.
public
static
void
RegisterCustomScheduledTasks()
{
using
(SchedulingManager manager = SchedulingManager.GetManager())
{
List<ScheduledTaskData> allTasks = manager.GetTaskData().ToList();
// get all defined custom Scheduled tasks in this assembly
var scootTasks =
typeof
(CustomScheduledTaskBase).Assembly
.GetTypes()
.Where(t => t.IsSubclassOf(
typeof
(CustomScheduledTaskBase)) && !t.IsAbstract)
.Select(t => (CustomScheduledTaskBase)Activator.CreateInstance(t));
foreach
(var task
in
scootTasks)
{
var taskData = allTasks.Where(t => t.TaskName == task.TaskName).ToList();
if
(taskData.Count == 0)
{
task.ScheduleCrontabTask();
}
else
{
foreach
(var td
in
taskData)
{
// delete failed or stopped tasks and those that have been running for
// more than 3 hours
if
(
td.Status == TaskStatus.Failed || td.Status == TaskStatus.Stopped ||
(td.IsRunning && td.LastModified.AddHours(3) < DateTime.UtcNow)
)
{
manager.DeleteTaskData(td);
manager.SaveChanges();
}
}
}
}
}
}
public
void
ScheduleCrontabTask()
{
using
(var manager = SchedulingManager.GetManager())
{
var task = (CustomScheduledTaskBase)Activator.CreateInstance(
this
.GetType());
task.Id = Guid.NewGuid();
task.ScheduleSpecType = "crontab";
task.Title = task.TaskName;
task.ScheduleSpec = CrontabExpression;
task.ExecuteTime = CrontabHelper.GetExecuteTime(CrontabExpression, scheduleSpecType);
manager.AddTask(task);
manager.SaveChanges();
}
}
You can see here, that each task is registered with its CrontabExpression which will define when the task will run.
But, CrontabExpression itself is not enough initially to determine the right ExecuteTime of the task, .e.g. if you task is to run at 11:50AM but at the time of registering the task it is already 11:55AM - then the ExecuteTime will be tomorrow at 11:50.
Similarly, if it was 11:40AM, then the ExecuteTime would be today at 11:50.
To get the proper execute time based on the current time and the cron expression, we use the GetExecuteTime method of the CrontabHelper. It will calculate when is the next run of the task, depending on the expression.
The full source code is included in the gist link at the end of this article.
Now, let's have a look at the concrete task implementation, which is pretty simple in this case:
using
System;
using
System.Threading;
namespace
SitefinityWebApp.Custom.ScheduledTasks
{
public
class
DemoScheduledTask : CustomScheduledTaskBase
{
public
override
string
CrontabExpression
{
get
{
// you can read the value from a custom config section
// execute the task every day at 11:50AM
return
"50 11 * * *"
;
}
}
protected
override
void
ExecuteTheTask()
{
for
(
int
i = 1; i <= 100; i++)
{
// this is useful if you are going to provide a UI that shows the current task progress
this
.UpdateProgress(i,
"Doing stuff {0}"
.Arrange(i));
// do your stuff...
Thread.Sleep(2000);
}
}
}
}
The task is not very fun - it just loops through 1 to 100 and updates its progress and then sleeps for 2s.
The updateProgress part is optional, but it is great if you know roughly how long your task is going to run or how many items it is going to process, etc.
Next, let's see how we can show a progress bar of the status of the running task.
We'll create a new MVC widget - the controller will do nothing, just return a view, the view will do all the work, which is client-side.
For our demo purposes, we will add a Start button, that will manually start the task so that we can test the progress bar (we don't want to wait for 11:50AM which is how we set our task to run.
<
div
class
=
"row"
>
<
label
>
You can manually start the task by clicking the button below
</
label
>
<
div
>
<
button
class
=
"sfLinkBtn sfSave"
onclick
=
"return start();"
>Manual Start Task</
button
>
</
div
>
</
div
>
<
div
class
=
"row"
style
=
"margin-top:50px;"
>
<
div
id
=
"progressbar"
></
div
>
<
div
id
=
"status"
style
=
"margin-top:30px;"
></
div
>
</
div
>
<script>
var
$progress =
null
;
var
$status = $(
"#status"
);
var
_intervalHandle =
null
;
$(
function
() {
// .container is the wrapper class of the parent Layout widget
var
$editDiv = $(
".container"
);
if
($editDiv.parent().hasClass(
"sfHeader"
)) {
// edit div is in the wrong place for some reason in SF 10.1
// we need to move it out of sfHeader, to become a sibling instead of child
$editDiv.insertAfter(
".sfHeader"
);
}
// init kendo
$progress = $(
"#progressbar"
).kendoProgressBar({
type:
"percent"
}).data(
"kendoProgressBar"
);
// in case user refreshes the page
beginPolling();
})
function
start() {
$.ajax({
url: window.location.pathname +
"/startScheduledTask?taskName=SitefinityWebApp.Custom.ScheduledTasks.DemoScheduledTask"
,
type:
"GET"
})
.done(
function
(data) {
if
(data ==
"Success"
) {
alert(
"The task has started successfully, you can check its status below"
);
beginPolling();
}
})
.fail(
function
(jqXHR, textStatus) {
console.log(jqXHR);
alert(jqXHR.responseText);
});
return
false
;
}
function
beginPolling() {
refreshProgressBar();
_intervalHandle = window.setInterval(
function
() {
refreshProgressBar();
}, 1500);
}
function
_removeHandlers() {
if
(_intervalHandle) {
window.clearInterval(_intervalHandle);
_intervalHandle =
null
;
}
}
function
refreshProgressBar() {
// get task progress from Sitefinity Scheduling Service
$.ajax({
url:
"/Sitefinity/Services/SchedulingService.svc/taskName/SitefinityWebApp.Custom.ScheduledTasks.DemoScheduledTask/progress?providerName="
,
type:
"GET"
})
.done(
function
(data) {
$progress.value(data.ProgressStatus);
if
(data.StatusMessage)
$status.text(data.StatusMessage);
else
$status.text(
""
);
if
(data.ProgressStatus == 100 || data.Status != 1) {
_removeHandlers();
}
})
}
</script>
Now, if we start the task, we will see this:
To get the current status of the task we use the SchedulingService - we just pass the name of our task, like this:
/Sitefinity/Services/SchedulingService.svc/taskName/SitefinityWebApp.Custom.ScheduledTasks.DemoScheduledTask/progress?providerName=
The json result provides a ProgressStatus and StatusMessage (the ones that we set via UpdateProgress server method).
Full source code is here:
https://gist.github.com/VesselinVassilev/89b04f4ce7cef2610590643bafe089ca