I am building a to-do-list project as a practice. It has one relationship to the Member model and the Member model has many relationships to 'to-do-list'
Member controller create method works without any issue but the to-do-list controller throws model state is invalid on Member object property of to-do-list
ToDoList
using System.ComponentModel.DataAnnotations;
namespace To_Do_List.Models
{
public class ToDoList
{
[Key]
public int Id { get; set; }
[Required]
[StringLength(200, MinimumLength = 1, ErrorMessage = "To Do List Item cannot be longer than 200 characters.")]
public string Title { get; set; }
public string Description { get; set; }
[DataType(DataType.Date)]
public DateTime DueDate { get; set; }
public string Priority { get; set; }
public int AssignToId { get; set; }
public Member AssignTo { get; set; }
[Required]
[StringLength(15, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.")]
[RegularExpression("^(Completed|Not Completed)$", ErrorMessage = "The status must be Completed or Not Completed")]
public string Status { get; set; }
}
}
Member
using Microsoft.Build.Framework;
namespace To_Do_List.Models
{
public class Member
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
[Required]
public ICollection<ToDoList> ToDoLists { get; set; }
}
}
create method of to do list controller
public async Task<IActionResult> Create([Bind("Id,Title,Description,DueDate,Priority,AssignToId,AssignTo, Status")] ToDoList toDoList)
{
if (ModelState.IsValid)
{
_context.Add(toDoList);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
ViewData["AssignToId"] = new SelectList(_context.Members, "Id", "Id", toDoList.AssignToId);
return View(toDoList);
}
View method of to do list
public IActionResult Create()
{
return View();
}
Create.cshtml
#model To_Do_List.Models.ToDoList
#{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>ToDoList</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Description" class="control-label"></label>
<input asp-for="Description" class="form-control" />
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="DueDate" class="control-label"></label>
<input asp-for="DueDate" class="form-control" />
<span asp-validation-for="DueDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Priority" class="control-label"></label>
<input asp-for="Priority" class="form-control" />
<span asp-validation-for="Priority" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Status" class="control-label"></label>
<input asp-for="Status" class="form-control" />
<span asp-validation-for="Status" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Member controller create method works without any issue but the
to-do-list controller throws model state is invalid on Member object
property of to-do-list
Well, I have succssfully reproducced your issue and your ModelState.IsValid false is logical. Because, when you leave public Member AssignTo { get; set; } with default annotation it means required. whilist, you haven't pass any property from your view consequently, your bindings always be false as you have defined it into the [Bind] property that is AssignTo However, value has not been pass to it.
How to resolve:
In this scenario, you either has to pass AssignTo to your create action or make it nullable using ? annotation as following:
public Member? AssignTo { get; set; }
Note: If you don't want to set AssignTo as nullable then you have to pass all property value from your view as following:
<div class="form-group">
<label asp-for="AssignTo.Name" class="control-label"></label>
<input asp-for="AssignTo.Name" class="form-control" />
<span asp-validation-for="AssignTo.Name" class="text-danger"></span>
</div>
Here, I am passing only AssignTo.Name you have to pass rest of the values.
Output:
Note: If you would like to know more details on it you could check our official document here.
Related
Controller
public async Task<IActionResult> Create(IFormFile? StaffPhoto, CollectionViewModel collectionModel)
{
if (StaffPhoto != null){...} // issue is StaffPhoto value is null
}
View Model
namespace Website.Models
{
public class CollectionViewModel
{
public Staff staff { get; set; }
public Contact contact { get; set; }
}
}
Entity Model
public class Staff
{
public int StaffId { get; set; }
[DisplayName("First Name")]
[Required]
public string StaffFirstName { get; set; }
[DisplayName("Last Name")]
[Required]
public string StaffLastName { get; set; }
[DisplayName("Photo")]
public string? StaffPhoto { get; set; }
}
View
#model CollectionViewModel
<form asp-action="Create" enctype="multipart/form-data" method="post" class="row g-3 mt-0">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="col">
<label asp-for="staff.StaffFirstName" class="form-label"></label>
<input asp-for="staff.StaffFirstName" class="form-control" />
<span asp-validation-for="staff.StaffFirstName" class="text-danger"></span>
</div>
<div class="col">
<label asp-for="staff.StaffLastName" class="form-label"></label>
<input asp-for="staff.StaffLastName" class="form-control" />
<span asp-validation-for="staff.StaffLastName" class="text-danger"></span>
</div>
<div class="col-md-3">
<label asp-for="staff.StaffPhoto" class="form-label"></label>
<input asp-for="staff.StaffPhoto" type="file" accept="image/*" class="form-control" />
<span asp-validation-for="staff.StaffPhoto" class="text-danger"></span>
#{if (ViewBag.fileUploadErrorMessage != null)
{
<span class="text-danger">#ViewBag.fileUploadErrorMessage</span>
}
}
</div>
<div class="col">
<input type="submit" value="Create" class="btn btn-primary" />
<a asp-action="Create" class="btn btn-secondary">Reset All</a>
</div>
</form>
You should add IFormFile in model.
public class CollectionViewModel
{
public Staff staff { get; set; }
public IFormFile StaffPhoto { get; set; }
public Contact contact { get; set; }
}
set StaffPhoto to asp-for in view.
<input asp-for="StaffPhoto" type="file" accept="image/*" class="form-control" />
I have a nested model and I use one model named (BootstrapTool) to make the view strongly typed.
I use ViewBag to populate a dropdown list with the nested Model named (BootstrapCategory). But my problem is when I select from it always returns validation null exception.
BootstrapTool Model
public class BootstrapTool
{
public Guid Id { get; set; }
[MaxLength(100,ErrorMessage ="Max 100")]
[Required]
public string tool { get; set; }
[MaxLength(300, ErrorMessage = "Max 300")]
[Required]
public string descreption { get; set; }
[Required]
public string html { get; set; }
[Required] // here where nisting the other Model BootstrapCategory
public BootstrapCategory category { get; set; }
}
BootstrapCategory Model
public class BootstrapCategory
{
[Key]
public Guid Id { get; set; }
[MaxLength(20)]
[Required]
public string Category { get; set; }
}
The Controller
public IActionResult Create()
{
List<BootstrapCategory> bootstrapCategories = _context.bootstrapCategories.ToList();
ViewBag.bpCategories = new SelectList(bootstrapCategories, "Id", "Category");
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create(BootstrapTool bootstrapTool)
{
if (ModelState.IsValid)
{
bootstrapTool.Id = Guid.NewGuid();
_context.Add(bootstrapTool);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(bootstrapTool);
}
Here where the dropdown list is to display BootstrapCategory
The View
#model BootstrapTool
<div class="row">
<div class="col-10 offset-1">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="gap-2 d-flex justify-content-between">
<div class="form-group w-50">
<label asp-for="tool" class="control-label"></label>
<input asp-for="tool" class="form-control" />
<span asp-validation-for="tool" class="text-danger"></span>
</div>
<div class="form-group w-50">
<label class="control-label">category</label>
<select asp-for="category.Id" class="form-control" asp-items="#ViewBag.bpCategories">
<option value="" disabled hidden selected>Select Section ...</option>
</select>
<span asp-validation-for="category" class="text-danger"></span>
#*<select asp-for="Id" asp-items="#Html.GetEnumSelectList<Enum>()" class="form-control">
<option value="" hidden disabled selected> -- please selecgt -- </option>
</select>*#
</div>
</div>
<div class="form-group">
<label asp-for="descreption" class="control-label"></label>
<textarea asp-for="descreption" class="form-control" rows="2"></textarea>
<span asp-validation-for="descreption" class="text-danger"></span>
</div>
<div class="form-group mb-2">
<label asp-for="html" class="control-label"></label>
<textarea asp-for="html" class="form-control" rows="5"></textarea>
<span asp-validation-for="html" class="text-danger"></span>
</div>
<div class="form-group mt-3">
<input type="submit" value="Create" class="btn btn-primary d-grid col-6 mx-auto" />
</div>
</form>
</div>
</div>
Add CategoryId
public class BootstrapTool
{
[Key]
public Guid Id { get; set; }
[Required]
public Guid? CategoryId { get; set; }
[ForeignKey(nameof(CategoryId))]
[InverseProperty("BootstrapTools")]
public BootstrapCategory Category { get; set; }
}
public class BootstrapCategory
{
[Key]
public Guid Id { get; set; }
[MaxLength(20)]
[Required]
public string Category { get; set; }
[InverseProperty(nameof(BootstrapTool.Category))]
public virtual ICollection<BootstrapTool> BootstrapTools { get; set; }
}
and fix the view
<select asp-for="CategoryId" class="form-control" asp-items="#ViewBag.bpCategories">
</select>
```
When in view mode, I try to fill in and get the date, it always turns out to be empty, but for example, the string field is filled in. And if I don't use the ViewModel, but only the Booking model, the date is filled in. I would like the date to be filled in with the viewmodel as well...
Controller:
public async Task<IActionResult> CheckOut3(CheckoutModel broom)
{
if (ModelState.IsValid)
{
_context.Add(broom);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(broom);
}
Model:
public class Booking
{
public int Id { get; set; }
public Room Room { get; set; }
public string LastName { get; set; }
public string FirstName { get; set; }
[DisplayFormat(ApplyFormatInEditMode =true, DataFormatString = "{0:yyyy-MM-dd}")]
public DateTime? CheckInDate{ get; set; }
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyyy-MM-dd}")]
public DateTime? CheckOutDate { get; set; }
}
ViewModel:
public class CheckoutModel
{
public Booking booking {get; set;}
public Room room { get; set; }
}
View:
<form asp-action="Checkout3">
<div class="row">
<!--Grid column-->
<div class="col-lg-6 col-md-12 mb-4">
<label asp-for="booking.CheckInDate">CheckIn</label>
<input asp-for="booking.CheckInDate" class="form-control" type="datetime-local" name="CheckInDate" />
<span asp-validation-for="booking.CheckInDate" class="text-danger"></span>
</div>
<!--Grid column-->
<div class="col-lg-6 col-md-6 mb-4">
<label asp-for="booking.CheckOutDate">CheckOut</label>
<input asp-for="booking.CheckOutDate" class="form-control" type="date" name="CheckOutDate" />
<span asp-validation-for="booking.CheckOutDate" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<input type="submit" value="Checkout" class="btn btn-primary" />
</div>
</form>
The problem is, you're posting CheckInDate and CheckOutDate in the html form, while it should be booking.CheckInDate and booking.CheckOutDate. Remove the name in your view, so name will be set with the value provided in asp-for.
<!--Grid column-->
<div class="col-lg-6 col-md-12 mb-4">
<label asp-for="booking.CheckInDate">CheckIn</label>
<input asp-for="booking.CheckInDate" class="form-control" type="datetime-local" />
<span asp-validation-for="booking.CheckInDate" class="text-danger"></span>
</div>
<!--Grid column-->
<div class="col-lg-6 col-md-6 mb-4">
<label asp-for="booking.CheckOutDate">CheckOut</label>
<input asp-for="booking.CheckOutDate" class="form-control" type="date" />
<span asp-validation-for="booking.CheckOutDate" class="text-danger"></span>
</div>
I'm trying to get the result of my enum for a location as a string but for some reason, it's only showing it as an integer in the index page.
Model
public class UserLicense
{
[Key]
public string Location { get; set; }
}
Enum Class
public enum LocationType
{
Brazil,
USA,
UK
//etc .. here i will add around another 30 items
}
Controller
public async Task<IActionResult> Create(IFormCollection col)
{
if (!User.Identity.IsAuthenticated) RedirectToAction("Index", "Home");
UserLicense uc = new UserLicense();
if (ModelState.IsValid)
{
try
{
DateTime? ends = null;
if (DateTime.TryParse(col["ends"], out DateTime tmp)) ends = tmp;
string username = col["user.username"].ToString().Normalize();
var user = _context.Users.FirstOrDefault(x => x.NormalizedUsername == username);
uc = new UserLicense
{
Ends = ends,
Starts = DateTime.Parse(col["starts"]),
UserId = user.Id,
LicenseId = int.Parse(col["licenseid"]),
Location = Enum.GetName(typeof(LocationType),col["Location"]), //the issue is here.
};
_context.Add(uc);
await _context.SaveChangesAsync();
Create HTML
<div class="form-group">
<label asp-for="Location" class="control-label"></label>
<select asp-for="Location" class="form-control" asp-items="#Html.GetEnumSelectList<LocationType>()"></select>
<span asp-validation-for="Location" class="text-danger"></span>
</div>
Since you are using IFormCollection to transfer the parameters to the controller, not matter using public string Location { get; set; } or public string Location { get; set; }, in the controller the Location value in the IFormCollection will be always an integer. So, you could based on the int value to get string value from the Enum:
var location = ((LocationType)int.Parse(col["location"])).ToString();
The result like this:
Besides, there has another solution, you could use the Strongly typed data (use UserLicense model in the action method, instead of IFormCollection) to transfer the parameter to the controller.
Code like this:
public class UserLicense
{
[Key]
public int License { get; set; }
public DateTime Ends { get; set; }
public DateTime Starts { get; set; }
public int UserId { get; set; }
public LocationType Location { get; set; }
}
public enum LocationType
{
Brazil,
USA,
UK
}
and
public IActionResult Create()
{
return View();
}
[HttpPost]
public IActionResult Create(UserLicense userLicense)
{
if (ModelState.IsValid)
{
var location = userLicense.Location;
return RedirectToAction(nameof(Index));
}
return View();
}
Code in the Create.cshtml:
#model WebApplication1.Models.UserLicense
#{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<h1>Create</h1>
<h4>UserLicense</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="License" class="control-label"></label>
<input asp-for="License" class="form-control" />
<span asp-validation-for="License" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Ends" class="control-label"></label>
<input asp-for="Ends" class="form-control" />
<span asp-validation-for="Ends" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Starts" class="control-label"></label>
<input asp-for="Starts" class="form-control" />
<span asp-validation-for="Starts" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="UserId" class="control-label"></label>
<input asp-for="UserId" class="form-control" />
<span asp-validation-for="UserId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Location" class="control-label"></label>
<select asp-for="Location" class="form-control" asp-items="#Html.GetEnumSelectList<LocationType>()"></select>
<span asp-validation-for="Location" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
The screenshot as below:
Make your Location property a type of LocationType instead of string.
public class UserLicense
{
[Key]
public LocationType Location { get; set; }
}
Pass Location to the view from the Create action like this
uc = new UserLicense
{
Ends = ends,
Starts = DateTime.Parse(col["starts"]),
UserId = user.Id,
LicenseId = int.Parse(col["licenseid"]),
Location = uc.Location
};
Then in the view
<select asp-for="Location" class="form-control" asp-items="#Html.GetEnumSelectList<LocationType>()">
<option selected="selected" value="">Please Select</option>
</select>
I'm following this tutorial: https://learn.microsoft.com/en-us/aspnet/core/tutorials/razor-pages/validation?view=aspnetcore-2.2
Here's my movie model class:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public class Movie
{
public int ID { get; set; }
[StringLength(60, MinimumLength = 3)]
[Required]
public string Title { get; set; }
[Display(Name = "Release Date")]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[Range(1, 100)]
[DataType(DataType.Currency)]
[Column(TypeName = "decimal(18, 2)")]
public decimal Price { get; set; }
[RegularExpression(#"^[A-Z]+[a-zA-Z""'\s-]*$")]
[Required]
[StringLength(30)]
public string Genre { get; set; }
}
But when I test it on my local, these two fields: Title and Genre don't seem to have the validation that I was expecting, see screenshot below:
Here's my controller form of actions:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using RazorPagesMovie.Models;
namespace RazorPagesMovie.Pages.Movies
{
public class CreateModel : PageModel
{
private readonly RazorPagesMovie.Models.RazorPagesMovieContext _context;
public CreateModel(RazorPagesMovie.Models.RazorPagesMovieContext context)
{
_context = context;
}
public IActionResult OnGet()
{
return Page();
}
[BindProperty]
public Movie Movie { get; set; }
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Movie.Add(Movie);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
Here's my Create.cshtml:
#page
#model RazorPagesMovie.Pages.Movies.CreateModel
#{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Movie</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form method="post">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Movie.Title" class="control-label"></label>
<input asp-for="Movie.Title" class="form-control" />
<span asp-validation-for="Movie.Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.ReleaseDate" class="control-label"></label>
<input asp-for="Movie.ReleaseDate" class="form-control" />
<span asp-validation-for="Movie.ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Genre" class="control-label"></label>
<input asp-for="Movie.Genre" class="form-control" />
<span asp-validation-for="Movie.Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Movie.Price" class="control-label"></label>
<input asp-for="Movie.Price" class="form-control" />
<span asp-validation-for="Movie.Price" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Here's my folder structure:
At first I thought there might be a sequence of validation, but actually after I filled out Release Date and Price, I was able to create a movie even without filling in Title and Genre. So apparently the validation for these two fields didn't work.
Could anyone share any insight please?
Thanks!
your required field is never null it is empty string and you must prevent your field from being passed by empty strings like this :
[StringLength(60, MinimumLength = 3)]
[Required(AllowEmptyStrings =false)]
public string Title { get; set; }
For required field validation decorate your function with [BindRequired(ErrorMessage = "Title Required")] instead of using [Required(ErrorMessage="Title Required")]