Is there a way for Ajax.BeginForm return JSON and automatically update Form
What I want to do is use DataAnnotation to identify validation on my model view. I also want to be able to call an action with Ajax.BeginForm or other feature, and that action returns JSON that would automatically update the form.
I have written code where I submit the form's data using JQuery feature $.Ajax to the server. The action returns a model view that includes the errors and other info which is converted into JSON. But then on the client I had to write custom JavaScript to displ开发者_如何学运维ay the errors and such. This seems overly convoluted. It seems there should be a better way of doing this. Am I missing something
Is anyone aware of a better way of doing this, or know of any resource that can point me in the correct direction?
BarDev
I've been working through this problem and think I've come up with a decent solution. Here are my contrived business requirements. Create a login screen that does not refresh when submitting the form. If the User does not successfully login, display a message to the user. User Name and Password has to be entered. User Name cannot have any white spaces. If the user does not enter User Name or Password display Error.
Here are some technical requirements. Validation should use data annotation in the model. The validation should be DRY (Do Not Repeat Yourself). Client side validation should validate UserName and Password before submitting the data. Json should be returned to the client with any errors that may have been identified on the server.
Here are the models. The first is the LoginModelView. It has two properties: UserName, and Password. Both of the properties have data annotation attributes. These attributes will be used on the client side validation and server side validation. The ErrorModelView will be used to return the results of the login with any errors that might have been identified.
using System.ComponentModel.DataAnnotations;
using System.Collections.Generic;
namespace TestClientSideValidation.Models
{
public class LoginModelView
{
[Required(ErrorMessage = "UserName Is Required")]
[RegularExpression(@"(\S)+", ErrorMessage = "White space is not allowed")]
[StringLength(12, MinimumLength = 3)]
public string UserName { get; set; }
[Required(ErrorMessage = "Password Is Required")]
[StringLength(20, MinimumLength = 3)]
public string Password { get; set; }
public bool RememberMe { get; set; }
}
public class ErrorModelView
{
public bool IsLogInSuccessful { get; set; }
public List<Error> Errors { get; set; }
}
public class Error
{
public string Key { get; set; }
public List<string> Messages { get; set; }
}
}
The AccountServices class simulates checking the Login Credential against a data store. First we check to see if the model state is currently valid. If it's not valid then there is no reason to access the data store. If the model state is valid then validate the login credentials with the data store. If the login credentials are not valid then add an Error to ModelState.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using TestClientSideValidation.Models;
namespace TestClientSideValidation.Services
{
public static class AccountServices
{
public static void ValidateLogin(ModelStateDictionary modelState, LoginModelView loginModelView)
{
if (modelState.IsValid) //If modelState is Invalid there is not reason tho check the login is valid
{
if (loginModelView.UserName != "Admin" || loginModelView.Password != "Password")
{
modelState.AddModelError("_FORM", "The username or password provided is incorrect.");
}
}
}
}
}
The controller contains two actions, Login and JsonLogin. The Login action is very simple; create an empty LoginViewModel and pass it to the Login view.
The LoginJson does the majority of our work in this application. Initially the LoginModelView that gets passed in is validated against the AccountSerices.Validation that we created earlier. Then the ModelState is checked. If the modes state is not valid then move data from the ModelState to ErrorModelView. Forgive me for the LINQ, I know it's pretty ugly. Maybe I will refactor this or some else can. Also, the ModelViewError.IsLogInSuccessful is set to false. We then send back the model view using the Json method to serialize the data to Json.
If everything is valid, FormsAuthenticationTicket is created, set the ErrorModelView.IsLogInSuccessful to true and return Json back to the client.
using System.Web;
using System.Web.Mvc;
using TestClientSideValidation.Models;
using TestClientSideValidation.Services;
using System.Web.Security;
using System;
using System.Linq;
namespace TestClientSideValidation.Controllers
{
public class AccountController : Controller
{
public ActionResult Login()
{
LoginModelView loginModelView = new LoginModelView();
return View(loginModelView);
}
[System.Web.Mvc.OutputCache(NoStore = true, Duration = 0, VaryByParam = "*")]
public virtual JsonResult LoginJson(LoginModelView loginModelView)//Share Model
{
ErrorModelView modelView = new ErrorModelView();
AccountServices.ValidateLogin(ModelState, loginModelView);
if (!ModelState.IsValid)
{
modelView.Errors = (from d in
(from ms in ModelState
where ms.Value.Errors.Count > 0
select ms
).ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Errors.Select(e => e.ErrorMessage).ToArray())
select new Error
{
Key = d.Key,
Messages = d.Value.ToList()
}).ToList();
modelView.IsLogInSuccessful = false;
return Json(modelView, JsonRequestBehavior.AllowGet);
}
FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket
(
1,
loginModelView.UserName,
DateTime.Now,
DateTime.Now.AddDays(30),
loginModelView.RememberMe,
loginModelView.UserName.ToString()
);
string encTicket = FormsAuthentication.Encrypt(authTicket);
this.Response.Cookies.Add(new HttpCookie(FormsAuthentication.FormsCookieName, encTicket));
modelView.IsLogInSuccessful = true;
return Json(modelView, JsonRequestBehavior.AllowGet);
}
}
}
The last part of this example displays the view and accepts the returning JSON from action controller. The first line identifies a strongly type model of LoginModelView. This mode is the one we create earlier that include the UserName and Passwords and Validation attributes. For the validation and Ajax calls to work, we need to include a few JavaScript files. If you skip to the bottom of this code block you will notice we are calling Ajax.BeginForm when the page is submitted. This will call the LoginJson action and pass the values that are in the form. Also the following JavaScript function that I declared earlier in this code block will also be triggered at certain points in the process; Only OnSuccess has code. In this example the @Html.TextBoxFor and @Html.ValidationMessageFor does some magic. This is where client side validation comes in.
When the Json is retuned from the LoginJson action, the JavaScript function OnSucces is triggered. If there are errors, the DisplayErrors function is called and loops through the errors and adds them to the Errors Div. If there are no errors then alert the user that login was successful.
@model TestClientSideValidation.Models.LoginModelView
<html>
<head>
<title>Login</title>
<script src="/Scripts/jquery-1.4.4.min.js" type="text/javascript"></script>
@*This is need for validation*@
<script src="/Scripts/jquery.validate.js" type="text/javascript"></script>
<script src="/Scripts/jquery.validate.unobtrusive.js" type="text/javascript"></script>
@*This is need for validation Ajax.BeginForm*@
<script src="/Scripts/jquery.unobtrusive-ajax.js" type="text/javascript"></script>
<script type="text/javascript">
function OnSuccess(e) {
if (e.IsLogInSuccessful) {
ClearErrors();
alert("You logged in");
}
else {
ClearErrors();
DisplayErrors(e.Errors);
}
}
function OnFailure(e) {
}
function OnComplete(e) {
}
function DisplayErrors(errors) {
for (error in errors) {
for (message in errors[error].Messages) {
if (message != null && message != "") {
$("#Errors").append(errors[error].Messages[message]);
}
}
}
}
function ClearErrors() {
$("#Errors").text("");
}
</script>
</head>
<body>
<div>
<div id="Errors"></div>
@using (Ajax.BeginForm("LoginJson", new AjaxOptions { HttpMethod = "Post", OnFailure = "OnFailure", OnSuccess = "OnSuccess", OnComplete = "OnComplete" }))
{
<div>
@Html.LabelFor(m => m.UserName)
@Html.TextBoxFor(m => m.UserName)
@Html.ValidationMessageFor(m => m.UserName)
</div>
<div>
@Html.LabelFor(m => m.Password)
@Html.TextBoxFor(m => m.Password)
@Html.ValidationMessageFor(m => m.Password)
</div>
<input type="submit" id="loginDialog_submitForm" value="Submit Form" />
}
</body>
</html>
First, sorry for the extended example. But hopefully this will help some people out. Or if there is a better or different way to do this, some will reply.
So this solves the primary issue where all the validation rules are in one place. I also believe we fulfill the contrived business/technical requirements also.
I tried to make this very simple, and with that there are many way to improve this, but I believe this is a great start. I currently use this with JQuery UI, by making the view a partial view and displaying the view in a modal dialog.
Again, I hope this helps people out.
Update
Make sure the following is in the web.config
<appSettings>
<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
</appSettings>
BarDev
No, you are not missing anything. If you return JSON you need to handle it on the client manually in order to update your GUI. An easier solution could be to return a partial view containing the form html containing all the validation errors.
精彩评论