There are many cases where you need a relatively long running task in Kentico. It could be a data import, processing existing data, doing calculations, etc.
One common approach is to write code, in a webpart or an .aspx
page, visit the page, and wait for things to happen.
This usually ends in a "Request timed out" exception. Then you start the hunt for web.config
settings to prevent the timeout from occurring.
There are various techniques used for constantly retrieving information from a server, without user interaction (such as a refresh button) or page loads (F5 until you get what you expected). One common technique is AJAX polling, mainly due to it's compatibility with older browsers and technology.
The trick I usually use to detect this is to open up Chrome Developer Tools, and click on the Network tab. If there are requests constantly popping up in regular intervals, it's probably AJAX polling.
The "AsyncControl" user control is used throughout the Kentico administration area for long running operations. Whenever you see a loading bar or loading animation, it's most likely an async process taking place.
Let's give it a go. We can start by adding the control to a new custom webpart.
<cms:AsyncControl runat="server" ID="elemAsync" ProvideLogContext="True" OnOnFinished="elemAsync_OnOnFinished" />
Setting the ProvideLogContext
property to true
ensures a log is created for our progress messages. The OnOnFinished
event is fired when our async operation is completed.
How do we run our async operation then?
Let's expand our webpart layout with a few more controls:
<cms:AsyncControl runat="server" ID="elemAsync" ProvideLogContext="True" OnOnFinished="elemAsync_OnOnFinished" />
<asp:Button runat="server" ID="btnRun" Text="Run" OnClick="btnRun_OnClick" />
<asp:PlaceHolder runat="server" ID="plcSpinner" Visible="False">
<i class="icon-spinner spinning"></i>
</asp:PlaceHolder>
Alongisde the AsyncControl, we now have a 'Run' button to trigger the process, and a loading spinner to indicate progress.
Now onto the codebehind:
// Executes when the 'Run' button is clicked
protected void btnRun_OnClick(object sender, EventArgs e)
{
// Show the loading spinner
plcSpinner.Visible = true;
// Run the async method
elemAsync.RunAsync(LongRunningMethod, WindowsIdentity.GetCurrent());
}
// Our long-running async method
private void LongRunningMethod(object parameter)
{
for (var i = 1; i <= 5; i++)
{
Thread.Sleep(10000);
elemAsync.AddLog("Iteration " + i);
}
}
// Executes after our async method (LongRunningMethod) completes its execution
protected void elemAsync_OnOnFinished(object sender, EventArgs e)
{
plcSpinner.Visible = false;
}
The control has a GetCancelScript
method which returns JavaScript code to stop the async method from running any longer.
We can then add a simple link or button to stop the execution:
<asp:HyperLink runat="server" ID="btnCancel" Text="Cancel"></asp:HyperLink>
And in the codebehind:
btnCancel.Attributes.Add("onclick", elemAsync.GetCancelScript(true));
If you are unable to use the AsyncControl for your asynchronous work, you could use the AsyncWorker
class directly.
// Create a new async worker
var worker = new AsyncWorker();
// Run your async method
worker.RunAsync(x = {
for(var i=0; i<5; i++)
{
Thread.Sleep(5000);
}
}, WindowsIdentity.GetCurrent());
The AsyncWorker
class has a Stop
method, as well as OnError
and OnFinished
events.
It is important that you keep track of your worker instance, in case you need to perform actions later.
A good way would be to set the ProcessGUID
property when creating an instance of the AsyncWorker
class. You can then save the GUID somewhere (possibly in the database), and access the same worker process just by creating an instance of the AsyncWorker
class with the exact same GUID.
// Generate a GUID
var guid = Guid.NewGuid();
// Create a worker
var worker = new AsyncWorker()
{
ProcessGUID = guid
};
// Run it
worker.RunAsync(parameter => {
for(var i=0; i<5; i++)
{
Thread.Sleep(5000);
}
}, WindowsIdentity.GetCurrent());
// Create another worker with the same GUID
var worker2 = new AsyncWorker()
{
ProcessGUID = guid
};
if(worker2.Status == AsyncWorkerStatusEnum.Running)
{
// The worker is already running!
}
The Debug application has a "Worker threads" tab, where you can see which threads are running, and see the thread progress messages. This is useful information for debugging purposes and performance optimisation.
Out of the box, most scheduled tasks in Kentico run on the main thread. Most custom-built scheduled tasks also run on the main thread, as this approach is easier than figuring out what the "run task in separate thread" checkbox does. As per the Kentico docs, as long as you do not need access to context variables (current user, current page, etc), the application performance can be improved by ticking this checkbox.
A very useful benefit is the ability to log information during the execution of your scheduled task. A 'live feed' of this information can then be viewed in the aformentioned Debug application.
public class MyScheduledTask : ITask
{
public string Execute(TaskInfo task)
{
// Get the log context of the currently running asynchronous task
var logContext = LogContext.Current;
// Add a message to the log
logContext.AppendText("Running task...");
for(var i=0; i<10; i++)
{
// Run some complex operations
// . . .
// Add a message to the log
logContext.AppendText("Processed 1234 objects");
}
}
return "Done!";
}
Here it is in action: