A Degradable jQuery AJAX Email Form for ASP.NET MVC

Pretty much every web site on the Internet features a form for users to provide feedback via email to site owners. This site is no different. Migrating to ASP.NET MVC requires a slightly different approach to that used by Web Forms development, so this article looks at one way to implement a web site contact form using the MVC framework and jQuery that degrades nicely. AJAX functionality is said to be "degradable" if a way is provided for the process to work, even though users don't have Javascript available to them.

The form will be very simple. It will contain fields to accept the user's name, email address and comments. In addition, it will contain a simple method to verify that the submitter of the form is human. Once submitted, the contents of the form will be sent via email. The form itself can be seen on the Contact page and uses some css to set the style (which is easily borrowed from the site's css file if you want it). The View code is as follows:

  

<div id="contactform">
    <p>
      If you feel like contacting me, please use this form to do so.</p>

    <form id="contact" action="<%= Url.Action("Index") %>" method="post">

    <fieldset>
      <legend>Message/Comments</legend>
      <div class="row">
        <span class="label">

          <label for="name">
            Your name:</label></span>
        <%=Html.TextBox("name", ViewData["name"] ?? "")%>

        <%=Html.ValidationMessage("name")%>
      </div>
      <div class="row">

        <span class="label">
          <label for="email">
            Your email address:</label></span>

        <%=Html.TextBox("email", ViewData["email"] ?? "")%>
        <%=Html.ValidationMessage("email")%>

      </div>
      <div class="row">
        <span class="label">
          <label for="comments">

            Your comments:</label></span>
        <%=Html.TextArea("comments", 
                ViewData["comments"] != null ? ViewData["comments"].ToString() : "", 
                new{cols="60", rows="8"})%>

        <%=Html.ValidationMessage("comments")%>
      </div>
      <div class="row">

        <span class="label">
          <label for="preventspam">
            &nbsp;
          </label>

        </span>
        <%=Html.TextBox("preventspam", ViewData["email"] ?? "")%>
        <%=Html.ValidationMessage("preventspam")%>

      </div>
      <div class="row">
        <span class="label">&nbsp;
          </span>

        <input type="submit" id="action" name="action" value="Submit" />
      </div>
    </fieldset>

    </form>
  </div>

HtmlHelpers are used to construct the form. At the moment, there is nothing in the label next to the preventspam input (TextBox). This will be looked at next along with the Controller Action. The whole form (including some text welcoming comments etc) is wrapped in a div with the id of contactform. This will be used by jQuery when the form is submitted. More of that a bit later. In the meantime, here is the promised code for the Index() action of the ContactController:


using System;
using System.Web.Mvc;
using System.Net.Mail;
using System.Text.RegularExpressions;

namespace MikesDotnetting.Controllers
{
  public class ContactController : BaseController
  {
    public ActionResult Index()
    {
      if (HttpContext.Session["random"] == null)
      {
        var r = new Random();
        var a = r.Next(10);
        var b = r.Next(10);
        HttpContext.Session["a"] = a.ToString();
        HttpContext.Session["b"] = b.ToString();
        HttpContext.Session["random"] = (a + b).ToString();
      }
      return View();
    }
  }
}


This is called when the page requested. It contains a little bit of code that intialises two random numbers between 0 and 10. These are used to prevent spammer bots from repeatedly submitting the form. The user is presented with these two numbers and asked to perform some simple addition. Then they enter the sum of the numbers in the preventspam box. Both numbers and the total are stored in session variables. As far as I am concerned, you can keep your impossible-to-read Captcha stuff. Before I implemented this approach in the Web Forms version of the site, I used to get tons of spam each day, and the volume was growing rapidly. As soon as I implemented this, it stopped completely. I have not had one single bot-submitted comment. Well, I had one - or at least the person claimed to be a bot and said my prevention measures did not work because they had got around it. What a loser. Anyway, I digress....

We need to see how these session values are presented to the user, so here's the amended portion of the View that's relevant:

      

<div class="row">
  <span class="label">

    <label for="preventspam">
      <%= HttpContext.Current.Session["a"] %>
       +
      <%= HttpContext.Current.Session["b"]%>

    </label></span>
    <input type="text" id="preventspam" name="preventspam" class="required" />

</div>

And here's how the rendered page looks (part way through a redesign...)

The second Controller action, an overloaded version of Index()is marked with the AcceptVerbs attribute with a parameter of POST passed in, as this is the method for the HTTP Request that comes from the form:


[AcceptVerbs("POST")]

public ActionResult Index(string name, string email, string comments, string preventspam)
{
  const string emailregex = @"\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*";
  var result = false;
  
  ViewData["name"] = name;
  ViewData["email"] = email;
  ViewData["comments"] = comments;
  ViewData["preventspam"] = preventspam;

  if (string.IsNullOrEmpty(name))
    ViewData.ModelState.AddModelError("name", "Please enter your name!");
  if (string.IsNullOrEmpty(email))
    ViewData.ModelState.AddModelError("email", "Please enter your e-mail!");
  if (!string.IsNullOrEmpty(email) && !Regex.IsMatch(email, emailregex))
    ViewData.ModelState.AddModelError("email", "Please enter a valid e-mail!");
  if (string.IsNullOrEmpty(comments))
    ViewData.ModelState.AddModelError("comments", "Please enter a message!");
  if(string.IsNullOrEmpty(preventspam))
    ViewData.ModelState.AddModelError("preventspam", "Please enter the total");
  if (!ViewData.ModelState.IsValid)
    return View();

  if (HttpContext.Session["random"] != null && 
    preventspam == HttpContext.Session["random"].ToString())
  {
    var message = new MailMessage(email, "[email protected]")
                    {
                      Subject = "Comment Via Mikesdotnetting from " + name,
                      Body = comments
                    };

    var client = new SmtpClient("localhost");
    try
    {
      client.Send(message);
      result = true;
    }
    catch
    {
    }
  }
  if (Request.IsAjaxRequest())
  {
    return Content(result.ToString());
  }
  return result ? View() : View("EmailError");
}


Initially a bool is initiated along with a string containing a Regular Expression pattern for matching a valid email address structure. Having obtained the values which ASP.NET MVC passes in to the method, the code checks to ensure that they all validate against business rules - that there is something there and that the email is valid. If the form was submitted via AJAX, all of these should pass, since the clientside validation will have come into play. However, if the form was submitted manually and any of the validation rules are not met, the View is returned with the values and error messages held within the ViewDataDictionery. If the ModelState is valid (all test have passed) the next step is to make sure that Session["random"] is valid, and that it matches the value submitted by the user. Once that test is passed satisfactorily, an email message is constructed and sent. The bool result, which was initially set to false is set to true if everything is successful. It is at this stage that we now know whether all the tests were passed and whether the email was sent, so a suitable response is prepared for the user. jQuery automatically applies values to the Request header to say that the request was initiated via xmlhttprequest, and the Request.IsAjaxRequest() method returns a bool to indicate if indeed this request was via AJAX. If the form was submitted via AJAX, the return value is simply a string which reads either "True" or "False". This is returned via the Controller.Content() method that allows for a customised content to be returned by the controller action.

At this stage, however, there is no provision for those with Javascript disabled. All they will see is a blank page with a single "True" or "False" written to it. To cater for this, another View has been created - EmailError


EmailError.aspx

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" 

                                               Inherits="System.Web.Mvc.ViewPage" %>

<asp:Content ID="Content1" ContentPlaceHolderID="TitleContent" runat="server">
    Contact Me

</asp:Content>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>Contact Me</h2>
      <div id="oops">
    <p>
      Unfortunately, something went wrong and your message or comments have not been submitted
      successfully. I'll try to fix whatever the problem is as soon as I can.</p>

  </div>
</asp:Content>


If the form was not submitted by AJAX, the appropriate View is returned instead using the Controller.View() method.

So, how do these values get posted to the SendMail() action in the Controller via AJAX? This question can be answered by looking at the jQuery code that has been added to the Index View.


<script type="text/javascript">
  $(document).ready(function() {


    $("#name,#comments,#preventspam").addClass("required");
    $("#email").addClass("required email");
    $("#contact").validate({
      submitHandler: function(form) {
        $.ajax({
          type: "POST",
          url: $("#contact").attr('action'),
          data: $("#contact").serialize(),
          dataType: "text/plain",
          success: function(response) {
            $("#contactform").hide('slow');
            response == "True" ? $("#thanks").show('slow') : $("#oops").show('slow');
          },
          error: function(response) {
            $("#contactform").hide('slow');
            $("#oops").show('slow');
          }
        });
      }
    });
    return false;
  });

</script>

This might look a bit of a chunk, but it is straightforward really. The $(document).ready() event occurs once the page has fully loaded so that jQuery can make sure all elements in the DOM are accessible. I have used the jQuery Validation plugin to perfom clientside validation, which checks to see that there are values in each of the form fields, and that the email address is at least of the right format. This is illustrative only, and makes no use of the options available within the plugin to check for minimum lengths, customise the error messages and so on. It's not the focus of this article.

However, it is worth pointing out that at a basic level, validation can be hooked up through the use of class attributes on the various <input> elements. This leads to warnings in Visual Studio unless you declare those classes in your css file, or you apply the css classes through Javascript. I opted for the latter approach because if the page is NOT submitted via Javascript, ASP.NET MVC's built-in validation will automatically add class attributes which can mean problems in getting css styles to work as desired. Basically, they get munged. It is also worth mentioning that this does not replace the server-side validation that was covered within the overloaded Index() action earlier. Clientside validation should be seen purely as a convenience for the user, and not a gate keeper for your application.

Once the form is in a valid state, the submitHandler option manages the posting of the values to the overloaded Index() controller action. The response will be either True or False, depending on whether there was an error or not somewhere along the line, and the jQuery then manipulates the divs showing the appropriate one below (which appear at the bottom of the Index.aspx file) depending on the returned value. In both cases, the form itself is slowly hidden and replaced with a message - confirming success, or apologising for the lack of success in the process.


<div id="oops" style="display:none">
  <p>

    Unfortunately, something went wrong and your message or comments have not been submitted
    successfully. I'll try to fix whatever the problem is as soon as I can.</p>
</div>
<div id="thanks" style="display:none">

  <p>
    Thanks for you comments. They have been successfully sent to me. I will try to respond
    if necessary as soon as I can.</p>
</div>

Room For Improvement

The email sending is not particularly testable if you are taking a TDD approach. It's certainly not testable if you have no SMTP Service available on the local machine. Ideally, the mailing function would be housed in a Service Layer, implemented perhaps as ISendMail. Dependency Injection can then be used to resolve the respective types for Development and Production. The same could be said of the server-side validation. Something very similar will be used for the Comments form at the bottom of each article on my web site. At that point, I will move the validation to a Service Layer too.

PLEASE NOTE: The form below is NOT an example of the form the article refers to, so please don't send comments through it with stuff like "Just Testing".