Logo: TechTrax...brought to you by MouseTrax Computing Solutions

Background Processing, and Processing E-mails from ASP.NET

by Adrian Forbes, MVP
Skill rating level 11.

ASP.net offers many advantages over classic ASP in terms of what the .net framework offers you. One thing you can now do is safely put objects into the Session and Application collections. In this example we will create an object that is stored in the Application that has multiple threads running which will be "doing things" in the background. In this example one thread will be receiving and processing e-mails, and another will be tidying up old data from a database.

We'll combine both of these functions into a single website whose function is receiving e-mails and sorting and storing them into a database as threads of discussion for view via the web. We will also be tidying up old posts from the database as they expire. We will also be dabbling a little in some new ASP.net features such as data-bound tables, automatic paging of data etc. This example will be done in c# and SQL Server.

Database Basics

This web site is going to process incoming e-mails and store them in threads to view in a forum manner. Before we look at any of the code we'll need to create the database tables and stored procedures.

We're using SQL Server for this example, so create a new database and run the following script to create the relevant tables.

CREATE TABLE [dbo].[Thread] (
	[ThreadID] [int] IDENTITY (1, 1) NOT NULL ,
	[ThreadName] [varchar] (100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
	[UpdatedDate] [datetime] NOT NULL 
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Message] (
	[MessageID] [int] IDENTITY (1, 1) NOT NULL ,
	[ThreadID] [int] NOT NULL ,
	[MessageSubject] [varchar] (100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
	[MessageFrom] [varchar] (100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
	[MessageBody] [text] COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
	[InsertedDate] [datetime] NOT NULL 
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO

As you can see there are two tables, one to store a list of threads, and one to store a list of messages in that thread. The web site will be showing a list of all threads and the user will click on one to reveal all of the messages in it. We'll look at the stored procedures required when we get to the relevant bits in the example.

Code Basics

Start off by creating a new c# Web Application called MessageManager and this will give you a global.asax file, a Web.config file and a default form called WebForm1.aspx. Rename this form to default.aspx (we don't want to get too far from classic ASP, do we?).

Add a new class called Poll.cs, this is the object that we'll store in the Application collection that will poll various resources for us. This class is going to be manipulating a database, creating threads and interacting with the Web's configuration file so make sure you have these usings at the top;

	using System.Threading;
	using System.Collections;
	using System.Data;
	using System.Data.SqlClient;
	using System.Configuration;

When the object is created we will get all the static data we need and set up any initial state. The class will also kick off worker threads. Add these variables to the class. Don't worry about them too much now, their use will be explained later on.

Public class Poll
{
	private DateTime created;
	private bool isRunning;
	private ManualResetEvent resetEventPOP;
	private bool stopRequested;
	private System.Threading.Thread workThreadPOP;
	private string connectString;
	private int delayPOP;
	private int age;
	private string server, username, password;

This is our constructor where we read the various values from the config files.

public Poll()
{
	created = DateTime.Now;
	isRunning = false;
	connectString = ConfigurationSettings.AppSettings["connectionString"];
	delayPOP = int.Parse(ConfigurationSettings.AppSettings["delayPOP"]);
	age = int.Parse(ConfigurationSettings.AppSettings["age"]);
	server = ConfigurationSettings.AppSettings["server"];
	username = ConfigurationSettings.AppSettings["username"];
	password = ConfigurationSettings.AppSettings["password"];
}

These configuration values are read from the appSettings section of the Web.config file. By default there is no appSettings section so you'll need to create it yourself. Simply add it to the top of the file inside the configuration section.

<configuration>
    <appsettings>
		<add key="connectionString" value=" ... " />
		<add key="delayPOP" value="60"/>
		<add key="age" value="5"/>
		<add key="server" value=" ... "/>
		<add key="username" value=" ... "/>
		<add key="password" value=" ... "/>
    </appsettings>

The appSettings are simple key/value pairs. Note that IIS monitors this file and when it changes the application is shutdown so making a change to the config file will force your site to effectively "reboot" so that the new values can be read. Obviously you will need to put your own settings in for the connection string to your database and the settings to connect to your POP3 server and mail account.

We want some of these internal variables exposed as parameters so add these to your Poll class;

	public DateTime Created
	{
		get
		{
			return created;
		}
	}
	
	public bool IsRunning
	{
		get
		{
			return isRunning;
		}
	}

Thread Management

The main part of this object is its thread management. Once you have that sorted you can create more threads to do any functions you need. The way this object is going to work is that it will create a worker thread which will do some work then go to sleep for the required amount of time, then start the loop again. The object itself will be stored in the Application and as it is the worker threads that are polling/sleeping the main thread can still be used to issue a request to stop running.

We'll add a Start method to our Poll class;

public void Start()
{
	if (isRunning)
		return;

	workThreadPOP = new System.Threading.Thread(new ThreadStart(RunPOP));
	workThreadPOP.Start();
	isRunning = true;
}

The first thing we do is check to see if the isRunning variable is true. If it is then the threads are already working so we just drop out of the method. If we are not currently running then we create a new thread for the POP3 polling to run on. When creating a new thread you must supply a delegate that references the method you want to run on the thread. Our example has a method called RunPOP which contains our check->sleep loop. We create a new delegate for that method with the "new ThreadStart(RunPOP)" command and this is passed to the constructor of the Thread class. All that is left to do is call the Start method of our new Thread and a new thread will be created and RunPOP will execute on it. Finally we set the isRunning flag to true.

This is our RunPOP method;

private void RunPOP()
{
	resetEventPOP = new ManualResetEvent(false);
	while (!stopRequested)
	{
		CheckMail();
		TimeSpan runInterval = TimeSpan.FromSeconds(delayPOP);
		resetEventPOP.WaitOne(runInterval, true);
	}
}

As you can see it is quite simple. When we enter the method we create a new ManualResetEvent object. This can be used to control when we want the thread to sleep and to wake up. This method will infinitely loop until the stopRequested variable has been set to true (we'll see how this is done later on). We call the CheckMail method, then get a TimeSpan object representing the number of seconds we wish to sleep for (if you remember this value has been read from the web.config file). By calling WaitOne the thread will sleep until it is "signalled". By passing a TimeSpan to the method we are telling it to be signalled after a certain amount of time.

In order to stop the object polling we need a way of gracefully exiting the running threads. Here is our stop method.

public void Stop()
{
	stopRequested = true;
	resetEventPOP.Set();
	workThreadPOP.Join();
	isRunning = false;
	stopRequested = false;
}

First we set the stopRequested flag to true. Next we call the Set method of the POP thread's ManualResetEvent object. What this does is signal the thread to start again. The reason we need this is to stop long delays in shutting down. If we just set the thread to sleep, and it had to sleep for 10 minutes, then on calling the Stop method we will need to wait for the POP thread to wake up again before it can see that the stopRequested flag has been set. By using reset events in this way we are signalling the thread which will wake it up if it is sleeping, it will go around the loop again and exit out as the stopRequested flag is set. Next we call the Join method of the POP thread. What this will do is block the execution of our Stop method until the POP thread has completed processing. Now we clear the isRunning and stopRequested flags.

In the global.asax file put the following code behind these events;

protected void Application_Start(Object sender, EventArgs e)
{
	Poll pollObject = new Poll();
	pollObject.Start();
	Application["PollObject"] = pollObject;
}

protected void Application_End(Object sender, EventArgs e)
{
	try
	{
		Poll pollObject = (Poll) Application["PollObject"];
		if (pollObject.IsRunning)
			pollObject.Stop();
	}
	catch (Exception exp)
	{
		System.Diagnostics.Trace.WriteLine (exp.ToString());
	}
}

This will ensure our object is created and stored when the Application starts, and is stopped when it ends.

That is the threading side of things sorted out, now we will look at the CheckMail method to see what it actually does. For this example I am using a .net POP3 component from Chilkat (http://www.chilkatsoft.com/). There is no particular reason for this, if you have your own component then you will just need to modify the code so that it fits how your component works.

private void CheckMail()
{
	try
	{
		// Create a new mail component
		MailMan mail = new MailMan();
		mail.UnlockComponent("trial version");

		// Set up the server, username and password.  These are values
		// we have previously read from the web.config file
		mail.MailHost = server;
		mail.PopUsername = username;
		mail.PopPassword = password;

		// Create a new email bundle
		EmailBundle bundle;

		// Read the POP3 mailbox and create a collection of mail
		// items in the bundle
		bundle = mail.TransferMail();
		if (bundle == null)
		{
			Trace.WriteLine (mail.LastErrorText);
			return;
		}

		// Set up a variable to refer to an email
		Email message;

		// Create an ArrayList to hold the email objects
		ArrayList messageArray = new ArrayList();

		// Loop through each email in the bundle
		for (int i = 0; i <= bundle.MessageCount; i++)
		{
			// Get the email object
			message = bundle.GetEmail(i);

			// Add it to the ArrayList
			if (message != null)
				messageArray.Add (message);
		}
		// Dispose of the email object
		mail.Dispose();

		// Call the ProcessMessages method and pass in
		// our ArrayList of email objects
		if (messageArray.Count > 0)
			ProcessMessages(messageArray);
	}
	catch (Exception exp)
	{
		Trace.WriteLine (exp.ToString());
	}
}

It is mainly code to use our POP3 component so I have commented it in-line rather than give a detailed explanation of how it works. The meat is really in the ProcessMessages method which we will look at now.

private void ProcessMessages(ArrayList messages)
{
	string subject;
	string subjectRaw;
	int i;

	foreach (Email message in messages)
	{
		subjectRaw = message.Subject.Trim();
		i = subjectRaw.LastIndexOf(":");
		if (i < 0)
			subject = subjectRaw.Trim();
		else
			subject = subjectRaw.Substring(++i).Trim();

		PersistMessage(PersistThread(subject), subjectRaw, message.From, message.Body);
	}
}

Note that;

	PersistMessage(PersistThread(subject), subjectRaw, message.From, message.Body);

Is just a fancy way of writing this;

	int threadID = PersistThread(subject);
	PersistMessage(threadID, subjectRaw, message.From, message.Body);

What we're trying to do here is something clever with the subjects of the e-mails. Let's look at an example. Say I send an e-mail with the subject "Hello" and someone replies to it. The reply will be something like "Re: Hello". Maybe someone else will forward it so the subject will now be "Fw: Re: Hello". We want to retain the actual subject of the e-mail, but we also want to strip out the added re/fw markings so that we know which e-mail it was originally related to. This method will give us a "subjectRaw" (which will be the literal subject, "Fw: Re: Hello" in this example) and just "subject" which gives us the subject with all re/fw formatting removed (just "Hello" in this example).

To help you visualise, here is an example of what we are doing;

Note that although one message has the subject of "Hello" and the other "Re: Hello", they are both stored in the "Hello" thread. So we use the "subject" when deciding what thread to store the message in, but the "subjectRaw" when storing the thread itself.

	subjectRaw = message.Subject.Trim();

This gets us the literal subject, for example "Re: Hello"

	i = subjectRaw.LastIndexOf(":");

This gets us the index of the last colon in the subject. For our example that will be 2 (strings are zero-based).

	if (i < 0)
		subject = subjectRaw.Trim();

If there is no colon then LastIndexOf will return -1 so make the subject the same as subjectRaw.

	else
		subject = subjectRaw.Substring(++i).Trim();

If there was a colon then the subject is the subjectRaw from the colon onwards. Although i gives us the starting position of the last colon, we want the substring from everything after the colon, so we need to increment the i variable before we use it. We use ++i instead of i++ as ++i ensures the variable is incremented before it is passed to Substring. Had we used i++ the variable is only incremented after i is sent to Substring.

Now we have the base thread we want to add this message to, "Hello", as well as the subject of the actual message itself, "Re: Hello". To post the thread we call PersistThread, which returns the ID of the thread the message is posted on. PersistThread will create a new thread if one with that subject does not exist, or it will return the ID of the existing thread if one does exist. PersistMessage adds the information from the message to that relevant thread.

We can see that PersistThread just calls a stored procedure called StoreThread, and it is in there that all the work is done.

	private int PersistThread(string threadName)
	{
		int threadID = 0;
		SqlParameter param;
		SqlConnection conn = new SqlConnection(connectString);
		conn.Open();
		SqlCommand comm = new SqlCommand("StoreThread", conn);
		comm.CommandType = CommandType.StoredProcedure;
	
		param = new SqlParameter("@Title", SqlDbType.VarChar, 100);
		param.Value = threadName;
		comm.Parameters.Add (param);
		
		SqlDataReader reader = comm.ExecuteReader();
		while (reader.Read())
			threadID = reader.GetInt32 (0);
	
		return threadID;
	}

Here is the procedure;

	CREATE PROCEDURE StoreThread
	@Title varchar(100)
	 AS
	
	SET NOCOUNT ON
	
	DECLARE @ID int
	
	SELECT @ID = ThreadID FROM Thread WHERE ThreadName = @Title
	
	IF @ID IS NULL BEGIN
		INSERT INTO Thread
		(
			ThreadName,
			UpdatedDate
		)
		VALUES
		(
			@Title,
			GetDate()
		)
	
		SET @ID = @@IDENTITY
	END ELSE BEGIN
		UPDATE Thread
		SET	UpdatedDate = GetDate()
		WHERE	ThreadID = @ID
	END
	
	SELECT @ID

As you can see it checks if such a thread already exists and updates the UpdatedDate if it is and returns that thread's ID, otherwise it creates a new thread for us.

PersistMessage is very much the same;

private int PersistMessage(int threadID, string subject, string from, string body) {
	decimal messageID = 0;
	SqlParameter param;
	SqlConnection conn = new SqlConnection(connectString);
	conn.Open();
	SqlCommand comm = new SqlCommand("StoreMessage", conn);
	comm.CommandType = CommandType.StoredProcedure;
	param = new SqlParameter("@ID", SqlDbType.Int, 4);
	param.Value = threadID;
	comm.Parameters.Add (param);
	param = new SqlParameter("@Subject", SqlDbType.VarChar, 100);
	param.Value = subject;
	comm.Parameters.Add (param);
	param = new SqlParameter ("@From", SqlDbType.VarChar, 100);
	param.Value = from;
	comm.Parameters.Add (param);
	param = new SqlParameter ("@Body", SqlDbType.Text, body.Length);
	param.Value = body;
	comm.Parameters.Add (param);

	SqlDataReader reader = comm.ExecuteReader();
	while (reader.Read())
		messageID = reader.GetDecimal (0);

	return (int) messageID;
}

And its procedure;

CREATE PROCEDURE StoreMessage
@ID int,
@Subject varchar(100),
@From varchar(100),
@Body text
 AS

SET NOCOUNT ON

INSERT INTO Message
	(
		ThreadID,
		MessageSubject,
		MessageFrom,
		MessageBody,
		InsertedDate
	)
	VALUES
	(
		@ID,
		@Subject,
		@From,
		@Body,
		GetDate()
	)

SELECT @@IDENTITY

That covers the basics of processing e-mails and storing the threads in the database, but we also need a page to view the threads and to view the messages in a thread.

We will return the thread list from a stored procedure that will give us a list of threads, the date they were last updated and how many messages are in each thread;

	CREATE PROCEDURE ListThreads AS
	
	SELECT
		t.ThreadID,
		t.ThreadName,
		t.UpdatedDate,
		count(*) as MessageCount
	FROM
		Thread t
	JOIN	Message m on m.ThreadID = t.ThreadID
	GROUP BY
		t.ThreadID,
		t.ThreadName,
		t.UpdatedDate
	ORDER BY
		t.UpdatedDate DESC
	GO

Nothing overly complicated there. We will use a DataGrid to show the results, which is a server-side control. Server controls are a new feature of asp.net and they are pieces of mark-up that get substituted for the real data at run-time. In the old days of ASP you would loop through your results using a "while not objRS.EOF" and use response.write to construct your table elements for each row. Server controls give us the same flexibility but with none of the coding.

Refer back to the sample images to see what we're aiming at.

In Visual InterDev view your default.aspx page in Design mode and drag on a DataGrid from the Web Forms section of the Toolbox and rename it ThreadList. As well as giving us the initial mark-up in the HTML of the page, it will also add the relevant code to the "code behind" page that lets you program it. In HTML mode of your default.aspx make the DataGrid look like this.

	<asp:DataGrid ID="ThreadList" AutoGenerateColumns="False" CellPadding="3" runat="server">
		<headerstyle BackColor="Cornsilk"></headerstyle>
		<columns>
			<asp:HyperLinkColumn headertext="Thread" DataNavigateUrlField="ThreadID" DataTextField="ThreadName" DataNavigateUrlFormatString="thread.aspx?ID={0}"></asp:HyperLinkColumn>
			<asp:BoundColumn HeaderText="Last updated" DataField="UpdatedDate" DataFormatString="{0:d MMM yyyy} {0:T}"></asp:BoundColumn>
			<asp:BoundColumn HeaderText="Post count" DataField="MessageCount"></asp:BoundColumn>
		</columns>
	</asp:DataGrid>

The first thing to note about the DataGrid line;

<asp:DataGrid ID="ThreadList" AutoGenerateColumns="False" CellPadding="3" runat="server">

Is the runat="server". This denotes our control as being a server-side control that needs processed by asp.net. The id property is the name of the DataGrid and is the name you use when manipulating it programmatically. If you look at your "code behind" page you'll see the following has been added;

public class Default : System.Web.UI.Page
{
	protected System.Web.UI.WebControls.DataGrid ThreadList;

The CellPadding attribute affects the style of the table that will be produced. We also have AutoGenerateColumns set to false. If you set this to true then asp.net will create one column for each column in your result set. We want to be more sophisticated with our results so we will be creating our own columns.

Next is the HeaderStyle;

	<headerstyle BackColor="Cornsilk"></headerstyle>

This is the style that will be attached to the "header" of the table which is the first row that contains the column names. Inside the section we list our columns. From the example results we need three columns. The first is a hyperlink to the message list for that thread, the second is the date the thread was last updated, and the third is the message count.

This is the definition for the first column.

<asp:HyperLinkColumn headertext="Thread" DataNavigateUrlField="ThreadID" DataTextField="ThreadName" DataNavigateUrlFormatString="thread.aspx?ID={0}"></asp:HyperLinkColumn>

First of all we are defining the column as a HyperLinkColumn. This is because we want the text to be a hyperlink. The URL under the thread name will be something like;

Thread.aspx?ID=1

For the hyperlink we need two bits of data; the text for the link and the ID of the thread that we can plug into the URL. The DataNavigateUrlField attribute is the name of the field that you want to plug into the URL and the DataTextField is the fieldname you want the text itself to be bound to. Here is an example row of data in raw format.

	ThreadID    ThreadName            UpdatedDate              MessageCount 
	----------- --------------------- ------------------------ ------------ 
	1           Hello                 2004-08-24 15:49:35.227  2

We are binding DataTextField to ThreadName as that is what we want the text of the link to be, and we bind DataNavigateUrlField to ThreadID as we want to insert that into the underlying URL. Now that both fields are bound, all that is left is to say what we want the URL to be. To do this we set DataNavigateUrlFormatString to "thread.aspx?ID={0}". t will insert the value of ThreadID at the position of the {0} placeholder.

The next column is the updated date one.

<asp:BoundColumn HeaderText="Last updated" DataField="UpdatedDate" DataFormatString="{0:d MMM yyyy} {0:T}"></asp:BoundColumn>

This is just a standard BoundColumn so will just contain the text of the bound field, but note that we are supplying a DataFormatString so that the data is formatted for us in the way we want. Finally the post count;

<asp:BoundColumn HeaderText="Post count" DataField="MessageCount"></asp:BoundColumn>

And that is the end of our DataGrid. In the code of the Page_Load event we need to get the results and bind the grid to them. All we do is get a DataSet based on the results of the Stored Procedure, set the DataSource of the list, then call DataBind.

	private void Page_Load(object sender, System.EventArgs e)
	{
		SqlConnection conn = new SqlConnection(ConfigurationSettings.AppSettings["connectionString"]);
		SqlCommand comm = new SqlCommand("ListThreads", conn);
		comm.CommandType = CommandType.StoredProcedure;
		SqlDataAdapter adapter = new SqlDataAdapter(comm);
		DataSet data = new DataSet();
		adapter.Fill(data);
		ThreadList.DataSource = data;
		ThreadList.DataBind();
	}

When someone navigates to this page, asp.net will create the relevant TABLE for us and add all of the rows without us actually writing any code other than the code to get the results.

Now the only other thing we need to code is the thread.aspx page which will show all of the message in a given thread. For this we will use a repeater rather than a datagrid. Here is the HTML;

<asp:Repeater ID="Messages" runat="server">
	<headertemplate>
		<p>Thread</p>
		<table border="1" style="border-collapse:collapse" width="100%">
	</headertemplate>
	<itemtemplate>
		<tr>
			<td bgcolor="Cornsilk" align="left" width="70%"><b><%# DataBinder.Eval(Container.DataItem, "MessageSubject")%></b></td>
			<td bgcolor="Cornsilk" align="right" width="30%" nowrap><b><%# DataBinder.Eval(Container.DataItem, "MessageFrom")%>
					-
					<%# DataBinder.Eval(Container.DataItem, "InsertedDate")%>
				</b>
			</td>
		</tr>
		<tr>
			<td colspan="2">
				<pre><%# Server.HtmlEncode((string) DataBinder.Eval(Container.DataItem, "MessageBody"))%></pre>
			</td>
		</tr>
	</itemtemplate>
	<footertemplate>
		</table>
	</footertemplate>
</asp:Repeater>

In the HeaderTemplate we put the HTML we want at the start, in the ItemTemplate we put the HTML we want written for each item in the result set, and the FooterTemplate has the HTML at the end. Repeater controls can be used to output lists of data as anything you want. Here we are using a table but it could be anything. Rather than binding certain elements like we did with the DataList, we get the item's values from the Container.DataItem object. It is a little clunky to use, as you can see. Again we need code in the Page_Load event to supply data to bind the repeater to;

	private void Page_Load(object sender, System.EventArgs e)
	{
		SqlConnection conn = new SqlConnection(ConfigurationSettings.AppSettings["connectionString"]);
		SqlCommand comm = new SqlCommand("ListThread " + Request["ID"], conn);
		comm.CommandType = CommandType.Text;
		SqlDataAdapter adapter = new SqlDataAdapter(comm);
		DataSet data = new DataSet();
		adapter.Fill(data);
		Messages.DataSource = data;
		Messages.DataBind();
	}

We get the ID on the URL by reading the Request object and using it to construct the relevant SQL to get the results for just that thread. Here also is the ListThread stored procedure.

	CREATE PROCEDURE ListThread
	@ID int
	 AS
	
	SELECT
		MessageID,
		MessageSubject,
		MessageFrom,
		InsertedDate,
		MessageBody
	FROM
		Message
	WHERE
		ThreadID = @ID
	ORDER BY
		MessageID

As well as polling to read new e-mails, we also have a stored procedure that tidies up old messages.

CREATE PROCEDURE DeleteOldMessages
@Age int
 AS

DELETE FROM
	message
WHERE
	DateDiff(d, InsertedDate, GetDate()) > @Age
GO

If you pass in an age in days, then any message older than that age will be deleted.

Here is the asp.net code in its entirety, with our second thread to delete old messages and other bits and bobs included.

Default.aspx

<%@ Page language="c#" Codebehind="Default.aspx.cs" AutoEventWireup="false" Inherits="MessageManager.Default" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
	<head>
		<title>Thread list</title>
		<meta name="GENERATOR" content="Microsoft Visual Studio 7.0">
		<meta name="CODE_LANGUAGE" content="C#">
		<meta name="vs_defaultClientScript" content="JavaScript">
		<meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
	</head>
	<body>
		<form id="Form1" method="post" runat="server">
			<p>
				Running:
				<asp:Label ID="Status" runat="server">Status</asp:Label></p>
			<p>
				<asp:Button ID="cmdStart" runat="server" Text="Start" Width="50px"></asp:Button> 
				<asp:Button ID="cmdStop" runat="server" Text="Stop" Width="50px"></asp:Button> 
				<asp:Button ID="cmdRefresh" runat="server" Text="Refresh"></asp:Button></p>
			<p> </p>
			<p>
				<asp:DataGrid ID="ThreadList" AutoGenerateColumns="False" CellPadding="3" runat="server">
					<headerstyle BackColor="Cornsilk"></headerstyle>
					<columns>
						<asp:HyperLinkColumn headertext="Thread" DataNavigateUrlField="ThreadID" DataTextField="ThreadName" DataNavigateUrlFormatString="thread.aspx?ID={0}"></asp:HyperLinkColumn>
						<asp:BoundColumn HeaderText="Last updated" DataField="UpdatedDate" DataFormatString="{0:d MMM yyyy} {0:T}"></asp:BoundColumn>
						<asp:BoundColumn HeaderText="Post count" DataField="MessageCount"></asp:BoundColumn>
					</columns>
				</asp:DataGrid></p>
		</form>
	</body>
</html>

Default.aspx.cs

using System;
using System.Collections;
using System.Configuration;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;

namespace MessageManager
{
	public class Default : System.Web.UI.Page
	{
		protected System.Web.UI.WebControls.Button cmdStart;
		protected System.Web.UI.WebControls.Button cmdStop;
		protected System.Web.UI.WebControls.Label Status;
		protected System.Web.UI.WebControls.Button cmdRefresh;
		protected System.Web.UI.WebControls.DataGrid ThreadList;
		private Poll pollObject;

		private void Page_Load(object sender, System.EventArgs e)
		{
			pollObject = (Poll) Application["PollObject"];
			if (pollObject.IsRunning)
				Status.Text = "Yes";
			else
				Status.Text = "No";

			SqlConnection conn = new SqlConnection(ConfigurationSettings.AppSettings["connectionString"]);
			SqlCommand comm = new SqlCommand("ListThreads", conn);
			comm.CommandType = CommandType.StoredProcedure;
			SqlDataAdapter adapter = new SqlDataAdapter(comm);
			DataSet data = new DataSet();
			adapter.Fill(data);
			ThreadList.DataSource = data;
			ThreadList.DataBind();
		}
		
		#region Web Form Designer generated code
		override protected void OnInit(EventArgs e)
		{
			InitializeComponent();
			base.OnInit(e);
		}
		
		/// <summary>
		/// Required method for Designer support - do not modify
		/// the contents of this method with the code editor.
		/// </summary>
		private void InitializeComponent()
		{    
			this.cmdStart.Click += new System.EventHandler(this.cmdStart_Click);
			this.cmdStop.Click += new System.EventHandler(this.cmdStop_Click);
			this.cmdRefresh.Click += new System.EventHandler(this.cmdRefresh_Click);
			this.Load += new System.EventHandler(this.Page_Load);

		}
		#endregion

		private void cmdStart_Click(object sender, System.EventArgs e)
		{
			if (!pollObject.IsRunning)
			{
				pollObject.Start();
				Status.Text = "Yes";
			}
		}

		private void cmdStop_Click(object sender, System.EventArgs e)
		{
			if (pollObject.IsRunning)
			{
				pollObject.Stop();
				Status.Text = "No";
			}

		}

		private void cmdRefresh_Click(object sender, System.EventArgs e)
		{
		
		}

	}
}

Poll.cs

using System;
using System.Diagnostics;
using System.Threading;
using Chilkat;
using System.Collections;
using System.Data;
using System.Data.SqlClient;
using System.Configuration;

namespace MessageManager
{
	public class Poll
	{
		private DateTime created;
		private bool isRunning;
		private ManualResetEvent resetEventPOP, resetEventSQL;
		private bool stopRequested;
		private System.Threading.Thread workThreadPOP;
		private System.Threading.Thread workThreadSQL;
		private string connectString;
		private int delayPOP;
		private int delaySQL;
		private int age;
		private string server, username, password;

		public Poll()
		{
			Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] Thread Poll created");
			created = DateTime.Now;
			isRunning = false;
			connectString = ConfigurationSettings.AppSettings["connectionString"];
			delayPOP = int.Parse(ConfigurationSettings.AppSettings["delayPOP"]);
			delaySQL = int.Parse(ConfigurationSettings.AppSettings["delaySQL"]);
			age = int.Parse(ConfigurationSettings.AppSettings["age"]);
			server = ConfigurationSettings.AppSettings["server"];
			username = ConfigurationSettings.AppSettings["username"];
			password = ConfigurationSettings.AppSettings["password"];
		}

		public DateTime Created
		{
			get
			{
				Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] Inside Poll.Created");
				return created;
			}
		}

		public bool IsRunning
		{
			get
			{
				Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] Inside IsRunning");
				return isRunning;
			}
		}

		public void Start()
		{
			Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] In Start");
			if (isRunning)
				return;

			workThreadPOP = new System.Threading.Thread(new ThreadStart(RunPOP));
			workThreadPOP.Start();
			workThreadSQL = new System.Threading.Thread(new ThreadStart(RunSQL));
			workThreadSQL.Start();
			isRunning = true;
		}

		private void RunPOP()
		{
			Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] In RunPOP");
			resetEventPOP = new ManualResetEvent(false);
			while (!stopRequested)
			{
				CheckMail();
				TimeSpan runInterval = TimeSpan.FromSeconds(delayPOP);
				Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] Waiting " + delayPOP.ToString() + " seconds...");
				resetEventPOP.WaitOne(runInterval, true);
			}
			Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] Out of RunPOP loop");
		}

		private void RunSQL()
		{
			Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] In RunSQL");
			resetEventSQL = new ManualResetEvent(false);
			while (!stopRequested)
			{
				CheckSQL();
				TimeSpan runInterval = TimeSpan.FromSeconds(delaySQL);
				Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] Waiting " + delaySQL.ToString() + " seconds...");
				resetEventSQL.WaitOne(runInterval, true);
			}
			Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] Out of RunSQL loop");
		}

		public void Stop()
		{
			Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] In Stop");
			stopRequested = true;
			resetEventPOP.Set();
			workThreadPOP.Join();
			resetEventSQL.Set();
			workThreadSQL.Join();
			isRunning = false;
			stopRequested = false;
			Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] Out Stop");
		}

		private void CheckMail()
		{
			Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] In CheckMail");
			try
			{
				// Create a new mail component
				MailMan mail = new MailMan();
				mail.UnlockComponent("trial version");

				// Set up the server, username and password.  These are values
				// we have previously read from the web.config file
				mail.MailHost = server;
				mail.PopUsername = username;
				mail.PopPassword = password;

				// Create a new email bundle
				EmailBundle bundle;

				// Read the POP3 mailbox and create a collection of mail
				// items in the bundle
				bundle = mail.TransferMail();
				if (bundle == null)
				{
					Trace.WriteLine (mail.LastErrorText);
					return;
				}

				// Set up a variable to refer to an email
				Email message;

				// Create an ArrayList to hold the email objects
				ArrayList messageArray = new ArrayList();
				
				// Loop through each email in the bundle
				for (int i = 0; i <= bundle.MessageCount; i++)
				{
					// Get the email object
					message = bundle.GetEmail(i);

					// Add it to the ArrayList
					if (message != null)
						messageArray.Add (message);
				}
				// Dispose of the email object
				mail.Dispose();

				// Call the ProcessMessages method and pass in
				// our ArrayList of email objects
				if (messageArray.Count > 0)
					ProcessMessages(messageArray);
			}
			catch (Exception exp)
			{
				Trace.WriteLine (exp.ToString());
			}
		}

		private void ProcessMessages(ArrayList messages)
		{
			string subject;
			string subjectRaw;
			int i;

			foreach (Email message in messages)
			{
				subjectRaw = message.Subject.Trim();
				i = subjectRaw.LastIndexOf(":");
				if (i < 0)
					subject = subjectRaw.Trim();
				else
					subject = subjectRaw.Substring(++i).Trim();

				PersistMessage(PersistThread(subject), subjectRaw, message.From, message.Body);
			}
		}

		private int PersistThread(string threadName)
		{
			int threadID = 0;
			SqlParameter param;
			SqlConnection conn = new SqlConnection(connectString);
			conn.Open();
			SqlCommand comm = new SqlCommand("StoreThread", conn);
			comm.CommandType = CommandType.StoredProcedure;

			param = new SqlParameter("@Title", SqlDbType.VarChar, 100);
			param.Value = threadName;
			comm.Parameters.Add (param);
			
			SqlDataReader reader = comm.ExecuteReader();
			while (reader.Read())
				threadID = reader.GetInt32 (0);

			return threadID;
		}

		private int PersistMessage(int threadID, string subject, string from, string body)
		{
			decimal messageID = 0;
			SqlParameter param;
			SqlConnection conn = new SqlConnection(connectString);
			conn.Open();
			SqlCommand comm = new SqlCommand("StoreMessage", conn);
			comm.CommandType = CommandType.StoredProcedure;
			param = new SqlParameter("@ID", SqlDbType.Int, 4);
			param.Value = threadID;
			comm.Parameters.Add (param);
			param = new SqlParameter("@Subject", SqlDbType.VarChar, 100);
			param.Value = subject;
			comm.Parameters.Add (param);
			param = new SqlParameter ("@From", SqlDbType.VarChar, 100);
			param.Value = from;
			comm.Parameters.Add (param);
			param = new SqlParameter ("@Body", SqlDbType.Text, body.Length);
			param.Value = body;
			comm.Parameters.Add (param);

			SqlDataReader reader = comm.ExecuteReader();
			while (reader.Read())
				messageID = reader.GetDecimal (0);

			return (int) messageID;
		}

		private void CheckSQL()
		{
			Trace.WriteLine ("[" + System.Threading.Thread.CurrentThread.GetHashCode().ToString() + "] In CheckSQL");
			using(SqlConnection conn = new SqlConnection(connectString))
			{
				SqlParameter param;
				conn.Open();
				SqlCommand comm = new SqlCommand("DeleteOldMessages", conn);
				comm.CommandType = CommandType.StoredProcedure;
				param = new SqlParameter("@Age", SqlDbType.Int, 4);
				param.Value = age;
				comm.Parameters.Add (param);
				comm.ExecuteNonQuery();
			}
		}
	}
}

Thread.aspx

<%@ Page language="c#" EnableViewState="False" Codebehind="Thread.aspx.cs" AutoEventWireup="false" Inherits="MessageManager.Thread" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN" >
<html>
	<head>
		<title>Thread</title>
		<meta name="GENERATOR" content="Microsoft Visual Studio 7.0">
		<meta name="CODE_LANGUAGE" content="C#">
		<meta name="vs_defaultClientScript" content="JavaScript">
		<meta name="vs_targetSchema" content="http://schemas.microsoft.com/intellisense/ie5">
	</head>
	<body>
		<p>
			<a href="../../2004_09_Sept/default.aspx">Back to thread list</a>
		</p>
		<asp:Repeater ID="Messages" runat="server">
			<headertemplate>
				<p>Thread</p>
				<table border="1" style="border-collapse:collapse" width="100%">
			</headertemplate>
			<itemtemplate>
				<tr>
					<td bgcolor="Cornsilk" align="left" width="70%"><b><%# DataBinder.Eval(Container.DataItem, "MessageSubject")%></b></td>
					<td bgcolor="Cornsilk" align="right" width="30%" nowrap><b><%# DataBinder.Eval(Container.DataItem, "MessageFrom")%>
							-
							<%# DataBinder.Eval(Container.DataItem, "InsertedDate")%>
						</b>
					</td>
				</tr>
				<tr>
					<td colspan="2">
						<pre><%# Server.HtmlEncode((string) DataBinder.Eval(Container.DataItem, "MessageBody"))%></pre>
					</td>
				</tr>
			</itemtemplate>
			<footertemplate>
				</table>
			</footertemplate>
		</asp:Repeater>
		<p>
			<a href="../../2004_09_Sept/default.aspx">Back to thread list</a>
		</p>
	</body>
</html>

Thread.aspx.cs

using System;
using System.Collections;
using System.ComponentModel;
using System.Data;
using System.Data.SqlClient;
using System.Drawing;
using System.Web;
using System.Web.SessionState;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.HtmlControls;
using System.Configuration;

namespace MessageManager
{
	/// <summary>
	/// Summary description for Thread.
	/// </summary>
	public class Thread : System.Web.UI.Page
	{
		protected System.Web.UI.WebControls.Repeater Messages;
	
		private void Page_Load(object sender, System.EventArgs e)
		{
			SqlConnection conn = new SqlConnection(ConfigurationSettings.AppSettings["connectionString"]);
			SqlCommand comm = new SqlCommand("ListThread " + Request["ID"], conn);
			comm.CommandType = CommandType.Text;
			SqlDataAdapter adapter = new SqlDataAdapter(comm);
			DataSet data = new DataSet();
			adapter.Fill(data);
			Messages.DataSource = data;
			Messages.DataBind();
		}

		#region Web Form Designer generated code
		override protected void OnInit(EventArgs e)
		{
			InitializeComponent();
			base.OnInit(e);
		}
		
		/// <summary>
		/// Required method for Designer support - do not modify
		/// the contents of this method with the code editor.
		/// </summary>
		private void InitializeComponent()
		{    
			this.Load += new System.EventHandler(this.Page_Load);

		}
		#endregion
	}
}

Web.config

<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <appsettings>
		<add key="connectionString" value=" ... " />
		<add key="delayPOP" value="300"/>
		<add key="delaySQL" value="600"/>
		<add key="age" value="5"/>
		<add key="server" value=" ... "/>
		<add key="username" value=" ... "/>
		<add key="password" value=" ... "/>
    </appsettings>

.... Remainder of web.config ....

</configuration>

Click to rate this article.

 

Go up to the top of this page.
This site powered by the Logical Web Publisher™: Content management by Logical Expressions, Inc.