Validate MVC field at client side - c#

I have problem validating one field at client side, Here is my code:
Model:
public class Registration
{
public int Id { get; set; }
[IsUserExistAttribute(ErrorMessage = "Email already exists!")]
[EmailAddress(ErrorMessage = "Please enter valid Email")]
[Required(ErrorMessage = "Email is required")]
public string Email
{
get; set;
}
[MustBeTrue(ErrorMessage = "Please Accept the Terms & Conditions")]
public bool TermsAndConditions { get; set; }
}
ValidationAttribute:
public class IsUserExistAttribute : ValidationAttribute,IClientValidatable
{
public override bool IsValid(object email)
{
DemoDbContext demoContext=new DemoDbContext();
string emailString = Convert.ToString(email);
if (demoContext.Registrations.Any(a=>a.Email.Contains(emailString)))
{
return false;
}
return true;
}
public IEnumerable<ModelClientValidationRule> GetClientValidationRules(ModelMetadata metadata, ControllerContext context)
{
yield return new ModelClientValidationRule
{
ErrorMessage = this.ErrorMessage,
ValidationType = "emailvalidate"
};
}
}
View:
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<h4>Registration</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.Email, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Email, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Email, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.TermsAndConditions, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<div class="checkbox">
#Html.EditorFor(model => model.TermsAndConditions)
#Html.ValidationMessageFor(model => model.TermsAndConditions, "", new { #class = "text-danger" })
</div>
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
Everthing upto this works fine, But my question is This IsUserExistAttribute doesn't validate it on client side, on server side it validates and give the message like Email already exists!
But, I want it to validate client side too. So, what is the good way to achieve this? Other approach is also welcome :)
I tried something like this seems no success:
$.validator.addMethod("emailvalidate", function (value, element) {
var is_valid = false;
$.ajax({
// as before...
async: false,
success: function (data) {
is_valid = data === 'True';
}
});
return is_valid;
}, "Username not available.");

MVC already comes with a RemoteAttribute for client side validation (refer How to: Implement Remote Validation in ASP.NET MVC) so there is little point in you trying to reinvent the wheel. (As a side note, your currently missing the $.validator.unobtrusive.adapters.add() method necessary to add the rules for client side validation).
But there are a few issues with your current validation attribute. A validation attribute should not be accessing the database and you have made it impossible to unit test your code. And you will be repeating the code in the attribute again in the controller method that is called by the ajax function, violating the DRY principal and making it harder to maintain your code.
Instead, create a private method in your controller (or a public method in a separate service) for you database code, for example
private bool IsEmailUnique(string email)
{
return !db.Registrations.Any(a => a.Email.Contains(email)))
}
Then for client side validation, decorate you property with the RemoteAttribute
[Remote("IsEmailValid", "ControllerName", ErorMessage = "...")]
public string Email { get; set; }
and add the controller method
public JsonResult IsEmailValid(string email)
{
bool isValid = IsEmailUnique(email);
return Json(isValid, JsonRequestBehavior.AllowGet);
}
and then if you also want to have server side validation when you submit the form, call the same method and if not valid, add a ModelStateError
public ActionResult Register(Registration model)
{
bool isValid = IsEmailUnique(model.Email);
if (!isValid)
{
ModelState.AddModelError("Email", "...");
}
if (!ModelState.IsValid)
{
return View(model);
}
// save and redirect
}

Related

Remote validation not working in ASP.NET MVC 5

Here I have create form which consists customer email field for which I am trying to check whether the entered email already exits or not and if exist show email already exists message.
To do this i have tried to use remote validation but the problem is that its not showing any error even though email exists, it not even hitting the controller in IsEmailExists method which is used for remote validation
Any help with my code will be a great help. Thank you
Below is my action in controller
public JsonResult IsEmailExists(string CustomerEmail)
{
emedicineEntities _db = new emedicineEntities();
return Json(!_db.Customers.Any(x => x.CustomerEmail == CustomerEmail), JsonRequestBehavior.AllowGet);
}
Below is my metadata
namespace eMedicine.Model
{
public class CustomerMetaDta
{
[Remote("IsEmailExists", "Customers", ErrorMessage = "EmailId already exists.")]
[Required(ErrorMessage = "Please Enter Emailw")]
public string CustomerEmail { get; set; }
}
}
Below is my partial class
namespace eMedicine.Model
{
[MetadataType(typeof(CustomerMetaDta))]
public partial class Customer
{
}
}
Below is my view consisting customer email
<link href="~/Content/Site.css" rel="stylesheet" />
<script src="~/Scripts/jquery-3.3.1.min.js"></script>
<script src="~/Scripts/jquery.validate.min.js"></script>
<script src="~/Scripts/jquery.validate.unobtrusive.min.js"></script>
#using (Html.BeginForm("Create", "Customers", FormMethod.Post, new { #id = "register" }))
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.CustomerName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.CustomerName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CustomerName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.CustomerEmail, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.CustomerEmail, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CustomerEmail, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.PasswordHash, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.PasswordHash, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.PasswordHash, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
Change your method signature as follows to include the Bind Prefix attribute/property.
public JsonResult IsEmailExists([Bind(Prefix="Customer.CustomerEmail")] string CustomerEmail)
{
emedicineEntities _db = new emedicineEntities();
return Json(!_db.Customers.Any(x => x.CustomerEmail == CustomerEmail), JsonRequestBehavior.AllowGet);
}
Now it should work!
Not sure what real problem with your source code, but i tried to reproduce in my side, it worked well.
Here are my source code.
namespace WebApplication1.Controllers
{
public class CustomerMetaDta
{
[Remote("IsEmailExists", "Customer", ErrorMessage = "EmailId already exists.")]
[Required(ErrorMessage = "Please Enter Emailw")]
public string CustomerEmail { get; set; }
}
[MetadataType(typeof(CustomerMetaDta))]
public partial class Customer
{
}
public partial class Customer
{
public string CustomerEmail { get; set; }
public string CustomerName { get; set; }
public string PasswordHash { get; set; }
}
public class CustomerController : Controller
{
public JsonResult IsEmailExists(string CustomerEmail)
{
//emedicineEntities _db = new emedicineEntities();
List<Customer> _db = new List<Customer>
{
new Customer { CustomerEmail = "hien#gmail.com"},
new Customer { CustomerEmail = "hien1#gmail.com"}
};
return Json(!_db.Any(x => x.CustomerEmail == CustomerEmail), JsonRequestBehavior.AllowGet);
}
// GET: Customer
public ActionResult Index()
{
return View();
}
}
}
Index.cshtml file:
#model WebApplication1.Controllers.Customer
#{
ViewBag.Title = "Index";
}
<script src="https://code.jquery.com/jquery-3.3.1.min.js" integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8=" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.19.0/jquery.validate.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validation-unobtrusive/3.2.11/jquery.validate.unobtrusive.min.js"></script>
#using (Html.BeginForm("Create", "Customer", FormMethod.Post, new { #id = "register" }))
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.CustomerName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.CustomerName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CustomerName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.CustomerEmail, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.CustomerEmail, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.CustomerEmail, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.PasswordHash, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.PasswordHash, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.PasswordHash, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</div>
</div>
}
It jumps to method IsEmailExists() and this is result output
May be you missed setting
<add key="ClientValidationEnabled" value="true"/>
<add key="UnobtrusiveJavaScriptEnabled" value="true"/>
in web.config?
If it's not hitting the controller that suggests it isn't a problem with the actual validation logic, but more an issue with how you're addressing the server.
There are a few things to check:
Is your remote validation code available to the client at all?
The first thing to consider is could this be a security/authentication issue. There are a few simple things you can do to check that:
If you have authentication attributes set on your controllers or methods, try commenting them out
try commenting out any other authentication code
If that doesn't fix it, then when you've got the app running in debug, try using Postman to call your remote validation endpoint and see whether:
Postman gets a 200 back from your method.
If so, put a breakpoint in your code and check it is actually getting executed.
If Postman can get to your endpoint then...
Is there an issue in your code?
I can't see anything obviously wrong in your code, but it is different to how I write validation code. This is an example of some working remote validation straight out of my code
This is the model property with the remote validation set:
[System.Web.Mvc.Remote(
action: "CheckExistingDocumentCode",
controller: "Documents",
AdditionalFields = "DocumentId",
HttpMethod = "POST",
ErrorMessage = "Code already exists")]
public string DocumentCode { get; set; }
This is the corresponding method in the Documents controller:
[HttpPost]
public async Task<ActionResult> CheckExistingDocumentCode(string DocumentCode, int DocumentId)
{
try
{
if (!await _documentValidationRules.IsExistingDocumentCodeAsync(DocumentCode, DocumentId))
{
return Json(true, JsonRequestBehavior.AllowGet);
}
return Json("This Document Code is already in use", JsonRequestBehavior.AllowGet);
}
catch (Exception ex)
{
return Json(ex.ToString(), JsonRequestBehavior.AllowGet);
}
}
You can see that I've explicitly named all the parameters on the model property just to be clear exactly what's going on.
Comparing my code to yours the main differences are:
Mines async. That shouldn't make any difference.
My contrller method is a POST (so is has the HttpPost attribute, which also means I needed to tell the model the HttpMethod was POST too)
My remote validation method takes two parameters, so I'm passing in an extra property via the AdditionalFields argument
I can't see what the issue is in your code, but try changing it a piece at a time to work more like mine (particularly, try making it a post method and naming the parameters) and see if that exposes any issues.
Hopefully something in the above will get you closer.
One thing no one mentioned which will cause your symptons: If the method needs to be executed by Anonymous users and you don't have the AllowAnonymous attribute on your method, the method will not fire (and the submit button won't do anything.
You can try to add this code to the bottom of your View:
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
This is the issue I had.
//[AcceptVerbs("Get,Post")]
//[AllowAnonymous]
public async Task<IActionResult> IsEmailInUse(string email)
{
var registration = await
_context.Registration.FirstOrDefaultAsync(m => m.Email == email);
if(registration == null)
{
return Json(true);
}
else
{
return Json($"Email {email} is already in use");
}
}
//In Model Class
public class Registration
{
[System.ComponentModel.DataAnnotations.Key]
public int EmpId { get; set; }
public string UserName { get; set; }
[Required]
[EmailAddress]
[Remote(action: "IsEmailInUse",controller: "Registrations")]
public string Email { get; set; }
}

ASP.NET MVC: ModelState error message not being displayed

I am just a beginner of ASP.NET MVC. I just started with creating my login page it works fine but I have a problem when the user inputs wrong credentials. This is what I have done:
UserProfile
public partial class UserProfile
{
public int UserId { get; set; }
[Display(Name = "User name")]
[Required(ErrorMessage = "Username is required.")]
public string UserName { get; set; }
[Display(Name = "Password")]
[DataType(DataType.Password)]
[Required(ErrorMessage = "Password is required.")]
public string Password { get; set; }
public bool IsActive { get; set; }
}
HomeController:
public class HomeController : Controller
{
public ActionResult Login()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(UserProfile objUser)
{
if (ModelState.IsValid)
{
using (DB_Entities db = new DB_Entities())
{
var obj = db.UserProfiles.Where(a => a.UserName.Equals(objUser.UserName) && a.Password.Equals(objUser.Password)).FirstOrDefault();
if (obj != null)
{
Session["UserID"] = obj.UserId.ToString();
Session["UserName"] = obj.UserName.ToString();
return RedirectToAction("UserDashBoard");
}
}
}
else
{
ModelState.AddModelError("", "Invalid Credentials");
}
return View(objUser);
}
public ActionResult UserDashBoard()
{
if (Session["UserID"] != null)
{
return View();
}
else
{
return RedirectToAction("Login");
}
}
}
And the View
#model MyWebApplication.Models.UserProfile
#{
ViewBag.Title = "Login";
}
#using (Html.BeginForm("Login", "Home", FormMethod.Post))
{
#Html.AntiForgeryToken()
<div class="form-horizontal">
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.UserName, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.UserName, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.UserName, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Password, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.Password, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Password, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Login" class="btn btn-default" />
</div>
</div>
</div>
}
#section Scripts {
#Scripts.Render("~/bundles/jqueryval")
}
I am not sure why the message "Invalid Credentials" is not being displayed when the user types a username that isn't stored in the database.
In your question you don't state whether or not in your if statement if the line ModelState.AddModelError("", "Invalid Credentials"); is being hit, which I doubt it is.
Your if statement is constructed wrong, because as of now you're only wanting to display the Invalid Credentials error if the ModelState is not valid.. not whether or not the user's credentials exist.
So you need to rewrite your if statement to this:
if (ModelState.IsValid)
{
using (DB_Entities db = new DB_Entities())
{
var obj = db.UserProfiles.Where(a => a.UserName.Equals(objUser.UserName) && a.Password.Equals(objUser.Password)).FirstOrDefault();
if (obj != null)
{
Session["UserID"] = obj.UserId.ToString();
Session["UserName"] = obj.UserName.ToString();
return RedirectToAction("UserDashBoard");
}
else
{
ModelState.AddModelError("", "Invalid Credentials");
}
}
}
Not to be picky, but hopefully your UserName property is unique, because if you have 2 users that have the same exact UserName and Password and you get the FirstOrDefault occurrence, then you could possibly be letting a user sign in with another's credentials. So I would ensure that the UserName property is unique and change FirstOrDefault to SingleOrDefault.
But if it does, then here is what needs to change:
Option 1
If you want the error message to be displayed under the textbox then in your controller change this:
ModelState.AddModelError("", "Invalid Credentials");
To:
ModelState.AddModelError("UserName", "Invalid Credentials");
AddModelError takes 2 parameters.. the key and the errorMessage for that key. The key is the property name in your model.
public void AddModelError(
string key,
string errorMessage
)
Option 2
If you do not want to display the error message under the textbox, but rather at the top of the form:
Then change this:
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
To:
#Html.ValidationSummary(false, "", new { #class = "text-danger" })
For option 2, you need to read the overloaded methods carefully. Per MSDN in correlation with how you have your ValidationSummary setup.. the first parameter is bool excludePropertyErrors.. you had that set to true so you were excluding Property errors, which is why I proposed to change that to false, so Property errors would be included.
public static MvcHtmlString ValidationSummary(
this HtmlHelper htmlHelper,
bool excludePropertyErrors,
string message,
IDictionary<string, object> htmlAttributes
)
Let me know if this helps.

ModelState is cleared, but data still shows

When a user logs in, the normal thing to do is NOT give the password back. I am attempting this in MVC 5, and cannot make it happen. I have tried model.PASSWORD = "", as well as ModelState.Clear(), but the data is still showing.
Controller:
public async Task<ActionResult> Login(Login model) {
if(ModelState.IsValid) {
User temp = await db.Users.Where(u => u.USERNAME == model.USERNAME).FirstOrDefaultAsync();
if(temp != null) {
try {
if(Password.VerifyPass(model.PASSWORD, temp.PASSWORD)) {
LoginUser(temp);
return RedirectToAction("Index", "EMR");
}
} catch { }
}
}
ModelState.Clear();
model.PASSWORD = "";
ModelState.AddModelError("", "Username/password is unknown, or model was invalid!");
return View();
}
Model:
public class Login {
[Display(Name = "Username")]
public string USERNAME { get; set; }
[Display(Name = "Password")]
public string PASSWORD { get; set; }
}
}
View:
#using(Html.BeginForm()) {
#Html.AntiForgeryToken()
<div class="form-horizontal">
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.USERNAME, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.USERNAME, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.USERNAME, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.PASSWORD, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.EditorFor(model => model.PASSWORD, new { htmlAttributes = new { #class = "form-control", #type = "password" } })
#Html.ValidationMessageFor(model => model.PASSWORD, "", new { #class = "text-danger" })
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Login" class="btn btn-success" />
</div>
</div>
</div>
}
Upon looking at both the ModelState variable and the model variable in VisualStudio debug mode, they BOTH show the data as "". What could possibly be causing this?
As I had commented, one possible solution is to simply use #Html.PasswordFor(). This will render an input that is semantically correct for password input, which implicitly will hint for the browser to not autofill. So, instead of #Html.EditorFor, try this:
#Html.PasswordFor(model => model.PASSWORD, new { htmlAttributes = new { #class = "form-control" })
You are not passed the model back to view after clear it.
return View(model);
Still not working try as below.
var newModel = new Login { Password = "" };
ModelState.Clear();
return View(newModel);
i think its due to the browser cashing, annotate login action with
[OutputCacheAttribute(VaryByParam = "*", Duration = 0, NoStore = true)]
in addition to
return View(model);
First decorate your password field with Password Datatype.
public class Login
{
[Display(Name = "Username")]
public string USERNAME { get; set; }
[Display(Name = "Password")]
[DataType(DataType.Password)]
public string PASSWORD { get; set; }
}
And in the view, use the PasswordFor helper method.
#Html.PasswordFor(x=>x.PASSWORD)
With this, you do not need to explicitly set the password field to empty string. MVC will do that for you.
#CalebHuggins it looks like it is the browser who is rememberign the input fields values and populates them. You can try setting autocomplete attribute of your textboxes to "off" to avoid this.
Your model bound textbox may look like as following.
#Html.TextBoxFor(x => x.Something, new { autocomplete="off" } )
Thnaks and regards,
Chetan Ranpariya

Missing form validation in MVC application

I have a form containing only check boxes, and I know that I can make each one required to enforce validation errors. But what I am looking for, is an error if none of the boxes has been checked. How would I go about achieving this?
I am looking for an error message like: "You must select at least one property."
I should clarify that none of the fields are required individually, there should just be at least one chosen option.
Edit for clarification:
My view looks something like this:
#using (Html.BeginForm("Method", "Controller", FormMethod.Post, new {id = "id"}))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="form-group">
#Html.LabelFor(model => model.Property1, htmlAttributes: new { #class = "control-label col-md-4" })
<div class="col-md-8">
#Html.CheckBoxFor(model => model.Property1)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Property2, htmlAttributes: new { #class = "control-label col-md-4" })
<div class="col-md-8">
#Html.CheckBoxFor(model => model.Property2)
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Property3, htmlAttributes: new { #class = "control-label col-md-4" })
<div class="col-md-8">
#Html.CheckBoxFor(model => model.Property3)
</div>
</div>
}
My View model looks something like this:
public class FormVM
{
[Display(Name = "One")]
public bool Property1 {get;set;}
[Display(Name = "Two")]
public bool Property2 {get;set;}
[Display(Name = "Three")]
public bool Property3 {get;set;}
}
You can implement the IValidatableObject interface on your viewmodel:
public class FormVM : IValidatableObject
{
[Display(Name = "One")]
public bool Property1 {get;set;}
[Display(Name = "Two")]
public bool Property2 {get;set;}
[Display(Name = "Three")]
public bool Property3 {get;set;}
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
var results = new List<ValidationResult>();
if (!(Property1 || Property2 || Property3))
{
results.Add(new ValidationResult("You must select at least one property."));
}
return results;
}
}
The benefit of using this is that this will be fired automatically if you call ModelState.IsValid in your controller, and the error message added to the ModelState errors.
If this validation is to be used in one place only, then you can validate it in the Action method:
[HttpPost]
public ActionResult MyAction(FormVM model)
{
if (!(model.Property1 || model.Property2 || model.Property3))
{
ModelState.AddModelError(nameof(model.Property1), "You must select at least one");
}
if (ModelState.IsValid)
{
// Do something
}
return View(model);
}
If you are likely to be reusing this validation a lot more, I would suggest writing a validation attribute.
Since the question doesn't mention which server-side or client-side validation were preferred to use, there are 2 approaches.
1) Server-side validation (without ModelState.AddModelError):
Controller.cs
[HttpPost]
public ActionResult Method(FormVM model)
{
if (ModelState.IsValid)
{
// ... other processing code and action returns
}
if (!(model.Property1 || model.Property2 || model.Property3))
{
ViewData["Error"] = "You must select at least one property." // this can be changed with ViewBag
// immediately return the same page
return View(model);
}
}
View.cshtml:
<p>#ViewData["Error"]</p>
2) Client-side validation with vanilla JS, using id to identify required elements:
<script type="text/javascript">
var check1 = document.getElementById("check1").value;
var check2 = document.getElementById("check2").value;
var check3 = document.getElementById("check3").value;
if (!(check1 || check2 || check3))
{
document.getElementById("error").innerHTML = "You must select at least one property.";
}
else
{
document.getElementById("error").innerHTML = '';
}
</script>
<div class="form-group">
#Html.LabelFor(model => model.Property1, htmlAttributes: new { #class = "control-label col-md-4" })
<div class="col-md-8">
#Html.CheckBoxFor(model => model.Property1, htmlAttributes: new { #id = "check1" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Property2, htmlAttributes: new { #class = "control-label col-md-4" })
<div class="col-md-8">
#Html.CheckBoxFor(model => model.Property2, htmlAttributes: new { #id = "check2" })
</div>
</div>
<div class="form-group">
#Html.LabelFor(model => model.Property3, htmlAttributes: new { #class = "control-label col-md-4" })
<div class="col-md-8">
#Html.CheckBoxFor(model => model.Property3, htmlAttributes: new { #id = "check3" })
</div>
</div>
</div>
<div id="error"></div>

Partial views for login can not call differently named action methods in controller

I am building a members' only ASP.NET MVC site that will offer its users more than one way to log in using FormsAuthentication. So the idea is that there would be one form for the typical username and password authentication and then another form for a mobile number for electronic certificate authentication.
Now I have read a lot of documentation on how to solve multiple forms in one view and I have now a fairly elegant solution of having one view with two partial views containing the forms and one viewmodel with two model properties. This provides a good solution for validation so each form is validated separately instead of both when pressing the respective submit button.
Here is the code:
Login.cshtml:
#model OneMeetingPortal2.Models.ViewModel
#{
ViewBag.Title = "Login";
}
<div class="container">
<div class="well">
<h1>#ViewBag.Title.</h1>
</div>
#Html.Partial("_LoginUsernamePasswordPartial", Model.UserPass)
#Html.Partial("_LoginMobilePartial", Model.Mobile)
</div>
_LoginUsernamePasswordPartial.cshtml:
#model OneMeetingPortal2.Models.LoginUsernamePasswordModel
#using (Html.BeginForm("Login", "Account", FormMethod.Post, new { #class = "form-horizontal", #role = "form", #id = "_userNameForm" }))
{
#Html.AntiForgeryToken()
//Birtingu validation elements er stjórnað með CSS. Sjá .validation-summary-valid í Main.css. Birtist ef villa kemur fram.
#Html.ValidationSummary("Eftirfarandi villur komu upp:", new { #class = "alert alert-danger col-sm-offset-2 col-sm-10" })
<div class="form-group">
#Html.LabelFor(m => m.UserName, new { #class = "control-label col-sm-2" })
<div class="col-sm-10">
#Html.TextBoxFor(m => m.UserName, new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.UserName)
</div>
</div>
<div class="form-group">
#Html.LabelFor(m => m.Password, new { #class = "control-label col-sm-2" })
<div class="col-sm-10">
#Html.PasswordFor(m => m.Password, new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.Password)
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input type="submit" value="Log in" class="btn btn-default" />
</div>
</div>
}
_LoginMobilePartial.cshtml. Note that if I change the action name parameter to Login I get an error about an ambigious action name:
#model OneMeetingPortal2.Models.LoginMobileModel
#using (Html.BeginForm("LoginM", "Account", FormMethod.Post, new { #class = "form-horizontal", #role = "form", #id = "_GSMForm" }))
{
#Html.AntiForgeryToken()
#Html.ValidationSummary("Eftirfarandi villur komu upp:", new { #class= "alert alert-danger col-sm-offset-2 col-sm-10" })
<div class="form-group">
#Html.LabelFor(m => m.MobileNumber, new { #class = "control-label col-sm-2" })
<div class="col-sm-10">
#Html.TextBoxFor(m => m.MobileNumber, new { #class = "form-control" })
#Html.ValidationMessageFor(m => m.MobileNumber)
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input type="submit" value="Log in" class="btn btn-default" />
</div>
</div>
}
And here are the models:
public class ViewModel
{
public LoginMobileModel Mobile { get; set; }
public LoginUsernamePasswordModel UserPass { get; set; }
}
public class LoginUsernamePasswordModel
{
[Required(ErrorMessage="Ekki má sleppa notandanafni.")]
[Display(Name = "Notandanafn")]
[StringLength(50, ErrorMessage = "Notandanafnið má ekki vera lengra en 50 stafir")]
[MinLength(3, ErrorMessage = "Notandanafnið verður að vera a.m.k. 3 stafir")]
public string UserName { get; set; }
[Required(ErrorMessage="Ekki má sleppa lykilorði.")]
[DataType(DataType.Password)]
[Display(Name = "Lykilorð")]
[StringLength(50, ErrorMessage="Lykilorðið má ekki vera lengra en 50 stafir")]
[MinLength(3, ErrorMessage="Lykilorðið verður að vera a.m.k. 3 stafir")]
public string Password { get; set; }
}
public class LoginMobileModel
{
[Required(ErrorMessage = "Ekki má sleppa símanúmeri.")]
[Display(Name = "Farsímanúmer")]
[StringLength(7, ErrorMessage = "Símanúmer má ekki vera lengra en 7 stafir.")]
[MinLength(7, ErrorMessage = "Símanúmer má ekki vera styttra en 7 stafir.")]
public string MobileNumber { get; set; }
}
And then the Controller methods:
// GET: /Account/Login
[HttpGet]
[AllowAnonymous]
public ActionResult Login(string returnUrl)
{
ViewBag.ReturnUrl = returnUrl;
ViewModel m = new ViewModel();
m.Mobile = new LoginMobileModel();
m.UserPass = new LoginUsernamePasswordModel();
return View(m);
}
//
// POST: /Account/Login
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginUsernamePasswordModel model, string returnUrl)
{
if (ModelState.IsValid)
{
if (_accountService.Login(model.UserName, model.Password))
{
string xmlString = _accountService.GetEmployeeDetails(model.UserName);
Session.Add("ProfileXml", xmlString);
Classes.Profile profile = new Classes.Profile(xmlString);
FormsAuthentication.RedirectFromLoginPage(profile.Subject, false);
}
}
// If we got this far, something failed, redisplay form
ViewModel m = new ViewModel();
m.UserPass = model;
m.Mobile = new LoginMobileModel();
return View(m);
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public ActionResult LoginM(LoginMobileModel model, string returnUrl)
{
if (ModelState.IsValid)
{
MobileLoginReturn mlr = _accountService.LoginGSM(model.MobileNumber);
if(mlr.Error == null)
{
Session.Add("ProfileXml", mlr.EmployeeXmlString);
Classes.Profile profile = new Classes.Profile(mlr.EmployeeXmlString);
FormsAuthentication.RedirectFromLoginPage(profile.Subject, false);
}
else
{
ModelState.AddModelError("", mlr.Error.Message);
}
}
// If we got this far, something failed, redisplay form
return View(model);
}
However I get a weird behavior that seems to be connected only to the login page. I would like to have two different action methods called LoginUserPass and LoginMobile but I can't. The actions will simply not be called if I name them thus. Funnily enough when I set up the same scheme elsewhere in the site after authentication, for example in the Home controller everything works fine and I can call different action methods in the controller based on the action name parameter in the partial views, for example I can have:
#using (Html.BeginForm("LoginMobile", "Account", FormMethod.Post, new { #class = "form-horizontal", #role = "form", #id = "_GSMForm" }))
{
[Code omitted]
}
and
public ActionResult LoginMobile(LoginMobileModel model, string returnUrl){ ... }
in the controller.
So, right now, I have to let the first parameter name in one of the the partial views be Login so the Login action method will be called. There can only be two methods with that name in the controller, one for get and one for post, if more I get the ambiguous method exception. So at the moment I am faced with the fact that I have to choose if I want to let the user log in using mobile number OR Username and password, for I can not have both methods available on the login page. That of course is not acceptable, surely there is a way to have both, right?
I hope someone knows how to solve this dilemma. Does this have something to be do with FormsAuthentication?
When I finally figured out that it was FormsAuthentication that was causing this behavior I found the answer here
In short delete the <authorization> node from web.config and then in FilterConfig.cs add filters.Add(new AuthorizeAttribute());

Categories