Sunday, May 3, 2009

ASP.NET MVC Validation with Ajax and Json

AjaxValidationASP.NET MVC has greate ajax integration with no doubt. Simply by using ajax forms you can use partial page rendering and controller callbacks. One thing that will break if you switch to Ajax / JsonResult is the built-in Validation result (ModelErrors) rendering of the MVC engine.

It won’t work because of a simple fact: The built-in validation will take place when the page is being rendered on the server. When you use ajax and callbacks page will not re-render on the server-side, hence validation can not mark fields with error and add validation information to the already rendered page.


Let’s see how you can do this, without using a client-side validation framework.

Creating a Contact Form

First, let's create a form using Ajax.BeginForm and provide a javascript action to process the results. You can get rid of the ValidationMessage and ValidationSummary, since we don’t need it anymore:

<% using (Ajax.BeginForm(new AjaxOptions { OnComplete = "ProcessResult" })) { %>
<p>
<
label for="Name" class="contactLabel">Your Name:</label>
<%= Html.TextBox("SenderName", Model.SenderName)%>
</p>
<
p>
<
label for="Email" class="contactLabel">Your Email:</label>
<%= Html.TextBox("Email", Model.Email)%>
</p>
<
p>
<
label for="Subject" class="contactLabel">Subject:</label>
<%= Html.TextBox("Subject", Model.Subject)%>
</p>
<
p>
<
label for="Message" class="contactLabel">Message:</label>
<%= Html.TextBox("Message", Model.Message)%>
</p>
<
p>
<
br />
<
input type="submit" value="Send" class="inputButton" />
<
br />
</
p>
<
div id="operationMessage"><ul></ul></div>
<% } %>
Notice that there is a div at the bottom which acts as an error placeholder and will be used to display validation information.

To actually bind these fields to data, we need to create an POCO class acting as a backing entity. In this example, I’ve used a poco class and decorate it with NHibernate Validator's attributes to do the validation. See here for more info on how to configure NHibernate Validator using ASP.NET MVC:

public class ContactMessage : IValidatable
{
public ContactMessage()
{
MessageDate = DateTime.Today;
}

public virtual int Id
{
get; private set;
}

[NotNullNotEmpty]
public virtual string SenderName
{
get; set;
}

[NotNullNotEmpty]
[Email]
public virtual string Email
{
get; set;
}

[NotNullNotEmpty]
public virtual string Subject
{
get; set;
}

[NotNullNotEmpty]
public virtual string Message
{
get; set;
}

public virtual DateTime MessageDate
{
get; private set;
}
}

Controller Action

Two methods are needed to handle to Post / Get separately. Our Get method returns a new instance of our contact entity to the view, and Post method is where the operation results is returned to the client using Json. The OperationResultInfo class containing the elaborate error information is what returned as Json to the client:

public class OperationResultInfo
{
public OperationResultInfo()
{
Errors = new List<ErrorReasonInfo>();
}

public bool Successfull { get; set; }

public string Message { get; set; }

public IList<ErrorReasonInfo> Errors { get; set; }
}

public class ErrorReasonInfo
{
public string PropertyName { get; set; }

public string Error { get; set; }
}
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Contact()
{
return View(new ContactMessage());
}

[AcceptVerbs(HttpVerbs.Post)]
[AutoValidate]
public ActionResult Contact(ContactMessage msg)
{
if (ModelState.IsValid)
{
try
{
this.messengerService.SendMail(msg);

return Json(new OperationResultInfo
{
Successfull = true,
Message = "Thank you. Your message was sent successfully."
});
}
catch (Exception ex)
{
ModelState.AddUnhandledError(ex);
return Json(new OperationResultInfo
{
Successfull = false,
Message = "There was an error processing your request. Retry later."
});
}
}

return Json(new OperationResultInfo
{
Successfull = false,
Message = "Could not send your information.",
Errors = ModelState.GetAllErrors()
});
}

public static class
ModelStateExtensions
{
public static IList<ErrorReasonInfo> GetAllErrors(this ModelStateDictionary modelState)
{
var errors = new List<ErrorReasonInfo>();

foreach (var state in modelState)
{
if (state.Value.Errors.Count > 0)
{
var err = new ErrorReasonInfo {PropertyName = state.Key};
state.Value.Errors.ForEach(x => err.Error += x.ErrorMessage);

errors.Add(err);
}
}

return errors;
}
}

Displaying The Json Result

The Json result returned to the client contains all the fields having errors and the related error message. A typical result would look like this:

{
"Successfull":false,
"Message":"Could not send your information:",
"Errors":[
{"PropertyName":"SenderName",
"Error":"may not be null or empty"},
{"PropertyName":"Email",
"Error":"may not be null or empty"},
{"PropertyName":"Subject",
"Error":"may not be null or empty"},
{"PropertyName":"Message",
"Error":"may not be null or empty"}]
}

How do we format this data on the client and display it in a user-friendly fashion on our page? Let’s process the Json object using javascript and add the result to the error place holder using jQuery:

<script type="text/javascript">
function ProcessResult(content) {

var json = content.get_response().get_object();
var result = eval(json);

$("#operationMessage > span").empty();
$("#operationMessage > ul").empty();
$(':input').removeClass('input-validation-error');

if (result.Successfull) {
$('#operationMessage').append('<span>' + result.Message + '</span>')
.removeClass('error')
.addClass('success');
}
else {

$('#operationMessage').append('<span><br>' + result.Message + '</span>')
.removeClass('success')
.addClass('error');

for (var err in result.Errors) {
var propertyName = result.Errors[err].PropertyName;
var errorMessage = result.Errors[err].Error;
var message = propertyName + ' ' + errorMessage;

$('#' + propertyName).addClass('input-validation-error');
$('#operationMessage > ul').append('<li># ' + message + '</li>');
}
}
}
</script>

Pretty neat, ha? Hope this helps.


Submit this story to DotNetKicks Shout it

5 comments:

Chris Patterson said...

Great post! I love seeing examples of people using the data model validation on the client side.

ExistMe said...

Nice Article, Thank you

Anonymous said...

Good article. However, if the user needs to be redirected to another view, return View("Index"); on the controller's action on success is throwing an error. Well and error in the MSAjax.js file. It seems as though it is not able to parse the result which is a new form.

Any idea about this scenario? I'd like to redirect the user to a new page.

Hadi Eskandari said...

@Anonymous: Well, you can not redirect to another view or controller action when asynchronously calling controller's action. You can do this on the client side do, using a few lines of javascript. See here for more info : http://is.gd/M11p

Marc Chouteau said...

Thank you your example is very instructive.