ASP.NET MVC Save List<T> to database - c#

I have a problem saving List items to database. When a field is to be saved(in HTTP POST Create) some of the Lists properties don't have to be saved so I have allowed nullabe for such. However there is one field that I retrieve from the Form and save it. Since the classes are complex I'll restrict the code I'll post here (I'll use one field since the Exception generated is the same).
StringValues is the Class that several List fields inherit from, such as TestPlanChecklist in this case.
public class TestPlanChecklist:StringValues
{
}
public class StringValues
{
[Key]
public int id { get; set; }
public int ChangeRequestsID { get; set; }
public string Value { get; set; }
public ChangeRequests ChangeRequests { get; set; }
}
Part of my Model class
public class ChangeRequests
{
[Required]
public List<TestPlanChecklist> TestPlanChecklist { get; set; }
[Required]
public List<PostActivityChecklist> PostActivityChecklist { get; set; }
[Required]
public List<CMBApproval> CMBApproval { get; set; }
[Required]
public List<TechnicalFeasibility> TechnicalFeasibility { get; set; }
}
In my Create view, this is the code that renders textboxes for TestPlanChecklist field
<div class="form-group">
#Html.LabelFor(model => model.TestPlanChecklist, htmlAttributes: new { #class = "control-label col-md-2" })
<div class="col-md-10">
<div>
<label class="numbers"> 1 </label>
<input type="text" class="TestPlanChecklist" name="TestPlan" />
<input type="button" value="+" class="roundButton" onclick="add('TestPlanChecklist', 'TestPlan')" />
<input type="button" value="-" class="roundButton" onclick="removeElement('TestPlan')" />
</div>
<div>
<label class="numbers"> 2 </label>
<input type="text" class="TestPlanChecklist" name="TestPlan" />
</div>
<div>
<label class="numbers"> 3 </label>
<input type="text" class="TestPlanChecklist" name="TestPlan" />
</div>
#Html.ValidationMessageFor(model => model.TestPlanChecklist, "", new { #class = "text-danger" })
</div>
</div>
</div>
And HttpPost Create method
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "TestPlanChecklist,PostActivityChecklist,PostActivityChecklist,CMBApproval,TechnicalFeasibility")] ChangeRequests changeRequests,
string[] TestPlan)
{
changeRequests.TestPlanChecklist = new List<TestPlanChecklist>();
foreach (var test in TestPlan)
changeRequests.TestPlanChecklist.Add(new TestPlanChecklist { Value = test });
//SendEmails(TechnicalFeasibility, User.Identity.Name, ChangeUrgency, Priority, DescriptionOfChange, Reason);
//SendEmails(CMBApproval, User.Identity.Name, ChangeUrgency, Priority, DescriptionOfChange, Reason);
if (ModelState.IsValid)
{
db.ChangeRequests.Add(changeRequests);
db.SaveChanges();
return RedirectToAction("List");
}
return View(changeRequests);
}
Kindly note that am just using one field to ask this question, thats why I have removed code for initializing the other List fields.
ModelState.IsValid
returns false. I realize that all the List fields have an error, which states that its impossible to typecast from System.String to the particular class. This is funny since I assign only one field value retrieved from the form and the rest are nullable which makes sense.
Where am I going wrong?
Thanks in advance.

ModelState.IsValid check validation rules, that comes from DataAnnotations in your case [Required] Attribute in ChangeRequests ViewModel.
If you want to use this validation and make some of your List properties nullable you should delete this attribute.

You can check the errors of the validation.
var errores = new List<ModelError>();
foreach (ModelState modelState in ViewData.ModelState.Values)
{
foreach (ModelError error in modelState.Errors)
{
errores.Add(error);
}
}

You get ModelState errors because ChangeRequest has [Required] attributes and gets validated by Mvc. It's unclear to me why you're using that action signature but it's a bad approach. You should rely on ViewModels and not Models directly.
public ActionResult Create(ChangeRequestViewModel viewModel)
{
if(ModelState.IsValid == false) return View(viewModel);
var changeRequest = new ChangeRequest();
foreach(var testPlan in viewModel.TestPlans) {
changeRequest.TestPlanChecklist.Add(new TestPlanChecklist { Value = testPlan }
}
// ...
}

Related

Why is Entity Framework trying to store my foreign key for a second time?

like the title says: my .SaveChanges() method is trying to insert my Property (ParameterType) again, which should not be the case. The property should just update when a user picks a different item from the Dropdownlistfor. It does not matter if you keep the old ParameterType or choose a different one in the Edit view, the compiler will 10/10 throw this error.
I have an ASP.NET Core application and using Entity Framework Core I have a setup like this:
Model
public class Parameter
{
public int ParameterId { get; set; }
public string Name { get; set; }
public string Value{ get; set; }
public ParameterType ParameterType { get; set; }
}
View ( Edit )
<form method="post">
<div class="row">
<div class="col-md-6">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label>Name</label>
<input asp-for="Name" class="form-control" required />
<label>Value</label>
<textarea asp-for="Value" class="form-control" required style="width:1250px"> </textarea>
<label>ParameterType</label>
#Html.DropDownListFor(m => m.ParameterType.ParameterTypeId, new SelectList(Model.ParameterTypes, "ParameterTypeId", "TypeName"), new { #class = "form-control" })
</div>
</div>
</div>
<div class="row">
<div class="col-md-1 col-md-offset-10">
<button class="btn btn-primary" type="submit">Confirm</button>
</div>
<div class="col-md-1">
<a asp-controller="Parameter" asp-action="Index" class="btn btn-default">Cancel</a>
</div>
</div>
</form>
Function to map EVM to Model
[HttpPost]
public IActionResult Edit(ParameterEditViewModel parameterEditViewModel, int id)
{
Parameter parameter = null;
parameter = _parameterRepository.GeefParameterMetId(id);
MapParameterEditViewModelToParameter(parameterEditViewModel, parameter);
try
{
_parameterRepository.SaveChanges();
}
catch
{
TempData["error"] = $"Edit failure";
}
TempData["message"] = $"Parameter Edit success";
return RedirectToAction(nameof(Index));
}
SaveChanges() throws this exception:
The instance of entity type 'ParameterType' cannot be tracked because another instance with the same key value for {'ParameterTypeId'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values.
UPDATE : MapParameterEditViewModelToParameter Method
private void MapParameterEditViewModelToParameter(ParameterEditViewModel parameterEditViewModel, Parameter parameter)
{
parameter.Name= parameterEditViewModel.Naam;
parameter.Value= parameterEditViewModel.Waarde;
parameter.ParameterType = parameterEditViewModel.ParameterType;
}
Kind regards,
Glanie
You will have to extend your Parameter class like this:
public class Parameter
{
public int ParameterId { get; set; }
public string Name { get; set; }
public string Value{ get; set; }
public int ParameterTypeId { get; set; } // FK to parameter type
public ParameterType ParameterType { get; set; }
}
And then adjust the binding:
private void MapParameterEditViewModelToParameter(ParameterEditViewModel parameterEditViewModel, Parameter parameter)
{
parameter.Name= parameterEditViewModel.Naam;
parameter.Value= parameterEditViewModel.Waarde;
parameter.ParameterTypeId = parameterEditViewModel.ParameterTypeId; // also change this property name and type to just carry only the Id of parameter type
}

Why am I getting the 'field is required' validation error even though the field is passed with the form?

I am writing a .NET Core 3.0 MVC Web app. I have a JobApplication model that looks like this:
public class JobApplication
{
[Key]
public int Id{ get; set; }
[Required]
public DateTime CreatedOn { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyy-MM-dd}")]
[Display(Name = "Edited on:")]
public DateTime? EditedOn { get; set; }
[DataType(DataType.Date)]
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = "{0:yyy-MM-dd}")]
[Display(Name = "Deleted on:")]
public DateTime? DeletedOn { get; set; }
[Required]
public User Applicant { get; set; }
[Required]
public JobOffer JobOffer { get; set; }
[Required]
public ApplicationStatus ApplicationStatus { get; set; }
public string CvHandle { get; set; }
public string AdditionalInformation { get; set; }
}
As you can see, the model holds references to the Job Offer it was created for and the applicant that created the application.
I also have a Controller JobApplicationController that has 2 Create methods:
public async Task<ActionResult> Create(int? id)
{
var offer = await _context.JobOffers.Include(x => x.CreatedFor).FirstOrDefaultAsync(x => x.Id == id.Value);
var user = await _context.Users.FirstOrDefaultAsync(x => x.Name == "Filip");
var model = new JobApplication()
{
JobOffer = offer,
Applicant = user
};
return View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Create([FromForm]JobApplication model)
{
if (!ModelState.IsValid)
{
return View(model);
}
JobApplication ja = new JobApplication
{
...
};
await _context.JobApplications.AddAsync(ja);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
As you can see, one of them returns the Create view, the other gets the model from the view and adds it to the database. I also emphasize that in the first method, the 'JobOffer' and 'Applicant' fields are taken from the database and passed to the form with a model. Here's how the view is set up:
#model HRpest.BL.Model.JobApplication
#{
ViewData["Title"] = "Create";
Layout = "~/Views/Shared/_Layout.cshtml";
}
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
#Html.Hidden("Offer", Model.JobOffer)
#Html.Hidden("Applicant", Model.Applicant)
<div asp-validation-summary="None" class="text-danger"></div>
<div class="form-group">
<label asp-for="CvHandle" class="control-label"></label>
<input asp-for="CvHandle" class="form-control" />
<span asp-validation-for="CvHandle" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AdditionalInformation" class="control-label"></label>
<input asp-for="AdditionalInformation" class="form-control" />
<span asp-validation-for="AdditionalInformation" class="text-danger"></span>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
It all seems fine. However, when I try to add an application and submit the form, I get an error:
{"type":"https://tools.ietf.org/html/rfc7231#section-6.5.1","title":"One or more validation errors occurred.","status":400,"traceId":"|a72c03fc-4f6501d7781e4a9a.","errors":{"JobOffer":["The JobOffer field is required."],"Applicant":["The Applicant field is required."]}}
I don't understand it. I have both fields in my model. How to make this go away?
Thank you so much in advance.
You need to use HiddenFor() Instead of Hidden().
Read here to understand the difference between the two.
I think is because required too. Because navigation property is complex object and should not be required
public virtual User Applicant { get; set; }
public virtual JobOffer JobOffer { get; set; }
public virtual ApplicationStatus ApplicationStatus { get; set; }
In the view, you are saying, #Html.Hidden("Offer", Model.JobOffer). It should instead be #Html.Hidden("JobOffer", Model.JobOffer) because that's the property name. You wouldn't make that mistake if you were using HiddenFor.
On a more abstract level, you are binding directly to the database entity. It's never a good idea. You should use a model to get the posted values and then copy those values to the entity. You can use AutoMapper to do that automatically for you.

Select Tag Helper to Display 2 Fields in the Drop Down List

Using the standard scaffolding only displays the indexes in the drop down of related tables. How to display multiple fields so the drop down list is meaningful?
I have attempted to use a solution outlined by Shyju in the section 'Getting data from your database table using entity framework'. (Select Tag Helper in ASP.NET Core MVC).
I have 2 classes in the model (Books with related Author):
namespace SimpleAppwithTwoTables.Data
{
public class Book
{
public int BookID { get; set; }
[StringLength(255)]
[Display(Name ="Book Title")]
public string Title { get; set; }
public int AuthorID { get; set; }
public Author Author { get; set; }
}
}
and
namespace SimpleAppwithTwoTables.Data
{
public class Author
{
public int AuthorID { get; set; }
[Display(Name = "First Name")]
[StringLength(50)]
public string FirstName { get; set; }
[Display(Name = "Last Name")]
[StringLength(50)]
public string LastName { get; set; }
public ICollection<Book> Books { get; set; } = new List<Book>();
}
}
The BooksController has a Create() method similar to what was described in the post from Shyju.
public IActionResult Create()
Author vm = new Author();
vm.Books = _context.Book.Select(a => new SelectListItem()
{ Value = a.AuthorID.ToString(), Text = a.Author.LastName }).ToList();
return View(vm);
}
This line in the controller
vm.Books = _context.Book.Select(a => new SelectListItem()
{ Value = a.AuthorID.ToString(), Text = a.Author.LastName }).ToList();
Generates this error:
"Cannot implicitly convert type
'System.Collections.Generic.List<Microsoft.AspNetCore.Mvc.Rendering.SelectListItem>'
to
'System.Collections.Generic.ICollection<SimpleAppwithTwoTables.Data.Book>'.
An explicit conversion exists (are you missing a cast?).
Note that I am new to C# and understand that this is a type conversion issue and that the line with the error is expecting that the Author model contains an ICollection<>.
I am looking for the code needed in the vm.books line that does not do a type conversion and works with the ICollection.
You're trying to stuff a list of SelectListItem into an property defined as a list of Book. SelectListItem quite obviously is not the same things as Book, so it fails.
The simple solution is that you need a property on your view model specifically for holding your select list options:
public IEnumerable<SelectListItem> AuthorOptions { get; set; }
You will also need something to bind the select items to, which will be primitive types, not actual Author instances:
public List<int> SelectedAuthorIds { get; set; }
In your view, then:
<select asp-for="SelectedAuthorIds" asp-items="AuthorOptions">
On post, you will then need to use that collection of ids to query the actual author objects, if you need to associate them with a book or something else:
var selectAuthors = await _context.Authors.Where(x => vm.SelectedAuthorIds.Contains(x.Id)).ToListAsync();
I followed the advice Chris Pratt provided above. It took a while to figure out but almost having working. See details below.
See the model for Book and Author above.
Created a new ViewModel:
namespace SimpleDropDownList.Models
{
[NotMapped]
public class AuthorViewModel
{
//Property to hold the list of authors in the GET
public IEnumerable<SelectListItem> AuthorOptions { get; set; }
//Property to bind the selected author used in the POST
public List<int> SelectedAuthorIds { get; set; }
}
}
Changed the Create() method on the BooksController to:
public IActionResult Create()
{
//ViewData["AuthorID"] = new SelectList(_context.Set<Author>(), "AuthorID", "AuthorID");
AuthorViewModel vm = new AuthorViewModel();
vm.AuthorOptions = _context.Book.Select(x => new SelectListItem()
{ Value = x.AuthorID.ToString(), Text = x.Author.LastName }).ToList();
return View(vm);
}
Change the Create view to:
#model SimpleDropDownList.Models.Book
#{
ViewData["Title"] = "Create";
}
<h1>Create</h1>
<h4>Book</h4>
<hr />
<div class="row">
<div class="col-md-4">
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<label asp-for="Title" class="control-label"></label>
<input asp-for="Title" class="form-control" />
<span asp-validation-for="Title" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="AuthorID" class="control-label"></label>
#*<select asp-for="AuthorID" class ="form-control" asp-items="ViewBag.AuthorID"></select>*#
<select asp-for="#Model.AuthorViewModel.SelectedAuthorIds" asp-items="#Model.AuthorViewModel.AuthorOptions"></select>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-primary" />
</div>
</form>
</div>
</div>
<div>
<a asp-action="Index">Back to List</a>
</div>
#section Scripts {
#{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}
Model: AuthorViewModel
When I run this and click the Create button the following error is generated:
An unhandled exception occurred while processing the request.
InvalidOperationException: The model item passed into the
ViewDataDictionary is of type
'SimpleDropDownList.Models.AuthorViewModel', but this
ViewDataDictionary instance requires a model item of type
'SimpleDropDownList.Models.Book'.
I'm flagging this question as completed and have created a new question for the issue describe just above. See here: Error when Razor page opens and have a related drop down list

MVC 5 remote validation not fired

I'm trying to implement remote validation in MVC. I have read several tutos and questions already posted here, but there is no answer.
Controller :
public class GroupsController: Controller
{
[HttpPost]
public ActionResult TestRemoteValidation(string Name)
{
return Json(false);
}
}
View :
#using (Html.BeginForm("Index", "Defaults", FormMethod.Post))
{
#Html.TextBoxFor(model => model.Name, new { htmlAttributes = new { #class = "form-control" } })
#Html.ValidationMessageFor(model => model.Name, "", new { #class = "text-danger" })
<input type="submit" class="btn btn-primary" value="Enregistrer" />
}
And Model :
public class Group
{
[Key]
public int Id { get; set; }
[Display(Name = "Nom du Groupe")]
[Required]
[Remote("TestRemoteValidation", "Groups", HttpMethod = "POST", ErrorMessage = "Remote fired")]
//[CustomRemoteValidation("TestRemoteValidation", "Groups", AdditionalFields = "Id")]
public string Name { get; set; }
public virtual ICollection<ApplicationUser> ApplicationUsers { get; set; }
}
Generated HTML Code:
<input data-val="true" data-val-remote="Remote fired" data-val-remote-additionalfields="*.Name" data-val-remote-type="POST" data-val-remote-url="/Groups/TestRemoteValidation" data-val-required="Le champ Nom du Groupe est requis." htmlAttributes="{ class = form-control }" id="Name" name="Name" type="text" value="" />
I'm using Metadata because it's an entity-->not the problem, I checked with an other ViewModel and it's the same.
[Required] and [StringLength(10)] are fired.
When I put a breakpoint in TestRemoteValidation, nothing happens.
For instance I'm able to perform the remote validation with a custom remote attribute class and Model.IsValid override, but I don't understand why this way doesn't work. Any idea?
Assuming Vehicule is the #model being used in the view
#model Vehicule
then the controller should expected that model
public class DefaultsController : Controller {
[HttpGet]
public ActionResult Index() {
var model = new Vehicule();
return View(mdoel);
}
[HttpPost]
public ActionResult Index(Vehicule model) {
if(ModelState.IsValid) {
//...do something
//..possible redirect
}
//if we get this far something is wrong with data
return View(model);
}
}
The model binder will take validation into account when binding the model from the request.
add
[AllowAnonymous]
to
[HttpPost]
public ActionResult TestRemoteValidation(string Name)
Please make sure that you have included below libraries and in correct order in your view
<script src="~/scripts/jquery.js"></script>
<script src="~/scripts/jquery.validate.js"></script>
<script src="~/scripts/jquery.validate.unobtrusive.js"></script>
these libraries are required for remote validation to work.

ViewModel data is all null when passed to my controller? [duplicate]

This question already has answers here:
Asp.Net MVC: Why is my view passing NULL models back to my controller?
(2 answers)
Closed 6 years ago.
I'm trying to follow best practices to add data validation to my UI. I want to add the data validation to the ViewModel and then if that data is valid, submit the form to the controller. However, the data the controller receives is always null values. Any idea what I'm doing wrong? This whole MVVC architecture is confusing me. I had it working when I was submitting the form using the model but I can't get data validation on the model?
Controller:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> CreateResource(AddResourceViewModel model)
{
if (ModelState.IsValid)
{
await (//does something);
}
return View();
}
ModelView:
public class AddResourceViewModel
{
public string User { get; set; }
public string ResourceName { get; set; }
public int ResourceId { get; set; }
public string Model { get; set; }
public float Latitude { get; set; }
public float Longitude { get; set; }
public decimal Amount { get; set; }
public int UnitId { get; set; }
public int PrimaryEsfId { get; set; }
public IEnumerable<string> Capabilities { get; set; }
public IEnumerable<int> AdditionalEsfs { get; set; }
public Resource Resource { get; set; }
}
Beginning of cshtml form:
#model erms.ViewModel.AddResourceViewModel
<form asp-controller="AddResource" asp-action="NewResource" method="post" class="form-horizontal">
<div class="panel panel-default">
<div class="panel-body">
<form asp-controller="AddResource" asp-action="CreateResource" method="post" class="form-horizontal">
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ResourceId" class="col-md-2 control-label">Resource ID:</label>
<div class="col-md-10">
<input asp-for="ResourceId" class="form-control" value="#Model.Resource.ResourceId" readonly />
<span asp-validation-for="ResourceId" class="text-danger"></span>
</div>
</div>
replace return View(); to return View(model);.
This kind of stuff makes me so frustrated when learning new architecture but I've figured it out. It is a naming convention issue. I've addressed the issue and it is now working properly:
The conflicting names were:
public string Model { get; set; }
from my ViewModel, and:
public async Task<IActionResult> NewResource(AddResourceViewModel model)
from my controller. So the Model is conflicting with the model...
According to: http://ideasof.andersaberg.com/development/aspnet-mvc-4-model-binding-null-on-post
Do not name your incoming variables in the Action the same as you do in the model being posted. That will mess up the Model Binder.
Perhaps I'm thinking the problem would be you're not using razor to submit the form. This is what I have in mind:
#model <AddResourceViewModel>
#using(Html.BeginForm()) {
<div asp-validation-summary="All" class="text-danger"></div>
<div class="form-group">
<label asp-for="ResourceId" class="col-md-2 control-label">Resource ID:</label>
<div class="col-md-10">
#Html.TextboxFor(x => x.ResourceId)
#Html.ValidationMessageFor(x => x.ResourceId)
</div>
</div>
}
Since this is what I usually use to validate my form, perhaps this is worth considering. I might be wrong though.

Categories

Resources