The best solution for this is to run the process on a background thread so that it
doesn't interfere with the user being able to do other things in your Web Application. Here
is a relatively simple way to do this:
For this demo I am going to use the ParameterizedThreadStart method to kick off
a background thread with a method that acts as a surrogate for an actual long-running
process. Here is the demo class:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Threading;
namespace LongProcess
{
public class MyLongProcessClass
{
public void DoLongProcess (object parms)
{
object[] stuff = (object[]) parms;
// delete any previous files
System.IO.File.Delete(Global.FilePath );
// the line below is a surrogate for whatever your business logic does that takes
10 seconds.
Thread.Sleep(10000);
string path = Global.FilePath;
// write the ready message to the file
System.IO.File.WriteAllText(path, "READY " +(string)stuff[0] +"|" + Convert.ToString(stuff[1]));
}
}
}
You can see above that the DoLongProcess method accepts a single parameter of type
Object (this is required for the ParameterizedThreadStart method) and that in
this case that parameter will be an Object[] array containing more than one value.
I also use a file to keep track of results in order not to be dependent on Session
or Cache, as that context may not be available on a background thread.
In my Global.asax.cs class, I have the following method:
public static string FilePath;
public static void ProcessList(object[] parms)
{
var pc = new MyLongProcessClass();
ParameterizedThreadStart ts = new ParameterizedThreadStart(pc.DoLongProcess);
Thread thd = new Thread(ts);
thd.IsBackground = true;
thd.Start(parms);
}
Because it is public and static, this method can be kicked off by any of the pages
in the app via "Global.ProcessList( parms)". However, a new instance
of the MyLongProcess class is created on every call to the static method. Note
also that the thread we create is set to be a background thread. This method
call will not block - it will return almost immediately because it is not being
called on the main thread.
In my Default.aspx page I have the following code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
namespace LongProcess
{
public partial class _Default : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
if(User.Identity.IsAuthenticated==false)
Response.Redirect("~/Account/Login.aspx");
txtUserName.Text = User.Identity.Name;
Global.FilePath = Server.MapPath("~/App_Data/stuff.txt");
if (Request.QueryString["msg"] != null)
this.lblMessage.Text = Request.QueryString["msg"];
}
protected void btnSubmit_Click(object sender, EventArgs e)
{
string user = txtUserName.Text;
int processId = int.Parse(txtProcessId.Text);
object[] parms = new object[] {user, processId};
Global.ProcessList(parms);
Label1.Text = "Process is Running.";
}
protected void LinkButton1_Click(object sender, EventArgs e)
{
Response.Redirect("Wait.aspx");
}
}
}
I'm using a modified version of Mads Kristensen's XmlMembershipProvider to allow
for standard ASP.NET log-ins without a database. So we require the user to be
logged in. If they are, I set the Global FilePath to a specified file located
in the App_Data folder, where it will not cause my ASP.NET app to recycle if
the file is changed. Also, since I redirect back to this page when the process
is done, I have some code to grab and display the message from the querystring.
The Submit button click handler gets the username and an integer value for a process
id from a couple of textboxes, creates the object[] parms array, and calls my
Global.ProcessList method. I show the user that the process is now running, and
i provide a link that will take them to "Wait.aspx" page. Note that
this is just a demo, you'll want to validate your inputs in the real world.
The Wait.aspx page has the following logic:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Threading;
namespace LongProcess
{
public partial class Wait : System.Web.UI.Page
{
protected void Page_Load(object sender, EventArgs e)
{
string x = null;
try
{
// see if the "completed" file is there
x = System.IO.File.ReadAllText(Global.FilePath);
}
catch
{
// Could use File.Exists to avoid an exception you know!
// file not there yet.
}
if (x == null)
{
}
else
{
// Delete the file since we are done with it
System.IO.File.Delete(Global.FilePath);
// redirect back to the Default.aspx page with our completed message
Response.Redirect("Default.aspx?msg="+x);
}
}
}
}
The markup simply has some script that makes the page reload every 2 seconds:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="Wait.aspx.cs"
Inherits="LongProcess.Wait" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<script>
var done = null;
function reload() {
document.location.href = 'Wait.aspx';
}
</script>
<title></title>
</head>
<body onload="done=setTimeout('reload()', 2000)">
<form id="form1" runat="server">
<div>
<asp:Label ID=lblMessage runat=server Text="Waiting..." />
</div>
</form>
</body>
</html>
Of course you do not have to show a Wait page; you could choose to simply inform
the user that the process will take a long time and that you'll send them an
email with a link as soon as it's done. You would do something like this at the very end of the body of the DoLongProcess method.
I also have an animated gif progress indicator on the Default.aspx page just in case
you want to do it that way.
And that is it! You can try this out via the downloadable Visual Studio 2010 solution
below, which is 100% self-contained and needs no setup of any kind. The initial
username and password to use are "test", "test".
Download the Visual Studio 2010 Solution that accompanies this article.