'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:
public class AsynchronousFormViewModel: IDataErrorInfo
{
  #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
}
As you can see, the ViewModel implements IDataErrorInfo interface. Thanks to that, it will easily integrate with ASP.NET MVC ModelState.
Now we can move on to controller actions:
/// <summary>
///
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());
  }
}
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):
public class IsAjaxRequestAttribute : FilterAttribute, IActionFilter
{
  #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
}
So 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:
public static class ModelStateDictionaryExtensions
{
  /// <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;
  }
}
What is left, is preparing our form (I'm using strongly typed view):
<div id="dvRegister">
  <% 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>
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):
<script src="<%= Url.Content("~/Scripts/jquery-1.3.2.min.js") %>" type="text/javascript"></script>
<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>
Our example is complete. Be aware that it still doesn't use any of new ASP.NET MVC 2 model validation features.