I have some issue with binding a collection to a model. I followed this link to create a list of checkbox. I am not sure of how much code to provide, but my view has the following models
Public class Model1{
public string param1{get; set;}
public string param2{get; set;}
}
Public class Model2{
public string param1{get; set;}
public string param2{get; set;}
}
Public class Model3: Model2{
public bool chk {get; set;}
public list<string> param{get; set;}
public Model3()
{
param = new List<string>(){"one", "two", "three"};
}
}
public class superModel{
public Model1 first{get; set;}
public IEnumerable<Model2> second{get; set;}
public List<Model3> third{get; set;}
}
In my controller, I have defined the parametr third as
third = AnEnumerableList.Select( e=> new Model3{chk=false, param1 = e.Param, param2 = e.Param1}).ToList();
and in my view, I pass the supermodel holding all my class model. I create a checkbox like this:
for(int i=0; i<Model.third.Count; i++){
#Html.CheckBoxFor(m => m.third[i].chk, new {onchange="test()"})
#Html.HiddenFor(m => m.third[i].param)
#Html.HiddenFor(m => m.third[i].param1)
#Html.HiddenFor(m => m.third[i].param2)
}
The javascript look like
function test(){
alert('#Model.third[0].chk')
}
When I look at the generated source code, everything looks fine, but the javascript code always returns false regardless of if the checkbox is checked or not. And when the view form is submitted, the whole view does not show. Instead, I get like a partial view with the name of the first selected checkbox. When nothing was selected in this collection, the returning view is submitted alright. I tried to debug the code, but nothing breaks during the process.
I know, this I may not have issolated this problem enough, but this is as much as I can...
That's because you're looking at the Model value and not the current value of the checkbox. You'd need to change it, firstly, change your checkbox declaration to pass this, like so:
#Html.CheckBoxFor(m => m.third[i].chk, new {onchange="test(this)"})
Then change your test function slightly, to be:
function test(item){
alert(item.checked)
}
Related
See below for updated summary...
I understand that using the 'For' Html Helpers is preferred but I'm having a problem with DropDownListFor when using it as a multi-select.
This example (DropDownList) works perfectly:
#Html.DropDownList(
"ProtocolDisciplines",
new MultiSelectList(Model.Disciplines, "DisciplineId", "Discipline", Model.ProtocolDisciplines.Select(pd => pd.DisciplineId)),
new { #class = "form-control", multiple = "multiple", size = "8" }
)
This example (DropDownListFor) works perfectly EXCEPT the default value(s) does not get set:
#Html.DropDownListFor(
model => model.ProtocolDisciplines,
new MultiSelectList(Model.Disciplines, "DisciplineId", "Discipline", Model.ProtocolDisciplines.Select(pd => pd.DisciplineId)),
new { #class = "form-control", multiple = "multiple", size = "8" }
)
UPDATES
Based on what I'm learning I've updated from the original post. Here is the code that is still not working. To be clear, it is doing everything perfectly EXCEPT it is not selecting the default value when rendered. In the example I'm working with there is only a single default value.
#Html.ListBoxFor(
model => model.ProtocolDisciplines,
new MultiSelectList(Model.Disciplines, "DisciplineId", "Discipline", Model.ProtocolDisciplines),
new { #class = "form-control", size = "8" }
)
I've have made certain that Disciplines (the list of all 16 Disciplines in the db) and ProtocolDisciplines (the list of Disciplines that belong to the Protocol) are the same type (DisciplineViewModel). Further, that class (see below) contains only 2 properties (DisciplineId and Discipline).
I have a breakpoint where the model is returns to the view and I have verified that both Disciplines and ProtocolDisciplines have the values expected so I am currently focusing on the view and the ListBoxFor helper. As a note, I have also tried the exact same code with a DropDownListFor helper with identical behavior).
I suspect the problem is in the creation of the MultiSelectList. As you can see, I'm using the overload (IEnumerable ListItems, string DataValue, string DataText, IEnumerable SelectedValues). It would seem that the SelectedValues are simply not getting a match on anything in the ListValues but I can't figure out why. The types used in the two are the same, the DataValue and DataTypes names match the members of the types (just to be safe). I know the ListItems is correct because the list renders them correctly.
I'm at a loss.
Reference:
public partial class DisciplineViewModel
{
public Guid DisciplineId { get; set; }
public string Discipline { get; set; }
}
Here is the model:
public partial class ProtocolViewModelEdit
{
[Key]
public Guid ProtocolId { get; set; }
[Display(Name = "Name")]
public string Protocol { get; set; }
public string ProtocolType { get; set; }
[Display(Name = "Type")]
public Guid ProtocolTypeId { get; set; }
[Display(Name = "Status")]
public Guid ProtocolStatusId { get; set; }
public virtual ICollection<ProtocolTypeViewModel> ProtocolTypes { get; set; }
public virtual ICollection<ProtocolStatusViewModel> ProtocolStatuses { get; set; }
public virtual ICollection<DisciplineViewModel> ProtocolDisciplines { get; set; }
public virtual ICollection<ProtocolXProgramViewModel> ProtocolPrograms { get; set; }
public virtual ICollection<DisciplineViewModel> Disciplines { get; set; }
public virtual ICollection<ProgramViewModel> Programs { get; set; }
}
You referred to a post on the MSDN forums wherein the OP describes the following:
1) The selectedValues parameter must be populated with a collection of key values only. It cannot be a collection of the selected objects, as the HtmlHelper does not apply the dataValueField to this collection for you.
2) If using the ListBox you cannot set the name parameter the same as a Model property. Also you cannot name the ViewBag property that will contain the collection of items the same as the Model property.
3) If using the ListBoxFor it gets even more wonky. You should name the ViewBag property that will contain the collection of items the same as the Model property name. Now when you use the ListBoxFor within the View you must use a ViewBag property that does not exist (this is important!). The HtmlHelper.ListBoxFor will look automatically for a ViewBag property with the assigned Model property name.
None of these are actual issues. A SelectList ultimately has to be translated to/from an HTML select element, which can only work with simple types (string, int, etc.). Actually, everything is a string, and it's only the work of the model binder that translates the posted values into more specific types like int. As a result, it's obvious why you cannot bind a list of objects.
The other two mentioned issues are a result of ModelState. The values of bound form fields are determined by what's in ModelState, which is composed of values from Request, ViewData/ViewBag, and finally Model, as a last resort. If you set a SelectList in ViewBag with the same name as a property on Model, then the value for that key in ModelState will be that SelectList rather than the actual selected values, and your select will therefore have no selected items, because none of the option values will of course match that SelectList instance. Again, this is just standard behavior, and it's only a "bug" if you're not aware of how things work, and don't realize the implications of what you're doing.
Your issue here is exactly the first problem. You're passing a list of objects as the selected values, and there's simply no way to bind that properly to an HTML select element. However, things are far easier if you don't even bother to create your own MultiSelectList anyways. All the helper needs is IEnumerable<SelectListItem>. Razor will take care of creating a SelectList/MultiSelectList and setting the appropriate selected values. Just do:
#Html.ListBoxFor(
m => m.ProtocolDisciplines,
Model.Disciplines.Select(d => new SelectListItem { Value = d.DisciplineId.ToString(), Text = d.Discipline }),
new { #class = "form-control", size = 8 }
)
UPDATE
To answer you question about how Razor "knows", like I said in my answer, the info comes from ModelState. However, as pointed out by Stephen in the comments below, the property you're binding this to is a collection of objects. That's never going to work. Again, the posted values from an HTML select element will always be simple types, not objects. As a result, you need a property that the model binder can bind the posted data to, and then you need to use that information to lookup the actual objects you need, before finally setting something like your ProtocolDisciplines property. In other words:
public List<int> SelectedProtocolDisciplines { get; set; }
public IEnumerable<SelectListItem> DisciplineOptions { get; set; }
Since you're using a view model, it's better if you include the select list items on that view model, so I added a property for that. In your actions (GET and POST), you'll need to set this property:
model.DisciplineOptions = model.Disciplines..Select(d => new SelectListItem {
Value = d.DisciplineId.ToString(),
Text = d.Discipline
});
Since you'll need to call that in both the GET and POST actions, you might want to factor it out into a private method on your controller that both can call. Then, in your view:
#Html.ListBoxFor(m => m.SelectedProtocolDisciplines, Model.DisciplineOptions, new { #class = "form-control" })
Finally, in your POST action:
var protocolDisciplines = db.Disciplines.Where(m => model.SelectedProtocolDisciplines.Contains(m.DisciplineId));
Then, if this is a "create" method, you can simply set the appropriate property on your entity with that. If you're editing an existing entity, you'll need to do a little bit more work:
// Remove deselected disciplines
entity.ProtocolDisciplines
.Where(m => !model.SelectedProtocolDisciplines.Contains(m.DisciplineId))
.ToList()
.ForEach(m => entity.ProtocolDisciplines.Remove(m));
// Add new selected disciplines
var addedDisciplineIds = model.SelectedProtocolDisciplines.Except(entity.ProtocolDisciplines.Select(m => m.DisciplineId));
db.Disciplines
.Where(m => addedDisciplineIds.Contains(m.DisciplineId))
.ToList()
.ForEach(m => entity.ProtocolDisciplines.Add(m));
This extra footwork is necessary to maintain the existing, unchanged M2M relationships.
hopefully I'm missing something obvious, but I have a bit of an issue with some code I've written, and it's not feeling like I've written my code correctly.
So, let's say I have the following Model:
public class SampleViewModel {
[Required]
public string Property1 { get; set; }
[Required]
[EmailAddress]
public string Property2 { get; set; }
public IList<AbstractModel> Items { get; set; }
}
And then I have this abstract view model:
public abstract AbstractModel {
[Required(ErrorMessage = "This field is required")]
public virtual string Value { get; set; }
}
And these concrete view models:
public ConcreteModel1 : AbstractModel { }
public ConcreteModel2 : AbstractModel { }
Within my Controller, I have the following code (this is actually being done elsewhere, but for this sample, this is fine):
var model = new SampleViewModel();
var fields = new List<AbstractModel>() {
new ConcreteModel1() { Value = model.Property1 },
new ConcreteModel2() { Value = model.Property2 },
};
model.Fields = fields;
return View(model);
Within the SampleViewModel partial view (as I have one for each view model type), I have the following:
#model SampleViewModel
#for(var i = 0; i < Model.Items; i++) {
#Html.EditorFor(m => Model.Items[i])
}
Lets say that I also have a distinct partial view (with very different layout requirements) per each AbstractModel.
Example for the ConcreteModel1:
#model ConcreteModel1
#Html.TextboxFor(m => m.Value)
And for the ConcreteModel2:
#model ConcreteModel2
#Html.DisplayFor(m => m.Value)
This is all working, but as I've had to pass the ViewModel's properties (Property1) into the AbstractModel, I have lost the connection between the view and the underlying model. I have been able to bind the form fields back to the model, using a custom Model Binder, but the main thing I'm missing are the model validators which have been added to the SampleViewModel class.
Ideally I want this information to be available to the AbstractModel. Validation is happening, but I'm only getting basic validation on the client (via AbstractModel's Value required attribute), but I'd like to be able to pass along validation needs from my SampleViewModel into the AbstractModel.
Expectation
What I'd really like to happen is for the AbstractModel's Value property to somehow impersonate the property that is passed into it, so that it is just acting as a proxy to the original model, but has just reshaped the SampleViewModel (or specifically it's Property1 property).
So the important thing is, considering the following creation of my fields:
var fields = new List<AbstractModel>() {
new ConcreteModel1() { Value = model.Property1 },
new ConcreteModel2() { Value = model.Property2 },
};
How do the AbstractModels know that their Values are supposed to be: Required, and also Required and an EmailAddress, based on the properties that are used to create them?
Thank you for your input.
I am attempting to rid my code of the dependency on System.Web.Mvc, since I have extracted my model out to a separate .dll (outside of the MVC project) so that it can be shared among many projects.
I am attempting to create a "custom SelectListItem" which is simply just a class with two properties of string Text and string Value and looks like:
public class DropDownListItem
{
public string Text { get; set; }
public string Value { get; set; }
}
However, I am trying to find a way to pass this object into a DropDownListFor in my view. Is there anyway to manipulate this so that I can do something like:
#Html.DropDownListFor(x => x.Value, Model.Values, new { #class="form-control" })
Where Models.Values is of type List<DropDownListItem>.
Yo don't need to create a DropDownListItem class - instead you can create your own POCO (Plain-Old-C#-Object) to contain List Options:
For example, let say you have the following class :
class MyClass
{
public int Id {get; set; }
public string name {get; set; }
}
to have dropDownlist which contains a list of MyClass, you can simply use :
#Html.DropDownListFor(model => model.Entity.Id, new SelectList(MyClass, "Id", "Name"))
The id represent the value of the selection, and the name will be the text displayed within the DropDownList.
Hope this helps!
You can put the list item in viewbag and get the value in your view like these:
#Html.DropDownList("Ligas")
and your codebehind
Modelo.AdminSitio = Modelo.GetAdminSitio();
ViewBag.Ligas = new SelectList(Modelo.AdminSitio.ListadoLigas, "Value", "Text");
when listdoLigas is equals
ListadoLigas= (from x in db.Ligas select new DropDownListItem { Value = x.Oid_Liga, Text = x.NombreDeLiga}).ToList();
I have something like this
public class ResumeVm
{
public ResumeVm()
{
EducationVms = new List<EducationVm>();
}
public Guid UserId { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public List<EducationVm> EducationVms { get; set; }
}
public class WorkExperienceVm
{
public string Title { get; set; }
}
I now want to make an editor template for each of the EducationVms, I made a Template for 1 Education Vm and tried to use
#Html.EditorForModel("WorkExperienceVm")
but it does not know how to pass in the EducationVms
If I do
#Html.EditorForModel("WorkExperienceVm", #Model.EducationVms )
It gets made as it expects only 1 Vm to be sent in.
// View (WorkExperienceVm)
#model ViewModels.WorkExperienceVm
#Model.Title
The EditorForModel overload that you're using is incorrect. EditorForModel produces an editor template for the current model (i.e. the ResumeVm) and the string you're passing in is the additional view data object, not the name of the view.
I'm assuming that "WorkExperienceVm" is the name of the view. Try using EditorFor:
#for(int i = 0; i < Model.EducationVms.Count; i++)
{
Html.EditorFor(m => m.EducationVms[i], "WorkExperienceVm")
}
An alternative is to create a template that's actually called EducationVm.cshtml and type it to EducationVm, then you can just do the following and the framework will figure out that you want the template called for each item in the collection:
#Html.EditorFor(m => m.EducationVms)
Unfortunately this approach can't be achieved using UIHints or passing in the view name manually into the helper, though that's fairly unlikely to get in your way if you don't mind adhering to strict naming conventions for your templates.
I wrote another answer a while ago explaining the differences between the different helpers for editor templates. It deals specifically with the "label" helpers but the same principles apply to the "editor" helpers.
When I am changing the "model => model.id" to "model => model.Supplierid" i am getting below error
"The parameter 'expression' must evaluate to an IEnumerable when
multiple selection is allowed."
please have look on below code
// this my model class
public class clslistbox{
public int id { get; set; }
public int Supplierid { get; set; }
public List<SuppDocuments> lstDocImgs { get; set; }
public class SuppDocuments
{
public string Title { get; set; }
public int documentid { get; set; }
}
public List<SuppDocuments> listDocImages()
{
List<SuppDocuments> _lst = new List<SuppDocuments>();
SuppDocuments _supp = new SuppDocuments();
_supp.Title = "title";
_supp.documentid = 1;
_lst.Add(_supp);
return _lst;
}
}
// this my controller
[HttpGet]
public ActionResult AddEditSupplier(int id)
{
clslistbox _lst = new clslistbox();
_lst.lstDocImgs= _lst.listDocImages();
return View(_lst);
}
// this is view where i am binding listboxfor
#model clslistbox
#using (Html.BeginForm("AddEditSupplier", "Admin", FormMethod.Post))
{
#Html.ListBoxFor(model => model.id, new SelectList(Model.lstDocImgs, "documentid", "title"))
}
Can anyone see the reason for it?
I think the changing of the property in the expression here is a red-herring - it won't work in either case.
Update
However, see at the end of my answer for some probably needlessly detailed exposition on why you didn't get an error first-time round.
End Update
You're using ListBoxFor - which is used to provide users with multiple selection capabilities - but you're trying to bind that to an int property - which cannot support multiple selection. (It needs to be an IEnumerable<T> at least to be able to bind a list box to it by default in MVC)
I think you mean to be using DropDownListFor - i.e. to display a list of items from which only one can be selected?
If you're actually looking for single-selection semantics in a listbox, that's trickier to do in MVC because it's Html helpers are geared entirely around listboxes being for multiple selection. Someone else on SO has asked a question about how to get a dropdown to look like a list box: How do I create a ListBox in ASP.NET MVC with single selection mode?.
Or you could generate the HTML for such a listbox yourself.
(Update) - Potentially needlessly detailed exposition(!)
The reason you don't get an exception first time round is probably because there was no value for id in ModelState when the HTML was generated. Here's the reflected MVC source (from SelectExtensions.SelectInternal) that's of interest (the GetSelectListWithDefaultValue call at the end is the source of your exception):
object obj =
allowMultiple ? htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string[])) :
htmlHelper.GetModelStateValue(fullHtmlFieldName, typeof(string));
if (!flag && obj == null && !string.IsNullOrEmpty(name))
{
obj = htmlHelper.ViewData.Eval(name);
}
if (obj != null)
{
selectList =
SelectExtensions.GetSelectListWithDefaultValue(selectList, obj, allowMultiple);
}
Note first that the control variable allowMultiple is true in your case, because you've called ListBoxFor. selectList is the SelectList you create and pass as the second parameter. One of the things that MVC (unfortunately in some cases) does is to use ModelState to modify the select list you pass when re-displaying a view in order to ensure that values which were set in ModelState via a POST are re-selected when the view is reloaded (this is useful when page validation fails because you won't copy the values to your underlying model from ModelState, but the page should still show those values as being selected).
So as you can see on the first line, the model's current value for the expression/field you pass is fished out of model state; either as a string array or as a string. If that fails (returns null)then it makes another go to execute the expression (or similar) to grab the model value. If it gets a non-null value from there, it calls SelectExtensions.GetSelectListWithDefaultValue.
As I say - what you're trying to do will ultimately not work in either the case of Id or SupplierId (because they would need to be IEnumerable) but I believe this ModelState->Eval process is yielding a null value when you use Id, so the process of getting an 'adjusted' SelectList is skipped - so the exception doesn't get raised. The same is not true when you use SupplierId because I'll wager that there's either a value in ModelState at that point, or the ViewData.Eval successfully gets an integer value.
Not throwing an exception is not the same as working!.
End update
Try changing your property from int to int[]
public class SuppDocuments
{
public string Title { get; set; }
public int documentid { get; set; }
}
Assuming above is the class used for binding the model , try changing the documentid property as below
public class SuppDocuments
{
public string Title { get; set; }
public int[] documentid { get; set; }
}