[Spring MVC - Thymeleaf]- Form validation and error messages - forms

I´m trying to create a form and validate its data via #Valid on the command object.
The validation performs well, but an error is ocurring going back to web.
This is what I have:
HTML
<div id="content" layout:fragment="contenido">
<div sec:authorize="isAnonymous()">
<form class="form-horizontal" action="#" th:action="#{register}" th:object="${userForm}" method="post">
<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
<fieldset>
<label for="alias" th:text="#{form.register.alias}">Alias</label>
<input id="alias" type="text" th:field="*{alias}" placeholder="Su alias" required="required" autofocus="autofocus"/>
<label for="pass" th:text="#{form.register.password}">Contraseña</label>
<input id="pass" type="password" th:field="*{password}" pattern="[\w\d-_]{5,15}" required="required" th:title="#{form.error.password}"/>
<p th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Error en el dato ingresado</p>
<button type="submit" name="save" class="btn btn-primary" th:text="#{control.register}">Registrarme</button>
</fieldset>
</form>
</div>
</div>
Controller
#RequestMapping(value = "/register", params = {"save"}, method = RequestMethod.POST)
public String register (final ModelMap model, #Valid final UsuarioForm userForm, final BindingResult result) {
if (result.hasErrors()) {
return "register";
} else {
return "redirect:/" + HomeController.PAGE_NAME;
}
}
When Clicking on "submit" the "register" method is called, result.hasErrors() is true so the same page should be displayed, but this error occurs.
Stack
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'userForm' available as request attribute
org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:144)
org.thymeleaf.spring4.util.FieldUtils.getBindStatusFromParsedExpression(FieldUtils.java:396)
org.thymeleaf.spring4.util.FieldUtils.getBindStatus(FieldUtils.java:323)
org.thymeleaf.spring4.util.FieldUtils.getBindStatus(FieldUtils.java:289)
org.thymeleaf.spring4.processor.attr.AbstractSpringFieldAttrProcessor.processAttribute(AbstractSpringFieldAttrProcessor.java:98)
org.thymeleaf.processor.attr.AbstractAttrProcessor.doProcess(AbstractAttrProcessor.java:87)
org.thymeleaf.processor.AbstractProcessor.process(AbstractProcessor.java:212)
org.thymeleaf.dom.Node.applyNextProcessor(Node.java:1017)
org.thymeleaf.dom.Node.processNode(Node.java:972)
If I add "userForm" to the model in the Controller this way:
Controller Modified
#RequestMapping(value = "/register", params = {"save"}, method = RequestMethod.POST)
public String register (final ModelMap model, #Valid final UsuarioForm userForm, final BindingResult result) {
if (result.hasErrors()) {
model.addAttribute("userForm", userForm); //THIS LINE IS ADDED
return "register";
} else {
return "redirect:/" + HomeController.PAGE_NAME;
}
}
The error disappears, BUT... the expression in the HTML ${#fields.hasErrors('password')} results false, so I cant show the error messages to the user.
Any idea of why this behaviour is happening?
Thanks in advance!
PS: I am using Spring MVC 4.1.2 with Thymeleaf 2.1.4

This
public String register(final ModelMap model,
#Valid final UsuarioForm userForm,
final BindingResult result)
should be:
public String register(final ModelMap model,
#ModelAttribute("userForm") #Valid final UsuarioForm userForm,
final BindingResult result)
Notice the #ModelAttribute annotation.

Related

How can I bind form fields to a nested model on post?

I am coding a solution where the user will submit a form, posting the values back to my ASP.NET MVC controller. My model is complex and the form fields are contained in a nested object (I'm using CQRS via MediatR). When I submit the form, the values come across as null. How can I get the complex model to recognize the form fields?
Here is my code:
Controller:
[HttpPost]
[Route("edit")]
public async Task<IActionResult> Edit(UpdateApplicationCommand command)
{
await _mediator.Send(command)
.ConfigureAwait(false);
return RedirectToAction("Index");
}
Models:
public class UpdateApplicationCommand : IRequest<Unit>
{
public ApplicationEditGeneralViewModel ApplicationEditGeneralViewModel { get; set; } = null!;
}
public class ApplicationEditGeneralViewModel
{
[Required]
public string Name { get; set; } = string.Empty;
public string Description { get; set; } = string.Empty;
}
View:
#model ApplicationEditGeneralViewModel
<form method="post" asp-action="Edit" asp-controller="Applications">
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.Name, new { #class = "form-control", placeholder = "Application Name"})
<label for="Name">Application Name</label>
</div>
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.Description, new { #class = "form-control", placeholder = "Application Description"})
<label for="Description">Application Description</label>
</div>
<div class="d-flex flex-row-reverse bd-highlight">
<input type="submit" value="Submit" class="btn btn-primary mt-2" />
</div>
</form>
I've tried to reduce the complex model to its fields, by placing the contents of the ApplicationEditGeneralViewModel directly into the UpdateApplicationCommand class. This worked, but I'd really like to keep the nested structure so that I can reuse the ApplicationEditGeneralViewModel object.
I saw this solution here:
How to bind nested model in partial view
But I'd rather avoid adding the name as a route object (if possible) for every form field. Is there another, more simple way that I can do this?
The first way, you can custom model binding like below:
public class CustomModelBinder : IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
if (bindingContext == null)
throw new ArgumentNullException(nameof(bindingContext));
var model = new UpdateApplicationCommand()
{
ApplicationEditGeneralViewModel = new ApplicationEditGeneralViewModel()
{
Description = bindingContext.ValueProvider.GetValue("Description").ToString(),
Name = bindingContext.ValueProvider.GetValue("Name").ToString()
}
};
bindingContext.Result = ModelBindingResult.Success(model);
return Task.CompletedTask;
}
}
Apply the custom model binding like below:
[HttpPost]
public async Task<IActionResult> Edit([ModelBinder(typeof(CustomModelBinder))]UpdateApplicationCommand model)
{
//.....
}
The second way, just change your razor view like below:
#model UpdateApplicationCommand
<form method="post">
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.ApplicationEditGeneralViewModel.Name, new { #class = "form-control", placeholder = "Application Name"})
<label for="Name">Application Name</label>
</div>
<div class="form-floating mb-3">
#Html.TextBoxFor(m => m.ApplicationEditGeneralViewModel.Description, new { #class = "form-control", placeholder = "Application Description"})
<label for="Description">Application Description</label>
</div>
<div class="d-flex flex-row-reverse bd-highlight">
<input type="submit" value="Submit" class="btn btn-primary mt-2" />
</div>
</form>

How to submit a form multiple times (asp.net core razor)

I am trying to create a form that can be submitted multiple times with different information, while retaining a common value in one field.
I have a list view from a SQL table in ASP.NET Core Razor that is a list of construction projects. For each row in the list I have a link that goes to a "create" template page where users can create a bid entry for the project which is stored in a different table. The Project Number is assigned to a route value (asp-route-Number = "the project number from the previous list")and populates a hidden field in the "create new bid" form.
Using the default code for the razor page, everything works great. You click submit and are taken back to the list of projects.
What I want to do is have another option on the "create new bid" form that will allow you to save and enter another bid for the same project. I created another button and handler to do this but I am stuck on actually implementing it. If I use return Page() the form posts and the page is returned with route data intact, but the text fields still contain the previous data and the drop-down list is empty. If I use return RedirectToPage(CreateNewBid, Route data) the form posts but the route data does not seem to be passed along and creates a null value error.
This is the link from the Projects list (inside the foreach table), which takes you to the "Create Bid" form and works fine.
<a asp-page="CreateBid" asp-route-Number="#item.ProjectNumber" asp-route-opwid="#item.Id">New Bid</a>
The Create Bid form has the following to submit and create another entry
int num = int.Parse(Request.Query["Number"]);
int idnum = int.Parse(Request.Query["opwid"]);
<input type="submit" value="Save and enter another"
asp-page-handler="Another" asp-route-opwid="#idnum"
asp-route-Number="#num" class="btn btn-primary"/>
And the handler:
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return Page();
//return RedirectToPage("./CreateBid", (Number == num, opwid == idnum));
}
I have also tried several things in the route parameters (as opposed to using the variables) in the "Redirect to Page" and nothing seems to work.
Is there an easier way, or am I just missing something?
This is the cshtml file:
#page
#model Authorization_AD.Pages.GenSvc.BidEntry.CreateBidModel
#{
ViewData["Title"] = "CreateBid";
}
#{ int num = int.Parse(Request.Query["Number"]);
int idnum = int.Parse(Request.Query["opwid"]);
}
<h1>Create Bid</h1>
<h4>OPW number #num</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">
<input asp-for="OpwBids.OpwProject" value="#idnum" hidden class="form-control" />
</div>
<div class="form-group">
<label asp-for="OpwBids.OpeningDate" class="control-label"></label>
<input asp-for="OpwBids.OpeningDate" class="form-control" />
<span asp-validation-for="OpwBids.OpeningDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="OpwBids.Contractor" class="control-label"></label>
<select asp-for="OpwBids.Contractor" class="form-control" asp-items="ViewBag.Contractor">
<option disabled selected>--- SELECT ---</option>
</select>
</div>
<div class="form-group">
<label asp-for="OpwBids.BidAmount" class="control-label"></label>
<input asp-for="OpwBids.BidAmount" class="form-control" />
<span asp-validation-for="OpwBids.BidAmount" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Save and enter another"
asp-page-handler="Another" asp-route-opwid="#idnum"
asp-route-Number="#num" class="btn btn-primary"/>
<input type="submit" value="Save and return to list" asp-page-handler="Done" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-page="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
This is the C# file:
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 Authorization_AD.Models;
namespace Authorization_AD.Pages.GenSvc.BidEntry
{
public class CreateBidModel : PageModel
{
private readonly Authorization_AD.Models.OPWContext _context;
public CreateBidModel(Authorization_AD.Models.OPWContext context)
{
_context = context;
}
public IActionResult OnGet()
{
ViewData["Contractor"] = new SelectList(_context.Contractors, "Id", "ContractorName");
ViewData["OpwProject"] = new SelectList(_context.MainProjectsListing, "Id", "ProjectNumber");
return Page();
}
[BindProperty]
public OpwBids OpwBids { get; set; }
public async Task<IActionResult> OnPostDoneAsync()
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return RedirectToPage("./Index");
}
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
return Page();
//return RedirectToPage("./CreateBid", (Number == OpwBids.OpwProjectNavigation.ProjectNumber, opwid == OpwBids.OpwProject));
}
}
}
You can add a property to your page that will be used to bind the value of the clicked button.
public class CreateBidModel : PageModel {
//...
//Add this property to your page.
[BindProperty]
public string Button {get;set;}
public void OnGet(int number,string opwid){
//Set the number and opwid to the target properties
}
public Task<IActionResult> OnPostAsync(){
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
if(Button == "finish"){
return RedirectToPage("./Index");
}
else {
return RedirectToPage("./CreateBid", (Number == OpwBids.OpwProjectNavigation.ProjectNumber, opwid == OpwBids.OpwProject));
}
}
}
To the view you need to add two buttons that have the same name and that value will be mapped to the Button property.
<form method="post">
... Other content goes here
<button name="#Html.NameFor(m => m.Button)" value="another">Create another</button>
<button name="#Html.NameFor(m => m.Button)" value="finish">Finish</button>
</form>
The value of the clicked button will be parsed to the Button property of the Pagemodel. Based on the value you can decide how to further handle the response of the request (Finish / Create another one in your case).
Thanks for everyone's help. I got it to do what I want by adding the following to the "OnPostAnotherAsync" task:
public async Task<IActionResult> OnPostAnotherAsync(int Number, int opwid)
{
if (!ModelState.IsValid)
{
return Page();
}
_context.OpwBids.Add(OpwBids);
await _context.SaveChangesAsync();
ViewData["Contractor"] = new SelectList(_context.Contractors, "Id", "ContractorName");
ModelState.SetModelValue("OpwBids.BidAmount", new ValueProviderResult(string.Empty, CultureInfo.InvariantCulture));
ModelState.SetModelValue("OpwBids.Contractor", new ValueProviderResult(string.Empty, CultureInfo.InvariantCulture));
return Page();
}
After the "Save Changes" I needed to re-load the view data for the "Contractor" drop down list. Then it was just a matter of clearing the form fields before returning the page.

BindingResult nor plain target object for bean name 'fileName' available as request attribute " while trying to update a raw in my db,

I'm getting this exception while trying to update a row in my database. I performed a lot of research in Google all I find is that I should add #modelAttribute which is already done.
I also found that I need to add bindind result after #ModelAttribute but this also didn't work so I removed it. I'm using JPA for persistence to manipulate my data, spring boot and thymeleaf for my views.
These are my Controllers one for updating and rendering views
#GetMapping("/edit/{id}")
public ModelAndView UpdateList(#PathVariable(name="id") String id) {
ModelAndView mav = new ModelAndView("updateList");
com.pfe.ClientRest.model.Files files = fileServ.get(id);
mav.addObject("Files", files);
return mav ;
}
#PostMapping("/Save")
public String saveRepport(#ModelAttribute("Files") com.pfe.ClientRest.model.Files dbfile) {
fileServ.save(dbfile);
return "/redirect:/ListFile";
}
this is my entity class I have getter setters and constructors
#Table( name="Files")
#Entity
public class Files {
#GeneratedValue(generator = "uuid")
#GenericGenerator(name = "uuid", strategy = "uuid2")
#Id
private String id;
private String FileName;
private String Verif;
public String getId() {
return id;
}
this is my template.
<div class="container">
<h1> Modifier les informations du Rapports</h1>
<form action="#" th:action="#{/Save}" th:objects="${Files}"
method="post" >
<input type="text" th:field=*{id} readonly="readonly"/>
<input type="text" th:field="*{fileName}" placeholder="Nom du Fichier"
class="form-control mb-4
col-4">
<input type="text" th:field="*{verif}" placeholder="Accepted/Rejected"
class="form-control mb-4
col-4">
<button type="submit" class="btn btn-info col-2"> Mettre à jour</button>
</form>
</div>
The field names in the html page and entity class does not match. These are case sensitive. So the correct one should be
private String fileName;
private String verif;

Bean property 'empname' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?

I am trying to perform a simple submit operation from a form. I use spring boot framework with thyme leaf template for my project. Language used is java in eclipse IDE.
All I am looking to do is to take the empname and empid (refer Employee class) from the form and store it in a java object.
When I run the application, the application opens and when i navigate to edit.html, i get this error message in the browser -
Whitelabel Error Page
This application has no explicit mapping for /error, so you are seeing this as a fallback.
Mon Jun 18 16:14:40 EDT 2018
There was an unexpected error (type=Internal Server Error, status=500).
An error happened during template parsing (template: "class path resource [templates/edit.html]")
I also get this error message on the console -
Caused by: org.springframework.beans.NotReadablePropertyException: Invalid property 'empname' of bean class [com.cardinalcommerce.model.Employee]: Bean property 'empname' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:622) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:612) ~[spring-beans-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at org.springframework.validation.AbstractPropertyBindingResult.getActualFieldValue(AbstractPropertyBindingResult.java:104) ~[spring-context-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at org.springframework.validation.AbstractBindingResult.getFieldValue(AbstractBindingResult.java:228) ~[spring-context-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at org.springframework.web.servlet.support.BindStatus.(BindStatus.java:129) ~[spring-webmvc-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at org.springframework.web.servlet.support.RequestContext.getBindStatus(RequestContext.java:903) ~[spring-webmvc-5.0.6.RELEASE.jar:5.0.6.RELEASE]
at org.thymeleaf.spring5.context.webmvc.SpringWebMvcThymeleafRequestContext.getBindStatus(SpringWebMvcThymeleafRequestContext.java:227) ~[thymeleaf-spring5-3.0.9.RELEASE.jar:3.0.9.RELEASE]
at org.thymeleaf.spring5.util.FieldUtils.getBindStatusFromParsedExpression(FieldUtils.java:305) ~[thymeleaf-spring5-3.0.9.RELEASE.jar:3.0.9.RELEASE]
at org.thymeleaf.spring5.util.FieldUtils.getBindStatus(FieldUtils.java:252) ~[thymeleaf-spring5-3.0.9.RELEASE.jar:3.0.9.RELEASE]
at org.thymeleaf.spring5.util.FieldUtils.getBindStatus(FieldUtils.java:226) ~[thymeleaf-spring5-3.0.9.RELEASE.jar:3.0.9.RELEASE]
at org.thymeleaf.spring5.processor.AbstractSpringFieldTagProcessor.doProcess(AbstractSpringFieldTagProcessor.java:174) ~[thymeleaf-spring5-3.0.9.RELEASE.jar:3.0.9.RELEASE]
at org.thymeleaf.processor.element.AbstractAttributeTagProcessor.doProcess(AbstractAttributeTagProcessor.java:74) ~[thymeleaf-3.0.9.RELEASE.jar:3.0.9.RELEASE]
... 67 common frames omitted
This is my snippet of the html document where the error occurs.
<form class="form-horizontal" action="#" th:action="#{/employee/edit}" th:object="${employee}" method="POST">
<div class="form-group">
<label class="control-label col-sm-3">File Prefix:</label>
<div class="col-sm-7">
<input type="text" class="form-control" th:field="*{empname}" placeholder="Enter employee name" />
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">File Prefix:</label>
<div class="col-sm-7">
<input type="text" class="form-control" th:field="*{empid}" placeholder="Enter the employee ID" />
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-7">
<button type="submit" class="btn btn-default" id="blackButton" th:value="Submit">Submit</button>
<button type="reset" class="btn btn-default" id="blackButton" th:value="Reset">Cancel</button>
</div>
</div>
This is my class where with the setters and getters -
public class Employee {
private String empid;
private String empname;
public String getEmployeeId() {
return empid;
}
public void setEmployeeId(String empid) {
this.empid = empid ;
}
public String getEmployeeName() {
return empname;
}
public void setEmployeeName(String empname) {
this.empname = empname;
}
}
This is the controller snippet -
#Controller
#RequestMapping(value="/")
public class GreetingController {
private static final Logger logger = LoggerFactory.getLogger(GreetingController.class);
#Autowired
private SomeRecord someRecord;
#GetMapping("/")
public String greeting() {
return "about";
}
#RequestMapping("/about")
public String about() {
return "about";
}
#GetMapping("/edit")
public ModelAndView edit() {
ModelAndView modelAndView = new ModelAndView("edit");
modelAndView.addObject("employee", new Employee());
return modelAndView;
}
#PostMapping("/edit")
public ModelAndView createRecord(#Valid Employee employee, BindingResult result) {
ModelAndView modelAndView = new ModelAndView();
if (result.hasErrors()) {
logger.info("Validation errors while submitting form.");
modelAndView.setViewName("CreateRecord");
modelAndView.addObject("employee", employee);
return modelAndView;
}
someRecord.addRecord(employee);
modelAndView.addObject("allRecords", someRecord.getAllRecordData());
modelAndView.setViewName("recordsInfo");
logger.info("Form submitted successfully.");
return modelAndView;
}
#GetMapping("/view")
public String view() {
return "view";
}
}
Let me know if anything else is required.
Thanks for your help.
You should use *{employeeName} and *{employeeId} rather than *{empname} and *{empid}. (Matching the getters and setters, rather than your private variables.)

Spring MVC #ModelAttribute getting filled Entity

Today I am stuck with the spring-form with the POST method which doesn't give posted item to the Controller which I wanted. Here is my code.
Controller.java
#Controller
#RequestMapping("/cart")
public class CartController extends CommonController
{
#RequestMapping(value = "/add", method = RequestMethod.POST)
public ModelAndView addCart(#ModelAttribute("productList") Item item, BindingResult result,Model model){
System.out.println(item.getId()); /// <-- doesn't gives me the ID
return new ModelAndView("cart");
}
}
ProductList.jsp
/// Loop through the products of search itemlist and generates the forms with the correct items
<c:forEach var="item" items="${productList.items}" varStatus="status">
${item.name}
<div class="addCart">
<c:url value="/cart/add.html" var="addURL" />
<form:form method="POST" action="${addURL}" modelAttribute="productList">
<form:hidden path="items[${status.index}].id"/>
<input type="submit" class="addCartBtn" value="Add to cart" />
</form:form>
</div>
BackingBean.java
public class SearchForm implements Serializable
{
private Collection<Item> items;
private String term;
// getters and setters
}
The ${productList} is the backingbean which loops through all items.
I don't really know what the problem is why it isn't giving me the correct data it passed through the POST.
Many thanks.
Covert your spring:hidden tag to normal html hidden tag:
<form:hidden path="items[${status.index}].id"/>
to
<input type="hidden" name="id" value="${item.id}"/>