Why select asp-for has no value in post handler - c#

I have this form on my page. It's for adding some project to DB. Project requires director for itself, so i pass SelectList of workers to this form.
<form asp-controller="Project" asp-action="Edit" method="post">
...some other fields...
#if (ViewBag.workers != null)
{
<div>
<label asp-for="director">Director</label>
<select asp-for="director" asp-items=#ViewBag.workers>
</select>
<span asp-validation-for="director"></span>
</div>
}
...
</form>
Select tag working, there are my workers. But when i'm trying to submit, i have error "The director field is required."
I've checked form with js and it has data from select option, but my post handler don't.
There is code of hadler
[HttpPost]
public IActionResult Edit(Project model)
{
// here model hasn't director field
if (ModelState.IsValid)
{
dataManager.project.SaveProject(model);
return RedirectToAction(nameof(HomeController.Index), nameof(HomeController).Replace("Controller", ""));
}
ViewBag.workers = new SelectList(dataManager.worker.GetAllWorkers(), nameof(Worker.id), nameof(Worker.name));
return View(model);
}
And Project class code
public class Project
{
[Required] public Guid id { set; get; }
[Required] public string name { set; get; }
public string customer { set; get; }
public string executor { set; get; }
public Worker director { set; get; }
public DateTime start { set; get; }
public DateTime end { set; get; }
public uint priority { set; get; }
}

Post the solution mentioned in the comment as the answer post as the reference for the future reader.
Issue & Concern
From this line:
ViewBag.workers = new SelectList(dataManager.worker.GetAllWorkers(), nameof(Worker.id), nameof(Worker.name));
You are setting the value of the drop-down list option as id. Unsure what is the id type, (possibly it is an int, Guid type) but I am sure that it is not a Worker (object) type.
While in the Project model, you specify director as Worker type.
public class Project
{
...
public Worker director { set; get; }
}
Since an int/Guid type is unmatched with the Worker type, the API action is unable to bind the value that you passed from the View to the model. Thus, you will get Project as null in the API action.
Solution
Would suggest that modify the property type in Project model as int/Guid (depending on your Worker ID type in the database) rather than using the Worker (object) type. And remove the Worker property as it is no longer needed and avoid the error in ModelState due to the value is not provided.
Note: If this Project model is the entity model that was generated (scaffolded) by Entity Framework, would suggest creating another class that acts as the DTO (Data Transfer Object). Then map the received DTO to entity model.
public class Project
{
...
public Worker directorId { set; get; }
}
In the View, bind the drop-down list with asp-for="workerId" to pass the selected value as workerId.
<select asp-for="directorId" asp-items=#ViewBag.workers></select>
Back-end side
3.1. Key point: Query the Worker object with the received project.workerId. Then bind the Worker object to the entity model before inserting.
3.2. (If implementing the DTO as mentioned in 1) You need to implement the logic (or look for mapping library such as AutoMapper) to map from DTO to the entity model.

Related

Use model in c# without a database?

I am relatively new to c# and ASP.NET MVC 5, but I was hoping for some help. I have an application where I have a class with about 20 variables. It has some Guid, some List<string>, int, string, etc. relatively simple. My controller will be passed this class, and then I need to send the data from the class down to the view, and then get it back to the controller on a form submit. Everywhere I see tutorials on how to use a model with entity framework, but nowhere have I found anything on just using a model without a database. Is it possible, (and if so, how) to use model without a database? If it is not possible, what other ways could I go about using this? I thought about a session variable, but that seemed like a lot of overhead per user, i'm kind of stumped here. Thanks in advance :)
Here's an example of how you use models in MVC...
The Controller
public class ExampleController : Controller
{
public ActionResult Test()
{
TestViewModel model = new TestViewModel
{
Id = Guid.NewGuid().ToString(),
Name = "Foo bar"
};
return this.View(model);
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Test(TestViewModel model)
{
if (!this.ModelState.IsValid)
{
return this.View(model);
}
return this.Content("Success");
}
}
The View Model
public class TestViewModel
{
[Required]
public string Id { get; set; }
[Required]
public string Name { get; set; }
}
The View
#model TestViewModel
#{
Layout = "~/Views/Shared/_Layout.cshtml";
}
#using (this.Html.BeginForm())
{
#(this.Html.ValidationSummary(false))
#(this.Html.AntiForgeryToken())
#(this.Html.EditorFor(model => model.Id))
#(this.Html.ValidationMessageFor(model => model.Id))
#(this.Html.EditorFor(model => model.Name))
#(this.Html.ValidationMessageFor(model => model.Name))
<input type="submit" value="Submit!"/>
}
Also by default MVC generates your routing table based on a simple naming convention, in this instance,navigate to http://yoursite/example/test to see this example in action
You do not need a database to have models. Models just define the data to pass back and forth, so a database is definitely not necessary. In fact, the model should ideally be database agnostic. For instance, you might have a CustomerOrder model and Product model.
public class CustomerOrder
{
public int Id {get; set;}
public List<Product> Products {get; set;}
public int CustomerId {get; set;}
}
public class Product
{
public int Id {get; set;}
public string Name {get; set;}
public string Description {get; set;}
public double Price {get; set;}
}
You probably will need some sort of data storage (flat files, relational database, non-relational database, web based storage) but depending on the data you want to pass, it came from anywhere.
mvc models are pure c# objects to cary your information between controller and view. So you can use them independent from data base.
If you later need your information be persist you can use EntityFramework code first or serialize them in a flat file.

Posting objects containing other models

I've been searching for a way to post all the information of a model which contains other models and I believe I can just send the object to my view and go off of the 50 examples I've looked at and can render everything just fine.
Here's my model I'm talking about named Equipment.
public int id { get; set; }
public String name { get; set; }
public ManufacturerItem manufacturerItem { get; set; }
public EquipmentType equipmentType { get; set; }
public SupportItem supportItem{ get; set; }
public Placement placement{ get; set; }
public Boolean status { get; set; }
public DateTime endOfLife{ get; set; }
public String notes{ get; set; }
public Purchase purchase{ get; set; }
public Boolean mes{ get; set; }
public DateTime reviewedDate{ get; set; }
Based on the tons of examples I've read I know I can render these like this:
#Html.EditorFor(model => model.name)
#Html.EditorFor(model => model.manufacturerItem.model.name)
In other research I did stumble upon building forms for deep View Model graphs in ASP.NET MVC which I may consider in using, but that was posted back in MVC 2 days. I'm using MVC 5. So I don't know how relative that is today.
So let's say I have another model named Book with {id, Title, Author} and you could edit the book name and author. Now in this model, on edit, my controller could be as such:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit([Bind(Include="ID,Title,Author)"] Book book)
{ ... -insert code- ...}
Going off of this idea, what would be my controller method signature be for the Equipment model? Do I include the other objects as their own types?
I'm not using EF or linq-to-sql because I have to use stored procedures. So I want to get all this information neatly packaged and passed off to the repository that will take care of parameter assignment and calling of the stored procedure.
Going off of this idea, what would be my controller method signature
be for the Equipment model?
Have you tried using the following signature:
[HttpPost]
public ActionResult Edit(Equipment model)
{
...
}
By the way if your view doesn't contain a form allowing to edit all the properties of the Equipment model object graph you may consider using a view model containing only the properties that are included as input fields in your form. Then on the server you will get the corresponding Equipment instance from your backend using the id, update only the properties that were sent from the HTML form and save the results back.
For example:
[HttpPost]
public ActionResult Edit(EquipmentViewModel model)
{
Equipment equipement = backend.GetById(model.Id);
// set the properties that are coming from the UI:
equipment.name = model.Name;
equipment.supportItem = model.SupportItem;
...
// save the updated entity back
backend.Update(equipment);
}
In this example the EquipmentViewModel will contain only the properties that you have corresponding input fields in your view and which the user is supposed to edit and not the entire domain model object graph.

Checkbox with inverted binding

I have two entities, Customer and User as follows:
public class User
{
[Key]
public int Id { get; set; }
[StringLength(20)]
public string CustomerId { get; set; }
public string Password { get; set; }
public bool Locked { get; set; }
[ForeignKey("CustomerId")]
public virtual Customer Customer { get; set; }
}
public class Customer
{
[Key]
[Column("Id", TypeName = "nvarchar")]
[StringLength(20)]
public string Id { get; set; } // nvarchar(20)
[Required]
public string GivenName { get; set; } // nvarchar(100)
[Required]
public string Surname { get; set; } // nvarchar(100)
public virtual ICollection<User> Users { get; set; }
}
I have a simple strong typed view for editing a customer, and I want to add to the view a check-box with following logic - the check-box should be selected, when there is at least one user for that customer and the Locked property of the first user is set to false. I just can't find a way to accomplish this. What's the proper way to do this in MVC? And how the processing method (the [HttpPost]Edit) receives the value of this check-box, currently it simply gets the Customer object? Should I create an additional model for this view? Or there is another way?
Anticipating this question I should say that I'm taking care that there wont be more than one user for a customer.
Updates:
I've added a view model for customer and updated the edit view and the controller to work with this model:
public class CustomerViewModel
{
public Model.Data.Customer BaseCustomer { get; set; }
public bool HasActiveUser { get; set; }
}
My edits saving method looks now like this:
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(ViewModel.Data.Customer customer)
{
if (ModelState.IsValid)
{
//db.Entry(customer.BaseCustomer).Collection("Users").Load();
db.Entry(customer.BaseCustomer).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Index");
}
ViewBag.CustomerTypeId = new SelectList(db.CustomerTypes, "Id", "Name", customer.BaseCustomer.CustomerTypeId);
return View(customer);
}
The only question remains is how do I access the Users navigation property which is null, I've tried to reload it but got an InvalidOperationException with error that reads Member 'Load' cannot be called for property 'Users' because the entity of type 'Customer' does not exist in the context. To add an entity to the context call the Add or Attach method of DbSet<Customer>. I've also tried to get the Customer again with Customer baseCustomer = db.Customers.Find(customer.Id); but then I can't set db.Entry(customer.BaseCustomer).State = EntityState.Modified; since it tells me that An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key. Any ideas, please?
I decided to go with creating a dedicated view model that will contain the domain model object(as suggested as second pattern ASP.NET MVC View Model Patterns by Steve Michelotti) and an additional property for binding to my check-box. Then in the controller I handle all the logic regarding when to show the check-box selected, and when to create a new user if one not exists. I've encountered several problems so I want to post my solutions, maybe they are far from best-practices but I think they might be of use to beginners, and I definitely would like to see comments or other solutions.
Objects aren't persisted if they don't participate in view since EF recreates them on post-back from the data received from the view. This prevented me from adding a User property that will be an accessor for the first User in navigation collection Users property of Customer (when I added it, it couldn't access Users since this property is null after post-back, as I understood this is because of the recreated Customer is detached from context).
In order to be able to use navigation properties I had to attach the recreated(by EF, as explained earlier) Customer object to the context by setting db.Entry(customer.BaseCustomer).State = EntityState.Modified;(thanks to Using DbContext in EF 4.1 Part 4: Add/Attach and Entity States ยง Attaching an existing but modified entity to the context) and to reload the collection by calling db.Entry(customer.BaseCustomer).Collection("Users").Load();

Null values on model after sending model to controller

I'm using .net MVC.
I have some values in a form like:
#Html.TextBoxFor(model => model.Name, new { #class = "form-control" })
On the controller, I get the data like:
public ActionResult NovaPessoa(Person person)
{
The problem is that I just can get values that I have placed the #Html.TextBoxFor markup.
All the other complex information, like person.ContactInformation is lost after submiting and I can't use the SaveChanges in Entity Framework, because it will give me an invalid object after using the Atach method.
The question is: Do I need to use the #Html.TextBoxFor markup for all my model properties, even if I'm not using then to display anything, just to have them on Controller?
You are correct. What people (incorrectly) do normally, is use HiddenFor:
#Html.HiddenFor(model => model.ContactInformation)
What you should be doing, is cutting down your model into a view model with only the appropriate properties.
So, don't use this model:
public class PersonVM {
public int PersonId { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
public string ContactInformation { get; set; }
}
..if all you're doing is updating the contact information. Instead, create a new class for your model:
public class PersonContactInfoEditVM {
public int PersonId { get; set; }
public string ContactInformation { get; set; }
}
That's all you need. This saves you from creating invalid objects when you don't add 30 HiddenFor elements to your page .. resulting in very broken data.
You might be thinking "ugggghhhh, all that manual mapping from PersonContactInfoEditVM to Person... I don't want to be writing that sort of code". No one does.. which is why the following libraries exist:
AutoMapper
ValueInjector

ASP.NET MVC Exclude Data Annotation Attribute in Html.ValidationMessageFor

I use the EF-CF, and have the following entities:
Countries, Companies
Companies has a property called CountryId with StringLength attribute and the min and max restrictions (min 3 chars, max 3 chars, country id is ISO-Alpha-3). When the user needs to create a Company, I show a html element with all available countries. This is perfect!
However, when the I execute the jquery validator to the form, this checks for 3 selected options and not the length selected option value.
I need the StringLengthAttribute in my Country Model, I cannot remove it.
I hope to "remove" or "hide" the StringLengthAttribute in the call:
#Html.ValidationMessageFor(model => model.CountryId)
Thanks!
I think I understand your question. A possible solution would be to use a ViewModel to pass to the view as oppose to using the Company entity directly. This would allow you to add or remove data annotations without changing the entity model. Then map the data from the new CompanyViewModel over to the Company entity model to be saved to the database.
For example, the Company entity might look something like this:
public class Company
{
public int Id { get; set; }
[StringLength(25)]
public string Name { get; set; }
public int EmployeeAmount { get; set; }
[StringLength(3, MinimumLength = 3)]
public string CountryId {get; set; }
}
Now in the MVC project a ViewModel can be constructed similar to the Company entity:
public class CompanyViewModel
{
public int Id { get; set; }
[StringLength(25, ErrorMessage="Company name needs to be 25 characters or less!")]
public string Name { get; set; }
public int EmployeeAmount { get; set; }
public string CountryId { get; set; }
}
Using a ViewModel means more view presentation orientated annotations can be added without overloading entities with unnecessary mark-up.
I hope this helps!
Ready!
I remove the rule for the html control.
$("##(Html.HtmlIdNameFor(model => model.CountryId))").rules("remove", "rangelength");
The "rangelength" is the jquery validation rule for the StringLengthAttribute.
Where "Html.HtmlIdNameFor" is a helper to get the "Id" generated by ASP.NET MVC.
Review How to get the HTML id generated by asp.net MVC EditorFor

Categories