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, "me@me.com")
                    {
                      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".

Date Posted: Friday, May 29, 2009 3:58 PM
Last Updated: Friday, October 10, 2014 9:08 PM
Posted by: Mikesdotnetting
Total Views to date: 71562

26 Comments

Wednesday, June 3, 2009 6:59 AM - Balaji Birajdar

Excellent work Mike.. This is one of the best articles I have ever gone through

Tuesday, June 9, 2009 4:17 AM - NoEmptyCatchBlocksPlease

Nice article..
However I wouldn't suggest this ever (even in examples, rather just let it get thrown)..

try
{
/* some code here 8?
client.Send(message);
result = true;
}
catch
{
/* no code here !! */
}

Its always better for you and the user to know something when wrong...

Tuesday, June 9, 2009 5:12 AM - Darren Oster

Thanks for the excellent article, Mike. Just one issue I'm having: When using the AJAX call to the controller, the controller is returning Content("True") as expected, but jQuery is getting a null response (and hence displaying the error message) on the page. Any ideas?

Tuesday, June 9, 2009 8:36 AM - Mike

@NoEmptyCatchBlocksPlease

You need to read the disclaimer on my Contact page ;-) But, yes. A call to a logging component would ideally go into the catch block. The user already knows that something didn't work even with the code as-is.

Tuesday, June 9, 2009 8:42 AM - Mike

@Darren,

Without knowing how you have tried to apply the code, I have no idea what the problem could be. I suggest that you post a question to the forums at www.asp.net showing the code you are attempting to use, and a reference to this article so that people who answer questions there know what you are trying to do.

Friday, June 19, 2009 9:35 AM - swamy

pls send me contact us asp page

Friday, June 19, 2009 10:43 PM - Mike

@Swamy

Please send me £500.00

Thursday, August 6, 2009 12:26 AM - Bruno Nepomuceno

Hi, congratulations, great article! Are you planning to add an attachment file feature? Thanks a lot for your attention

Friday, August 7, 2009 5:49 PM - Mike

@Bruno

Not to my coments form - no.

Wednesday, August 12, 2009 5:23 AM - test

good article

Wednesday, August 12, 2009 5:35 AM - Saurabh

Hi
Thanx for this nice post.
But I have a question.

if (Request.IsAjaxRequest())
{
return Content(result.ToString());
}

is this condition always true??? because you are not using
any kind on ajax like (<%=Ajax.BeginForm() %>) in your HTML page.


Kind Regards,
Saurabh

Wednesday, August 12, 2009 6:03 AM - roger

$("#contact").serialize()

what this line does?

Wednesday, August 12, 2009 9:14 PM - Mike

@Suarabh

I am using jQuery for Ajax, not ASP.NET Ajax. I could have hard-coded the xmlhttprequest, or used any one of a number of libraries, but I chose jQuery. So there is AJAX in the page.

Wednesday, August 12, 2009 9:18 PM - Mike

@roger

It does what it says on the tin - it basically serialises all the contents of the form fields and constructs the data for the HTTP POST request: http://docs.jquery.com/Ajax/serialize

Saturday, August 15, 2009 4:50 PM - Hari

Nice article

Monday, August 31, 2009 8:38 AM - Michael

Hi Mike,

Is it possible to have an attachment using your sample?


cheers,

Wednesday, September 16, 2009 4:39 PM - adham

thnak you

Thursday, January 14, 2010 3:47 PM - Tony

Great article. I appreciate you posting this. Any possibility of providing the code as a download? I tried following the article and the code would be helpful to fully understand.

thanks in advance.

Monday, June 21, 2010 3:36 PM - AJAX Development

Great stuff :) Big Thanks.

Saturday, August 7, 2010 3:07 AM - Mike

Great stuff!! Only problem i am having is when i hit submit on the form i get a script error Microsoft JScript runtime error: Object doesn't support this property or method
The break happens on

$("#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');
}
});
}
});

and everything was copied straight from your example, so im not sure whats going on.

if i figure it out i will reply.

Thanks again for the concept though!

Sunday, August 8, 2010 2:33 PM - Mike

@Mike

Did you include the jQuery Validate scripts?

Monday, October 4, 2010 1:06 PM - Miraç

Thank you for article :)

Thursday, March 17, 2011 12:37 AM - borat

Did anyone find a solution to Mike's problem? Did Mike found a solution?
If yes, please post it here.
I'm having the same problem: my code breaks at the same place as Mike's:
$("#contact").validate({......

and I'm including the jQuery validate scripts!

Please help!

Thursday, March 17, 2011 9:03 PM - Mike

@borat,

I'll ask you the same question I asked Mike - are you sure you referenced the Validate script correctly? If you did, try posting a question the forums at www.asp.net. This site isn't a forum, and it can take me a while to get round to publishing comments.

Sunday, June 19, 2011 10:50 AM - Guru

Very good article. Thank you for posting.

Thursday, September 4, 2014 4:39 PM - David

Hi Mike,

I have to tell you, you have a great site! I've learned a lot from you and thank you for it.

For the past few months I've referenced your website to learn ASP.NET (MVC, Web Forms & Web Pages). I was all set to redeploy my website (classic ASP) using ASP.NET Web Pages when they announced vNext - that motivated me to stick with classic ASP a while longer. So I redesigned everything using my tried and true (since 2001) code, adding some bootstrap and jQuery and frankly, it took me a couple of days instead of months. Okay, I'm getting a little long.

To the point: You wrote, "As far as I am concerned, you can keep your impossible-to-read Captcha stuff." I couldn't agree more and really wanted YOUR solution on MY antiquated website.

I wanted to tell you that the snippet of code found in this article was inspirational!!! I was receiving hundreds of spambot messages from our contact form. Since I am sticking with classic ASP, I had to "revise" your code just a little to make it work.

FROM
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();
To
Dim max, min
max = 10
min = 0
If ISNULL(Session("spamcheck")) or Session("spamcheck") = "" then
Randomize
Session("a") = Int((max-min+1)*Rnd+min)
Session("b") = Int((max-min+1)*Rnd+min)
Session("spamcheck") = Int(Session("a") + Session("b"))
End If

Yeah, that easy! I clear the spamcheck session when the form submits successfully. I share to thank you - perhaps a reader or two will find this useful too.

If you're interested in seeing the whole site, send me an email.

Thank you again!!!

David
Add your comment

If you have any comments to make about this article, please use this form to do so. Make sure that your comment relates specifically to the article above. More general comments can be posted through the form on the Contact page.

Please note, all comments are moderated, and some may not be published. The kind of things that will ensure your comment is deleted without ever seeing the light of day are as follows:

  • Not relevant to the article
  • Gratuitous links to your own site or product
  • Anything abusive or libellous
  • Spam
  • Anything in a language I don't understand including gibberish.

I do not pass email addresses on to spammers, so a valid one will assist me in responding to you personally if required.

Recent Comments

Bino 11/27/2014 7:05 PM
In response to MVC 5 with EF 6 in Visual Basic - Async and Stored Procedures with the Entity Framework
Copy +...

Manas 11/27/2014 5:30 AM
In response to Scheduled Tasks In ASP.NET With Quartz.Net
Hi Mike, Thank you for awesome article. My concern is it might impact website if we use or is...

priya 11/26/2014 6:50 PM
In response to Create PDFs in ASP.NET - getting started with iTextSharp
very nice.....its save my time...

ransems 11/24/2014 12:29 AM
In response to Adding A Controller
Love the article. I dislike that the world thinks c# articles are the way to go. Love the VB, keep...

Gautam 11/20/2014 8:01 AM
In response to I'm Writing A Book On WebMatrix
Hello Mike, I read your book, loved it! However, I have a few request/suggestions: 1) an example...

Bret Dev 11/19/2014 8:39 PM
In response to The Difference Between @Helpers and @Functions In WebMatrix
Excellent post! One concern - where can you place global @Functions code within an MVC project to Is...

Rob Farquharson 11/19/2014 4:28 PM
In response to iTextSharp - Links and Bookmarks
How can I place text at an absolute position on the page? Also, how can I rotate text?...

Andy 11/17/2014 8:08 PM
In response to MVC 5 with EF 6 in Visual Basic - Sorting, Filtering and Paging
Hello I'm testing your sorting instructions above. This is great and I was able to get it to work...

Gautam 11/17/2014 5:51 PM
In response to WebMatrix - Database Helpers for IN Clauses
Hi Mike, I am very new to programming: In the above example if I want to use a delete button the...

donramon 11/17/2014 3:22 PM
In response to Entity Framework 6 Recipe - Alphabetical Paging In ASP.NET MVC
Congratulations on your new website look and the excellent articles. Thank you!...