Create DropDownList for an ASP.NET MVC5 Relation [duplicate] - c#

This question already has answers here:
Populating a razor dropdownlist from a List<object> in MVC
(9 answers)
Closed 8 years ago.
I'm stuck creating a proper create/edit view in ASP.NET MVC5. I've got two models Dog and Human. A dog belongs to one Human. I'm trying to create a dropdown in the create and edit views for Dog that'll allow me to select a Human by name for that particular Dog. Here are my models:
Human:
public class Human
{
public int ID { get; set; }
public string Name { get; set; }
}
Dog:
public class Dog
{
public int ID { get; set; }
public string Name { get; set; }
public Human Human { get; set; }
}
My create action:
// GET: /Dog/Create
public ActionResult Create()
{
ViewBag.HumanSelection = db.Humen.Select(h => new SelectListItem
{
Value = h.ID.ToString(),
Text = h.Name
});
return View();
}
And here is the relevant part of my view:
<div class="form-group">
#Html.LabelFor(model => model.Human.Name, new { #class = "control-label col-md-2" })
<div class="col-md-10">
#Html.DropDownListFor(model => model.Human, ViewBag.HumanSelection);
</div>
</div>
I get the following error when I run this:
Compiler Error Message: CS1973: 'System.Web.Mvc.HtmlHelper<Test.Models.Dog>' has no applicable method named 'DropDownListFor' but appears to have an extension method by that name. Extension methods cannot be dynamically dispatched. Consider casting the dynamic arguments or calling the extension method without the extension method syntax.
I'm new to C# & the Entity framework. What am I doing wrong? Is there a way of doing this without manually querying the database? Something like the collection form helpers in Rails?
I've followed a bunch of tutorials that are either old or too complicated for me to follow.

Important to note is that if you use DropDownListFor(x => x.Human), the returned value of the dropdownlist should be a Human object.
It isn't. In your own code snippet, you set the value of the SelectListItem to the ID of the Human. Therefore, when you submit your form, you will receive the ID that you selected.
Add the following to your model:
public int HumanId { get; set; }
Bind your dropdownlist to that int:
#Html.DropDownListFor(model => model.HumanId, (SelectList)ViewBag.HumanSelection);
Now, when you get back to the controller, use that ID to look up the actual Human you want:
[HttpPost]
public ActionResult Create (CreateModel model)
{
if(model.HumanId > 0)
{
model.Human = GetHumanByID(model.HumanId);
//or however you want to get the Human entoty from your database
}
}
It's a simplified solution, but I suspect your main confusion stems from the fact that you're expecting to receive a Human from the DropDownList, while it will actually only return an int (the ID).
Edit
I don't have much information on your data model, but if you're using entity framework, odds are that your Dog class will have a foreign key property called HumanId. If that is the case, you don't even need to get the Human entity like I showed you before. If you put the selected ID in the HumanId property, Entity Framework should be able to use that to create the relation between Human/Dog you want.
If this is the case, it would seems best to elaborate on this in your question, as this would otherwise be more guesswork than actual confirmation.
Edit 2 going offtopic here
Your code:
db.Humen
The plural form of man is men, woman is women; but for human, it's humans :) Humen does sounds like an awesome suggestion though ;)

The problem is that you are attempting to bind a Human type to a dropdown in the UI, a dropdown whose values are strings (the IDs of Human instances) and text are also strings (the names of Human instances).
What you should be binding to the dropdown instead is the ID of the Human, to match the fact that the ID is being used as the value. So with a view model such as
public class CreateDogModel
{
public string Name { get; set; }
[Range(0, int.MaxValue)]
public int Human { get; set; }
public IEnumerable<Human> Humans { get; set; }
}
And the GET controller action
[HttpGet]
public ActionResult Create()
{
var model = new CreateDogModel
{
Humans = db.Human.ToList()
};
return View(model);
}
The view then becomes
#Html.DropDownListFor(
model => model.Human,
Model.Humans.Select(h => new SelectListItem
{
Text = h.Name,
Value = h.ID.ToString()
}),
"Please select a Human");
In your POST controller action, you now look up the chosen human by the Human property value from the View model
[HttpPost]
public ActionResult Create(CreateDogModel model)
{
if (!ModelState.IsValid)
{
// fetch the humans again to populate the dropdown
model.Humans = db.Human.ToList();
return View(model);
}
// create and persist a new dog
return RedirectToAction("Index");
}

Related

There is no ViewData item of type 'IEnumerable<SelectListItem>' that has the key "Auteur_du_Livre"

(First topic for me -> Amazing !)
I'm actually learning how to Handle ASP.NET MVC and i'm having some problem with he Html Helper DropDownListFor.
So the class concerned looks like that :
public class Auteur {
[required]
public int Id {get; set;}
public string Nom {get; set;}
}
My controller looks like that :
[HttpGet]
public ActionResult Ajout_livre()
{
Donnee_a_persister data = new Donnee_a_persister();
List<Auteur> auteur_possible = new List<Auteur>();
foreach (Auteur temp in data.afficher_auteurs())
auteur_possible.Add(temp);
ViewBag.auteur_possible = new SelectList(auteur_possible,"Id","Nom");
return View();
}
And I use it in my view like that :
<div>
#Html.LabelFor(model => model.Auteur_du_livre)
#Html.DropDownListFor(model => model.Auteur_du_livre, (SelectList)ViewBag.auteur_possible)
#Html.ValidationMessageFor(model => model.Auteur_du_livre)
</div>
Here is my post-method (as you can see i tried to populate the select list here too)
[HttpPost]
public ActionResult Ajout_livre(Livre detaillivreaajouter)
{
Donnee_a_persister data = new Donnee_a_persister();
List<Auteur> auteur_possible = new List<Auteur>();
foreach (Auteur temp in data.afficher_auteurs())
auteur_possible.Add(temp);
ViewBag.auteur_possible = new SelectList(auteur_possible, "Id", "Nom");
if (ModelState.IsValid == false)
{
return View(detaillivreaajouter);
}
//Work out
return View("Livre_eligible", detaillivreaajouter);
}
Auteur_du_livre reffers to a variable of type Auteur as this view is strongly typed.
So I get the view and my dropdown list is populated with the content of the ViewBag but when I submit the form I get the following error
There is no ViewData item of type 'IEnumerable' that has the key "Auteur_du_Livre"
I've seen that this problem is frequent so i've tried a lot of thing but i can't figure out what is really happening
Is it possible to get the whole object directly from the form(due to the binding) or do I have to get the property Id and find the corresponding Object in my post method ?
I finally get what's wrong in my Model : i thought that the binding made with the SelectList could directly get the object of type Auteur when the user submit the form but it's wrong. The binding can only get a primitive variable (here it would get the property Id of type int) that was assert on the value fo the form via the selectlist.
Thanks for all.
So i've just changed the helper in my view in the following way :
#Html.DropDownListFor(model => model.Auteur_du_livre.Id, (SelectList)ViewBag.auteur_possible)

Model dropping List<>s after DropDownList

I'm working on a search criteria building page. In addition to several string and numerical type fields, there are several "multiple choice" options.
I'm using the [Get] signature without parameters(pass the CriteriaModel to the view) >> [Post] signature with CriteriaModel parameter (redirect to searching controller)
I've built lightweight option classes (just value, name pairs) and am populating several List<> with the primitive options.
Using Html.DropDownListFor, I'm able to get them to display.
...but...
When I enter the [Post] version, the List<>s are all set to null and empty. Further, the other criteria fields supposed to be populated afterwards are also default and empty.
Technically, I don't need a whole list of values back - if I could even just have the index of the selected value - but I'm up against a wall here.
Pertinent model data:
public class CriteriaModel
{
[DisplayName("Owner Name")]
public string OwnerName { get; set; }
[DisplayName("Subdivision")]
public List<Subdivision> Subdivision { get; set; }
[DisplayName("PIN")]
public string PIN { get; set; }
}
public class Subdivision
{
public int ID { get; set; }
public string Name { get; set; }
}
Pertinent controller code:
[HttpGet]
public ActionResult Index()
{
CriteriaModel criteria = new CriteriaModel();
...fill in the Subdivisions...
View(criteria);
}
[HttpPost]
public ActionResult Index(CriteriaModel search_criteria)
{
return View("Search obtained" + search_criteria.Subdivision.First().Name);
}
And pertinent View markup:
#model REOModern.Models.CriteriaModel
...bunch of HTML...
#Html.LabelFor(model => model.Subdivision)
#Html.DropDownListFor(x => x.Subdivision, new SelectList(Model.Subdivision, "ID", "Name", Model.Subdivision.First().ID))
...other HTML...
<button type="submit" class="btn btn-primary" value="Index">Search</button>
I should clarify: I know that my 'return View("Search obtained" + ...' will fail, but it should show the piece of data that I need. The problem is it's a null reference exception. Until I can fix that, there's no point in building a user-friendly View for submitted search criteria.
MVC does not repopulate the List<> elements.
You would split the selected value out into another property of the model.
So in your model, include something like this
public int SelectedValue { get; set; }
Then for your Html.DropDownListFor helper you would use
Html.DropDownListFor(model => model.SelectedValue, Model.DropDownList, new { /* htmlAttributes */ });
Of course they're empty. The only data that exists in your post action is that which was posted via the form. Since the entire dropdown list, itself, was not posted, merely a selected item(s), the lists are empty. For anything like this, you need to rerun the same logic in your post action to populate them as you did in your get action. It's usually better to factor out this logic into a private method on your controller that both actions can use:
private void PopulateSomeDropDownList(SomeModel model)
{
// logic here to construct dropdown list
model.SomeDropDownList = dropdownlist;
}
Then in your actions:
PopulateSomeDropDownList(model);
return View(model);

Is my view model too "complex" to allow binding on post?

Using my model displaying a page works fine but the post does not return the bound model.
My classes:
public class ContactManager
{
public Contact Contact { get; set; }
public SelectList SalutationList { get; set; }
}
public class Contact
{
public int Id{get;set;}
public string FirstName{get; set;}
public SalutationType SalutationType{get; set;}
}
public class SalutationType
{
public int Id { get; set; }
public string Name { get; set; }
}
My View:
#model ViewModels.ContactManager
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.Contact.Id)
#Html.DropDownListFor(model => model.Contact.SalutationType.Id, Model.SalutationList)
#Html.EditorFor(model => model.Contact.FirstName)
<input type="submit" value="Save" />
}
The issue seems to be in the DropDownListFor. The dropdown list displays correctly with the proper value but when I post this page the complete model is blank. If I simplify the DropDownListFor like this the values are posted as expected.
#html.DroDownListFor(model=>model.MyPlaceHolderProp, Model.SalutationList)
Is my model too complex? Am I not doing something correctly?
The models are based off of several tables using EF that I have created in a separate project. I am trying to avoid creating more classes/models then I have to.
You should post your controller action as well, as your model coming back as blank really has nothing to do with this. Changing the DropDownListFor definition one way or another should not effect the posting of any other values.
That said, you will run into another issue eventually here, so you need to regroup, anyways. You can't just post back the id value of a related item. Entity Framework will either complain that there's already an object with that id, or worse, if the object attaches, it will update the row with that id with the new posted value for Name, which in this case, is nothing, so it'll just clear it out.
When you create a relationship with a single item (a foreign key basically), if you don't specify a property to hold that foreign key value, Entity Framework creates one for you behind the scenes to track the relationship. In your case here, that means your Contacts table has a column named SalutationType_Id. However, there's no way from your class to directly access this value. This is why I recommend that you always provide an explicit property to handle the relationship:
[ForeignKey("SalutationType")]
public int SalutationTypeId { get; set; }
public SalutationType SalutationType { get; set; }
If you do that, then you can directly stuff the posted id there and Entity Framework will create the relationship.
#Html.DropDownListFor(m => m.Contact.SalutationTypeId, Model.SalutationList);
If you insist on keeping the key implicit, then you must create the relationship yourself, by creating a field on your view model to hold the posted value, then using that value to look up the SalutationType instance from the database, and then finally adding that to the Contact instance.
Add to your view model
public int SalutationTypeId { get; set; }
In your view
#Html.DropDownListFor(m => m.SalutationTypeId, Model.SalutationList)
In your POST action
var salutationType = db.SalutationTypes.Find(model.SalutationTypeId);
contact.SalutationType = salutationType;
You could do it this way. This may be the more "MVC best practice" way to handle it. Everything stays neatly in their models, and no manual IDs are required. The views are intended to be representations of the underlying models they are built on. If you are creating a view that has a form, then create a model that represents the form and use it in the view.
Revise your models like:
public class PostModel
{
public int ContactID { get; set; }
public int SalutationID { get; set; }
public string FirstName { get; set; }
}
public class PostView
{
public ContactManager contact { get; set; }
public PostModel post { get; set; }
}
Then create the PostView in the controller:
public ActionResult Index()
{
//create the PostView model
var pv = new PostView();
pv.ContactManager = contactManager;
pv.post = new PostView()
{
ContactID = contactManager.Contact.Id,
SalutationID = contactManager.SalutationType.Id,
FirstName = contactManager.Contact.FirstName
};
return View(pv);
}
Then the view could be like:
#model ViewModels.PostView
#using (Html.BeginForm())
{
#Html.AntiForgeryToken()
#Html.ValidationSummary(true)
#Html.HiddenFor(model => model.post.ContactID)
#Html.DropDownListFor(model => model.post.SalutationID, model.contact.SalutationList)
#Html.EditorFor(model => model.post.FirstName)
<input type="submit" value="Save" />
}
Then the post action in the controller:
[HttpPost]
public ActionResult Index(PostView pv)
{
//post code
//the posted data will be in pv.post
}
Have you considered using a custom model binder? Custom model binding isn't all that complicated for models that are still relatively simple, and you can handle the serialization/deserialization however you need to.
http://msdn.microsoft.com/en-us/magazine/hh781022.aspx
http://ivonna.biz/blog/2012/2/2/custom-aspnet-model-binders-series,-part-3-subclassing-your-models.aspx
http://forums.asp.net/t/1944696.aspx?what+is+custom+model+binding+in+mvc
I am not sure this will help you... I wsa having a similar issue but I was using ajax to post back... anyway, I had forgotten to mark my binding class with the [Serializable] attribute.
so you might try
[Serializable]
public class Contract {
...
}
Again, I am using Json to post back to my controller so may not be related or help you. But, I guess could be worth a try.

ViewModel loses data on post

I'm trying to create a complex ViewModel that has both an Index Page and Create Page of Company Notes all within the Details Page of a Company, and would like some guidance as to whether I'm doing this properly.
My problem at the moment is that when I create a new Company Note, it doesn't have any information in the object beyond the EditorFor fields I include in my cshtml - it loses all the data in the ViewModel.
I have a Company model and CompanyController, and in my Details action, I populate all the notes that are relevant to the company, and a form to allow users to add a new note.
My Company and CompanyNotes model are very simple:
public class Company
{
public int CompanyID { get; set; }
// bunch of fields related to the company
public virtual ICollection<CompanyNote> CompanyNotes { get; set; }
}
public class CompanyNote
{
public int CompanyNoteID { get; set; }
public DateTime Date { get; set; }
public string Note { get; set; }
public Company Company { get; set; }
}
I have a ViewModel that looks like this:
public class CompanyViewModel
{
public Company Company { get; set; }
// List of all notes associated with this company
public IEnumerable<CompanyNote> CompanyNotes { get; set; }
// A CompanyNote object to allow me to create a new note:
public CompanyNote CompanyNote { get; set; }
}
This is my Details action, which populates the company record, gets a list of related notes, and displays a create form with a new, empty object:
public ActionResult Details(int id = 0)
{
var viewModel = new CompanyViewModel();
viewModel.Company = db.Companies.Find(id);
if (viewModel.Company == null)
{
return HttpNotFound();
}
viewModel.CompanyNotes = (from a in db.CompanyNotes
where a.Company.CompanyID.Equals(id)
select a).OrderBy(x => x.Date);
viewModel.CompanyNote = new CompanyNote
{
Date = System.DateTime.Now,
Company = viewModel.Company
};
return View(viewModel);
}
This is my CreateNote action in my CompanyController. (Should I split this out into a separate partial view? What would be the benefit?)
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult CreateNote(CompanyViewModel companyViewModel)
{
CompanyNote companyNote = companyViewModel.CompanyNote;
if (ModelState.IsValid)
{
db.CompanyNotes.Add(companyNote);
db.SaveChanges();
return RedirectToAction("Index");
}
return View(companyViewModel);
}
Finally, here's a simplified version of detail.cshtml:
#model Project.ViewModels.CompanyViewModel
// My company detail display is here, removed for sake of berevity
#using (Html.BeginForm("CreateNote", "Company"))
{
#Html.EditorFor(model => model.CompanyNote.Date)
#Html.TextAreaFor(model => model.CompanyNote.Note})
<input type="submit" value="Create" />
}
When I post, my CreateNote action has a companyViewModel that is basically empty, with the exception of companyViewModel.CompanyNote.Date and companyViewModel.CompanyNote.Note, which are the fields in my form - all the other data in the ViewModel is null, so I'm not sure how to even include a reference back to the parent company.
Am I even on the right path here?
Thanks,
Robbie
When I post, my CreateNote action has a companyViewModel that is
basically empty, with the exception of
companyViewModel.CompanyNote.Date and
companyViewModel.CompanyNote.Note, which are the fields in my form -
all the other data in the ViewModel is null, so I'm not sure how to
even include a reference back to the parent company.
That's perfectly normal behavior. Only information that is included in your form as input fields is sent to the server when you submit the form and this is the only information you could ever hope the model binder be able to retrieve.
If you need the CompanyNotes collection in your HttpPost action simply query your backend, the same way you did in your GET action. You could do this by passing the company ID as a hidden field:
#Html.HiddenFor(model => model.Company.CompanyID)
So the idea is to only include as input fields in your form information that the user is supposed to somehow modify. For all the other information, well, you've already have it in your backend so all you have to do is hit it to get it.
Contrary to classic WebForms, there's no longer any notion of ViewState in ASP.NET MVC. It is much closer to the stateless nature of the HTTP protocol.

Getting SelectedValue back from a DropDown in ASP.NET MVC (C#) using Entity Framework

Sorry if this is a repeated question, I scanned the related questions and didn't see anything obvious.
I'm using an EditModel with an Entity object, along with two SelectLists in it. The problem is, once I reach my POST action, the SelectedValues for both drop downs are still the same default values I set in the constructor for the model, no matter what I actually select on the browser.
My constructor sets some default values for the SelectedValues, but they are just 0 and "" (which aren't valid values in the dropdowns). I have a feeling the problem revolves around that somehow, but I'll give more details.
Here is a stripped down version of the model:
public class UserAccountModel
{
public UserAccount UserAccountData { get; set; } // Entity from EF
public SelectList Organizations { get; set; }
public SelectList Roles { get; set; }
public UserAccountModel() : this(null)
{
}
public UserAccountModel(UserAccount obj)
{
UserAccountData = (obj == null) ? new UserAccount() : obj;
// default selected value
int orgChildId = (UserAccountData.Organization == null) ? 0 : UserAccountData.Organization.ID;
string roleChildId = (UserAccountData.Role == null) ? "" : UserAccountData.Role.Name;
// go get the drop down options and set up the property binding
using (UserAccountRepository rep = new UserAccountRepository())
{
Organizations = new SelectList(rep.GetOrganizationOptions(), "ID", "ID", orgChildId);
Roles = new SelectList(rep.GetRoleOptions(), "ID", "Name", roleChildId);
}
}
public void UpdateModel()
{
UserAccountData.Organization = Organizations.SelectedValue as Organization;
UserAccountData.Role = Roles.SelectedValue as Role;
}
}
This is the Dropdowns portion of the view:
<div class="field">
<label for="ID">Organization:</label>
<%= Html.DropDownList("ID", Model.Organizations) %>
</div>
<div class="field">
<label for="Name">Role:</label>
<%= Html.DropDownList("Name", Model.Roles) %>
</div>
I might have done something stupid and obvious here. The examples are much more straight forward when using the ViewData dictionary, but I couldn't find too many examples trying to use straight model binding for SelectLists.
Any help is greatly appreciated!
Chris
Select elements only post back the actual value of the selected it. In this case, the parameters will be received back at the server/controller as ID (organization) and Name (role). The model you use for the update action should either contain these as properties or your controller action should accept them directly as parameters. The lists on the model won't be repopulated -- and the names don't match in any event.
Modify your model by adding:
public int ID { get; set; }
public string Name { get; set; }
with controller action as:
public ActionResult Update( UserAccountModel userAccount )
{
...
}
Note that if there is a validation error, you'll need to repopulate the SelectList properties (reconstituting the menus).
You simply forgot the navigational path to the ID property :)
<%= Html.DropDownList("Organization.ID", Model.Organizations) %>

Categories