'Unobtrusive' asynchronous Form in ASP.NET MVC
I have already written how to create asynchronous form using jQuery Validation plugin, but I have made a few shortcuts in that sample. So this is the for some improvements. We will start with ViewModel class:
Now we can move on to controller actions:
public class AsynchronousFormViewModel: IDataErrorInfoAs you can see, the ViewModel implements IDataErrorInfo interface. Thanks to that, it will easily integrate with ASP.NET MVC ModelState.
{
#region Fields
private IUsersRepository _usersRepository = null;
#endregion
#region Properties
public string UserName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
#endregion
#region Constructors
public AsynchronousFormViewModel()
: this(new DummyUsersRepository()) {
}
public AsynchronousFormViewModel(IUsersRepository usersRepository)
{
_usersRepository = usersRepository;
}
#endregion
#region IDataErrorInfo Members
public string Error
{
get { return null; }
}
public string this[string columnName]
{
get
{
if (columnName == "UserName")
{
if (String.IsNullOrEmpty(UserName) || UserName.Trim().Length == 0)
return "Please enter username";
if (UserName.Length < 5)
return "Please enter username with at least 5 characters";
if (!_usersRepository.ValidateUserNameUnique(UserName))
return String.Format("Username {0} is already in use", UserName);
}
else if (columnName == "Email")
{
if (String.IsNullOrEmpty(Email) || Email.Trim().Length == 0)
return "Please enter email address";
if (!Regex.IsMatch(Email, @"^...$"))
return "Please enter valid email address";
}
else if (columnName == "Password")
{
if (String.IsNullOrEmpty(Password) || Password.Trim().Length == 0)
return "Please enter password";
string passwordInvalidMessage = _usersRepository.ValidatePassword(Password);
if (!String.IsNullOrEmpty(passwordInvalidMessage))
return passwordInvalidMessage;
}
else if (columnName == "ConfirmPassword")
{
if (String.IsNullOrEmpty(ConfirmPassword) || ConfirmPassword.Trim().Length == 0)
return "Please repeat password";
if (!ConfirmPassword.Equals(Password))
return "Confirm password does not match password";
}
return null;
}
}
#endregion
}
Now we can move on to controller actions:
/// <summary>The GET action is pretty simple. Someone might ask why I pass an empty ViewModel to the view, but it will be clear after looking into POST action. First thing, which should raise a question about POST action, is IsAjaxRequest attribute. This attribute injects into parameter (which name is given in constructor) a value indicating whether we have an AJAX request or not. Why doing this instead of just checking Request.IsAjaxRequest()? This way we keep our controller independent from HttpContext, which makes it clean and friendly for unit testing. So here is how this attribute works (really simple):
/// GET Home/AsynchronousForm
/// </summary>
/// <returns>AsynchronousForm view</returns>
public ViewResult AsynchronousForm()
{
return View(new AsynchronousFormViewModel(null));
}
/// <summary>
/// POST Home/AsynchronousForm
/// </summary>
/// <param name="viewModel">The form post data</param>
/// <param name="isAjaxRequest">Value indicating if it is an AJAX request</param>
/// <returns>AsynchronousForm view or json result</returns>
[AcceptVerbs(HttpVerbs.Post)]
[ValidateAntiForgeryToken]
[IsAjaxRequest("isAjaxRequest")]
public ActionResult AsynchronousForm(AsynchronousFormViewModel viewModel, bool isAjaxRequest)
{
if (ModelState.IsValid)
{
try
{
_usersRepository.RegisterUser(viewModel.UserName, viewModel.Email, viewModel.Password);
}
catch (Exception ex)
{
ModelState.AddModelError("_FORM", ex.Message);
}
}
if (isAjaxRequest)
return Json(new
{
Success = ModelState.IsValid,
Errors = ModelState.GetModelErrors()
});
else
{
if (!ModelState.IsValid)
return View(viewModel);
else
return View(new AsynchronousFormViewModel());
}
}
public class IsAjaxRequestAttribute : FilterAttribute, IActionFilterSo back to the POST action. After checking if data are valid (this is where IDataErrorInfo comes in handy), we perform our business logic. Next, we are checking if we have an AJAX request. If yes, then we return JsonResult, if no then ViewResult (this why we were giving empty ViewModel to our view in GET action). One more thing I should show here is an extension class with GetModelErrors() method:
{
#region Fields
private string _actionParameterName;
#endregion
#region Constructor
public IsAjaxRequestAttribute(string actionParameterName)
{
_actionParameterName = actionParameterName;
}
#endregion
#region IActionFilter Members
public void OnActionExecuted(ActionExecutedContext filterContext)
{
return;
}
public void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.ActionParameters[_actionParameterName] = filterContext.HttpContext.Request.IsAjaxRequest();
}
#endregion
}
public static class ModelStateDictionaryExtensionsWhat is left, is preparing our form (I'm using strongly typed view):
{
/// <summary>
/// Returns model state errors in list
/// </summary>
/// <param name="modelState">Model state</param>
/// <returns>Model state errors</returns>
public static List<string> GetModelErrors(this ModelStateDictionary modelState)
{
List<string> errors = new List<string>();
if (!modelState.IsValid)
{
foreach (ModelState state in modelState.Values)
{
foreach (ModelError error in state.Errors)
errors.Add(error.ErrorMessage);
}
}
return errors;
}
}
<div id="dvRegister">and writing the necessary javascript (it is the same as here (it also uses the same remote validation actions), so forgive me that I will not describe it again):
<% using (Html.BeginForm("AsynchronousForm", "Home", FormMethod.Post, new { id = "frmRegister" })) { %>
<%= Html.AntiForgeryToken() %>
<div>
<fieldset>
<legend>Account Information</legend>
<p>
<label for="userName">Username:</label>
<%= Html.TextBox("UserName", Model.UserName) %>
</p>
<p>
<label for="email">Email:</label>
<%= Html.TextBox("Email", Model.Email) %>
</p>
<p>
<label for="password">Password:</label>
<%= Html.Password("Password") %>
</p>
<p>
<label for="confirmPassword">Confirm password:</label>
<%= Html.Password("ConfirmPassword") %>
</p>
<p>
<input type="submit" value="Register" />
</p>
</fieldset>
</div>
<% } %>
</div>
<script src="<%= Url.Content("~/Scripts/jquery-1.3.2.min.js") %>" type="text/javascript"></script>Our example is complete. Be aware that it still doesn't use any of new ASP.NET MVC 2 model validation features.
<script src="<%= Url.Content("~/Scripts/jquery.validate.min.js") %>" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
$("#frmRegister").validate({
rules: {
UserName: {
required: true,
minlength: 5,
remote: '<%=Url.Action("ValidateUserName", "Home") %>'
},
Email: {
required: true,
email: true
},
Password: {
required: true,
remote: '<%=Url.Action("ValidatePassword", "Home") %>'
},
ConfirmPassword: {
required: true,
equalTo: "#Password"
}
},
messages: {
UserName: {
required: "Please enter username",
minlength: $.format("Please enter at least {0} characters"),
remote: $.format("{0} is already in use")
},
Email: {
required: "Please enter email address",
email: "Please enter valid email address"
},
Password: {
required: "Please enter password"
},
ConfirmPassword: {
required: "Please repeat password",
equalTo: "Please enter the same password as above"
}
},
submitHandler: function() {
var registerData = $("#frmRegister").serialize();
$.ajax({
type: 'POST',
url: '<%=Url.Action("AsynchronousForm", "Home") %>',
dataType: 'json',
data: registerData,
success: function(registerResult) {
if (registerResult.Success) {
$('#dvRegister').empty().text('User successfully registered.');
}
else {
var errorsContainer = $('#registerErrors');
if (errorsContainer.length > 0) {
errorsContainer.empty();
}
else {
$('#frmRegister').after('<ul id="registerErrors" class="validation-summary-errors"></ul>');
errorsContainer = $('#registerErrors');
}
for (error in registerResult.Errors) {
errorsContainer.append('<li>' + registerResult.Errors[error] + '</li>');
}
}
}
});
}
});
});
</script>