So I'm having difficulties with my 'flight order management system' in MVC 5.
What I'm trying to do is to add an existing passenger to a flight using the requirements given in a specific questions.
Must use lists
this is my code:
Flight:
public class Flight
{
public int Id { get; set; }
public string Depart { get; set; }
public string Destination { get; set; }
public DateTime TakeOffTime { get; set; }
public DateTime LandingTime { get; set; }
public virtual List<Passenger> Passengers { get; set; }
}
Passenger:
public class Passagenger
{
public int Id { get; set; }
public string Name { get; set; }
public int Gender { get; set; }
[Range(0,130)]
public int Age { get; set; }
}
We need to make a redirect at the index page (showing all flights) and a redirect link with the details page, where an admin can add a passenger to a flight. He does this by using a dropdown of the existing passengers and a 'add' button.
However, after the 'add' button is pushed, a table needs to pop up showing all passenger on the flight that it redirected to.
How would I approach this? I have already the following controller:
public ActionResult Details()
{
ViewBag.PassengerId = new SelectList(db.Passengers, "Id", "Name");
return View();
}
[HttpPost]
public ActionResult Details(int passengerId)
{
Passenger = db.Passegers.Find(passengerId);
if(ModelState.IsValid)
{
//add the passenger to the selected flight that it redirected to, but how to get the id of it now?
}
//Get all passengers of the flight, how to get the id?
List<Passagier> Passagiers = db.Passagiers.ToList();
return View(Passagiers);
}
My details view:
#using (Html.BeginForm("Details", "Home", FormMethod.Post))
{
#Html.DropDownList("passengerId", (SelectList)ViewBag.PassengersId);
<input type="submit" value="Add" />
}
I can't use anything that the related answers are giving me (viewModel, ienumerable,...) hence why I'm asking this.
How would I be able to pass the current route id of the page (flightID) and the passengersID to the controllers action? What would be a good approach to this?
I am nearly there I think because the redirects work and so does my dropdown list showing all the passengers.
First, any requirements that prohibit good design should be fought against. Project owners pay developers because they do not have the requisite skills, themselves. It is your job as a developer to educate the project owner when there are requirements that do not make sense. If the project owner insists on doing something fundamentally wrong, in spite of your protests, then that's a sign to you to drop them like a hot rock. They will only be a source of problems for you. Don't be afraid to fire clients. There's plenty of fish in the sea, but a bad client will be an anchor around your neck.
That said, you may just be misunderstanding the requirements. While there's places where you can use List<T> and probably even should use List<T>, your database-persisted entity is not one of them. It must be ICollection<T>. Period. A property of List<T> will not be persisted by Entity Framework.
That's where view models come in, as your view model can and should use List<T>, and then you can map that back and forth to the ICollection<T> on your entity. Again, if the requirements prohibit the use of view models, then your client either is uninformed or stupid. Your duty is to make sure they are informed, and if it turns out they're just stupid, then drop them.
Next, whenever you have a list of something you're posting to, you need to use ListBoxFor, rather than DropDownListFor. The latter only allows a single selection, but here you'll need to persist multiple passengers. On your view model, you should have a property like:
public List<int> SelectedPassengerIds { get; set; }
And, then one to hold your passenger options:
public IEnumerable<SelectListItem> PassengerOptions { get; set; }
In your action, you will map the Flight entity to your view model as normal, but for your Passengers collection you would do:
model.SelectedPassengerIds = flight.Passengers.Select(m => m.Id).ToList();
Then, set PassengerOptions:
model.PassengerOptions = db.Passengers.Select(m => new SelectListItem { Value = m.Id.ToString(), Text = m.Name });
Finally, in your view, you'll have:
#Html.ListBoxFor(m => m.SelectedPassengerIds, Model.PassengerOptions)
On post, you need to map on the appropriate posted values from your view model back on to your Flight entity. For your Passengers collection, you will do:
// Remove deselected passengers
flight.Passengers.Where(m => !model.SelectedPassengerIds.Contains(m.Id)).ToList()
.ForEach(m => flight.Passengers.Remove(m));
// Add newly selected passengers
var newPassengerIds = model.SelectedPassengerIds.Except(flight.Passengers.Select(m => m.Id));
db.Passengers.Where(m => newPassengerIds.Contains(m.Id)).ToList()
.ForEach(m => flight.Passengers.Add(m));
Related
I've checked MVC post a list of complex objects as well as https://mhwelander.net/2014/03/26/asp-net-mvc-model-binding-not-occurring-when-posting-list-of-complex-types/ and a few other posts on the subject.
I would still rather ask the question, in order to understand and not simply copy paste an answer.
I have a complex type as below :
public interface ICaracteristic
{
int value { get; set; }
int max { get; set; }
string name { get; set; }
}
public class BaseAttributes : ICaracteristic
{
public int max { get; set; }
public int value { get; set; }
public string name { get; set; }
Random r = new Random();
}
And an object using multiples list of those types :
public class Character
{
// Infos
public string characterName { get; set; }
public string playerName { get; set; }
public IGame game { get; set; }
// Caracteristics
public List<ICaracteristic> baseAttr { get; set; }
public List<ICaracteristic> skills { get; set; }
public List<ICaracteristic> stats { get; set; }
public List<ICaracteristic> spendPoints { get; set; }
}
I spare you the constructors and a few other methods.
Now, for the creation, I simply ask my user to enter the names of character and player, no problem, it works wonderfully as these are simple strings.
For the edit, I get the character in "my db" (based on xml sheets but that doesn't matter here), and display it this way :
#model RPGmvc.Models.Character
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
<h4>Character #Html.TextBoxFor(model => model.characterName)</h4>
<hr />
#Html.ValidationSummary(true, "", new { #class = "text-danger" })
<div class="row">
<!--SECTION BASE ATTRIBUTES-->
<div class="col-md-3">
<!-- #foreach (var item in Model.baseAttr)
{
<div>
<label>#Html.DisplayFor(model => item.name)</label>
<p>#Html.EditorFor(model => item.value, new { #class = "form-control" })</p>
</div>
}
For the display, this works perfectly fine : it shows the correct names and values of each base attribute of my character (as well as the values of the other lists).
However when I click the "save" button below the form, the model that is posted reset the values of that list... but not the characterName.
What is weird is that it seems to mix that model's different constructors : the playerName, which I don't use in my edit template, is set to "toby determined" (as in my empty basic constructor), but the characterName is the one of the current character being edited, instead of "new character" (as in said constructor)
I've tried to use a for instead of a foreach, thinking that maybe the index in the list would help to pass the correct values; but as I use an Interface that gave an error "Could not create an instance of interface".
I've tried with a custom editor, but it created the same problem as the "foreach".
(Here's the custom editor, just in case :
#model RPGmvc.Models.Caracteristic.ICaracteristic
<p>
#Html.DisplayFor(m => m.name)
#Html.DisplayFor(m => m.value)
</p>
Note that using an interface as model didn't cause any problem. )
I understand that I could apparently add an awful lot of annotations through my page to sort this, but since the custom editor didn't work and neither did the for with index, I first would like an explanation of what is happening :
What happens with the model binder here?
Could I "force" him to create an instance of an implementation of my interface, instead of the interface itself?
How come the HttpPost creates a custom object mixing my constructors ?
Thanks for your help.
Edit : Stephen Muelcke helped me by advising to remove the Interfaces from my model and using the real implementations. That almost worked :
Now my Post takes the correct values of the BaseAttributes, but the names of those are "null".
This is problematic since my datas comes from XML sheets, in which I search this way :
foreach (ICaracteristic battr in myCharac.baseAttr)
{
var currentNode = myDoc.SelectSingleNode("/character_sheet/base_attributes/" + battr.name.Replace(" ", "_").ToLower());
currentNode.InnerText = battr.value.ToString();
}
Which obviously fails, as name is "null".
I only did the change from "foreach" to "for" in the "Base Attributes" section. In that section, all names are set to null. In the other sections, no values are taken.
Any idea?
So, since Stephen Muecke doesn't care for the reputation :
--You can't model bind to an interface.
--You can't do a foreach on a list because modelbinder needs the index.
--You need a "control" sort on every value you want to use, so for complex types you need one even on the display name.
A good solution is to do a for loop, in which you include an #Html.Hiddenfor in addition to a #Html.DisplayFor, so that the display name is not editable but still has a control.
For the Interface and modelbinder lack of interaction, the only solution is to use the implementation of the interface. Sadly.
I have created Comment box in Parent View as a partail view to add comment. below is my Comment model.
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int CMT_ID { get; set; }
private DateTime _date = DateTime.Now;
public DateTime cmd_ad
{
get { return _date; }
set { _date = value; }
}
public string cmd_content { get; set; }
public string t_email { get; set; }
public Nullable<int> SPID { get; set; }
public virtual service_provider service_provider { get; set; }
from partail View I have to submit cmd_content,t_email and SPID.Below is partail view.
#using (Html.BeginForm("AddComment", "Food")){
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
<table>
<tr><td></td> <td>#Html.TextAreaFor(model => model.cmd_content)</td></tr>
<tr><td>Email</td><td>#Html.EditorFor(model => model.t_email)</td></tr>
<tr><td></td> <td>#Html.ValidationMessageFor(model => model.t_email)</td</tr></table><p><input type="submit" value="Comment" class="btn btn-success" /></p>}
I have created action methods for submit data from partail View. Action method Details method for parent View.AddComment is Action method for _Comment partail View.Below is my Controller method.
public ActionResult Details(int id = 0)
{
ImageData details = new ImageData();
var sp_details = (from s in db.service_provider
join p in db.pictures on s.SPID equals p.SPID
join c in db.cities on s.City_ID equals c.City_ID
where s.SPID == id
select new ImageData()
{
Sp_name = s.Sp_name,
SPID = s.SPID,
pic = p.pic
});
return View(sp_details);
}
public ActionResult AddComment()
{
return PartialView("_Comment");
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult AddComment(comment cmt)
{
if (ModelState.IsValid)
{
db.comments.Add(cmt);
db.SaveChanges();
return RedirectToAction("Details", "Food");
}
return PartialView("_Comment", cmt);
}
When Someone add comment partail view should submit cmt_content,t_emil,SPID. My problem is How do I fetch SPID from parent View. It is same as parameter pass to Details Action method. Can Somebody help me to solve this problem.
Actually, the way you have this set up, there won't be anything other than the partial on post, anyways. Posting to an action that returns a partial view, should only be done via AJAX. If you're not using AJAX to post, then you should always return View, or you'll lose all your layout.
That said, you need to step back and understand the platform you're developing on: the Web. The web operates on the HTTP protocol and the TCP/IP protocols, on a lower level. Importantly, all of these are designed to be stateless. The whole idea was to create a mesh network where nodes could come online and drop off without bringing down the rest of the network. To achieve that, no individual server can have intimate knowledge of communication with a particular client, or if that server were to go down, then the client can no longer resume.
At a higher level, this translates into each request being a unique thing, uninformed by any request that proceeded it. When you post, the only thing that exists server-side is what you posted. That variable that existed before is long gone. If you need some value again in the post action, then you need to post it along with every thing else, or rerun whatever logic got you the value in the first place after the post. It's not just going to be there waiting for you.
I'm using Entity Framework Database First approach. Let's say I have a model class called Product and that class has a NumberOfViews property. In the Edit page I pass an instance of the product class to the controller.
The problem is I can't add #Html.EditorFor(model => model.NumberOfViews) in the Edit page, because it's supposed that NumberOfViews is updated with every visit to the product page, and NOT by the website Admin.
And I can't add it as #Html.HiddenFor(model => model.NumberOfViews), because if the Admin Inspected the element, he can edit it manually.
Also If I try to programmatically set the value on the server-side (e.g., Product.NumberOfViews = db.Products.Find(Product.Id).NumberOfViews;), I get the following error:
An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.
And if I don't add it to either the view or the controller, the value will be null, thus overriding any previous value.
So what should I do?
I have noticed a lot of people use the same model for their Entity Framework as they do for their MVC Controller. I generally discourage this practice. In my opinion, a database model is not the same as a view model.
Sometimes a view needs less information than what the database model is supplying. For example while modifying account password, view does not need first name, last name, or email address even though they may all reside in the same table.
Sometimes it needs information from more than one database table. For example if a user can store unlimited number of telephone numbers for their profile, then user information will be in user table and then contact information with be in contact table. However when modifying user profile, they may want to add/edit/delete one or more of their numbers, so the view needs all of the numbers along with first name, last name and email address.
This is what I would do in your case:
// This is your Entity Framework Model class
[Table("Product")]
public class Product
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public int ProductId { get; set; }
public string Name { get; set; }
public int NumberOfPageViews { get; set; }
}
// This is the model you will use in your Edit action.
public class EditProductViewModel
{
public int ProductId { get; set; }
public string Name { get; set; }
}
public class ProductController : Controller
{
IProductService service;
//...
[HttpGet]
public ActionResult Edit(int productId)
{
var product = service.GetProduct(productId);
var model = new EditProductViewModel()
{
ProductId = product.ProductId,
Name = product.Name
};
return View(model);
}
[HttpPost]
public ActionResult Edit(EditProductViewModel model)
{
if (ModelState.IsValid)
{
var product = service.GetProduct(model.ProductId);
product.Name = model.Name;
service.Update(product);
}
// ...
}
}
Consider this scenario:
class Book
{
int id;
List<Category> categories;
}
class Category
{
int id;
string name;
}
I have a strongly typed view for the class Book. When the librarian wants to edit the details of any Book, I want to display checkboxes with all possible categories. In case Book.categories contains a category, that check box will be checked.
The user can check and uncheck the boxes at will and then will click 'Submit'. I want that my controller function should be invoked with a parameter of type Book, with the new set of categories properly set.
How can this be done in ASP.NET MVC?
You might want to split this up into a couple of classes, one that is your book class, one for setting the view, and one for receiving the view's values. This may sound overdone but if nothing else it makes it so you aren't banging a square peg into a round hole. After all, if you just go with the Book class you have right now, you have to figure out how to make the view display everything you need (Say a bunch of check boxes for the categories... A list that your Book does not have completely) and then send it back to the controller still having to fit the same design as the Book class. To me that seems like in the long run a lot harder and maybe not possible without compromising the original book class design.
I would suggest something like this:
class ViewBookModel
{
public ViewBookModel(Book book, IList<Categories> categoryList)
{
BookId = book.Id;
BookName = book.BookName;
CategoryList = categoryList;
}
public Int32 BookId { get; private set; }
public String BookName { get; private set; }
IList<Categories> CategoryList { get; set; }
}
And for posting back to the controller:
class ViewBookInModel
{
public String BookId { get; set; }
public String BookName { get; set }
public Int32[] CategoryIds { get; set; }
}
And the markup would be something like:
<%
foreach(var category in Model.CategoryList)
{
%>
<input type="checkbox" name="CategoryIds" value="<%= category.Id %>" />
<%
}
%>
And the controller Method in:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult UpdateBook(ViewBookInModel book)
It can be done many ways.
I'd suggest keeping your views simple. A view should perform one particular function. You'd have a view to see details of the book, then a view to edit the book. This will make it easier to create a model which provides the information needed to store the book and its categories.
Update:
I agree checkboxes are the best way. For how to handle them on the trip back, see this question: How to handle checkboxes in ASP.NET MVC forms?
Does it make sense create an object that contains only those properties that the user will input on the webpage, use that for binding in the controller, and then map to the full Entity Object? Or should you just use the entity object, and use Include and Exclude to make restrictions on what gets bound on input?
I have come to like the idea of using interfaces to segregate which properties should be included when the object is updated.
For example:
To create and update an person object:
interface ICreatePerson
{
string Name { get; set; }
string Sex { get; set; }
int Age { get; set; }
}
interface IUpdatePerson
{
string Name { get; set; }
}
class Person : ICreatePerson, IUpdatePerson
{
public int Id { get; }
public string Name { get; set; }
public string Sex { get; set; }
public int Age { get; set; }
}
Then, when binding model, just use the appropriate interface as the type and it will only update the name property.
Here is an example controller method:
public ActionResult Edit(int id, FormCollection collection)
{
// Get orig person from db
var person = this.personService.Get(id);
try
{
// Update person from web form
UpdateModel<IUpdatePerson>(person);
// Save person to db
this.personService.Update(person);
return RedirectToAction("Index");
}
catch
{
ModelState.AddModelErrors((person.GetRuleViolations());
return View(person);
}
}
See this article (and the comments) for a very good discussion of the options.
I recommend using a separate presentation model type in most cases. Aside from the issue of binding (which is important, but there are other ways around this issue), I think that there are other reasons why using presentation model types is a good idea:
Presentation Models allow "view-first" development. Create a view and a presentation model at the same time. Get your user representative to give you feedback on the view. Iterate until you're both happy. Finally, solve the problem of mapping this back to the "real" model.
Presentation Models remove dependencies that the "real" model might have, allowing easier unit testing of controllers.
Presentation Models will have the same "shape" as the view itself. So you don't have to write code in the view to deal with navigating into "detail objects" and the like.
Some models cannot be used in an action result. For example, an object graph which contains cycles cannot be serialized to JSON.