I have the following 2 entities (omitting irrelevant fields for brevity) :
public class FacilityModel
{
public long Id { get; set; }
[Required]
public InterfaceModel Interface { get; set; }
}
public class InterfaceModel
{
public long Id { get; set; }
[Required]
[StringLength(50, ErrorMessage = "Name must be shorter than 50 characters")]
public string Name { get; set; }
}
I have the following EditForm in my AddFacility page :
<EditForm Model="#_facility" OnValidSubmit="#(e => {if (_edit) UpdateFacility(); else InsertFacility(); })">
<DataAnnotationsValidator />
<ValidationSummary />
<div class="row">
<div class="col-md-8">
<div class="form-group">
<label for="intfc" class="control-label">Interface</label>
<InputSelect id="intfc" class="form-control" #bind-Value="_facility.Interface">
#foreach (var intfc in Task.Run(() => _interfaceClient.GetAllInterfaces()).Result)
{
<option value="#intfc">#intfc.Name</option>
}
</InputSelect>
</div>
</div>
</div>
<div class="row">
<div class="col-md-4">
<div class="form-group">
<input type="submit" class="btn btn-primary" value="Save" />
<input type="button" class="btn btn-primary" #onclick="#Cancel" value="Cancel" />
</div>
</div>
</div>
The page correctly displays a list of interfaces in the respective dropdown but, no matter what I do, the form validation comes up with a "The Interface field is required." message. What am I doing wrong, why is the value of the selected interface not saved correctly in the backing model object?
EDIT
I moved the code that pulls the list of interfaces to the OnInitializedAsync() method, thanks for the tip! I tried changing the key to #intfc.Id in my InputSelect field to no avail. I don't see how I can wire anything into the validation process since my understanding is that it happens client-side directly in the form? I'll be happy to be wrong here, can I write server-side code to handle DataAnnotationsValidator myself?
Related
I have a question about model binding in ASP.NET Core Razor pages. I've spent a lot of time reading the Microsoft documentation, Stackoverflow posts and learnrazorpages.com, but haven't quite found the answer I'm looking for. So below is a stripped down version of three classes I have. As you can see, both Client and ReferralPerson contain Person objects and Client also contains a ReferralPerson object. On my razor page (the data entry form), I intentionally do not show all of the fields for the Person object that is part of the ReferralPerson. There are also other properties of the ReferralPerson object that are not shown such as NumberOfReferrals. If a user recalls a record, modifies some of the data and then saves (Post), only those properties that I have included inside of the element end up being populated in the model in the OnPost (OnPostSave). I understand why this is. My question is do I have to include every single property, individually on the page in order for those properties to be populated in the OnPost event? If you look at my Razor page code below, you will see that I add a number of hidden fields at the top of the element. Do I need to do that for every single property that isn't visible or included somewhere else inside that form? Ideally I would just be able to do something like the following to include the entire object versus having hidden fields for every single property. I just want to make sure there isn't something I'm not aware of in terms of how this can or should be handled.
<input type="hidden" asp-for="Client.ReferralPerson" />
Here are the C# classes (model)
public class Person
{
public int ID { get; set; }
[Required, StringLength(50)]
public string FirstName { get; set; } = "N/A";
[Required, StringLength(50)]
public string LastName { get; set; } = "N/A";
[StringLength(30)]
public string PhoneNumberPrimary { get; set; }
[StringLength(30)]
public string PhoneNumberSecondary { get; set; }
[StringLength(50)]
public string Address { get; set; }
[StringLength(50)]
public string EmailAddress { get; set; }
}
public class Client
{
public int ID { get; set; }
public Person Person { get; set; }
public DateTime InitalContactDate { get; set; }
[Required, StringLength(100)]
public string ReasonForVisit { get; set; }
public ReferralPerson ReferralPerson { get; set; }
//other properties removed for brevity
}
public class ReferralPerson
{
public int ID { get; set; }
public Person Person { get; set; }
[StringLength(100)]
public string BusinessName { get; set; }
public int NumberOfReferrals { get; set; }
}
Here's a simplified version of my Razor page:
<form class="needs-validation p-2" novalidate="" method="post">
<input type="hidden" asp-for="Client.ID" />
<input type="hidden" asp-for="Client.Person.ID" />
<input type="hidden" asp-for="Client.ContactType.ID" />
<input type="hidden" asp-for="Client.ReferralPerson.ID" />
<input type="hidden" asp-for="Client.ReferralPerson.Person.ID" />
<div class="row mb-3">
<div class="col-sm-6">
<label asp-for="Client.Person.FirstName" class="form-label">First name</label>
<input type="text" class="form-control" asp-for="Client.Person.FirstName">
<span asp-validation-for="Client.Person.FirstName" class="text-danger font-weight-bold"></span>
</div>
<div class="col-sm-6">
<label asp-for="Client.Person.LastName" class="form-label">Last name</label>
<input type="text" class="form-control" asp-for="Client.Person.LastName">
<span asp-validation-for="Client.Person.LastName" class="text-danger font-weight-bold"></span>
</div>
</div>
<div class="row mb-3">
<div class="col-sm-6">
<label for="type" class="form-label stacked-justify-left">Date</label>
<input type="date" class="form-control" asp-for="Client.InitalContactDate">
<span asp-validation-for="Client.InitalContactDate" class="text-danger font-weight-bold"></span>
</div>
<div class="col-6">
<label for="email" class="form-label">Email <span class="text-muted">(Optional)</span></label>
<input type="email" class="form-control" placeholder="you#example.com" asp-for="Client.Person.EmailAddress">
</div>
</div>
<div class="row mb-3">
<div class="col-6">
<label for="primaryPhone" class="form-label">Phone # (Primary)</label>
<input type="text" class="form-control" placeholder="Phone Number" asp-for="Client.Person.PhoneNumberPrimary">
</div>
<div class="col-6">
<label for="secodaryPhone" class="form-label">Phone # (Secondary)</label>
<input type="text" class="form-control" placeholder="Phone Number" asp-for="Client.Person.PhoneNumberSecondary">
</div>
</div>
<div class="row mb-3">
<div class="col-12">
<label for="address" class="form-label">Address</label>
<input type="text" class="form-control" placeholder="1234 Main St" asp-for="Client.Person.Address">
</div>
</div>
<div class="row mb-3" id="ReferralPersonDetails" style="display:none">
<div class="col-sm-6">
<label asp-for="Client.ReferralPerson.Person.FirstName" class="form-label">Referral By First name</label>
<input type="text" class="form-control" asp-for="Client.ReferralPerson.Person.FirstName">
</div>
<div class="col-sm-6">
<label asp-for="Client.ReferralPerson.Person.LastName" class="form-label">Referral By Last name</label>
<input type="text" class="form-control" asp-for="Client.ReferralPerson.Person.LastName">
</div>
</div>
<hr class="my-4">
<div class="row mb-3 ml-1 mr-1">
<hr class="my-4">
<button type="submit" class="w-100 btn btn-primary btn-lg" asp-page-handler="Save">Save</button>
</div>
</form>
Model binding is based on the name of properties in model, If you don't use
<input asp-for="xxx">
to bind the specified property, After submitting the form, These properties will not be populated in post method. So if you don't bind all properties, those that are not bound will not be populated in the post method.
But in your data form, I think you just want to let the user modify some properties. So in my opinion, You can create a viewmodel to include the properties whitch you want to change, In this method you don't need to bind all the properties in the form. Then in your controller, You just need to replace the original data with the data in the viewmodel.
You don't need to do the same with all id fields. I guess you are trying to edit the information of Client and ReferralPerson, adding the .Person.ID fields is not necessary because surely your database already has information about Person in each Client or ReferralPerson, get it out in controller process this action then edit it with data from the form.
I have the following model, which needs to be created via a page:
public class Exercise
{
public Guid Id{ get; set; }
public string Name { get; set; }
public string Description{ get; set; }
public ICollection<ExerciseCategory> Categories
...
public ICollection<Link> Links { get; set; }
}
public class ExerciseCategory
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public class Link
{
public string Name { get; set; }
public string Value { get; set; }
}
I have a view that lets me create this object, but I miss a form control for the property Exercise.Links:
<form id="profile-form" method="post">
<div class="row">
<div class="col-12 p-sm-2 p-md-3 p-lg-5 bg-custom-white rounded-4">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-floating mt-2 mb-2">
<input asp-for="Input.Name" class="form-control" />
<label asp-for="Input.Name" class="form-label"></label>
<span asp-validation-for="Input.Name" class="text-danger"></span>
</div>
<div class="form-floating mt-2 mb-2">
<input asp-for="Input.Description" class="form-control" />
<label asp-for="Input.Description" class="form-label"></label>
<span asp-validation-for="Input.Description" class="text-danger"></span>
</div>
<div class="form-floating mt-2 mb-2">
<select asp-for="Input.CategoryIds"
asp-items="Model.Input.Categories" class="form-control"></select>
<label asp-for="Input.Categories" class="form-label"></label>
</div>
<button id="create-exercise-button" type="submit" class="w-100 btn btn-lg btn-primary">Create</button>
</div>
</div>
</form>
The property Exercise.Categories is easy as Category is an entity which is created separately, so I just have a dropdown listing the existing categories.
Link is just a value object that is not persisted on its own. Therefore, the user should be able to create/remove/edit Link from the Exercise create/edit page.
I have tried the following:
#for (var i = 0; i < Model.Input.Links.Count; i++)
{
<div>
<input asp-for="Input.Links[i].Name">
<input asp-for="Input.Links[i].Value">
</div>
}
Which results in the following HTML:
<div>
<input type="text" id="Input_Links_0__Name" name="Input.Links[0].Name" value="some link">
<input type="text" id="Input_Links_0__Value" name="Input.Links[0].Value" value="https://somelink.com">
</div>
<div>
<input type="text" id="Input_Links_1__Name" name="Input.Links[1].Name" value="some link1">
<input type="text" id="Input_Links_1__Value" name="Input.Links[1].Value" value="https://somelink1.com">
</div>
But this doesn't populate the Input.Links property when posting back.
I started thinking that the problem is the fact that my list is a list of Link rather than a list of primitive types. In order to test this, I added a test List<string> to input model called Links1 and tried to populate that list as I did before:
<div class="form-floating mt-2 mb-2">
<input asp-for="Input.Links1[i]" data-val="true" class="form-control" />
<label asp-for="Input.Links1[i]" class="form-label"></label>
</div>
This works. I get the populated list back in the controller after submitting the form, which confirms the problem is the List<Link>
How can I add a form control for populating the property Exercise.Link (which is a List<Link>)?
After reading this answer to another question, I figured out what the problem was:
The class Link, on which the property Exercise.Links depends, has no setter for its Link.Name and Link.Value properties, so the binder cannot populate them when you submit the form.
This is due to Link being a domain model. The solution in this case was to create a presentation layer model that has public {get; set;} and use that on the form instead of the domain model.
So, to answer the question properly, this is how you could write a form control to edit a List<Link>:
#for (var i = 0; i < Model.Input.Links.Count; i++)
{
<div>
<input asp-for="Input.Links[i].Name">
<input asp-for="Input.Links[i].Value">
</div>
}
I have a Complex Model like this
public class MyModel
{
[ValidateComplexType]
public Student Student { get; set; } = new Student();
}
public class Student
{
[Required]
public string Name { get; set; }
[Required]
public string Subject { get; set; }
}
and my blazor code is like this
<EditForm Model="this.Model" OnValidSubmit="Submit" #ref="this.myEditForm">
<ObjectGraphDataAnnotationsValidator />
<div class="form-group row">
<label for="name" class="col-md-2 col-form-label">Name</label>
<div class="col-md-10">
<InputText id="name" class="form-control" #bind-Value="this.Model.Student.Name" />
<ValidationMessage For="#(() => this.Model.Student.Name)" />
</div>
</div>
<div class="form-group row">
<label for="supplier" class="col-md-2 col-form-label">Subject</label>
<div class="col-md-10">
<InputText id="supplier" class="form-control" #bind-Value="this.Model.Student.Subject" />
<ValidationMessage For="#(() => this.Model.Student.Subject)" />
</div>
</div>
<div class="row">
<div class="col-md-6 text-right">
<button type="button" class="btn btn-success" #onclick="Validate">Validate</button>
</div>
<div class="col-md-6 text-right">
<button type="submit" class="btn btn-success">Submit</button>
</div>
</div>
#code
{
private MyModel Model { get; set; } = new MyModel();
private EditForm myEditForm { get; set; }
private void Validate()
{
this.myEditForm.EditContext.NotifyFieldChanged(
this.myEditForm.EditContext.Field(nameof(this.Model.Student.Name)));
}
private void Submit()
{
}
}
I want validate a single field on Validate button click. If my model is non complex type, above code works fine. Since this is a complex type, above code is not working. Is i am doing any thing wrong here?
Your problem is that you are trying to valid a field that you've never updated. On single field validation it looks like ObjectGraphDataAnnotationsValidator is checking if the field has an entry in the Fields collection in EditContext to verify that some form of modification has taken place i.e. trying to be clever!. If you want what your trying to do then consider using a different validator. There are far better ones out there. Search Blazor Validation Controls. Amongst others there one by Blazorize and one I've written (and you can get the source code for and modify).
I have form in Razor Page look like this (i remove not needed data):
<form class="form-group form-popup"
asp-action="Update"
data-ajax="true"
data-ajax-method="POST">
#Html.AntiForgeryToken()
<div class="popup-section">
<partial name="_FailureCostPartial" model="#Model.CostEdit" />
</div>
<div class="text-center mt-5 popup-edit-buttons">
<button class="btn form-success" type="button" onclick="SubmitForm('#popupId')">
#Localizer.GetLocalizedString("button_Save")<i class="fas fa-check fa-fw"></i>
</button>
<button class="btn form-danger" type="button" data-dismiss="modal">
#Localizer.GetLocalizedString("button_Cancel")<i class="fas fa-times fa-fw"></i>
</button>
</div>
</form>
Model put to this view look like this:
public class FailureEditViewModel
{
public FailureForEditDto Failure { get; set; }
public FailureCostEditViewModel CostEdit { get; set; }
public List<SelectListItem> Suppliers { get; set; }
}
And _FailureCostPartial Look like this:
<div class="details-form-field">
<label class="col-sm-5">#Localizer.GetLocalizedString(nameof(Model.WorkingHoursCost))</label>
<div class="input-container col-sm-7">
<input id="Failure_WorkingHoursCost" asp-for="WorkingHoursCost" value="#Model.WorkingHoursCost" />
<span asp-validation-for="WorkingHoursCost" class="text-danger"></span>
</div>
</div>
<div class="details-form-field">
<label class="col-sm-5">#Localizer.GetLocalizedString(nameof(Model.GeneralCost))</label>
<div class="input-container col-sm-7">
<input id="Failure_GeneralCost" asp-for="GeneralCost" value="#Model.GeneralCost" />
<span asp-validation-for="GeneralCost" class="text-danger"></span>
</div>
</div>
<div class="details-form-field">
<label class="col-sm-5">#Localizer.GetLocalizedString(nameof(Model.TravelCost))</label>
<div class="input-container col-sm-7">
<input id="Failure_TravelCost" asp-for="TravelCost" value="#Model.TravelCost" />
<span asp-validation-for="TravelCost" class="text-danger"></span>
</div>
</div>
<div class="details-form-field">
<label class="col-sm-5">#Localizer.GetLocalizedString(nameof(Model.TotalFailureCost))</label>
<div class="input-container col-sm-7">
<input disabled="disabled" id="Failure_TotalFailureCostField" asp-for="TotalFailureCost" value="#Model.TotalFailureCost" />
<span asp-validation-for="TotalFailureCost" class="text-danger"></span>
</div>
</div>
And model for that partial is:
public class FailureCostEditViewModel
{
public decimal WorkingHoursCost { get; set; }
public decimal GeneralCost { get; set; }
public decimal TravelCost { get; set; }
public decimal TotalFailureCost { get; set; }
}
Now when i set value in input on my partial and i try submit form (from first view) to my Update method in controller back :
What should i do to correct back data from partial for field model.CostEdit?
For each property of the complex type, model binding looks through the sources for the name pattern prefix.property_name. If nothing is found, it looks for just property_name without the prefix.For your receive model is a nested model,so you need prefix to distinguish the property.
Try to add the name to the input element like:name="CostEdit.WorkingHoursCost" in your _FailureCostPartial.cshtml.
I am creating a web app using asp.net mvc.
I used the default scaffolding to create the controller and the view (I have user table and game table ofc database is created beforehand)
when I try to create new entity in the game table using the default create form it doesn't submit the data.
when I do the same in the user table it works fine.
the problem is that its the default code generated by visual studio and I'm not able to figure out what could be the cause.
this is the controller create action :
// GET: Games/Create
public IActionResult Create()
{
return View();
}
// POST: Games/Create
// To protect from overposting attacks, enable the specific properties you want to bind to, for
// more details, see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("ID,Name,ReleaseDate,Genre,ageRestriction,Developer,Description")] Game game)
{
if (ModelState.IsValid)
{
_context.Add(game);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(game);
}
this is the create view :
#model GameSense.Models.Game
#{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Game</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="Name" class="control-label"></label>
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ReleaseDate" class="control-label"></label>
<input asp-for="ReleaseDate" class="form-control" />
<span asp-validation-for="ReleaseDate" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Genre" class="control-label"></label>
<input asp-for="Genre" class="form-control" />
<span asp-validation-for="Genre" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="ageRestriction" class="control-label"></label>
<input asp-for="ageRestriction" class="form-control" />
<span asp-validation-for="ageRestriction" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="Developer" class="control-label"></label>
<input asp-for="Developer" class="form-control" />
<span asp-validation-for="Developer" 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">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
and this is the model for "game" :
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;
namespace GameSense.Models
{
public class Game
{
public int ID { get; set; }
[Required]
public string Name { get; set; }
[Required]
[DataType(DataType.Date)]
public DateTime ReleaseDate { get; set; }
[Required]
public string Genre { get; set; }
[Required]
public int ageRestriction { get; set; }
[Required]
public string Developer { get; set; }
[Required]
public Coordinates DeveloperLocation { get; set; }
[Required]
public string Description { get; set; }
}
}
as I said, its very "defaulty" i just want to see that everything works before i continue..
any ideas on what might be going on and why i cant create new "Game" items in the database using the default form?
You've set the Coordinates property as Required in your model, but you aren't asking the user to enter any value(s) for that in the form. So it will always be null (and thus always fail validation).
You can either remove the [Required] attribute from the property, or remove the property from the model entirely, or build some UI to allow the field to be populated by the user.