So, when putting in text for my model it is always valid, even though I explicitly asked for it to have a minLength despite it being empty or being less than the minLength.
Models:
public class CommentaarCreate_VM
{
public Stad Stad { get; set; }
[Required]
public Commentaar Commentaar { get; set; }
}
public class Commentaar
{
[Key]
public int CommentaarId { get; set; }
[Required]
public string UserId { get; set; }
[Required]
public int StadId { get; set; }
[Required(AllowEmptyStrings=false, ErrorMessage="You need to enter a comment of valid length")]
[MinLength(5, ErrorMessage ="You need to enter a comment of valid length")]
public string CommentaarText { get; set; }
[Required]
[DataType(DataType.DateTime)]
public DateTime Tijdstip { get; set; }
}
View:
#model DataGent.Web.ViewModels.CommentaarCreate_VM
#{
ViewData["Title"] = "Create new comment";
}
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="All" class="text-danger"></div>
<input type="hidden" asp-for="Stad.Id" />
<input type="hidden" asp-for="Stad.Naam" />
<input type="hidden" value="#Html.AntiForgeryToken()" />
<div class="form-group">
<label asp-for="Commentaar" class="control-label"></label>
<input asp-for="Commentaar" class="form-control" />
<span asp-validation-for="Commentaar.CommentaarText" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save" class="btn btn-default" />
</div>
</form>
</div>
Controller action:
public ActionResult Create(int id)
{
CommentaarCreate_VM vm = new CommentaarCreate_VM()
{
Stad = _dataGentService.GetStadFromId(id),
Commentaar = null
};
return View(vm);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind("CommentaarText, Tijdstip")] int id, IFormCollection collection) //Bind = protect from overposting
{
try
{
// Creating object to POST
Commentaar commentaar = new Commentaar
{
UserId = _userManager.GetUserId(HttpContext.User),
StadId = id,
CommentaarText = collection["Commentaar"],
Tijdstip = DateTime.Now
};
var result = _dataGentService.PostCommentaar(commentaar);
return RedirectToAction(nameof(Index));
}
catch
{
return View();
}
}
Is there something I'm missing? I thought all the work, except for the dataannotations, was done by MVC?
Your input is:
<input asp-for="Commentaar" class="form-control" />
You have to change asp-for from Commentaar to Commentaar.CommentaarText so that it is validated:
<div class="form-group">
<label asp-for="Commentaar.CommentaarText" class="control-label"></label>
<input asp-for="Commentaar.CommentaarText" class="form-control" />
<span asp-validation-for="Commentaar.CommentaarText" class="text-danger"></span>
</div>
Update:
Initialize Commentaar object in your viewmodel before you pass it to the view:
public ActionResult Create(int id)
{
CommentaarCreate_VM vm = new CommentaarCreate_VM()
{
Stad = _dataGentService.GetStadFromId(id),
Commentaar = new Commentaar()
};
return View(vm);
}
A good practice is to use ModelState.IsValid on your post methods, in order to check properties of the model that is being sent. Said that, ModelState.IsValid checks for Data Annotations written by you on your Model.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind("CommentaarText, Tijdstip")] int id, IFormCollection collection) //Bind = protect from overposting
{
if(ModelState.IsValid)
{
//If it is valid, do all your business logic, like creating a new entry.
}
else
{
//Handle it
return View();
}
}
Another thing is that I see that you use ViewModels which is good. So you could just send your viewmodel as a parameter for your action. You could do that as follows:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create(CommentaarCreate_VM viewmodel)
{
if(ModelState.IsValid)
{
//It is valid
//All your logic
}
else
{
//Not valid
return View(Viewmodel model)
}
}
By doing this, you have to add data annotations to CommentaarCreate_VM
public class CommentaarCreate_VM
{
public Stad Stad { get; set; }
[Required(AllowEmptyStrings=false, ErrorMessage="You need to enter a comment of valid length")]
[MinLength(5, ErrorMessage ="You need to enter a comment of valid length")]
public Commentaar Commentaar { get; set; }
}
So I've found atleast somewhat of a solution, but the underlying problem still stands.
The problem is that in the controller the Modelstate.IsValid is always true, even if some of the models should not be valid so it just does what I want it to before redirecting to another page.
Solution is that I can get the error messages working if in the controller I check if the string is null or empty, and if so just Return(viewmodel), and that gets the error messages working.
Obviously, the Modelstate.IsValid SHOULDNT be returning true, and I still don't know why it does.
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind("CommentaarText, Tijdstip")] int id, CommentaarCreate_VM viewModel, IFormCollection collection) //Bind = protect from overposting
{
try
{
//If incoming string is null or empty
if (string.IsNullOrEmpty(collection["Commentaar"]))
{
return View(viewModel);
}
//This always returns true. It really shouldn't, because otherwise I wouldn't need that earlier check.
//If the model isn't valid in the View, this one should be false, right?
if (ModelState.IsValid)
{
// Creating object to POST
//.....
return RedirectToAction(nameof(Index));
}
return View();
}
catch
{
return View();
}
}
Related
I did it according to youtube tutorial, but unfortunately for me it behaves extremely oddly.
Simple scenario: add comment to a post.
public class CommentViewModel
{
public Post Post { get; set; }
public Comment Comment { get; set; }
}
<p>#Model.Post.Title</p>
<p>#Model.Post.Body</p>
<form method="post" asp-action="NewComment">
<input asp-for="Post.Id" hidden />
<div class="border p-3">
#*<div asp-validation-summary="ModelOnly" class="text-danger"></div>*#
<div class="form-group row">
<h2 class="text-info pl-3">Write new comment</h2>
</div>
<div class="form-group row">
<label asp-for="Comment.Body"></label>
<textarea asp-for="Comment.Body" class="form-control"></textarea>
<span asp-validation-for="Comment.Body" class="text-danger"></span>
</div>
</div>
<div class="form-group row">
<div class="col-8 offset-2 row">
<div class="col">
<input type="submit" class="btn btn-info w-100" value="Create" />
</div>
<div class="col">
<a asp-action="Index" class="btn btn-success w-100"><i class="fas fa-sign-out-alt"></i> Back</a>
</div>
</div>
</div>
</form>
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult NewComment(CommentViewModel model)
{
if (ModelState.IsValid)
{
_service.AddNewComment(new Guid(), model.Post.Id, model.Comment.Body);
return RedirectToAction("Index");
}
return View();
}
Models:
public class Post
{
[BsonElement("id")]
public Guid Id { get; set; }
[BsonElement("title")]
[Required]
[MaxLength(64)]
public string Title { get; set; }
[BsonElement("post_body")]
[Required]
[Display(Name = "Post")]
[MaxLength(256)]
public string Body { get; set; }
}
public class Comment
{
[BsonElement("id")]
public Guid Id { get; set; }
[BsonElement("post_id")]
public Guid PostId { get; set; }
[BsonElement("comment_body")]
[Required]
[Display(Name = "Comment")]
[MaxLength(128)]
public string Body { get; set; }
}
When fields are not filled red notification appears, as it should. But once fields are filled and user clicks on create, ModelState.IsValid is still false, and for some reason application tries to reload view, but returns exception on <p>#Model.Post.Title</p> NullReferenceException.
It's extremely weird behavior. Adding new post is almost identical except <input asp-for="Post.Id" hidden /> (since there are no relations to anything else), and it works flawlessly. Here things are glitching out.
Removing if (ModelState.IsValid) and return View(); absolutely fixes the issue. Both validation and POST works. But it should work even with it.
Any clues? All laws of logic say it should work. Otherwise I will be forced to keep it the weird way.
I don't see any weird here. It works according to your code. But you can fix a bug:
if (ModelState.IsValid)
{
_service.AddNewComment(new Guid(), model.Post.Id, model.Comment.Body);
return RedirectToAction("Index");
}
return View(model);
in this case you will not have a null reference exeption.
and by the way I am using this code to find what is invalid in a ModelState:
public static string ValidModelState(ModelStateDictionary modelState)
{
var errorMessage = "";
if (!modelState.IsValid)
{
foreach (var item in modelState.Values)
{
foreach (var modelError in item.Errors)
{
errorMessage += "\n" + "Error: " + modelError.ErrorMessage;
}
}
}
return errorMessage;
}
And using this instead of if (ModelState.IsValid):
var errorMessage = ValidModelState(ModelState);
if( !string.IsNullOrEmpty(errorMessage)).... errorMessage;
It says The Post field is required., except that property does not exist.
You have a [Display(Name = "Post")] on the Post.Body property, And I think the model in the view doesn't have value for Post Body, so the validation failed, and the error message for it is The Post field is required. Go and check it.
I am developing a web app with ASP.NET Core MVC, and I have a problem with model validation.
When I set validation in class and then use it in view model, validation does not work. How can I struggle with it?
This is my code:
public class Il : IEntity
{
[Required(ErrorMessage = "Kodu boş geçilemez")]
public int IlKodu { get; set; }
public int UlkeId { get; set; }
[Required(ErrorMessage = "Ad boş geçilemez")]
public string Ad { get; set; }
public int Id { get; set; }
public DateTime? KayitTarihi { get; set; }
public DateTime? GuncellemeTarihi { get; set; }
}
ViewModel class;
public class IlAddViewModel
{
public Il Il { get; set; }
public List<Ulke> Ulkeler{ get; set; }
}
Then the view:
<div class="container pt-4 eklemeDiv col-4">
<form asp-controller="Il" asp-action="Add" asp-area="Admin" method="post">
<div class="form-group">
<label asp-for="Il.IlKodu">Il Kodu</label>
<input asp-for="Il.IlKodu" class="form-control" placeholder="İl Kodu Giriniz">
<span asp-validation-for="Il.IlKodu" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Il.Ad">Il Kodu</label>
<input asp-for="Il.Ad" class="form-control" placeholder="İl Kodu Giriniz">
<span asp-validation-for="Il.Ad" class="text-danger"></span>
</div>
<div class="form-group">
<label >Ülke</label>
<select style="width: 100%;height:30px" id="selectIl" asp-for="Il.UlkeId"
asp-items="#(new SelectList(Model.Ulkeler,"Id","Ad"))">
<option>Lütfen Seçim Yapınız</option>
</select>
</div>
<input id="btnIlEkle" type="submit" value="Ekle" class="btn btn-xs btn-success" />
<a class="btn btn-xs btn-primary" asp-action="Anasayfa" asp-area="Admin" asp-controller="Admin"><i class="fas fa-chevron-left"></i>Anasayfaya Dön</a>
</form>
</div>
The view model does not show validation messages in view. And also how can I show validation messages for list elements?
My friends, all parts of my code are correct for structure, just one error is missing and finally i found it today. I am sharing this solution to help others;
Also controller code;
[HttpPost]
public async Task<ActionResult> Add(IlAddViewModel ilAddViewModel)
{
var kayitVarmi = _ilService.BenzerKayitVarMi(ilAddViewModel.Il.IlKodu, ilAddViewModel.Il.Ad);
if (kayitVarmi)
{
TempData.Add("Hata", "Böyle bir kayıt mevcut");
return RedirectToAction("Add");
}
else
{
if (ModelState.IsValid && !kayitVarmi)
{
ilAddViewModel.Il.KayitTarihi = DateTime.Now;
await _ilService.Add(ilAddViewModel.Il);
TempData.Add("Message", String.Format("{0} başarıyla eklendi", ilAddViewModel.Il.Ad));
return RedirectToAction("Anasayfa", "Admin", new { Area = "Admin" });
}
}
return RedirectToAction("Add");
}
To achieve this validation error, you must add necessary javascript packages as below;
<script src="~.../jquery-validation/dist/jquery.validate.js"></script>
<script src="~.../jquery-validation-unobtrusive/dist/jquery.validate.unobtrusive.js"></script>
These javascript packages help to show validation messages without making post to action method.
In the controller you will have access to check the model state to verify if it is valid or not. The controller code should like this
public class HomeController : Controller
{
// GET: Home
public ActionResult Index()
{
return View();
}
[HttpGet]
public ActionResult Create()
{
return View();
}
[HttpPost]
public ActionResult Create(Employee employee)
{
try
{
//Here ModelState.IsValid will be true in case all the required fields that are mentioned in employee class
if (ModelState.IsValid)
{
return RedirectToAction("Index");
}
return View();
}
catch (Exception)
{
return View();
}
}
}
I've successfully created a view to show some records from votings. But on that same view i try to call another method on my controller to export those records and it only receives 0 as a parameter.
This is my model.
public class Vote
{
public int Id { get; set; }
public int CPF { get; set; }
public virtual Candidate Candidate { get; set; }
public int CandidateID { get; set; }
public DateTime Moment { get; set; }
}
This is my Index.
public async Task<IActionResult> Index()
{
var context = _context.Vote.Include(v => v.Candidate).Include(v => v.Candidate.Election);
//This is going to be on the view for the user to select the year to export data
ViewData["Election"] = new SelectList(_context.Election, "Id", "Year");
return View(await context.ToListAsync());
}
That's the action I'm tryig to call.
[HttpPost]
public IActionResult Export(int electionYear)
This is the form I'm using to POST to the controller.
#model IEnumerable<Models.Vote>
...
//Here i show the votes and then below i show this form with the Elections
<form asp-action="Export">
<div class="form-group">
<select class="form-control" asp-items="ViewBag.Election"></select>
</div>
<div class="form-group">
<input type="submit" value="Exportar" class="btn btn-default" />
</div>
</form>
I tried it with the [FormBody] and it gives me 415 ERROR
The Export endpoint is expecting a form value called electionYear. But your <select> doesn't have that name (or any name). Try:
<select name="electionYear" class="form-control" asp-items="ViewBag.Election"></select>
So I have these 2 classes representing tables in a local db using ef core migration in an asp.net core app
public class Teacher
{
public int ID { get; set; }
public string FistName { get; set; }
public string LastName { get; set; }
public Class Class { get; set; }
public string FullName
{
get
{
return $"{LastName} {FistName}";
}
}
public override string ToString()
{
return FullName;
}
}
and
public class Class
{
public int ID { get; set; }
public int TeacherID { get; set; }
public string Name { get; set; }
public Teacher Teacher { get; set; }
public ICollection<Student> Students { get; set; }
}
I want to add in my classes controller the functionality to create a new instance of the "Class" class by typing out a name in a textbox and selecting a teacher from a dropdownlist.
Here are my create http get & post methods
public IActionResult Create()
{
var teachers = from s in _context.Teachers
where s.Class == null
select s;
ViewData["Teachers"] = new SelectList(teachers.ToList());
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Class #class)
{
if (ModelState.IsValid)
{
#class.TeacherID = #class.Teacher.ID;
_context.Add(#class);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View("Index");
}
and the form for submitting user-typed data
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Teacher" class="control-label"></label>
<select asp-for="Teacher" class ="form-control" asp-items="ViewBag.Teachers"></select>
</div>
<div class="form-group">
<label asp-for="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
</form>
I can see the right data in the dropdown list but for some reason inside the POST method #class.Teacher is null. What am I doing wrong?
First update your Create GET method as follows:
public async Task<IActionResult> Create()
{
var teachers = await _context.Teachers.Where(t => t.Class == null).ToListAsync();
ViewData["Teachers"] = new SelectList(teachers,"ID","FullName");
return View();
}
Then write your FormGroup for select list as follows:
<div class="form-group">
<label asp-for="TeacherID" class="control-label">Teacher</label>
<select asp-for="TeacherID" class ="form-control" asp-items="ViewBag.Teachers">
</select>
<span asp-validation-for="TeacherID" class="text-danger"></span>
</div>
Now update your Create POST method as follows:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(Class #class)
{
if (ModelState.IsValid)
{
_context.Classes.Add(#class); // now your #class is containing the `TeacheID` value selected from drop-down.
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View("Index");
}
Now everything should work fine!
Make the following changes
Create http get method , view SelectList Class , set ID to the dataValueFieldof the selectList and set FullNameto the dataTextFieldof selectList
public IActionResult Create()
{
var teachers = from s in _context.Teacher
where s.Class == null
select s;
ViewData["Teachers"] = new SelectList(teachers.ToList(),"ID","FullName");
return View();
}
The select tag in the form
<select asp-for="TeacherID" class="form-control" asp-items="ViewBag.Teachers"></select>
A select list will only return a primitive type (string, int, etc.), not an entire class instance. As a result, you'll need to bind the value to a property that is also such a primitive type. Since you already have a TeacherID property, you can just use that. When you save, EF will fixup the Teacher reference with the corresponding id.
<select asp-for="TeacherID" class ="form-control" asp-items="ViewBag.Teachers"></select>
"Teacher" is a complex class, that will always be null, I think you're getting confused, replace "Teacher" by "teacherID". EF will do the job.
I have got some sign up form that has 2 otpions
Personal
Company
Some of the fields for them are the same like Password, Username, Email and other are different.
My question is which strategy of the MODEL we should implement in that case?
Have we i.e. use 2 "TABs" and 1 button "SUBMIT" but in that case we have duplicated UI fields... And I don't understand how the class MODEL should be in that case and how we have to validate it...
Or...
We need to use 2 buttons "SUBMIT" and use somehow 2 MODELS ....
I know we can use if (code is following) but which MODEL we have need for it?
<html>
...
<body>
<div>
<form action="/SignUp/Personal" method="post">
<input type="text" name="username" value="" />
<input type="text" name="passowrd" value="" />
<input type="submit" name="signup" value="SUBMIT" />
</form>
<form action="/SignUp/Company" method="post">
<input type="text" name="username" value="" />
<input type="text" name="passowrd" value="" />
<input type="submit" name="signup" value="SUBMIT" />
</form>
</div>
</body>
</html>
Well I don't know which approach we can use...
Any clue?
Thank you!!!
There is several approaches, but exists approach which which allow you don't duplicate UI fields and have single submit button, you can divide your model validation depending on selected user AccountType, custom ActionMethodSelectorAttribute help you to divide methods depending on user account type. Model will be automatically validated in appropriative action.
Here is sample implementation:
controller:
public class HomeController : Controller
{
public ActionResult Index()
{
return View(new SignUpModel
{
Common = new Common(),
Personal = new Personal(),
Company = new Company()
});
}
[HttpPost]
[SignUpSelector]
[ActionName("Index")]
public ActionResult PersonalRegistration(Personal personal, Common common)
{
if (ModelState.IsValid)
{
//your code
}
return View("Index", new SignUpModel()
{
Common = common,
Personal = personal,
Company = new Company()
});
}
[HttpPost]
[SignUpSelector]
[ActionName("Index")]
public ActionResult CompanyRegistration(Company company, Common common)
{
if(ModelState.IsValid)
{
//your code
}
return View("Index", new SignUpModel()
{
Common = common,
Personal = new Personal(),
Company = company
});
}
}
model:
public class SignUpModel
{
public string AccountType { get; set; }
public Common Common { get; set; }
public Company Company { get; set; }
public Personal Personal { get; set; }
}
public class Company
{
[Required]
public string CompanyName { get; set; }
public string Address { get; set; }
}
public class Personal
{
[Required]
public string FirstName { get; set; }
public int Age { get; set; }
}
public class Common
{
[Required]
public string UserName { get; set; }
[Required]
public string Passwrod { get; set; }
}
custom ActionMethodSelectorAttribute:
public class SignUpSelector : ActionMethodSelectorAttribute
{
public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)
{
return (controllerContext.HttpContext.Request.Form["AccountType"] + "Registration" == methodInfo.Name);
}
}
view:
#model MvcModelValidationTest.Controllers.SignUpModel
#{
ViewBag.Title = "Home Page";
}
#using(Html.BeginForm())
{
#Html.Display("Personal")
#Html.RadioButtonFor(x=>x.AccountType, "Personal",new { #checked = "checked" })<br/>
#Html.Display("Company")
#Html.RadioButtonFor(x=>x.AccountType, "Company")<br/>
#Html.TextBoxFor(x=>x.Common.UserName)<br/>
#Html.PasswordFor(x=>x.Common.Passwrod)<br/>
#Html.TextBoxFor(x=>x.Company.CompanyName)<br/>
#Html.TextBoxFor(x=>x.Company.Address)<br/>
#Html.TextBoxFor(x=>x.Personal.FirstName)<br/>
#Html.TextBoxFor(x=>x.Personal.Age)<br/>
<input type="submit"/>
}