I'm developing an Asp.Net Core 2.1 App using Razor Pages.I've come down with an odd behavior.The problem is when I submit a form,the client side validation passes with all required properties filled out,but then the validation fails with the ModelState.IsValid check,and the reason is that the ModelState contains the required string properties twice,one with the value entered and one with null value,So the validation fails!
{[BankName, Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary+ModelStateNode]}
{[BankAccount.BankName, Microsoft.AspNetCore.Mvc.ModelBinding.ModelStateDictionary+ModelStateNode]}
See,BankAccount is the model class,and BankName is a required property.I don't know why the property appears twice in ModelState dictionary,one with the model name(with the data entered) and one without the modelname(with null value)
Any idea why this is happening?
public class BankAccount
{
[DisplayName("")]
public int BankAccountId { get; set; }
[MaxLength(20, ErrorMessage = "")]
[Required(ErrorMessage = "")]
[DisplayName("")]
public string BankName { get; set; }
...
Here' the code OnPost() where the validation fails:
public async Task<IActionResult> OnPostAsync()
{
// TODO: Not ideal! But solves the problem of returning invalid model state.
ModelState.Remove("BankName");
if (!ModelState.IsValid)
{
return RedirectToPage();
}
_context.BankAccounts.Add(BankAccount);
await _context.SaveChangesAsync();
return RedirectToPage();
}
After searching a lot,I found a workaround,which isn't very ideal.That's to remove the additional property that has oddly been inserted in ModelState dictionary. I mean this line:
ModelState.Remove("BankName");
But that's not the right way.I'd like to figure out why it's happening?!
Here are two properties defined on the PageModel:
[BindProperty]
public BankAccount BankAccount { get; set; }
[BindProperty]
public BankAccount BankAccountEdit { get; set; }
One is used to insert new BankAccount and the other one is used to edit existing ones by clicking on a button from the table.
I figured out the the issue.The problem was that I have two properties of the same type(BankAccount class) in my page model,one for inserting new entity and the other one for editing existing entity all on the same page.
So to validate each form separately OnPost(),I used the following code:
public async Task<IActionResult> OnPostAsync()
{
var validateBankAccount = ModelState.GetFieldValidationState("BankAccount");
if (validateBankAccount ==
Microsoft.AspNetCore.Mvc.ModelBinding.ModelValidationState.Invalid)
{
return RedirectToPage();
}
_context.BankAccounts.Add(BankAccount);
await _context.SaveChangesAsync();
return RedirectToPage();
}
Related
I'm using aspnetboilerplate framework in a .Net Core project. I have a custom validation for my view model following on official documentation.
First I have a simple view model with custom validation:
public class EditPropertyViewModel : ICustomValidate
{
public long Id { get; set; }
public long? ParentId { get; set; }
public string Title { get; set; }
public void AddValidationErrors(CustomValidationContext context)
{
if (Id == ParentId)
context.Results.Add(new ValidationResult("Property cannot be parent of itself!", new [] { "ParentId" } ));
}
}
Then my controller is like this:
[HttpPost]
public async Task<IActionResult> Edit(EditPropertyViewModel model)
{
if (ModelState.IsValid)
{
/* Update property here and return */
}
return View(model);
}
But when I run the project, this exception occures:
AbpValidationException: Method arguments are not valid! See ValidationErrors for details.
That means my custom validation has been executed before ModelState.IsValid and there is no chance to handle that exception and show a user friendly message to the user. Disabling validation by [DisableValidation] skips this exception but my validation logic is skipped too. I also tryed to use .NET's standard IValidatableObject interface instead of the abp's ICustomValidate but this not helped me to solve the problem.
I use the built in model validation which works great for specifying required fields and such but on my model I have specified key constraints so the combination of two columns are unique.
How should I validate for this so it doesn't throw exception if user tries to add duplicate?
Here is my model:
public class EmailFilter
{
public int ID { get; set; }
[Required]
[StringLength(100)]
[Index("IX_FilterAndEmail", 2, IsUnique = true)]
public string Email { get; set; }
//This is an enum
[Required]
[Index("IX_FilterAndEmail", 1, IsUnique = true)]
public EmailFilterType Filter { get; set; }
}
And my create controller method. I tried to add an error but I am not doing it right. It stops the exception happening but it just returns to the listview. I'm not sure what the right way to validate here is.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "ID,Email,Filter")] EmailFilter emailFilter)
{
if (ModelState.IsValid)
{
if(db.EmailFilters.Any(x => x.Filter == emailFilter.Filter && x.Email == emailFilter.Email))
{
// This doesn't seem to do anything and returns to listview not create view
// "EFValidationSummary" is the id of my validation summary razor control
ModelState.AddModelError("EFValidationSummary", "This filter already exists");
return View(emailFilter);
}
else
{
db.EmailFilters.Add(emailFilter);
db.SaveChanges();
}
return RedirectToAction("Index");
}
return View(emailFilter);
}
How do I properly trigger an error and send back to the create page with the validation error displayed?
The first parameter of AddModelError() is the name of the property in your model. In your case, the error message would be displayed in the placeholder generated by
#Html.ValidationMessageFor(m => EFValidationSummary)
but your model does not contain a property named EFValidationSummary. In order to display validation messages in the placeholder generated by #Html.ValidationSummary(), you need to provide an empty string for the first parameter
ModelState.AddModelError("", "This filter already exists");
Problem
I have a list of fields that the user can edit. When the model is submitted I want to check if this items are valid. I can't use data notations because each field has a different validation process that I will not know until runtime. If the validation fails I use the ModelState.AddModelError(string key, string error) where the key is the name of the html element you want to add the error message to. Since there are a list of fields the name that Razor generates for the html item is like Fields[0].DisplayName. My question is there a method or a way to get the key of the generated html name from the view model?
Attempted Solution
I tried the toString() method for the key with no luck. I also looked through the HtmlHelper class but I didn't see any helpful methods.
Code Snippet
View Model
public class CreateFieldsModel
{
public TemplateCreateFieldsModel()
{
FreeFields = new List<FieldModel>();
}
[HiddenInput(DisplayValue=false)]
public int ID { get; set; }
public IList<TemplateFieldModel> FreeFields { get; set; }
public class TemplateFieldModel
{
[Display(Name="Dispay Name")]
public string DisplayName { get; set; }
[Required]
[Display(Name="Field")]
public int FieldTypeID { get; set; }
}
}
Controller
public ActionResult CreateFields(CreateFieldsModel model)
{
if (!ModelState.IsValid)
{
//Where do I get the key from the view model?
ModelState.AddModelError(model.FreeFields[0], "Test Error");
return View(model);
}
}
After digging around in the source code I have found the solution. There is a class called ExpressionHelper that is used to generate the html name for the field when EditorFor() is called. The ExpressionHelper class has a method called GetExpressionText() that returns a string that is the name of that html element. Here is how to use it ...
for (int i = 0; i < model.FreeFields.Count(); i++)
{
//Generate the expression for the item
Expression<Func<CreateFieldsModel, string>> expression = x => x.FreeFields[i].Value;
//Get the name of our html input item
string key = ExpressionHelper.GetExpressionText(expression);
//Add an error message to that item
ModelState.AddModelError(key, "Error!");
}
if (!ModelState.IsValid)
{
return View(model);
}
You have to frame the key(name of the input element) inside the controller based upon how you are rendering the fields in the form.
For ex. if the validation of the second item in the FreeFields collection of CreateFieldsModel fails you can frame the name of the input element i.e. key as FreeFields[1].DisplayName where the validation error is going to be mapped.
As far as I know you can't easily get that from controller.
I want to simply validate a single property of that model
public ActionResult Rate([Bind(Exclude="Score")]RatingModel model)
{
if(ModelState.IsValid)
{
//here model is validated without check Score property validations
model.Score = ParseScore( Request.Form("score"));
// Now i have updated Score property manualy and now i want to validate Score property
}
}
after assign Score manually, Mvc framework does not check validation on model. Now i want to validate Score property with all validation attributes which currently exist on model.
// How to do that easily ? Mvc Framework support this scenario ?
Here is my model
public class RatingModel
{
[Range(0,5),Required]
public int Score { get; set; }
}
I have found right solution. I simply call TryValidateModel and it validate properties include Score property.
public ActionResult Rate([Bind(Exclude="Score")]RatingModel model)
{
model.Score = ParseScore( Request.Form("score"));
if(TryValidateModel(model))
{
///validated with all validations
}
}
You're using MVC3. Any particular reason why you aren't setting some of the most basic validation rules in the Model?
You can set some validation rules directly in the model. For example, if you want to validate an email field, you can set the rules and even the error messages in the model itself.
[Required(ErrorMessage = "You must type in something in the field.")]
[RegularExpression(".+\\#.+\\..+", ErrorMessage = "You must type in a valid email address.")]
[Display(Name = "Email:")]
public string Email { get; set; }
Read more here:
http://www.asp.net/mvc/tutorials/validation-with-the-data-annotation-validators-cs
You need to check if the ModelState is valid in the Controller Action:
public ActionResult Action(RatingModel viewModel)
{
if (ModelState.IsValid)
{
//Model is validated
}
else
{
return View(viewModel);
}
}
I'm trying to add a form to allow users to comment on posts on my blogging application. So far, I've added a form to the post details view and I can submit comments, adding them to my database correctly. However, I have a problem with displaying validation errors to the user. The comment form is contained within a partial view and is rendered using Html.RenderAction inside the post details view. I'd like to stress that I don't want to use AJAX for this as I'd like to approach this from a progressive enhancement point-of-view.
Here's the relevant posting action:
[HttpPost, Authorize]
public ActionResult AddComment(CommentViewModel newComment)
{
if (ModelState.IsValid)
{
Comment comment = new Comment(_userRepository.GetByUsername(User.Identity.Name));
Mapper.Map(newComment, comment);
_commentRepository.Add(comment);
_postsRepository.CommentAdded(comment.Article);
return RedirectToAction("Index", new { id = newComment.PostID });
}
// What do I do here?
}
I've tried several ways of returning views here but my issue is further complicated by some controller parameter validation that I have going on in the parent action:
//
// GET: /Posts/5/this-is-a-slug
public ActionResult Index(int id, string slug)
{
PostViewModel viewModel = new PostViewModel();
var model = _postsRepository.GetByID(id);
if (model != null)
{
if (slug == null || slug.CompareTo(model.Slug) != 0)
{
return RedirectToActionPermanent("Index", new { id, slug = model.Slug });
}
else
{
_postsRepository.PostVisited(model);
Mapper.Map(model, viewModel);
viewModel.AuthorName = _userRepository.GetById(model.AuthorID);
}
}
return View(viewModel);
}
This action basically mimics how SO's URLs work. If a post ID is supplied, the post is fetched from the database along with a slug which is created when the post is created. If the slug in the URL doesn't match the one in the database, it redirects to include the slug. This is working nicely but it does mean I'm having issues trying to populate my parent viewmodel, which is the following:
public class PostViewModel
{
public int PostID { get; set; }
public string Title { get; set; }
public string Body { get; set; }
public string Slug { get; set; }
public DateTime DatePublished { get; set; }
public int NumberOfComments { get; set; }
public int AuthorID { get; set; }
public string AuthorName { get; set; }
public List<CommentViewModel> Comments { get; set; }
public CommentViewModel NewComment { get; set; }
}
What I was hoping would work is to populate PostViewModel.NewComment, test to see if it has data and then using it to display any model errors. Unfortunately, I'm lost as to how to accomplish that. This question helped me shape my approach, but it didn't quite answer my problem.
Could someone give me a gentle push in the right direction? If my approach seems unreasonable, I'd love to find out why and what a potential fix would be.
Many thanks in advance.
Forgot to fill in my answer here. For anyone that happens to stumble on this, the answer was to use TempData to store the ModelState errors and then repopulating ModelState in the relevant controller action.
Firstly, I declared a key in the controller which would be used to reference the data inside TempData. I decided to base this on the CommentViewModel type as both actions depend on it.
public class PostsController : Controller
{
private static readonly string commentFormModelStateKey = typeof(CommentViewModel).FullName;
// Rest of class.
}
In this first action, the code checks to see if TempData contains data assigned to the key. If it does, it's copied into ModelState.
// GET: /posts/comment
[ChildActionOnly]
public PartialViewResult Comment(PostViewModel viewModel)
{
viewModel.NewComment = new CommentViewModel(viewModel.PostID, viewModel.Slug);
if (TempData.ContainsKey(commentFormModelStateKey))
{
ModelStateDictionary commentModelState = TempData[commentFormModelStateKey] as ModelStateDictionary;
foreach (KeyValuePair<string, ModelState> valuePair in commentModelState)
ModelState.Add(valuePair.Key, valuePair.Value);
}
return PartialView(viewModel.NewComment);
}
This action determines if the ModelState is valid before adding a comment to the database. If the ModelState is not valid, it is copied to TempData, which makes it available to the first action.
// POST: /posts/comment
[HttpPost, Authorize]
public ActionResult Comment(CommentViewModel newComment)
{
if (!ModelState.IsValid)
{
TempData.Add(commentFormModelStateKey, ModelState);
return Redirect(Url.ShowPost(newComment.PostID, newComment.Slug));
}
// Code to add a comment goes here.
}