I have a parent class that have a children class with no required fields. The save form has the inputs for the parent and children class.
The problem is: submit is saving children class with all null fields when I dont fill any of then.
Question: How do I prevent that and save no children?
Example:
Classes:
class Parent {
String name
static hasOne = [children:Children]
}
class Children {
String name
static belongsTo = [parent : Parent]
static constraints = {
name nullable: true
}
}
HTML (.gsp):
<form controller="parent" action="save" method="post">
Parent Name:
<input type="text" name="name" value="parent name" />
Children Name:
<input type="text" name="children.name" />
<input type="submit" value="Save" />
</form>
Relational Tables:
Parent(id, name)
Children(id, name, parent_id)
Controller:
def save(Parent parent) {
parent.save()
}
Submit and Save:
Form data sended:
name: 'parent name'
children.name: ''
Persisted values:
Parent(1, 'parent name')
Children(1, null, 1)
-----------------
What I want is: The save method (controller) to receive the Parent class without a Children when the children have all null fields. Or another clean alternative that makes me save only the Parent class (no "empty" children).
PS: I try to solve with "#JsonInclude(Include.NON_EMPTY)" at the children's name, but it didn't work
Related
I tried using the Asp.Net Core TagHelper but it doesn't seem to work. However, when using HtmlHelpers it works as expected. My issue is that it always display the error message although the ModelState is valid. Am I doing something wrong or can someone reproduce this error?
<label class="control-label" asp-for="Firstname">Firstname</label>
<input type="text" class="form-control" asp-for="Firstname">
<span class="form-control-feedback" asp-validation-for="Firstname"> This field has an error. </span>
The property Firstname has a Required attribute in the ViewModel.
It works like this:
<label class="control-label" asp-for="Firstname">Firstname</label>
<input type="text" class="form-control" asp-for="Firstname">
#Html.ValidationMessageFor(x => x.Firstname)
Edit:
It seems to work if I don't add the custom error message to the Html element but instead to the ViewModel DataAnnotation, like this:
<label class="control-label" asp-for="Firstname">Firstname</label>
<input type="text" class="form-control" asp-for="Firstname">
<span class="form-control-feedback" asp-validation-for="Firstname"></span>
Model:
[Required(ErrorMessage = "This field has an error.")]
public string Firstname { get; set; }
TL;DR:
Consider putting text inside the tag helpers in scenarios when you really want something
different from the generated value.
Full answer
You practically find the solution on your own, but I think I can still throw in my two cents here.
Most tag helpers work in a manner of generating content on a condition when its content is empty or contain only whitespace characters. For example, the ValidationMessageTagHelper checks it in this way:
var tagHelperContent = await output.GetChildContentAsync();
// We check for whitespace to detect scenarios such as:
// <span validation-for="Name">
// </span>
if (!tagHelperContent.IsEmptyOrWhiteSpace)
{
message = tagHelperContent.GetContent();
}
It gets tag content and then fills up message variable if the content is null, empty or whitespace. The message variable is then used to generate the validation message:
var tagBuilder = Generator.GenerateValidationMessage(
ViewContext,
For.ModelExplorer,
For.Name,
message: message,
tag: null,
htmlAttributes: htmlAttributes);
If the message is null or empty then the generator will provide the model error (see line 858 of DefaultHtmlGenerator);
if (!string.IsNullOrEmpty(message))
{
tagBuilder.InnerHtml.SetContent(message);
}
else if (modelError != null)
{
modelExplorer = modelExplorer ?? ExpressionMetadataProvider.FromStringExpression(
expression,
viewContext.ViewData,
_metadataProvider);
tagBuilder.InnerHtml.SetContent(
ValidationHelpers.GetModelErrorMessageOrDefault(modelError, entry, modelExplorer));
}
The GetModelErrorMessageOrDefault() of ValidationHelpers:
public static string GetModelErrorMessageOrDefault(
ModelError modelError,
ModelStateEntry containingEntry,
ModelExplorer modelExplorer)
{
Debug.Assert(modelError != null);
Debug.Assert(containingEntry != null);
Debug.Assert(modelExplorer != null);
if (!string.IsNullOrEmpty(modelError.ErrorMessage))
{
return modelError.ErrorMessage;
}
// Default in the ValidationMessage case is a fallback error message.
var attemptedValue = containingEntry.AttemptedValue ?? "null";
return modelExplorer.Metadata.ModelBindingMessageProvider.ValueIsInvalidAccessor(attemptedValue);
}
So yes, if you put any text inside the <span> validation tag, the tag helper will choose your text over validation error from model state. Similar behaviour occurs if you put text inside the <label> tag as you did:
<label class="control-label" asp-for="Firstname">Firstname</label>
The tag helper will not overwrite the Firstname value you put inside the tag. It may not seem as bad behaviour, but if you would like to use display name for the Firstname property:
[Display(Name = "Fancy first name")]
public string Firstname { get; set; }
you would not see it work! Because the tag helper would again choose the text you put in-between <label> tags over the display name for Firstname.
What you should do is leave it as simple as i can be:
<label class="control-label" asp-for="Firstname"></label>
Consider putting text inside the tag helpers in scenarios when you really want something
different from the generated value.
At the begging I said that most tag helpers work that way. Most of them do, but not all of them. For example SelectTagHelper allows you to put any custom text inside the tag and if you provide a select list, it will generate the options by appending them to the existing content. It is extremely handy for adding custom <option> tags. For example I can easily add a selected and disabled option, so the dropdown does not have initial value, therefore the user is forced to manually select an option. These lines of code:
<select asp-for="LevelId" asp-items="#Model.Levels" class="custom-select">
<option selected disabled>Select option</option>
</select>
will result in:
<select class="custom-select" data-val="true" data-val-required="'Level Id' must not be empty." id="LevelId" name="LevelId">
<option selected disabled>Select parking level</option>
<option value="9">-2</option>
<option value="8">-1</option>
<option value="7">0</option>
</select>
I am using a template based form in angular. I also use bootstrap (v4) and I wish to show some validation messages when the form was submitted.
This is my form:
<form [ngClass]="{'was-validated': wasValidated}">
<div class="form-group">
<label for="name">Name</label>
<input type="text" id="name" name="name" class="form-control" [(ngModel)]="category.name" #name="ngModel" required maxlength="100"/>
<div *ngIf="name.invalid" class="invalid-feedback">
<div *ngIf="name.errors.required">
Name is required.
</div>
</div>
</div>
<button type="submit" class="btn btn-success" (click)="save()">Save</button>
</form>
My component looks as follows:
category: Category;
wasValidated: boolean = false;
ngOnInit() {
this.reset();
}
save() {
this.wasValidated = true;
this.categoriesService.createCategory(this.category).subscribe(
() => {
this.notificationService.add(notifications.category_saved, {name: this.category.name});
this.reset();
},
() => this.notificationService.add(notifications.save_category_failed)
);
}
reset() {
this.wasValidated = false;
this.category = {} as Category;
}
This works, but I have a feeling it's overly complex and more like a workaround rather than the right way. What is the best way to accomplish this?
Note: the class was-validated must be present on the form element in order to show the div with class invalid-feedback. I'm using this: https://getbootstrap.com/docs/4.0/components/forms/#validation
Note 2: I have currently no mechanism yet to prevent form submission on error. I'd like to know a good solution for that as well!
With the answer from #Chellappan V I was able to construct the solution I wanted.
I have applied to following changes:
First added #form="ngForm" to the form tag in the template. Secondly I changed the ngClass expression to reference the submitted state of the form, rather than referring to a boolean which was set to true manually when form was submitted. Last but not least I pass the form in the submit method on the save button.
<form novalidate #form="ngForm" [ngClass]="{'was-validated': form.submitted}">
<!-- form controls -->
<button type="submit" class="btn btn-success" (click)="submit(form)">Save</button>
</form>
In the component I injected the template variable in the component with #ViewChild.
#ViewChild("form")
private form: NgForm;
The submit method now takes a form parameter of type NgForm which is used to check if the form was valid before sending a request to the backend:
submit(form: NgForm) {
if (form.valid) {
this.categoriesService.createCategory(this.category).subscribe(
() => {
this.notificationService.add(notifications.category_saved, {name: this.category.name});
this.reset();
},
() => this.notificationService.add(notifications.save_category_failed)
);
} else {
this.notificationService.add(notifications.validation_errors);
}
}
Finally the reset method resets the form and the model so it can be re-entered to submit a next instance:
reset() {
this.form.resetForm();
this.category = {} as NewCategoryDto;
}
This question already has an answer here:
Submit same Partial View called multiple times data to controller?
(1 answer)
Closed 6 years ago.
Here is my scenario:
public class ComplexObject
{
public int SomeProperty {get;set;}
public List<SimpleObject> Object {get;set;}
}
public class SimpleObject
{
public string FirstName {get;set;}
public string LastName {get;set;}
}
I created a strongly typed partial view for SimpleObject
#model SimpleObject
<div>
<input type="button" value="Button" name="btn" />
<div>
<div>
#Html.TextBoxFor(Model => Model.FirstName, new { #class = "", #maxlength = "50" })
</div>
<div>
#Html.TextBoxFor(Model => Model.LastName, new { #class = "", #maxlength = "50" })
</div>
Now I want to Render this partial view inside another view (MainView). The idea is that a user can click SimpleObject partial view button and
generate the same partial view again on the MainView . SO here is how it looks :
MainView
SimpleView -> Add
SimpleView -> Add
I can create an ajax action and generate the simple view and append it to mainview but the problem is that simpleobject is NOT binding to the ComplexObject.
This is how I render partialview in MainView.
#Html.Partial("_PartialView", Model.SimpleObject, new ViewDataDictionary(ViewData)
{
TemplateInfo = new System.Web.Mvc.TemplateInfo
{
HtmlFieldPrefix = "Simple"
}
})
The MainView calls a controller action on submit click and the entire ComplexObject is submitted. Here my List of SimpleObject is always NULL.
public ActionResult CreateComplex(ComplexObject object)
{
// HERE LIST<SIMPLEOBJECT> is always NULL
}
Any ideas what am I doing wrong here ?
The first time you render the partial view you will have something like this:
<div>
<input type="text" name="FirstName" maxlength="50" />
</div>
<div>
<input type="text" name="LasttName" maxlength="50" />
</div>
When you add more than one partial view to the page, at the end only 2 text boxes will be posted to the server because they all have the same name.
To fix this issue, you have to change your partial view to make the input boxes like an array, you can add a property to your model and set it from the action
assuming you added a property called Index to your model, your code should look like this
#model SimpleObject
<div>
<input type="button" value="Button" name="btn" />
<div>
<div>
#Html.TextBox("FirstName["+Model.Index+"]", new { #class = "", #maxlength = "50" })
</div>
<div>
#Html.TextBox("LastName["+Model.Index+"]", new { #class = "", #maxlength = "50" })
</div>
With each button submit, you increment the Index and render the view.
Another solution, is to remove the partial view and add the input boxes using JavaScript, you can use Knockout
have a look at this article that explains how to do it: http://knockoutjs.com/examples/grid.html
Here are the changes that I did to code posted in original question to bind list of SimpleObjects to ComplexObject.
#Html.Partial("_PartialView")
The class looks like this :
public class ComplexObject
{
public int SomeProperty {get;set;}
public List<SimpleObject> Objects {get;set;}
}
and the partialview starts with BeginCollectionItem
#model AZSolutions.Framework.Model.SimpleObject
#using (Html.BeginCollectionItem("Objects")) {
...
}
I have project on ASP MVC 5. I have a model "Article". This model have HashSet and ICollection of Author. Author - second model:
public partial class Article
{
public Article()
{
Authors = new HashSet<Author>();
}
[DisplayName("Авторы")]
public virtual ICollection<Author> Authors { get; set; }
I need to add page of creating Article, on which you can increase the number of authors(using AJAX), and each author to register the fields. I decided to use partial view of Author's model, without "Create" button(Create button used only view of creating Article). I need in unlimited adding new partial views, and after fill them - get all data from them. How make it? I newbie in MVC, and can't imagine how it will works.
http://i.stack.imgur.com/0RHD0.png - an illustration of how it should look
Is there a need to use partials? wouldnt it be easier to write a small script that would instead clone the first author enclosing element and just change the names of the elements involved to create a new author?
<div id="enclosingDiv" data-count="x">
<div class="someClass" data-index='x1' >
Author1 name Aurthor1 Textboxname="CollectionList[Index].Property"...
</div>
Now when creating a new Authouther, you can just create:
<script>
function createNewAuthor()
{
//clone first author
var count = $('encolsingDiv').attr('data-count');
//var count = $('encolsingDiv').children().length;
var author = $('enclosingDiv').first().clone();
//change name and id,etc using data-count
author.find('*[name$='value'])attr('name','ListCollection[count + 1]");
author.find('*[name$='value'])attr('id',....);
author.attr('data-index',count +1)
$('enclosingDiv').append(author);
$('enclosingDiv').attr('data-count',count + 1 to it);//makes life easier
}
function deleteAuthor(authourIndex)
{
//assumes that an author can be removed
$('div[data-index="'+authorIndex+'"]").remove();
$('enclosingDiv').children.each(function()
{
//if delete functionality exists, change the names of the element indices using the count variable
change all indices of element properties concerned
$(this).find('*[name$='value']).attr('name','ListCollection['+count+'].sumproperty");
count++;
});
}
</script>
So you can use that for create and delete methods, you don't need partials for that.
The code might need some work as what I show is the concept
It is not that hard. Your partial views will be posted as a collection.
Suppose that your partial view has 2 values, FirstName and LastName. It should be something like this:
#{
Guid index = Guid.NewGuid();
}
<input type="hidden" name="People.index" value="#index" />
<input type="text" name="People[#index].FirstName" value="" />
<input type="text" name="People[#index].LastName" value="" />
The final output would be:
<input type="hidden" name="People.index" value="B756DAD8-5D5D-449E-A4B4-E61F75C1562C" />
<input type="text" name="People[B756DAD8-5D5D-449E-A4B4-E61F75C1562C].FirstName" value="" />
<input type="text" name="People[B756DAD8-5D5D-449E-A4B4-E61F75C1562C].LastName" value="" />
<input type="hidden" name="People.index" value="B78B7BBC-EB0E-41CB-BE18-C1E3F7526F32" />
<input type="text" name="People[B78B7BBC-EB0E-41CB-BE18-C1E3F7526F32].FirstName" value="" />
<input type="text" name="People[B78B7BBC-EB0E-41CB-BE18-C1E3F7526F32].LastName" value="" />
Your model must have a collection People object.
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Article
{
//other properties...
public ICollection<Person> People { get; set; }
}
Your Controller:
public ActionResult YourAction (Article model)
{
//...
}
Untested code, but it should work fine.
What is the significance 'label' and 'path' in the spring mvc jsp tag:
form:label path="someName"
label simply defines the text of the field within the page, for example:
<form:select path="dataVisArray"><br />
<form:option label="Select..." value=""/>
<form:options items="${dataVisArray} itemLabel="label" itemValue="value"/>
</form:select>
shows a dropdown, where the first element is "Select..." and the rest is defined in ${dataVisArray}
path links to a form backing object where you can save the input. In the example above, there would be a variable called "dataVisArray" within the backing object to save the value of the selected item once the form is submitted.
The label attribute is for displaying text corresponding to the form element for which we are using the label attribute
<label for="FirstName" >First Name :</label>
For Path attribute can be also used in validation of form element by jquery
<form:input path="name" id="name" name="name"/>
The path value name then can be used for validation purpose:
$("#ajaxForm").validate({
rules: {
name: {
required:true,
minlenght:3
},
messages: {
name : {
required : 'Enter Username',
maxlength :'Not more than 30 Charachters',
minlength :'Should be more than 3 characters'
},
Hope this helps.
In Spring MVC JSP tag lable signifies the value to be displayed as part of the tag and path is used to signify path to property for data binding.
<form:label path="company"> Enter company name: </form:label>