The first thing we need to think about is how we will store the subscription data, such as the user's e-mail address, first and last names, and other information that can be used to personalize the newsletters. In the previous edition of the book, I created a new database table to store this information, and wrote code to retrieve it, save it, and modify it. This time around, things are much easier because of the rich support provided by the membership and profiling system we already developed in Chapter 4. As you may recall, the membership data includes the username and e-mail address, while the profile includes, among other things, the user's first and last name and the format of newsletter she would like to subscribe to (plain-text format, HTML format, or no newsletter at all). To make things simpler and prevent storing the same data in multiple places, we'll use those same fields. You may think that this would require the user to register for the site just to receive the newsletter, and this is not required by many other sites, so this may be a bad idea. Yes, it will require users to register for the site, but the only information strictly required by the registration system is just the username, e-mail address, and password; all the other profile information is optional. For that reason, I don't think that requiring users to choose a username and password, in addition to an e-mail address (needed anyway to send the newsletter), is much of a problem, or a valid reason for us to avoid using the membership system and create a whole new user-data system from scratch just for use by the newsletter module. My proposed solution is much better integrated with the site, and by registering, the user will also gain access to protected articles, the archived polls and newsletters (in case the administrator chooses not to allow everybody to see them), the forums, and more. This approach also makes it easy for a subscriber to change her registration type, or other information. For example, if she wants to switch from plain-text newsletters to HTML newsletters, she can do this from her password-protected profile, and no one else could do this on her behalf.
Besides the registration work, there are other parts of the module, and other features, to design. First, the administrator or editor needs a protected page from which she can send a newsletter to all subscribers, and to enter the body in both plain-text and HTML formats. We also need the option to send multi-part MIME messages, i.e., e-mails that contain both the plain-text and the HTML versions of the body, and leave it to the user's e-mail client program to decide which version to display. However, the reason why the user may wish to subscribe to the plain-text newsletter, instead of the HTML version, is not because the user's e-mail client software doesn't support HTML, but rather because the user doesn't want to download a large message, or for security reasons doesn't want (or cannot, as dictated by their company's policies) to download images and scripts with the e-mails. Because of this, I decided that it's better to send distinct versions of the newsletter according to each subscriber's preferences. In addition to the administrative send page, there must be an archive page that allows users to read past newsletters online. The administrator must also be able to specify whether this page can be accessed by everyone, or only by registered members. This is the same type of security restriction code we've already implemented in the previous chapter, for the polls module. The reason for protecting the page against anonymous access is to encourage users to register, thus enabling you to gather new data about your users — data that you may later use for a number of marketing purposes, as explained in Chapter 4 when we discussed membership.
Because the membership registration system was already implemented, the next thing to design and implement is the system that sends out the newsletters. Sending a single e-mail message is an easy task, but sending out a newsletter to a mailing list of thousands of subscribers is a much more complex job that requires some analysis and a good design. In the next section, I'll present the classes of the .NET Framework that you can use to send e-mail messages, and then discuss the issues to consider and solve when delivering mass e-mails. After introducing this background information, we'll draw together all the considerations and techniques discussed, and design the module.
Before going any further I want to clarify that this module is not intended to send unsolicited e-mail newsletters, or messages of any kind, to users who did not request them. Many countries have laws against this, but regardless of the legality of doing this, I strongly believe that it hurts a site's reputation considerably to undertake this kind of action, and it ends up damaging the favorable impression that you've worked so hard to create. This newsletter system is only intended to send e-mail messages to users who have specifically opted in. Furthermore, users must be allowed to easily change their mind and stop any further e-mails from the site.
The System.Net.Mail namespace defined in the System.dll assembly contains all the classes used to send e-mails. The older System.Web.Mail namespace, and its related classes, that were used with ASP.NET 1.x are still there, but its use has been deprecated now in favor of these new classes in ASP.NET 2.0 that provide more features. The principal classes are MailMessage, which represents an e-mail message, and the SmtpClient class, which provides the methods used to send a MailMessage by connecting to a configured SMTP server (SMTP is the Simple Mail Transfer Protocol, which is the low-level protocol used by Microsoft Exchange and other mail servers).
MailMessage fully describes an e-mail message, with its subject, body (in plain-text, HTML, or in both formats), the To, CC, and BCC addresses, and any attachments that might be used. The simplest way to create an e-mail is using the MailMessage constructor, which takes the sender's address, the recipient's address, the mail's subject, and the body, as shown below:
MailMessage mail = new MailMessage( "from@somewhere.com", "to@somewhere.com", "subject", "body");
However, this approach will be too limited in most cases, because you may want to specify the sender's display name in addition to his e-mail address (the display name is what is displayed by the mail client, if present, instead of the address, and makes the mail and its sender look more professional). You may also want to send to more than one recipient, use an HTML body (as an alternative, or in addition, to the plain-text version), include some attachments, use a different encoding, modify the mail's priority, and so on. All these settings, and more, are specified by means of a number of instance properties of the MailMessage class. Their names should be self-explanatory, and some examples include the following: Subject, Body, IsBodyHtml, From, To, CC, Bcc, BodyEncoding, Attachments, AlternateViews, Headers, Priority, and ReplyTo. The class' constructor enables you to specify a From property of type MailAddress, Address, and UserName properties. The To, CC, and Bcc properties are of type MailAddressCollection and thus can accept multiple MailAddress instances (you can add them by means of the collection's Add method). Similarly, the MailMessage's Attachments property is of type AttachmentCollection, a collection of Attachment instances that point to files located on the server. The following example shows how to build an HTML-formatted e-mail message that will be sent to multiple recipients, with high priority, and that includes a couple of attachments:
// create the message MailMessage mail = new MailMessage(); // set the sender's address and display name mail.From = new MailAddress("mbellinaso@wrox.com", "Marco Bellinaso"); // add a first recipient by specifying only her address mail.To.Add("john@wroxfans.com"); // add a second recipient by specifying her address and display name mail.To.Add(new MailAddress("anne@wroxfans.com", "Anne Gentle")); // add a third recipient, but to the CC field this time mail.CC.Add("mike@wroxfans.com"); // set the mail's subject and HTML body mail.Subject = "Sample Mail"; mail.Body = "Hello, <b>my friend</b>!<br />How are you?"; mail.IsBodyHtml = true; // set the mail's priority to high mail.Priority = MailPriority.High; // add a couple of attachments mail.Attachments.Add( new Attachment(@"c:\demo.zip", MediaTypeNames.Application.Octet)); mail.Attachments.Add( new Attachment(@"c:\report.xls", MediaTypeNames.Application.Octet));
If you also wanted to provide a plain-text version of the body in the same mail, so that the display format (plain text or HTML) would depend on the user's e-mail client settings, you would add the following lines:
string body = "Hello, my friend!\nHow are you?"; AlternateView plainView = new AlternateView(body, MediaTypeNames.Text.Plain); mail.AlternateViews.Add(plainView);
Once a MailMessage object is ready, the e-mail message it describes can be sent out by means of the Send method of the SmtpClient class, as shown here:
SmtpClient smtpClient = new SmtpClient(); smtpClient.Send(mail);
Before calling the Send method, you may need to set some configuration settings, such as the SMTP server's address (the SmtpClient's Host property), port (the Port property) and its credentials (the Credentials property), whether the connection in encrypted with SSL (the EnableSsl property), and the timeout in milliseconds for sending the mail (the Timeout property, which defaults to 100 seconds). An important property is DeliveryMethod, which defines how the mail message is delivered. It's of type SmtpDeliveryMethod, an enumeration with the following values:
Network: The e-mail is sent through a direct connection to the specified SMTP server.
PickupDirectoryFromIis: The e-mail message is prepared and the EML file is saved into the default directory from which IIS picks up queued e-mails to send. By default this is <drive>:\Inetpub\mailroot\Queue.
SpecifiedPickupDirectory: The EML file with the mail being sent is saved into the location specified by the PickupDirectoryLocation property of the smtpClient object. This is useful when you have an external custom program that picks up e-mails from that folder and processes them.
The delivery method you choose can dramatically change the performance of your site when sending many e-mails and can produce different errors during the send operation. If you select the Network delivery method, the SmtpClient class takes care of sending the mail directly and raises an error if the destination e-mail address is not found or if there are other transmission problems. With the other two methods, instead of sending the message directly, an EML mail file is prepared and saved to the file system, where another application (IIS or something else) will pick them up later for the actual delivery (a queue accumulates the messages, which means the web application will not have to wait for each message to be sent over the Internet). However, when using the second and third delivery methods, your web application cannot be notified of any errors that may occur during transmission of the message, and it will be up to IIS (or another mail agent that might be used) to handle them. In general, the Pickup DirectoryFromIis method is the preferred one, unless your ASP.NET application is not given the right to write to IIS mail folders (check with your web hosting provider service if you don't use your own servers).
If you set all SmtpClient properties mentioned above directly in your C# code, you'll have to recompile the application or edit the source file every time you want to change any of these settings. This, of course, is not an option if you're selling a packaged application, or you want to let the administrator change these settings on his own without directly involving you. As an alternative to hard-coding the delivery method, you can set it declaratively in the web.config file, which now supports a new configuration section named <mailSettings>, located under <system.net>, which allows you to specify delivery settings. The SmtpClient class automatically loads those settings from web.config to configure itself at runtime, so you should generally not set your delivery and SMTP options directly in your C# code. Following is an extract of the configuration file that shows how to select PickupDirectoryFromIis as the delivery method, set up the sender's e-mail address and the SMTP server's name (or IP address) and port, and specify that you want to use the default credentials to connect to the server:
<configuration xmlns="http://schemas.microsoft.com/.NetConfiguration/v2.0"> <system.net> <mailSettings> <smtp deliveryMethod="PickupDirectoryFromIis" from="mbellinaso@wrox.com"> <network defaultCredentials="true" host="vmwin2003" port="25"></network> </smtp> </mailSettings> </system.net> <!-- other configuration sections... --> </configuration>
The SmtpClient's Send method used in the preceding code snippet sends the e-mail synchronously, which means that the task must complete before the execution of your application can resume. The term synchronous means "do what I asked, and I'll stop and wait for you to finish," and the term asynchronous means "do what I asked, but let me continue doing other work, and you should notify me when you're done." The SmtpClient class also has a SendAsync method to send the mail asynchronously. It returns immediately, and the e-mail is prepared and sent out on a separate thread. When the send task is complete, the SmtpClient's SendCompleted event is raised. This event is also raised in case of errors, and the Error and Cancelled properties of its second argument (of type AsyncCompletedEventArgs) tell you whether it was raised because the send was cancelled, because there was an error, or because the send completed successfully. Here's a sample snippet that shows how to send the mail asynchronously, and handle the resulting completion event:
SmtpClient smtpClient = new SmtpClient(); smtpClient.SendCompleted += new SendCompletedEventHandler(MailSendCompleted); smtpClient.SendAsync(message, null); ... public static void MailSendCompleted(object sender, AsyncCompletedEventArgs e) { if (e.Cancelled) Trace.Write("Send canceled."); if (e.Error != null) Trace.Write(e.Error.ToString()); else Trace.Write("Message sent."); }
An asynchronous send operation can be cancelled before completion by calling the SmtpClient's SendAsyncCancel method. Note that you can't send a second e-mail while a SmtpClient has another send in progress; if you try to do so, you'll receive an InvalidOperationException.
Because of the disconnected nature of the web, when you submit a form, you typically have to wait some time to get a response, and in the meantime you see nothing but a blank page. The browser can't check how the server-side processing is going, and it can't provide any feedback to the user. As long as the user only has to wait less than five seconds, that's normally fine. But if the application takes longer to produce a response, then you've got a problem because you can't leave users stranded without visual feedback for more than a few seconds, or they will start to think that the application got stuck, or had a serious problem, and they will close their browser and go away. If a user presses refresh to resend the data and restart the processing, that's bad as well, because that action actually requests the same operation a second time, and the server will do the same work twice, causing duplication and possibly data integrity problems. There are many situations where the server-side processing might take a long time to complete: you may execute long-running SQL queries, call an external web service, or forward a call to an external application (a payment processing application, for example) and wait for a response, and so on. In our case, you'll be sending potentially thousands of e-mails, and to do this you'll need to retrieve the profile for all registered members (that means multiple SQL queries), parse the newsletter's body and replace all the personalization placeholders, and insert the newsletter into the archive. This can possibly take many minutes, not just seconds! You can't expect your newsletter editor to look at a blank page for such a long period of time. In the previous edition of the book, we just set a much higher time-out for the page sending the newsletter, so that it wouldn't terminate with an error after the normal 90 seconds, but we didn't tackle the real problem: The page provided no feedback to the editor about the send progress, and this was a serious design flaw. In this new version we'll fix that flaw! Several techniques could be employed to solve this issue and provide some feedback to the user. Here is a list of some of the alternatives and their pros and cons:
When users click the Submit button, you can redirect them to a second page that shows a wait message. When this wait page arrives on the client browser it uses some simple JavaScript, or the refresh metatag, to immediately post back to the server — either to itself or to another page. For example, the following metatag declared at the top of the page makes the current page redirect to processing.aspx after two seconds:
<meta http-equiv="refresh" content="2; URL=http://www.contoso.com/processing.aspx">.
After this second postback, the long processing task will be executed. While the task is running on the server, the current wait page will remain visible on the client. You can provide an animated gif representing an incrementing progress bar, in addition to the wait message, so that users get the idea that processing is taking place behind the scenes. This simple approach doesn't provide any real feedback about the task's actual progress, but it would suffice for many situations.
When the user clicks the Submit button, on the server side you can start a secondary thread that will process the long task in the background. The page will then immediately redirect to a wait page that shows the user a message, while the real action continues on the server on that separate thread. The code executing on the background thread will also update some server-side variables indicating the percentage of the task completed, and some other optional information.
The wait page will automatically refresh every "n" seconds, and every time it loads it will read the variables written by the second thread and display the progress status accordingly. To refresh itself, it can use some JavaScript that submits the form, or it can use the refresh metatag, but without the URL of the page to load, as shown in the following:
<meta http-equiv="refresh" content="2">
This approach can be quite effective, because it gives the user some real feedback about the background processing; however, with the page refreshing every few seconds, the user will continuously see the browser's content flickering. A better solution would be to load the wait/progress page into a small IFRAME, which would show only the progress bar and nothing else. When the processing completes, the wait page will not show the progress bar again, but will instead redirect the user to the page showing the confirmation and the results. Still, when the progress bar is updated, it will disappear for a short time when the page in the IFRAME refreshes, and this creates an ugly visual effect.
The preceding option can be improved if, instead of refreshing the whole page or IFRAME, you just update the progress bar representing the processing status, and then use some JavaScript to call a server-side page that returns the required information, which uses this information to dynamically update some part of the page, such as the progress bar's width and the percentage text. The only disadvantage of this approach is that it requires JavaScript to be enabled on the client browser, but this shouldn't be a concern because all your users will likely have JavaScript enabled. Even if it were a problem, in our specific case the page requiring a real-time progress bar is only used in the administrative section, and thus it's not a problem to dictate some basic requirements (such as browser support for JavaScript) for your editors and administrators. Other than this small concern, this solution has everything you need: When the user submits the page, the page returns immediately and starts showing a progress bar that lets the user monitor how the long task is progressing in real time; in addition, you no longer have timeout problems because the page does almost nothing and completes instantaneously, while the background thread doing the real work can go on for a long time without problems.
Instead of showing you how to implement all three possible solutions, we will go straight to the best one, which is of course the third one. In the next section I'll provide you with some background information about multi-threaded programming and script programming for partial page updating, which you'll use in the "Solution" section to implement the newsletter delivery task.
Background, or secondary, threads can be used to execute long-running tasks without tying up the main UI task. Creating new threads in .NET is very easy, but you have to be careful about multiple threads trying to access the same memory at the same time. All the classes you need are under the System.Threading namespace, in the mscorlib.dll assembly. The basic steps required are as follows:
Create a ThreadStart delegate that points to the method that will run in the secondary thread. The method must return void and can't accept any input parameters.
Create a Thread object that takes the ThreadStart delegate in its constructor. You can also set a number of properties for this thread, such as its name (useful if you need to debug threads and identify them by name instead of by ID) and its priority. The Priority property, in particular, can be dangerous, because it can seriously affect the performance of the whole application. It's of type ThreadPriority, an enumeration, and by default it's set to ThreadPriority.Normal, which means that the primary thread and the secondary thread have the same priority, and the CPU time given to the process is equally divided between them. Other values of the ThreadPriority enumeration are AboveNormal, BelowNormal, Highest, and Lowest. In general, you should never assign the Priority property an AboveNormal or Highest value for a background thread. Instead, it's usually a good idea to set the property to BelowNormal, so that the background thread doesn't slow down the primary thread any noticeable degree, and it won't interfere with ASP.NET.
Call the Start method of the Thread object. The thread starts, and you can control its lifetime from the thread that created it. For example, to affect the lifetime of the thread you can call the Abort method to start terminating the thread (in an asynchronous way), the Join method to make the primary thread wait until the secondary thread has completed, and the IsAlive property, which returns a Boolean value indicating whether the background thread is still running.
The following snippet shows how to start the ExecuteTask method, which can be used to perform a long task in a background thread:
// create and start a background thread ThreadStart ts = new ThreadStart(Test); Thread thread = new Thread(ts); thread.Priority = ThreadPriority.BelowNormal; thread.Name = "TestThread"; thread.Start(); // main thread goes ahead immediately ... // the method run asynchronously by the background thread void ExecuteTask() { // execute time consuming processing here ... }
One problem of multi-threaded programming in .NET 1.x was the difficulty in passing parameters to the secondary thread. The ThreadStart delegate cannot point to methods that accept parameters, so the developers had to find workarounds for this. The most common one was to create a class with some properties, and the main thread would create an instance of the class and use the object's properties as parameters for the method pointed to by ThreadStart. Thankfully, this becomes much easier in .NET 2.0, thanks to the new ParameterizedThreadStart delegate, which points to methods that take an object parameter. Because an object can be anything, you can pass a custom object with properties that you define as parameters, or simply pass an array of objects if you prefer. The following snippet shows how you can call the ExecuteTask method and pass an array of objects to it, where the first object is a string, the second is an integer (that is boxed into an object), and the last is a DateTime. The ExecuteTask method takes the object parameter and casts it to a reference of type object array, and then it extracts the single values and casts them to the proper type, and finally performs the actual processing:
// create and start a background thread with some input parameters object[] parameters = new object[]{"val1", 10, DateTime.Now}; ParameterizedThreadStart pts = new ParameterizedThreadStart(ExecuteTask); Thread thread = new Thread(pts); thread.Priority = ThreadPriority.BelowNormal; thread.Start(parameters); // main thread goes ahead immediately ... // the method run asynchronously by the background thread void ExecuteTask(object data) { // extract the parameters from the input data object object[] parameters = (object[])data; string val1 = (string)parameters[0]; int val2 = (int)parameters[1]; DateTime val3 = (DateTime)parameters[2]; // execute time consuming processing here ... }
The most serious issue with multi-threaded programming is synchronizing access to shared resources. That is, if you have two threads reading and writing to the same variable, you must find some way to synchronize these operations so that one thread cannot read or write a variable while another thread is also writing it. If you don't take this into account, your program may produce unpredictable results and have strange behaviors, it may lock up at unpredictable times, and possibly even cause data integrity problems. A shared resource is any variable or field within the scope of the current method, including class-level public and private fields and static variables. In C#, the simplest way to synchronize access to these resources is through the lock statement. It takes a non-null object (i.e., a reference type — value types are not accepted), which must be accessible by all threads, and is typically a class-level field. The type of this object is not important, so many developers just use an instance of the root System.Object type for this purpose. You can simply declare an object field at the class level, assign it a reference to a new object, and use it from the methods running in different threads. Once the code enters a lock block, the execution must exit the block before another thread can enter a lock block for the same locking variable. Here's an example:
private object lockObj = new object(); private int counter = 0; void MethodFromFirstThread() { lock(lockObj) { counter = counter + 1; } // some other work... } void MethodFromSecondThread() { lock(lockObj) { if (counter >= 10) DoSomething(); } }
In many situations, however, you don't want to completely lock a shared resource against both read and write operations. That is, you normally allow multiple threads to read the same resource at the same time, but no write operation can be done from any thread while another thread is reading or writing the resource (multiple reads, but exclusive writes). To implement this type of lock, you use the ReaderWriterLock object, whose AcquireWriterLock method protects code following that method call against other reads or writes from other threads, until a call to ReleaseWriterLock is made. If you call AcquireReaderLock (not to be confused with AcquireWriterLock), another thread will be able to enter its own AcquireReaderLock block and read the same resources, but an AcquireWriterLock call would wait for all the other threads to call ReleaseReaderLock. Following is an example that shows how you can synchronize access to a shared field when you have two different threads that read it, and another one that writes it:
public static ReaderWriterLock Lock = new ReaderWriterLock(); private int counter = 0; void MethodFromFirstThread() { Lock.AcquireWriterLock(Timeout.Infinite); counter = counter + 1; Lock.ReleaseWriterLock(); // some other work... } void MethodFromSecondThread() { Lock.AcquireReaderLock(Timeout.Infinite); if (counter >= 10) DoSomething(); Lock.ReleaseReaderLock(); } void MethodFromThirdThread() { Lock.AcquireReaderLock(Timeout.Infinite); if (counter != 50) DoSomethingElse(); Lock.ReleaseReaderLock(); }
In our specific case, you'll have a business class that runs a background thread to asynchronously send out the newsletters; after sending each mail, it updates a number of server-side variables that indicate the total number of mails to send, the number of mails already sent, the percentage of mails already sent, and whether the task has completed. Then, an ASP.NET page from the presentation layer, and thus from a different thread, will read this information to update the status information on the client's screen. Because the information is shared between two threads, you'll need to synchronize the access, and the ReaderWriterLock will be used for this purpose.
Important |
Multi-threaded programming is a very complex subject, and there are further considerations regarding the proper way to design code so that it performs well and doesn't cause deadlocks that may freeze the entire application. You should also avoid creating too many threads if it's not strictly required, because the operating system and the thread scheduler (the portion of the OS that distributes the CPU time among the existing threads) consume CPU time and memory for managing them. There are also other classes that I haven't discussed here (such as Monitor, Semaphore, Interlocked, ThreadPoll, etc.) because they will not be necessary for implementing the solution of this specific module. If you are interested in digging deeper into the subject of multi-threading, I recommend you get a book that covers this subject in greater depth, such as Professional C# 2005 (Wiley, 2005). |
To update the progress bar and the other status information on the page that provides feedback about the newsletter being sent, you must find some way to refresh parts of the page without posting the whole page to the server, because that would take time and temporarily show blank pages (which would appear as nasty flashes, even with fast computers and a broadband connection). The solution is to use JavaScript plus DHTML to change the text of some container elements (DIVs or SPANs) and the width of the progress bar, but to do this you must still request the updated values from the server every few seconds. However, instead of making the request by posting the current page to the server, you can post the request programmatically with some client-side JavaScript code, thanks to the XmlHttpRequest object, which is available on both Internet Explorer (as an ActiveX control marked as safe) and Firefox/Netscape browsers (as an object built directly into the browser's document object model). You can post values to the page being called either as POST or GET parameters; the requested page will then do its normal work and the XmlHttpRequest object will retrieve the output from the server, as a string. The output can be anything, but it will usually be some plain-text or XML text containing the results of some server-side processing; it will be parsed using JavaScript in the browser, interpreted, and used to update some information on the current page. Therefore, you're actually making a request to the server from the browser without doing a page postback, and the user will not notice it; even if some information is updated, from the user's point of view everything looks like it happened on the client. I like to call this a transparent server request.
As a quick example, consider how to create a page that makes a transparent request to the server to multiply the number inserted into a textbox by two, when a button is clicked. When the button is clicked, it calls a client-side JavaScript function called DoCalc and passes the current value of the textbox (which is expected to be an integer number). The DoCalc function will then use the XmlHttpRequest object (instantiated as an ActiveX, or from the DOM, according to the browser's capabilities) to call the multiply.aspx page with its input value passed to the page on the querystring. When the request returns, the DoCall function takes the output produced and shows it to the user by means of an alert message box. Following is the code of the page making the request:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="Default" %> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Test</title> <script type="text/javascript"> function DoCalc(val) { var url = "multiply.aspx?val="+ val; var request = null; if (window.XMLHttpRequest) { request = new XMLHttpRequest(); request.open("GET", url, false); request.send(null); } else if (window.ActiveXObject) { request = new ActiveXObject("Microsoft.XMLHTTP"); request.open("GET", url, false); request.send(); } if (request) { var result = request.responseText; alert(result); } } </script> </head> <body> <form id="form1" runat="server"> <asp:TextBox ID="txtValue" runat="server" /> <input id="btnSubmit" runat="server" type="button" value="Calc" onclick="javascript:DoCalc(document.getElementById('txtValue').value);"/> </form> </body> </html>
The multiply.aspx page takes the val parameter passed on the querystring, converts it to an integer (in a real-world situation you would need to validate the input, and handle possible conversion exceptions), multiplies it by two, and returns the result as a string. It also calls Response.End to ensure that nothing else is sent to the client. Here's the code of its Page_Load event handler:
protected void Page_Load(object sender, EventArgs e) { int val = int.Parse(this.Request.QueryString["val"]); Response.Write((val*2).ToString()); Response.Flush(); Response.End(); }
The technique just described works in any version of ASP.NET, and it works basically the same in any other web programming platform, such as classic ASP, PHP, JSP, etc., because it's mostly based on platform-independent JavaScript that runs on the client. However, ASP.NET 2.0 introduces a new feature, called a script callback, that simplifies this task. It wraps all the plumbing related to XmlHttpResponse (preparing the request, sending it, and receiving the response) and lets the developer focus on the code that accepts the response and performs the partial page update. The following code presents a new and simplified version of the previous page, with the JavaScript block only containing a function that takes the result of the server-side processing, displaying it to the user:
<%@ Page Language="C#" AutoEventWireup="true" CodeFile="Default.aspx.cs" Inherits="_Default" %> <html xmlns="http://www.w3.org/1999/xhtml" > <head runat="server"> <title>Test</title> <script type="text/javascript"> function ShowResult(result, context) { alert(result); } </script> </head> <body> <form id="form1" runat="server"> <asp:TextBox ID="txtValue" runat="server" /> <input id="btnSubmit" runat="server" type="button" value="Calc" /> </form> </body> </html>
From the C# code-behind file you must get a reference to a JavaScript function that makes the transparent post and was automatically generated and added to the page by ASP.NET itself, and attach it to the button's OnClickonclick event. This is done by the GetCallbackEventReference of the page's ClientScript property. ClientScript is a new page property of type ClientScriptManager, which exposes all methods to work with scripts, such as registering scripts at the top of the page (RegisterClientScriptBlock) that run when the page finishes loading (RegisterStartupMethod), or just before it is submitted (RegisterOnSubmitStatement). The GetCallbackReference takes a reference to a page or control that implements the ICallbackEventHander (described in a moment), an argument passed from the client side to the server side (which may be a simple value or a script that returns a dynamically calculated value), the name of the client-side callback function that will consume the values returned from the server, and finally a value passed to the client-side callback function (just as for the second argument, this may be a client-side script that returns a value). In the example being written, the first argument is a reference to the page itself, the second argument a script that returns the value of the txtValue textbox, the third argument is a reference to a JavaScript function called ShowResult, and the last argument is just null. The string returned by the GetCallbackReference is the call to the ASP.NET-generated WebForm_DoCallback JavaScript function, which is assigned to the button's OnClick attribute, into the Attributes collection. This sounds complicated, but the actual code is pretty simple. Here's how the code-behind class begins, with the Page_Load event handler:
public partial class _Default : System.Web.UI.Page, ICallbackEventHandler { protected void Page_Load(object sender, EventArgs e) { string callbackRef = this.ClientScript.GetCallbackEventReference(this, "window.document.getElementById('txtValue').value", "ShowResult", null); btnSubmit.Attributes.Add("onclick", callbackRef); }
Now you have to implement the ICallbackEventHandler interface, which is composed of a couple of methods: RaiseCallbackEvent and GetCallbackResult. The first one is the server-side callback method that takes the parameters passed by the client, producing the result. The result is saved into a class-level field because it is returned to the client by GetCallbackResult. Here's the code:
string callbackResult = ""; public string GetCallbackResult() { return callbackResult; } public void RaiseCallbackEvent(string eventArgument) { callbackResult = (int.Parse(eventArgument) *2).ToString(); } }
When you run the page, you can type a number into the textbox, click the button, and almost immediately you'll get the result in an alert message box, without the page flashing at all for a postback; the round-trip to the server happened transparently without any obvious signs from the user's perspective (which, of course, was our goal). If you take a look at the page's HTML source code, this is what you'll see for the button:
<input name="btnSubmit" type="button" id="btnSubmit" value="Calc" onclick="WebForm_DoCallback( '__Page',window.document.getElementById('txtValue').value, ShowResult,null,null,false)" />
You won't see the WebForm_DoCallback function in the page at design time; instead, there's a <script> block that links to the WebResource.axd resource, which supplies all the required JavaScript to the browser at runtime:
<script src="/ScripCallbackDemo/WebResource.axd?d=u8VFVkrxgbyFhFIpAniF- Q2&t=632671582519943712" type="text/javascript"></script>
If you want to examine how Microsoft implemented the client-side part of the script callback feature, just point your browser to the URL copied from the <script> tag, and download that output locally for your inspection.
Note |
WebResource.axd is a standard HTTP handler introduced in ASP.NET 2.0 to replace the aspnet_client folder used to contain JavaScript code sent down to browsers in ASP.NET 1.x. This handler extracts resources (scripts, images, and more) from ASP.NET's standard assemblies, according to its querystring parameters, and returns the stream to the caller. This makes deployment much easier because you don't have to worry about how the correct version of the client scripts will be provided to the browser — it just seems to happen "auto-magically." |
On the server, the page calls the ICallbackEventHandler's methods only when it finds some special arguments from the posted data; in that case it calls the methods discussed earlier, and then it terminates immediately, without proceeding with the rest of the page life cycle, and without producing any other output. This streamlined method of sending parameters to the server and getting data back is often called a lightweight postback, or an out-of-band postback.
The example of adding numbers together on the server side is not an efficient way to do math, since you could just add the numbers using JavaScript, with no need to do a lightweight postback to the server, but this was just a simple example of how the script callback feature works in ASP.NET 2.0. There are many better ways to use this feature in the real world, such as performing complex validations and operations that can only be done on the server (checking the existence of a record, or retrieving some child records from the database, according to the parent ID passed as an input), so you can imagine how useful the script callback feature can be! Later in this chapter, in the "Solution" section, we'll use this feature in a more complex scenario — to periodically ask for the updated progress information about the newsletter being sent. The server-side information is updated by the background thread doing the real work, and a JavaScript function will use the information to update a progress bar and show the number of e-mails already sent.
ASP.NET pages are normally processed in a synchronous manner, which means that when a request arrives to the ASP.NET engine, it takes a free thread from its own internal thread pool, and executes the page; the page completes its whole life cycle to produce the output for the client, and finally the thread is put back into the thread pool and made available for other requests. If no thread is free when a new request arrives, the request is queued until a thread becomes available. In some situations a page might take a long time to process, maybe three to five seconds, not because of the processing that generates the HTML output, but rather because of some I/O-bound operation, such as a long query run on a remote database, or a call to a web service. In such cases, ASP.NET is mostly sitting idle while it waits for the external work to be done. The side effect of this is that the thread given to that page is unproductive because it's just waiting most of the time, and because the threads in the thread pools are limited this has the effect of slowing down any other simultaneous requests while they wait for this thread to become available. Asynchronous page processing is meant to be a solution for this problem: When a request arrives, a thread is assigned to process it, and when the page calls an external web service, that request's thread is put back into the thread pool and the external request will proceed on another thread taken from a different thread pool (not from the main ASP.NET pool). Finally, when that external call completes, ASP.NET spins off another thread from the thread pool to complete the original page request. ASP.NET only uses its own threads when it has real work to do, and it releases them when they would just wait for external processing, assuming the page was designed for asynchronous processing.
Asynchronous page processing was technically possible in ASP.NET 1.x, by implementing the IHttpAsyncHandler interface in your page's code-behind file. However, this was quite complex and difficult to use. Now, however, with ASP.NET 2.0, things became much more straightforward, and implementing asynchronous page processing is much easier.
The first thing you do is set the Async attribute of the @Page directive to true, as follows:
<%@ Page Language="C#" AutoEventWireup="true" Async="true" CodeFile="AsyncDemo.aspx.cs" Inherits="AsyncDemo" %>
Then, from the Page_Load event handler in the code-behind file, you call the page's AddOnPreRenderCompleteAsync method to specify the method that will begin the asynchronous call (as a BeginEventHandler delegate), and the method that will complete it in the second thread taken from the ASP.NET's thread pool (as an EndEventHandler delegate):
public partial class AsyncDemo : System.Web.UI.Page { private ecomm.OrderProcessor orderProcessor = null; protected void Page_Load(object sender, EventArgs e) { this.AddOnPreRenderCompleteAsync( new BeginEventHandler(OnPageBeginMethod), new EndEventHandler(OnPageEndMethod)); }
The page follows its normal life cycle until just after the PreRender event, when it calls the method pointed to by the BeginEventHandler delegate. The code in this method just makes the asynchronous call and immediately returns an IAsyncResult object that will allow ASP.NET to determine when the secondary thread completes. As you may recall, an object of this type is returned by the BeginXXX method of a web service, for example (where XXX is the real method name, e.g., BeginProcessOrder). The same metaphor that was used for asynchronous web services in .NET 1.x has now been applied to ASP.NET 2.0 page processing! Here's the code that instantiates a web service called ecomm.OrderProcessor, and calls its ProcessOrder method in the asynchronous manner, thus using the Begin prefix:
IAsyncResult OnPageBeginMethod(object sender, EventArgs e, AsyncCallback cb, object state) { orderProcessor = new ecomm.OrderProcessor(); return orderProcessor.BeginProcessOrder("order123", cb, state); }
After calling OnPageBeginMethod, the ASP.NET thread processing the page request is returned to its thread pool. When ASP.NET detects that the thread calling the web service has completed, it starts a new thread from its thread pool and calls the OnPageEndMethod method specified above, which just completes the call to the ProcessOrder — by calling EndProcessOrder — and shows the result in a Label control:
void OnPageEndMethod(IAsyncResult ar) { string result = orderProcessor.EndProcessOrder(ar); lblResult.Text = result; } }
Note |
There are many other details concerning asynchronous pages, such as the possibility to call multiple external methods and make ASP.NET wait for all of them before starting the thread to complete the request, the timeout options, etc. However, in this section I just wanted to give you an idea of how simple asynchronous processing can be; you should refer to a reference book such as Wrox's Professional ASP.NET 2.0 by Bill Evjen (Wiley, 2005), or the article written by Jeff Prosise for the October 2005 issue of MSDN Magazine, titled "Asynchronous Pages in ASP.NET 2.0" and available online at http://msdn.microsoft.com/msdnmag/issues/05/10/WickedCode/. |
In this chapter's "Solution" section you won't be using asynchronous pages, because although they are good for handling I/O-bound tasks that last longer than a few seconds, they don't fit into our model of providing some feedback to the user during the wait. I mentioned them here because they fit into the general discussion of multi-threaded web programming and can be very handy in some situations where you have heavily loaded servers that commonly wait on asynchronous external processing.
Now that we've covered all the background information, we can start the actual design for this module. As usual, we'll start by designing the database tables, then move on to design stored procedures and the data access and business logic layers, finishing with the presentation layer's ASP.NET pages and controls. The database design is very simple (see Figure 7-1). There's a single table to store newsletters that were sent out previously but were archived for future reference by online subscribers.
All fields are self-explanatory, so I won't spend time describing them, except to point out that Subject is of type nvarchar(256), while PlainTextBody and HtmlBody are of type ntext. The other fields, AddedDate and AddedBy, are common to all of the other tables, and we've discussed them in previous chapters.
Because this module has only a single table, the stored procedures are simple as well. You just need the basic procedures for the CRUD (Create, Retrieve, Update, and Delete) operations:
Property |
Description |
---|---|
Tbh_Newsletters_InsertNewsletter |
Inserts a newsletter with the specified subject and body in plain-text and HTML |
tbh_Newsletters_UpdateNewsletter |
Updates an existing newsletter with the specified ID |
tbh_Newsletters_DeleteNewsletter |
Deletes a newsletter with the specified ID |
tbh_Newsletters_GetNewsletters |
Retrieves all data for the rows in the tbh_Newsletters table, except for the PlainTextBody and HtmlBody fields. It takes a datetime parameter, and it returns only those rows with AddedDate less than or equal to that date. |
tbh_Newsletters_GetNewsletterByID |
Retrieves all data for the specified newsletter |
As an optimization, the tbh_Newsletters_GetNewsletters procedure doesn't include the body fields in the resultset, because the body won't be shown in the page that lists the archived newsletters; only the AddedData and Subject are required in that situation. When the user goes to the page showing the whole content of a specific newsletter, the tbh_Newsletter_GetNewsletterByID procedure will be used to retrieve all the details.
Like any other module of this book, the newsletter module has its own configuration setting, which will be defined as attributes of the <newsletter> element under the <theBeerHouse> section in web.config. That element is mapped by a NewsletterElement class, which has the following properties:
Property |
Description |
---|---|
ProviderType |
The full name (namespace plus class name) of the concrete provider class that implements the data access code for a specific data store |
ConnectionStringName |
The name of the entry in web.config's new <connectionStrings> section that contains the connection string to the module's database |
EnableCaching |
A Boolean value indicating whether the caching of data is enabled |
CacheDuration |
The number of seconds for which the data is cached if there aren't any inserts, deletes, or updates that invalidate the cache |
FromEmail |
The newsletter sender's e-mail address, used also as a reply address |
FromDisplayName |
The newsletter sender's display name, which will be shown by the e-mail client program |
HideFromArchiveInterval |
The number of days before a newsletter appears in the archive. |
ArchiveIsPublic |
A Boolean value indicating whether the polls archive is accessible by everyone, or restricted to registered members |
The first four settings are common to all modules. You may argue that the sender's e-mail address can be read from the built-in <mailSettings> section of web.config, as shown earlier. However, that is usually set to the postmaster's or administrator's e-mail address, which is used to send service e-mails such as the confirmation for a new registration, the lost password e-mail, and the like. In other situations you may want to differentiate the sender's e-mail address, and use one for someone on your staff, so that if the user replies to an e-mail, his reply will actually be read. In the case of a newsletter, you may have a specific e-mail account, such as newseditor@contoso.com, used by your newsletter editor.
The ArchiveIsPublic property has the same meaning as the similarly named property found in the poll module's configuration class — it enables the administrator to decide whether the archived newsletters can be read only by registered members; she may want to set this to True to give users another reason to subscribe. HideFromArchiveInterval is also very important, because it allows you to decide how many days must pass before the newsletter just sent is available from the archive. If you set this property to zero, some users may decide not to subscribe to the newsletter, and just go to the archive occasionally to see it. If you set this to 15 instead (which is the default value), they will have to subscribe to the newsletter if they want to read it without waiting 15 days to see it in the archives.
As usual, the DAL for this module is based on the provider model design pattern, and the SQL Server provider is included as an example, which is just a wrapper around the stored procedures described above. The DAL uses a NewsletterDetails custom entity class, which wraps the data of a single newsletter record. The diagram in Figure 7-2 describes the DAL classes and their relationships.
This is where things become interesting, because in addition to wrapping the data operations of the DAL in an object-oriented class, the MB.TheBeerHouse.BLL.Newsletters.Newsletter class also contains all the logic for sending out a newsletter asynchronously from a separate background thread, and for updating the progress status information. Figure 7-3 represents the UML diagram of this class (together with the usual Bizobject base class) and lists its public and private properties and methods.
When the user calls the SendNewsletter public static method, a new record is inserted into the tbh_Newsletters table, and the ID of the record is returned to the caller. Just before finishing the method, it spins off a new low-priority thread to asynchronously execute the SendEmails private static method. It's inside this method that the newsletter is actually sent to all subscribers, in plain text or HTML format according to their profile.
When the SendNewsletter method begins, it also sets the IsSending static property, so that the caller (from the administration page) can check whether another newsletter is currently being sent, to avoid calling SendNewsletter again before it has finished. As discussed previously, every time you access a property/field that's shared among multiple threads, you must synchronize this operation; and in this case it is done by means of the static field Lock, which is an instance of ReaderWriterLock. The Lock field is public because the caller will need to use the same ReaderWriterLock used by the business class in order for the synchronization to work properly. Note that the Common Language Runtime does not automatically handle locking, so if your code doesn't properly handle the synchronization, it could cause performance problems, occasionally lock-ups, and possibly even data corruption.
The TotalMails property is set from within the SendEmails method, before the first mail is set, and just after counting the number of site members having their Newsletter profile property set to a value other than SubscriptionType.None (this enumeration was defined in Chapter 4, when discussing membership and profiles). Every time SendEmails sends an e-mail, it also updates the SentMails and PercentageCompleted static properties, using the same synchronization approach based on the ReaderWriterLock.
Finally, as soon as SendEmails has sent the last e-mail, it sets IsSending back to false, so that the administrator can send another newsletter if desired. The SendEmails method uses the Has PersonalizationPlaceholders and ReplacePersonalizationPlaceholders methods, respectively, to check whether the newsletter's plain-text and HTML bodies contain personalization placeholders (the supported ones are <% username %>, <% email %>, <% firstname %>, and <% lastname %>, but you may add more if you wish), and to actually replace them for a given user. If HasPersonalization Placeholders returns false, the call to ReplacePersonalizationPlaceholders is completely skipped, to avoid running the replacement regular expression if you already know that no placeholders will be found. SendEmails calls HasPersonalizationPlaceholders just once for each of the two body formats, but it can save thousands of calls to ReplacePersonalizationPlaceholders, which is done separately for each user.
The last part of the design phase is to design the pages and user controls that make up the module's presentation layer. Here is list of the user interface files that we'll develop later in the "Solution" section:
~/Admin/SendNewsletter.aspx: This page lets the administrator or editor send a newsletter to current subscribers. If another newsletter is already being sent when this page is first loaded, an error message appears instead of the normal form showing the other newsletter's subject and body, and a link to the page that displays that newsletter's progress. The page must also take into account situations in which no newsletter is being sent when the page loads, but later when the user clicks the Submit button to send this newsletter another newsletter is found to be under way at that time, because another user may have sent it from another location while the first editor was completing the form on her browser. In this case, the current newsletter is not sent and a message explaining the situation is shown, but the form showing the current newsletter's data is kept visible so that the data is not lost and can be sent later when the other newsletter has completed transmission.
~/Admin/SendingNewsletter.aspx: This page uses the script callback feature to update the statistics on the number of e-mails already sent (out of the total number of e-mails) and a progress bar representing the percentage of completed work. The lightweight postback will take place every couple of seconds, but you can change the update interval. The editor is automatically redirected to this page after she clicks the Submit button from SendNewsletter.
~/ArchivedNewsletters.aspx: This page lists all past newsletters sent at least "x" days before the current date, where x is equal to the HideFromArchiveInterval custom configuration setting. Anonymous users will be able to access this page only if the ArchiveIsPublic setting is set to true in the web.config file's <newsletters> element, under the <theBeerHouse> section. The list of newsletters will show the date when each newsletter was sent and its subject. The subject is rendered as a link that points to another page showing the newsletter's entire content.
~/ShowNewsletter.aspx: This page displays the full plain-text and HTML body of the newsletter whose ID is passed on the querystring.
~/Controls/NewsletterBox.ascx: This user control determines whether the current site user is logged in; if not, it assumes she is not registered and not subscribed to the newsletter, and thus it displays a message inviting her to subscribe by typing her e-mail address into a textbox. When the user clicks the Submit button, the control redirects her to the Register.aspx page developed in Chapter 4, passing her e-mail on the querystring, so that Register.aspx will be able to read it and prefill its own e-mail textbox. If the user is already a member and is logged in, the control instead displays a message explaining that she can change her subscription type or cancel the subscription by going to the EditProfile.aspx page (also developed in Chapter 4) to which it links. In both cases, a link to the newsletter archive page is shown at the bottom of the control.