How to enable validation on MVC form? - c#

I am building a website with MVC and I have a view with a form. I want validation to appear when users leave a field blank, however I can not get this to work. I have tried adding the Data annotation tags in the model, the asp-validation-for span, and checking the ModelState in the post action.
Here is my Model:
using System;
using IssueTracker.Areas.Identity.Data;
using System.ComponentModel.DataAnnotations;
using System.Xml.Linq;
namespace IssueTracker.Models
{
public class EditProjectViewModel
{
public EditProjectViewModel()
{
this.Users = new List<ApplicationUser>();
this.OtherUsers = new List<ApplicationUser>();
}
public int Id { get; set; }
[Required]
public string? Name { get; set; }
[Required]
public string? Description { get; set; }
[Required]
public string? Status { get; set; }
[Required]
public string? ClientCompany { get; set; }
[Required]
public string? ProjectLeader { get; set; }
[Required]
public virtual List<ApplicationUser> Users { get; set; }
[Required]
public virtual List<ApplicationUser> OtherUsers { get; set; }
}
}
and here is my View:
#using IssueTracker.Areas.Identity.Data
#using Microsoft.AspNetCore.Identity
#inject UserManager<ApplicationUser> userManager
#model EditProjectViewModel
#{
ViewData["Title"] = "Edit Project: " + Model.Name;
}
#section Scripts2
{
<link rel="stylesheet" href="../../dist/plugins/select2/css/select2.min.css">
}
<!-- Main content -->
<section class="content">
<div class="row">
<div class="col-md-12">
<div class="card card-info">
<div class="card-header">
<h3 class="card-title">Edit Your Project</h3>
</div>
<div class="card-body">
<form method="post">
<div class="form-group">
<label for="inputName">Project Name:</label>
<textarea id="inputName" name="Name" class="form-control" rows="1">#Model.Name</textarea>
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label for="inputDescription">Project Description:</label>
<textarea id="inputDescription" name="Description" class="form-control" rows="3">#Model.Description</textarea>
<span asp-validation-for="Description" class="text-danger"></span>
</div>
<div class="form-group">
<label for="inputStatus">Status:</label>
<select asp-for="Status" id="inputStatus" name="Status" class="form-control custom-select">
<option selected value="#Model.Status">#Model.Status</option>
#if (Model.Status != "In Development")
{
<option value="In Development">In Development</option>
}
#if (Model.Status != "On Hold")
{
<option value="On Hold">On Hold</option>
}
#if (Model.Status != "Revising")
{
<option value="Revising">Revising</option>
}
#if (Model.Status != "Completed")
{
<option value="Completed">Completed</option>
}
#if (Model.Status != "Withdrawn")
{
<option value="Withdrawn">Withdrawn</option>
}
</select>
<span asp-validation-for="Status" class="text-danger"></span>
</div>
<div class="form-group">
<label for="inputCompany">Client Company:</label>
<textarea asp-for="ClientCompany" id="inputCompany" name="ClientCompany" class="form-control" rows="1">#Model.ClientCompany</textarea>
<span asp-validation-for="ClientCompany" class="text-danger"></span>
</div>
<div class="form-group">
<label for="inputLeader">Project Leader:</label>
<select asp-for="ProjectLeader" id="inputLeader" name="ProjectLeader" class="form-control custom-select">
<option selected value="#Model.ProjectLeader">#(userManager.FindByIdAsync(Model.ProjectLeader).Result.FirstName + " " + userManager.FindByIdAsync(Model.ProjectLeader).Result.LastName)</option>
#foreach (ApplicationUser user in userManager.Users)
{
if (user != userManager.FindByIdAsync(Model.ProjectLeader).Result)
{
<option value="#user.Id">#(user.FirstName + " " + user.LastName)</option>
}
}
</select>
<span asp-validation-for="ProjectLeader" class="text-danger"></span>
</div>
<div class="form-group">
<label>Select 1 or More Contributors:</label>
<div class="select2-blue">
<select asp-for="Users" class="select2" name="Contributors" multiple="multiple" data-dropdown-css-class="select2-blue" data-placeholder="Select a Contributors" style="width: 100%;">
#foreach (ApplicationUser user in Model.Users)
{
<option value="#user.Id" selected>#(user.FirstName + " " + user.LastName)</option>
}
#foreach (ApplicationUser user in Model.OtherUsers)
{
<option value="#user.Id">#(user.FirstName + " " + user.LastName)</option>
}
</select>
</div>
<span asp-validation-for="Users" class="text-danger"></span>
</div>
<div class="row">
<div class="col-12">
<a asp-controller="Home" asp-action="Projects" class="btn btn-secondary">Cancel</a>
<input type="submit" asp-route-id="#Model.Id" value="Update Project" class="btn btn-success float-right">
</div>
</div>
</form>
</div>
<!-- /.card-body -->
</div>
<!-- /.card -->
</div>
</div>
</section>
<!-- /.content -->
<!-- /.content-wrapper -->
#section Scripts{
<script src="../../dist/plugins/select2/js/select2.full.min.js"></script>
<script>$(function () {
$('.select2').select2()
})</script>
}
Lastly, here is my controller method:
[HttpPost]
public async Task<IActionResult> EditProject(int Id, string Name, string Description, string Status, string ClientCompany,
string ProjectLeader, List<string> Contributors)
{
if (ModelState.IsValid)
{
var project = await db.Projects.FindAsync(Id);
if (project == null)
{
return View("Error");
}
else
{
project.Name = Name;
project.Description = Description;
project.Status = Status;
project.ClientCompany = ClientCompany;
project.ProjectLeader = ProjectLeader;
}
db.Entry(project).Collection("Users").Load();
foreach (ApplicationUser user in project.Users)
{
db.Entry(user).Collection("Projects").Load();
user.Projects.Remove(project);
db.Users.Update(user);
}
project.Users.Clear();
foreach (string Contributor in Contributors)
{
project.Users.Add(await userManager.FindByIdAsync(Contributor));
userManager.FindByIdAsync(Contributor).Result.Projects.Add(project);
}
project.Users.Add(userManager.FindByIdAsync(ProjectLeader).Result);
userManager.FindByIdAsync(ProjectLeader).Result.Projects.Add(project);
db.Projects.Update(project);
await db.SaveChangesAsync();
return RedirectToAction("Projects");
}
return RedirectToAction("EditProject", Id);
}
Whenever I submit a form and leave fields blank, the page just refreshes and doesn't save any changes. Instead, I want the error validation messages to pop up. How can I achieve this? Thank you!

For the html validation in View to work with asp.net data validation attributes defined in EditProjectViewModel, you will need javascript/jquery unobtrusive behavior. That will help you generate much of the validation code as well as do the validation for you based on the data attributes defined in the model.
You can take this answer as reference and for help: What is jQuery Unobtrusive Validation?

Related

Populating dropdown via foreign key

I am trying to populate a dropdown on my razor page with info from the database(My website is for document upload/download). When they are uploading a file, it asks for vendor Id as this is the foreign key linking my 'Files' and 'Vendor(user)' together.
In the dropdown I want them to be able to select the vendor name, but in the files db - the vendorId gets entered.
I could populate it manually with the following code:
<select asp-for="Files.VendorId">
<option value="3950">Girvan Early Growers Ltd</option>
</select>
But at one point we may have anything up to 50 vendors, so not ideal.
Below I will include my page model and my cshtml page for this to see if it helps.
PageModel:
using FarmersPortal.Data;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion.Internal;
namespace FarmersPortal.Pages
{
[Authorize(Roles ="Admin")]
public class CreateModel : PageModel
{
private readonly FarmersPortal.Data.filedbContext _context;
public CreateModel(FarmersPortal.Data.filedbContext context)
{
_context = context;
}
public IQueryable<Data.Vendor> VendorList { get; set; }
[BindProperty]
public List<Data.Vendor> Vendores { get; set; }
public void OnGet()
{
Vendores = _context.Vendors.ToList();
}
[BindProperty]
public Data.File Files { get; set; }
public Data.Vendor Vendors { get; set; }
// To protect from overposting attacks, enable the specific properties you want to bind to, for
// more details, see https://aka.ms/RazorPagesCRUD.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.Files.Add(Files);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
}
}
.cshtml:
#page
#model FarmersPortal.Pages.CreateModel
#{
ViewData["Title"] = "Create";
}
<style>
body {
background-image: url('hero-range-1.jpg');
height: 100%;
background-position: center;
background-repeat: no-repeat;
background-size: cover;
}
</style>
<h1 style="color: white">Create</h1>
<h4 style ="color: white">Files</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="Files.Number" class="control-label" style="color: white"></label>
<input asp-for="Files.Number" class="form-control" />
<span asp-validation-for="Files.Number" class="text-danger"></span>
</div>
<div class="form-group">
<div class="form-group">
<label asp-for="Files.FileType" class="control-label" style="color: white"></label>
<input asp-for="Files.FileType" class="form-control" />
<span asp-validation-for="Files.FileType" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Files.VendorId" class="control-label" style="color: white"></label>
<input asp-for="Files.VendorId" class="form-control" />
<span asp-validation-for="Files.VendorId" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Files.Haulier" class="control-label" style="color: white"></label>
<input asp-for="Files.Haulier" class="form-control" />
<span asp-validation-for="Files.Haulier" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Files.Comments" class="control-label" style="color: white"></label>
<input asp-for="Files.Comments" class="form-control" />
<span asp-validation-for="Files.Comments" class="text-danger"></span>
</div>
<select asp-for="Files.VendorId">
<option value="3950">Girvan Early Growers Ltd</option>
</select>
<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");
}
}
How would I go about this?
From your description, In Vendor table, There are Id, Name and other properties, So If you want DropdownList show 'Name' as text, 'Id' as value, You can refer to this code:
public void OnGet()
{
List<SelectListItem> test = new List<SelectListItem>();
Vendores = _context.Vendors.ToList();
foreach (var item in Vendores)
{
test.Add(new SelectListItem {Text=item.Name,Value = item.Id.ToString() });
}
ViewData["demo"] = test;
}
In the View
<select asp-for="Files.VendorId" asp-items="#ViewData["demo"]"></select>
Then the dropdownlist will show name but pass id.
You can refer to this link to learn more.

why is my foreign key model not validating?

Having started studying asp net core 6, I ran into the problem of invalidity of my model.
What is the hidden problem?
Model of Product
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace TestWebApplication.Models
{
public class Product
{
[Key]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public string Description { get; set; }
[Range(0.1, int.MaxValue)]
public double Price { get; set; }
public string Image { get; set; }
[Display(Name = "Category Type")]
public int CategoryId { get; set; }
[ForeignKey("CategoryId")]
public virtual Category Category { get; set; }
}
}
Model of ProductVM
using Microsoft.AspNetCore.Mvc.Rendering;
namespace TestWebApplication.Models.ViewModels
{
public class ProductVM
{
public Product Product { get; set; }
public IEnumerable<SelectListItem> CategorySelectList { get; set; }
}
}
View
#model TestWebApplication.Models.ViewModels.ProductVM
#{
var title = "Create Product";
}
<form method="post" enctype="multipart/form-data">
#if(Model.Product.Id != 0)
{
title = "Edit Product";
<input asp-for="Product.Id" hidden />
}
<div class="border p-3">
<div class="form-group row">
<h2 class="text-info pl-3">#title</h2>
</div>
<br />
<div class="row">
<div class="col-8">
<div class="form-group row">
<div class="col-4">
<label asp-for="Product.Name"></label>
</div>
<div class="col-8">
<input asp-for="Product.Name" class="form-control"/>
<span asp-validation-for="Product.Name" class="text-danger"></span>
</div>
</div>
<br />
<div class="form-group row">
<div class="col-4">
<label asp-for="Product.Price"></label>
</div>
<div class="col-8">
<input asp-for="Product.Price" class="form-control"/>
<span asp-validation-for="Product.Price" class="text-danger"></span>
</div>
</div>
<br />
<div class="form-group row">
<div class="col-4">
<label asp-for="Product.Description"></label>
</div>
<div class="col-8">
<textarea asp-for="Product.Description" class="form-control summernote"> </textarea>
<span asp-validation-for="Product.Description" class="text-danger"></span>
</div>
</div>
<br />
<div class="form-group row">
<div class="col-4">
Image
</div>
<div class="col-8">
<input type = "file" name="files" id="uploadBox" multiple class="form-control"/>
</div>
</div>
<br />
<div class="form-group row">
<div class="col-4">
<label asp-for="Product.CategoryId"></label>
</div>
<div class="col-8">
<select asp-for="Product.CategoryId" asp-items="#Model.CategorySelectList" class="form-control">
<option disabled selected>---Select Category---</option>
</select>
<span asp-validation-for="Product.CategoryId" class="text-danger"></span>
</div>
</div>
<br />
<div class="form-group row">
<div class="col-8 offset-4 row">
<div class="col">
#if (Model.Product.Id != 0)
{
//update
<input type="submit" class="btn btn-info w-100" value="Update"/>
}
else
{
//create
<input type="submit" onclick="return validateInput()" class="btn btn-primary w-100" value="Create"/>
}
</div>
<div class="col">
<a asp-action="Index" class="btn btn-success w-100">
Back
</a>
</div>
</div>
</div>
</div>
<div class="col-4">
#* Keep this empty *#
</div>
</div>
</div>
</form>
#section Scripts
{
#{
<partial name= "_ValidationScriptsPartial.cshtml"/>
}
<script>
$(document).ready(function() {
$('.summernote').summernote(
{
height:250
});
});
function validateInput()
{
if (document.getElementById("uploadBox").value == "")
{
Swal.fire
(
'Error!',
'Please, upload an image',
'error'
)
return false;
}
return true;
}
</script>
}
Controller
using TestWebApplication.Data;
using TestWebApplication.Models;
using TestWebApplication.Models.ViewModels;
namespace TestWebApplication.Controllers
{
public class ProductController : Controller
{
private readonly ApplicationDbContext _db;
private readonly IWebHostEnvironment _webHostEnvironment;
public ProductController(ApplicationDbContext db, IWebHostEnvironment webHostEnvironment)
{
_db = db;
_webHostEnvironment = webHostEnvironment;
}
public IActionResult Index()
{
IEnumerable<Product> objList = _db.Product;
foreach (var item in objList)
{
item.Category = _db.Category.FirstOrDefault(category => category.Id == item.Id);
}
return View(objList);
}
// get - upsert
public IActionResult Upsert(int? id)
{
//IEnumerable<SelectListItem> CategoryDropDown = _db.Category.Select(i => new SelectListItem
//{
// Text = i.Name,
// Value = i.Id.ToString()
//});
//ViewBag.CategoryDropDown = CategoryDropDown;
//Product product = new Product();
ProductVM productVM = new ProductVM()
{
Product = new Product(),
CategorySelectList = _db.Category.Select(i => new SelectListItem
{
Text = i.Name,
Value = i.Id.ToString()
})
};
if(id == null)
{
// for create
return View(productVM);
}
else
{
productVM.Product = _db.Product.Find(id);
if (productVM.Product == null)
NotFound();
return View(productVM);
}
}
// post - upsert
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult Upsert(ProductVM obj)
{
if (ModelState.IsValid)
{
var files = HttpContext.Request.Form.Files;
string webRootPath = _webHostEnvironment.WebRootPath;
if(obj.Product.Id == 0)
{
//Creating
string upload = webRootPath + WC.ImagePath;
string fileName = Guid.NewGuid().ToString();
string extention = Path.GetExtension(files[0].FileName);
using(var fileStream = new FileStream(Path.Combine(upload, fileName + extention), FileMode.Create))
{
files[0].CopyTo(fileStream);
}
obj.Product.Image = fileName + extention;
_db.Product.Add(obj.Product);
}
else
{
//Updating
}
return RedirectToAction("Index");
}
return View();
}
}
List of fields that participated in validation
From one answer, I realized that the Category field will remain null, and ef core will substitute the values. But I don't quite understand why so many fields are validated without being marked required in the model.
By receives a POST action, ModelState takes all of the name-value pairs and adds them as individual instances of ModelStateEntry to an instance of ModelStateDictionary.
ModelState checks all properties regardless of whether they have a Data annotations or not, such as you get a number, text, file etc. But for those data annotations you put on the properties the ModelState makes a special check based on it.
Just the properties of the model you passed to controller would be validated,you could try custom model validation follow this document:
https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-6.0#custom-attributes
And you could try with ModelState.AddModelError(string.Empty, "errormessage");add model level error in backend.

Saving parent item and child items with properties from select element ASP.NET Core MVC

In ASP.NET Core MVC web app I'm working on, I have a problem that I'll illustrate with the following example.
Let's say the user has to save book with its title and number of pages, and one or more authors. For each author, user should choose his name and role from dropdown list.
Model has four classes:
public class Author
{
public int AuthorID { get; set; }
public string AuthorName { get; set; }
public Author(int id, string name)
{
AuthorID = id;
AuthorName = name;
}
}
public class AuthorRole
{
public int AuthorRoleID { get; set; }
public string AuthorRoleName { get; set; }
public AuthorRole(int id, string name)
{
AuthorRoleID = id;
AuthorRoleName = name;
}
}
public class BookAuthor
{
public int? BookAuthorID { get; set; }
public int? BookID { get; set; }
[DisplayName("Name")]
public int? AuthorID { get; set; }
[DisplayName("Role")]
public int? AuthorRoleID { get; set; }
}
public class BookViewModel
{
public int? BookID { get; set; }
[DisplayName("Title")]
public string Title { get; set; }
[DisplayName("Number of pages")]
public int NumberOfPages { get; set; }
public SelectList AuthorsList { get; set; }
public SelectList AuthorRolesList { get; set; }
[DisplayName("Authors")]
public List<BookAuthor> BookAuthors { get; set; }
public BookViewModel()
{
BookAuthors = new List<BookAuthor>();
}
}
The action for form initialization
[HttpGet]
public async Task<IActionResult> AddBook()
{
BookViewModel book = new BookViewModel();
PopulateDropdowns(book);
book.BookAuthors.Add(new BookAuthor());
return View(book);
}
private void PopulateDropdowns(BookViewModel book)
{
List<Author> authors = new List<Author>();
authors.Add(new Author(1, "Morgan Beaman"));
authors.Add(new Author(2, "Cleveland Lemmer"));
authors.Add(new Author(3, "Joanie Wann"));
authors.Add(new Author(4, "Emil Shupp"));
authors.Add(new Author(5, "Zenia Witts"));
book.AuthorsList = new SelectList(authors, "AuthorID", "AuthorName");
List<AuthorRole> authorRoles = new List<AuthorRole>();
authorRoles.Add(new AuthorRole(1, "Author"));
authorRoles.Add(new AuthorRole(2, "Editor"));
authorRoles.Add(new AuthorRole(3, "Translator"));
book.AuthorRolesList = new SelectList(authorRoles, "AuthorRoleID", "AuthorRoleName");
}
returns the view:
#{
Layout = "_DetailsLayout";
}
#model Test.BookViewModel
<div class="container-fluid">
<div class="row justify-content-center">
<div class="col-sm-8 col-lg-6">
<form asp-action="SaveBook" class="form-horizontal">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="BookID" />
<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="NumberOfPages" class="control-label"></label>
<input asp-for="NumberOfPages" class="form-control" />
<span asp-validation-for="NumberOfPages" class="text-danger"></span>
</div>
<label asp-for="BookAuthors" class="control-label"></label>
#await Html.PartialAsync("_AuthorDetails", Model)
<div id="AuthorDetailsWrapper"></div>
<div class="clearfix">
<a class="btn btn-link text-success btn-sm float-right" data-ajax="true"
data-ajax-url="#Url.Action("AddAuthor", new { id = Model.BookID })"
data-ajax-method="GET" data-ajax-mode="after"
data-ajax-loading-duration="400"
data-ajax-update="#AuthorDetailsWrapper" href="">ADD AUTHOR</a>
</div>
<div class="row mt-5">
<div class="col">
<a asp-action="Index" class="btn btn-outline-secondary block-on-mobile mb-2">Back</a>
</div>
<div class="col">
<button type="submit" class="btn btn-success float-right block-on-mobile">Save</button>
</div>
</div>
</form>
</div>
</div>
</div>
If book has more than one author, the user should click "ADD AUTHOR" which calls action
[HttpGet]
public IActionResult AddAuthor(int? bookid)
{
BookViewModel book = new BookViewModel();
BookAuthor newAuthor = new BookAuthor();
if (bookid != null)
{
newAuthor.BookID = bookid;
}
PopulateDropdowns(book);
book.BookAuthors.Add(newAuthor);
return PartialView("_AuthorDetails", book);
}
that returns view:
#model Test.BookViewModel
#for (var i = 0; i < Model.BookAuthors.Count; i++)
{
string guid = Guid.NewGuid().ToString();
<div class="form-row">
<input type="hidden" name="BookAuthors.Index" value="#guid" />
<input asp-for="#Model.BookAuthors[i].BookID" type="hidden" data-guid="#guid" />
<input asp-for="#Model.BookAuthors[i].BookAuthorID" type="hidden" data-guid="#guid" />
<div class="form-group col">
<label asp-for="#Model.BookAuthors[i].AuthorID"></label>
<select asp-for="#Model.BookAuthors[i].AuthorID" asp-items="Model.AuthorsList" data-guid="#guid" class="dropdowns">
<option></option>
</select>
<span asp-validation-for="#Model.BookAuthors[i].AuthorID" class="text-danger" data-guid="#guid"></span>
</div>
<div class="form-group col">
<label asp-for="#Model.BookAuthors[i].AuthorRoleID"></label>
<select asp-for="#Model.BookAuthors[i].AuthorRoleID" asp-items="Model.AuthorRolesList" data-guid="#guid" class="dropdowns">
<option></option>
</select>
<span asp-validation-for="#Model.BookAuthors[i].AuthorRoleID" data-guid="#guid" class="text-danger"></span>
</div>
</div>
}
On form submit, model should be passed to action:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SaveBook(BookViewModel book)
{
// validation and saving book to database
}
with all properties - title and number of pages, but also whole list of authors, each with AuthorID and AuthorRoleID.
The problem is that authors' properties (AuthorID and AuthorRoleID, which are choosen for dropdown) are not passed, i.e. have null values.
For example, if I fill the form like this
Filled form, with two authors
for both elements of list BookAuthors, preoperties AuthorID and AuthorRoleID are null:
Data not passed to action
I believe that the problem arises because "asp-for" tag helper for AuthorID (and AuthorRoleID) generates wrong values for "id" and "name" attributes of select element - it uses 0 as index, not guid. So instead of
<select data-guid="283a0dea-9d64-432f-a03d-8c1a167b062a" class="dropdowns" id="BookAuthors_283a0dea-9d64-432f-a03d-8c1a167b062a__AuthorID" name="BookAuthors[283a0dea-9d64-432f-a03d-8c1a167b062a].AuthorID">
<option></option>
<option value="1">Morgan Beaman</option>
<option value="2">Cleveland Lemmer</option>
<option value="3">Joanie Wann</option>
<option value="4">Emil Shupp</option>
<option value="5">Zenia Witts</option>
</select>
element looks like this
<select data-guid="283a0dea-9d64-432f-a03d-8c1a167b062a" class="dropdowns" id="BookAuthors_0__AuthorID" name="BookAuthors[0].AuthorID">
<option></option>
<option value="1">Morgan Beaman</option>
<option value="2">Cleveland Lemmer</option>
<option value="3">Joanie Wann</option>
<option value="4">Emil Shupp</option>
<option value="5">Zenia Witts</option>
</select>
If I edit this element in DevTools before submit (I replace zeros with guid value), everything is submitted as expected.
What confuses me is that the same code works if AuthorID is not select element, but input element. For example, in _AuthorDetails.cshtml partial view, if I replace
<select asp-for="#Model.BookAuthors[i].AuthorID" asp-items="Model.AuthorsList" data-guid="#guid" class="dropdowns">
<option></option>
</select>
with
<input asp-for="#Model.BookAuthors[i].AuthorID" data-guid="#guid" />
generated element is:
<input data-guid="926eb580-d52f-4132-ab43-b8fbb63e3d2c" type="number" id="BookAuthors_926eb580-d52f-4132-ab43-b8fbb63e3d2c__AuthorID" name="BookAuthors[926eb580-d52f-4132-ab43-b8fbb63e3d2c].AuthorID" value="" minlength="0">
If I manually enter author's ID and submit form, data is passed:
Filled part of form, 5 is entered for author's id
Data passed to action
So, how can I fix this code so that select elements are generated with correct indexes?
Also, alternative solutions and general improvement tips are appreciated. Possible alternative solution should enable adding (and later editing, using the same view) book with arbitrary number of authors on the same page...
if I fill the form like this Filled form, with two authors for both elements of list BookAuthors, preoperties AuthorID and AuthorRoleID are null
That is because your partial view contains an input element which is not the property in BookAuthors,but you define it as BookAuthors.Index,the model binding system would misunderstand it:
<input type="hidden" name="BookAuthors.Index" value="#guid" />
To fix such issue,just change the name like below,avoid using name like BookAuthors.xxx or BookAuthors[i].xxx:
<input type="hidden" name="data" value="#guid" />
Update:
The reason for why you could not pass the list to the backend is because every time you render the partial view,the index of the BookAuthors is always 0.You need render the html like below:
A simple workaround is to set a property to record the index of the BookAuthors.Change your code like below:
Model:
public class BookViewModel
{
//more properties...
//add Index property...
public int Index { get; set; }
[DisplayName("Authors")]
public List<BookAuthor> BookAuthors { get; set; }
public BookViewModel()
{
BookAuthors = new List<BookAuthor>();
}
}
_AuthorDetails.cshtml:
Do not use tag helper.Because when you click add author button,the index would be changed to 1,and tag helper does not allow the list count from 1.Use name property instead of tag helper.
#model BookViewModel
#{
string guid = Guid.NewGuid().ToString();
}
<div class="form-row">
<input type="hidden" name="Index" value="#guid" />
<input name="BookAuthors[#Model.Index].BookID" type="hidden" data-guid="#guid" />
<input name="BookAuthors[#Model.Index].BookAuthorID" type="hidden" data-guid="#guid" />
<div class="form-group col">
<label asp-for="BookAuthors[Model.Index].AuthorID"></label>
<select name="BookAuthors[#Model.Index].AuthorID" asp-items="Model.AuthorsList" data-guid="#guid" class="dropdowns">
<option></option>
</select>
<span asp-validation-for="BookAuthors[Model.Index].AuthorID" class="text-danger" data-guid="#guid"></span>
</div>
<div class="form-group col">
<label asp-for="BookAuthors[Model.Index].AuthorRoleID"></label>
<select name="BookAuthors[#Model.Index].AuthorRoleID" asp-items="Model.AuthorRolesList" data-guid="#guid" class="dropdowns">
<option></option>
</select>
<span asp-validation-for="BookAuthors[Model.Index].AuthorRoleID" data-guid="#guid" class="text-danger"></span>
</div>
</div>
Controller:
[HttpGet]
public IActionResult AddAuthor(int? bookid)
{
var index = HttpContext.Session.GetInt32("item").Value;
BookViewModel book = new BookViewModel();
BookAuthor newAuthor = new BookAuthor();
if (bookid != null)
{
newAuthor.BookID = bookid;
}
PopulateDropdowns(book);
book.BookAuthors.Add(newAuthor);
book.Index= index+1;
HttpContext.Session.SetInt32("item", book.Index);
return PartialView("_AuthorDetails", book);
}
public IActionResult AddBook()
{
BookViewModel book = new BookViewModel();
PopulateDropdowns(book);
book.BookAuthors.Add(new BookAuthor());
book.Index = 0;
HttpContext.Session.SetInt32("item", book.Index);
return View(book);
}
Be sure register session in Startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddSession();
//....
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseSession();
//...
}
Result:

Razor Pages: Values not staying from OnGet() to OnPost()

I'm working on a ASP.NET Core webpage using Razor Pages. On one page, a customer is added or selected and the customer_id is passed through correctly in the OnGet function (seen through debugging). When I then have the user fill out the form, I try to make a call to a SQL server database using Entity Framework. Everything should be working correctly, however, in OnPostAsync(), when I try to import the customer_id value I had set in OnGet(), I get an error because the value somehow gets set to 0 instead of what I had it previously set as. I don't know why this is happening as I'm generally new to Razor Pages and Entity Framework, but I've posted the code below. Any suggestions would be great!
AddAssessment.cshtml.cs:
namespace CustomerPageTest.Pages.Assessment
{
public class AddAssessmentModel : PageModel
{
private readonly CustomerPageTest.Data.CustomerPageTestContext _context;
public AddAssessmentModel(CustomerPageTest.Data.CustomerPageTestContext context)
{
_context = context;
}
public static List<SelectListItem> InDatabaseUserData()
{
List<SelectListItem> Users = new List<SelectListItem>();
string connString = "Data Source = DESKTOP-5A23I9M; Initial Catalog = DataWarehouse; User ID = sa; pwd = 2128Swan";
string commandString = "SELECT user_id, name FROM KelderUser";
SqlConnection conn = new SqlConnection(connString);
SqlDataAdapter adapter = new SqlDataAdapter(commandString, conn);
DataTable dtCustomers = new DataTable();
adapter.Fill(dtCustomers);
foreach (DataRow dataRow in dtCustomers.Rows)
{
SelectListItem temp = new SelectListItem()
{
Value = dataRow[0].ToString(),
Text = dataRow[1].ToString()
};
Users.Add(temp);
}
return Users;
}
[BindProperty]
public CustomerPageTest.Assessment Assessment { get; set; } = new CustomerPageTest.Assessment();
public List<SelectListItem> SelectUser { get; set; } = new List<SelectListItem>();
public void OnGet(int customerId)
{
SelectUser = InDatabaseUserData();
Assessment.customer_id = customerId;
if(Assessment == null)
{
return RedirectToPage("/Customer/List");
}
return Page();
}
// To protect from overposting attacks, enable the specific properties you want to bind to, for
// more details, see https://aka.ms/RazorPagesCRUD.
public async Task<IActionResult> OnPostAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
foreach(var item in SelectUser)
{
if (item.Selected)
{
try { Assessment.user_id = Convert.ToInt32(item.Value); } catch (Exception) { }
}
}
try { Assessment.imported_data_datetime = Convert.ToDateTime(Assessment.imported_data_datetime); } catch(Exception) { }
_context.Assessment.Add(Assessment);
await _context.SaveChangesAsync();
return RedirectToPage("/Customer/List");
}
}
}
Then AddCustomer.cshtml:
#page
#model CustomerPageTest.Pages.Assessment.AddAssessmentModel
#{
ViewData["Title"] = "AddAssessment";
}
<h1>AddAssessment</h1>
<h4>Assessment</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="Assessment.vcenter" class="control-label"></label>
<input asp-for="Assessment.vcenter" class="form-control" />
<span asp-validation-for="Assessment.vcenter" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Assessment.imported_data_datetime" class="control-label"></label>
<input asp-for="Assessment.imported_data_datetime" class="form-control" />
<span asp-validation-for="Assessment.imported_data_datetime" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Assessment.notes" class="control-label"></label>
<input asp-for="Assessment.notes" class="form-control" />
<span asp-validation-for="Assessment.notes" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Assessment.user_id"></label>
<select class="form-control" asp-for="Assessment.user_id" asp-items="Model.SelectUser">
<option value="">Select a User</option>
</select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="/Customer/List">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Also, If you have any questions about what is going on in my code, let me know as there are other classes and pages before this. The main idea is that I have an Assessment class with an integer: customer_id, that needs to be set and it's not getting set.
I write a demo,and i find you didn't bind the customer_id in the cshtml,so when you post the date to cshtml.cs,you didn't post the customer_id.You can add <input hidden asp-for="Assessment.customer_id" class="form-control" /> in the cshtml to bind customer_id.
Here is a demo worked:
cshtml:
<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="Assessment.vcenter" class="control-label"></label>
<input asp-for="Assessment.vcenter" class="form-control" />
<span asp-validation-for="Assessment.vcenter" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Assessment.imported_data_datetime" class="control-label"></label>
<input asp-for="Assessment.imported_data_datetime" class="form-control" />
<span asp-validation-for="Assessment.imported_data_datetime" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Assessment.notes" class="control-label"></label>
<input asp-for="Assessment.notes" class="form-control" />
<span asp-validation-for="Assessment.notes" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Assessment.user_id"></label>
<select class="form-control" asp-for="Assessment.user_id" asp-items="Model.SelectUser">
<option value="">Select a User</option>
</select>
</div>
<input hidden asp-for="Assessment.customer_id" class="form-control" />
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
cshtml.cs:
public class AddCustomerModel : PageModel
{
[BindProperty]
public Assessment Assessment { get; set; } = new Assessment();
public List<SelectListItem> SelectUser { get; set; } = new List<SelectListItem>();
public IActionResult OnGet()
{
for (int i = 1; i < 6; i++) {
SelectListItem temp = new SelectListItem()
{
Value = i+"",
Text = "user"+i
};
SelectUser.Add(temp);
}
Assessment.customer_id = 2;
Assessment.vcenter = "vcenter";
return Page();
}
public IActionResult OnPost() {
if (!ModelState.IsValid)
{
return Page();
}
return Page();
}
}
Result:
The reason this happens is because HTTP is stateless. Any state (data) that you created in one request is destroyed once that request has been processed unless you take steps to persist it.
There are multiple ways to manage state in Razor Pages (https://www.learnrazorpages.com/razor-pages/state-management), but in your case, you should use a hidden form field, posting the selected customer ID to the OnPost handler

Razor Page - Form with multiple handlers/server-side validation not populating asp-validation-for tags on submit

I have a form that is used to submit one of two objects. Using Fluent Validation, each object has separate validation rules, which are handled by executing ModelState.Clear() and TryValidateModel(objectName) in the respective post handler. Validation is working correctly, but is only displaying on the asp-validation-summary tag, not on the asp-validation-for tags that accompany each field. Does anyone have any idea how to get around this? (Using 2 forms is not an option.) Here is code you can use to replicate the issue:
TestValidation.cshtml:
#page
#model MyApp.Namespace.TestValidationModel
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>TestValidation</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<h1>Test Validation</h1>
<form method="post">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">ObjectToValidate1 - posts with empty handler, validates server side.</div>
<div class="card-body">
<div class="form-group">
<label class="control-label">Borrower Type</label>
<select asp-for="ObjectToValidate1.Item" asp-items="#Model.DDLItems" class="form-control" onchange="changeBorrowerType();"></select>
<span asp-validation-for="ObjectToValidate1.Item" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label"></label>
<label asp-for="ObjectToValidate1.RequiredString" class="control-label businessName"></label>
<input asp-for="ObjectToValidate1.RequiredString" class="form-control autofocus" />
<span asp-validation-for="ObjectToValidate1.RequiredString" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ObjectToValidate1.RequiredStringIfItem1Selected" class="control-label"></label>
<input asp-for="ObjectToValidate1.RequiredStringIfItem1Selected" class="form-control" />
<span asp-validation-for="ObjectToValidate1.RequiredStringIfItem1Selected" class="text-danger"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success" asp-page-handler="">Submit</button>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">ObjectToValidate2 - posts with handler of TestValidation, validates serverside</div>
<div class="card-body">
#*<div class="form-group">
<label class="control-label">Borrower Type</label>
<select asp-for="ObjectToValidate2.Item" asp-items="#Model.DDLItems" class="form-control" onchange="changeBorrowerType();"></select>
<span asp-validation-for="ObjectToValidate2.Item" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label"></label>
<label asp-for="ObjectToValidate2.RequiredString" class="control-label businessName"></label>
<input asp-for="ObjectToValidate2.RequiredString" class="form-control autofocus" />
<span asp-validation-for="ObjectToValidate2.RequiredString" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ObjectToValidate2.RequiredStringIfItem1Selected" class="control-label"></label>
<input asp-for="ObjectToValidate2.RequiredStringIfItem1Selected" class="form-control" />
<span asp-validation-for="ObjectToValidate2.RequiredStringIfItem1Selected" class="text-danger"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success" asp-page-handler="TestValidation">Submit</button>
</div>*#
</div>
</div>
</div>
</div>
</form>
#*<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.17.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>*#
</body>
</html>
TestValidation.cshtml.cs
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MyApp.Namespace
{
public class TestValidationModel : PageModel
{
[BindProperty]
public InputValues ObjectToValidate1 { get; set; }
[BindProperty]
public InputValues ObjectToValidate2 { get; set; }
public List<SelectListItem> DDLItems { get; set; }
public void OnGet()
{
ObjectToValidate1 = new InputValues();
ObjectToValidate2 = new InputValues();
InitializeDDLItems();
}
public ActionResult OnPost()
{
ModelState.Clear();
TryValidateModel(ObjectToValidate1);
if (ModelState.IsValid)
{
// Refresh current page
return RedirectToPage("./TestValidation");
}
InitializeDDLItems();
return Page();
}
public ActionResult OnPostTestValidation()
{
ModelState.Clear();
TryValidateModel(ObjectToValidate2);
if (ModelState.IsValid)
{
// Refresh current page
return RedirectToPage("./TestValidation");
}
InitializeDDLItems();
return Page();
}
private void InitializeDDLItems()
{
DDLItems = new List<SelectListItem>
{
new SelectListItem("-- Select an Item -- ", ""),
new SelectListItem("Item 1", "1"),
new SelectListItem("Item 2", "2"),
};
}
}
public class InputValues
{
[Display(Name ="Item")]
public int Item { get; set; }
[Display(Name = "Required String")]
public string RequiredString { get; set; }
[Display(Name = "Conditional Required String")]
public string RequiredStringIfItem1Selected { get; set; }
}
public class Validator : AbstractValidator<InputValues>
{
public Validator()
{
RuleFor(i => i.Item).NotEmpty().WithMessage("Item is required");
RuleFor(i => i.RequiredString).NotEmpty().WithMessage("Required String is required");
When(i => i.Item.Equals(1), () =>
{
RuleFor(e => e.RequiredStringIfItem1Selected).NotEmpty().WithMessage("Conditional Required String is required if Item 1 selected");
});
}
}
}
You need to add the following javascript files which support client-side validation :
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jqueryvalidate/1.17.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>
The jQuery Unobtrusive Validation script is a custom Microsoft front-end library that builds on the popular jQuery Validate plugin. Without jQuery Unobtrusive Validation, you would have to code the same validation logic in two places: once in the server-side validation attributes on model properties, and then again in client-side scripts.
Reference :https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-3.0#client-side-validation
The answer: Don't use ModelState.Clear(). Doing so clears out the ModelState to the extent that, even after running TryValidateModel(modelname), the resulting ModelState doesn't have enough to enable the validation tags to render. So, here is a simplified version of my earlier sample, with ModelState.Clear() commented out and ModelState.ClearValidationState() run for each object that we want to validate server side. To run for my original code, above, I would ClearValidationState for the fields that are not relevant to the current OnPost method.
The Razor Page:
#page
#model MyApp.Namespace.TestValidation2Model
#{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>TestValidation</title>
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
</head>
<body>
<h1>Test Validation</h1>
<form method="post">
#*<div asp-validation-summary="All" class="text-danger"></div>*#
<div class="row">
<div class="col-md-6">
<div class="card">
<div class="card-header">ObjectToValidate1 - posts with empty handler, validates server side.</div>
<div class="card-body">
<div class="form-group">
<label class="control-label">Borrower Type</label>
<select asp-for="ObjectToValidate1.Item" asp-items="#Model.DDLItems" class="form-control" onchange="changeBorrowerType();"></select>
<span asp-validation-for="ObjectToValidate1.Item" class="text-danger"></span>
</div>
<div class="form-group">
<label class="control-label"></label>
<label asp-for="ObjectToValidate1.RequiredString" class="control-label businessName"></label>
<input asp-for="ObjectToValidate1.RequiredString" class="form-control autofocus" />
<span asp-validation-for="ObjectToValidate1.RequiredString" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ObjectToValidate1.RequiredStringIfItem1Selected" class="control-label"></label>
<input asp-for="ObjectToValidate1.RequiredStringIfItem1Selected" class="form-control" />
<span asp-validation-for="ObjectToValidate1.RequiredStringIfItem1Selected" class="text-danger"></span>
</div>
<div class="form-group">
<button type="submit" class="btn btn-success" asp-page-handler="">Submit</button>
</div>
</div>
</div>
</div>
</div>
</form>
#*<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.17.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>*#
</body>
</html>
The PageModel:
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using FluentValidation;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MyApp.Namespace
{
public class TestValidation2Model : PageModel
{
[BindProperty]
public InputValues2 ObjectToValidate1 { get; set; }
public List<SelectListItem> DDLItems { get; set; }
public void OnGet()
{
ObjectToValidate1 = new InputValues2();
InitializeDDLItems();
}
public ActionResult OnPost()
{
ModelState.ClearValidationState(nameof(ObjectToValidate1.RequiredString));//.Clear();
ModelState.ClearValidationState(nameof(ObjectToValidate1.Item));//.Clear();
ModelState.ClearValidationState(nameof(ObjectToValidate1.RequiredStringIfItem1Selected));
//ModelState.Clear();
TryValidateModel(ObjectToValidate1);
var jsonString = Newtonsoft.Json.JsonConvert.SerializeObject(ModelState);
if (ModelState.IsValid)
{
// Refresh current page
return RedirectToPage("./TestValidation2");
}
InitializeDDLItems();
return Page();
}
private void InitializeDDLItems()
{
DDLItems = new List<SelectListItem>
{
new SelectListItem("-- Select an Item -- ", ""),
new SelectListItem("Item 1", "1"),
new SelectListItem("Item 2", "2"),
};
}
}
public class InputValues2
{
[Display(Name = "Item")]
public int Item { get; set; }
[Display(Name = "Required String")]
public string RequiredString { get; set; }
[Display(Name = "Conditional Required String")]
public string RequiredStringIfItem1Selected { get; set; }
}
public class Validator2 : AbstractValidator<InputValues2>
{
public Validator2()
{
RuleFor(i => i.Item).NotEmpty().WithMessage("Item is required");
RuleFor(i => i.RequiredString).NotEmpty().WithMessage("Required String is required");
When(i => i.Item.Equals(1), () =>
{
RuleFor(e => e.RequiredStringIfItem1Selected).NotEmpty().WithMessage("Conditional Required String is required if Item 1 selected");
});
}
}
}

Categories

Resources