I have an editor template for a custom object. Inside that editor template I use a couple of DropDownListFor helpers. In each of them I specify a unique model property (with the pre-selected value) and the select list containing all the select options.
Example:
<%=Html.DropDownListFor(m => m.DocumentCategoryType, Model.DocumentCategoryTypeList) %>
I know that the option values are being populated (from viewing source) and that my Model is passed in with the correct ID value (DocumentCategoryType).
When the view is rendered, there is no selected item in my dropdown and therefore it defaults to the first (non-selected) value.
Does anyone have any ideas?
Thanks.
We also solved the solution by populating a new SelectList that has the appropriate SelectListItem selected, but created this extension method to keep the call to DropDownListFor a little cleaner:
public static SelectList MakeSelection(this SelectList list, object selection)
{
return new SelectList(list.Items, list.DataValueField, list.DataTextField, selection);
}
Then your DropDownListFor call becomes:
<%= Html.DropDownListFor(m => m.DocumentCategoryType, Model.DocumentCategoryTypeList.MakeSelection(Model.DocumentCategoryType)) %>
Looking through the ASP.NET MVC 2 source code reveals some solutions to this problem. Essentially, any SelectListItem in the SelectList passed in the helper extension method that has the Selected property set to true does not have any bearing over the <option> element rendered with the selected attribute applied for the item.
The selected attribute on <option> elements is determined by
1) checking that the helper extension method was passed a SelectList. If this is null, the framework will look in the ViewData for a value corresponding to the key that is the view model property for which you wish to render the drop down list for. If the value is a SelectList, this will be used to render the <select> including taking any selected values, so long as the model state for the model property is null.
2) If a SelectList has been passed in the helper extension method and the model state for the model property is null, the framework will look in the ViewData for a default value, using the model property name as the key. The value in view data is converted to a string and any items in the SelectList passed to the helper extension method that have a value (if no value is set, then the Text will be checked) that matches the default value will have the Selected property set to true which in turn will render an <option> with the attribute selected="selected".
Putting this together, there are two plausible options that I can see to have an option selected and use the strongly typed DropDownListFor:
Using the following view model
public class CategoriesViewModel
{
public string SelectedCategory { get; private set ; }
public ICollection<string> Categories { get; private set; }
public CategoriesViewModel(string selectedCategory, ICollection<string> categories)
{
SelectedCategory = selectedCategory;
Categories = categories;
}
}
Option 1
Set a value in the ViewData in the controller rendering your view keyed against the property name of the collection used to render the dropdown
the controller action
public class CategoriesController
{
[HttpGet]
public ViewResult Select()
{
/* some code that gets data from a datasource to populate the view model */
ICollection<string> categories = repository.getCategoriesForUser();
string selectedCategory = repository.getUsersSelectedCategory();
CategoriesViewModel model = new CategoriesViewModel(selectedCategory, categories);
this.ViewData["Categories"] = selectedCategory;
return View(model);
}
[HttpPost]
public ActionResult Select(CategoriesViewModel model)
{
/* some code that does something */
}
}
and in the strongly typed view
<%: Html.DropDownListFor(m => m.Categories, Model.Categories.Select(c => new SelectListItem { Text = c, Value = c }), new { #class = "my-css-class" }) %>
Option 2
Render the dropdown using the name of the property of the selected item(s)
the controller action
public class CategoriesController
{
[HttpGet]
public ViewResult Select()
{
/* some code that gets data from a datasource to populate the view model */
ICollection<string> categories = repository.getCategoriesForUser();
string selectedCategory = repository.getUsersSelectedCategory();
CategoriesViewModel model = new CategoriesViewModel(selectedCategory, categories);
return View(model);
}
[HttpPost]
public ActionResult Select(CategoriesViewModel model)
{
/* some code that does something */
}
}
and in the strongly typed view
<%: Html.DropDownListFor(m => m.SelectedCategory, Model.Categories.Select(c => new SelectListItem { Text = c, Value = c }), new { #class = "my-css-class" }) %>
It is confirmed as a bug # aspnet.codeplex.com
and only behaves like this for strongly typed views.
Workaround: populate your SelectList in the view code
like
<%= Html.DropDown("DocumentCategoryType", new SelectList(Model.Categories,"id","Name",Model.SelectedCategory")) =>
Yuck. I ended up solving it like this. I hope this gets fixed for RTM.
<%if(Model!=null){ %>
<%= Html.DropDownListFor(m => m.DocumentCategoryType, new SelectList(Model.DocumentCategoryTypeList,"Value","Text", Model.DocumentCategoryType))%>
<%}else{%>
<%=Html.DropDownListFor(m => m.DocumentCategoryType, Model.DocumentCategoryTypeList) %>
<%}%>
Make sure you have a value assigned to m.DocumentCategoryType when you send it to the view.
Generally this value will get reset when you do a post back so you just need to specify the value
when returning to your view.
When creating a drop down list you need to pass it two values. 1. This is where you will store the selected value 2. Is the actual List
Example
<%=Html.DropDownListFor(m => m.DocumentCategoryType, Model.DocumentCategoryTypeList) %>
I made the mistake of setting the select list item Selected value to True. This won't do anything. Instead just assign a value to m.DocumentCategoryType in your controller and this will actually do the selection for you.
Here's another good solution if the source for your drop down list is an IEnumerable instead of a SelectList:
public static SelectList MakeSelection(this IEnumerable<SelectListItem> list, object selection, string dataValueField = "Value", string dataTextField = "Text")
{
return new SelectList(list, dataValueField, dataTextField, selection);
}
Model.DocumentCategoryTypeList
This is probably your problem. On the SelectListItems, do you set the value to the .ToString() output?
var list = new List<SelectListItem>()
{
new SelectListItem()
{
Value = Category.Book.ToString(),
Text = "Book"
},
new SelectListItem()
{
Value = Category.BitsAndPieces.ToString(),
Text = "Bits And Pieces" },
new SelectListItem()
{
Value = Category.Envelope.ToString(),
Text = "Envelopes" }
};
Works for me after doing that. It just needs to be able to match the value from the object
I managed to solve the same problem by saying the folling:
new SelectList(sections.Select(s => new { Text = s.SectionName, Value = s.SectionID.ToString() }), "Value", "Text")
This trick is converting to the value to a string. I know this has been mentioned in previous answers but i find my solution a little cleaner :). Hope this helps.
Copied na pasted from my project:
<%= Html.DropDownListFor(m => m.Profession.Profession_id, new SelectList(Model.Professions, "Profession_id", "Profession_title"),"-- Profession --")%>
Model that is passed:
...
public Profession Profession { get; set; }
public IList<Profession> Professions { get; set; }
...
Html generated:
<select id="Profession_Profession_id" name="Profession.Profession_id">
<option value="">-- Profesion --</option>
<option value="4">Informatico</option>
<option selected="selected" value="5">Administracion</option>
</select>
Works for me. I have this on the form and the only disadvantage is that if model is not valid and I return the model back to the view I have to reload the list of Professions.
obj.Professions = ProfileService.GetProfessions();
return View(obj);
I also had this problem with a field ProgramName. Turns out we used ViewBag.ProgramName in the BaseController and Layout.cshtml, and this was for a different purpose. Since the value in ViewBag.ProgramName was not found in the dropdownlist, no value was selected even though the SelectListItem.Selected was true for one item in the list. We just changed the ViewBag to use a different key and the problem was resolved.
Here is a drop-in DropDownListFor replacement that varies only slightly from the original MVC source.
Example:
<%=Html.FixedDropDownListFor(m => m.DocumentCategoryType,
Model.DocumentCategoryTypeList) %>
I was worried about the performance of making so many copies of my selectList, so instead, I added the selectedvalue as a custom attribute, then used jquery to actually perform the item select:
#Html.DropDownListFor(item => item.AttendeeID, attendeeChoices, String.Empty, new { setselectedvalue = Model.AttendeeID })
........
jQuery("select[setselectedvalue]").each(function () { e = jQuery(this); e.val(e.attr("setselectedvalue")); });
Related
I'm new to MVC so this may sound silly, but here goes: I have a model that contains two lists that need to be passed to an edit form:
public class BaseViewModel
{
public IEnumerable<portal_notifications_types> Types { get; set; }
public IEnumerable<portal_notifications_importances> Importances { get; set; }
}
In the edit form, i Have two dropdownlists for this fields:
#Html.DropDownListFor(m => m.Notification.TypeId, new SelectList(Model.Types, "Id", "Type"), "-- Select type --", new { onchange = "GetNotifType();", style = "width:150px;" })
#Html.DropDownListFor(m => m.Notification.ImportanceId, new SelectList(Model.Importances, "Id", "Importance"), "-- Select importance --", new { style = "width:150px;" })
When I first enter the edit view, everything is ok, the dropdownlists are populated and the corresponding value is selected.
However, when I submit the form, the dropdownlists throw an error, because the Model.Types and Model.Importances lists are null.
How could I overcome this ? I would like to avoid using ViewBag to store those lists, although I know it would work.
Pass the View Model again in your Post Action Method.
[HttpPost]
public ActionResult Index(ViewModel m)
{
return View(m); //Pass the View Model again.
}
You have to repopulate these two SelectLists in the Controller POST method for Edit and again pass the ViewModel in the view for edit. Please share your Controller code for Edit for more details.
I have an enum, which is a mapping for a description of a property against an index in the database. I have a property on my ViewModel that represents an instance of that enum. I've tried both returning a list of enum instances, which means I do this:
#Html.DropDownListFor(m => m.CurrentFilter,
Model.FilterTypes.Select(entry =>
new SelectListItem{ Text = entry.ToString(), Value = ((int)entry).ToString()}),
new { #class = "normalcell", style = "WIDTH: 132px;" })
and returning a list of SelectListItems, which means I do this:
#Html.DropDownListFor(m => m.CurrentFilter,
Model.FilterTypes.Select(entry =>
new SelectListItem{ Text = entry.Text, Value = entry.Value, Selected = entry.Selected}),
new { #class = "normalcell", style = "WIDTH: 132px;" })
In the second case, when I debug, I am certain that the Selected property on the entry object is true for the correct item. In both cases, there is no 'selected' attribute written in to my HTML and so the correct item is not selected. I've also set a breakpoint, and CurrentFilter DOES have the correct value and the rest of my page renders appropriately, so it's finding the value.
I've written plenty of drop lists that work, using similar code, I can't for the life of me see why this does not work, no matter how I try to do it ?
I have also tried:
#Html.DropDownListFor(m => m.CurrentFilter,
Model.FilterTypes,
new { #class = "normalcell", style = "WIDTH: 132px;" })
which seems to me to be the logical way to do it ( return a list of SelectListItems and just do no processing in the page ), but the Selected property is still ignored.
Update:
I tried to do it this way:
#Html.DropDownList("CurrentFilter", Model.FilterTypes, new { #class = "normalcell", style = "WIDTH: 132px;" })
and just read the value out of the request. It's still the case that I am returning a list with only one item that has Selected == true, and it's still the case that MVC is ignoring it.
This works, not surprisingly, but I'd love to know why all the other things don't.
<select class="normalcell" id="CurrentFilter" name="CurrentFilter" style="WIDTH: 132px;">
#foreach (SelectListItem item in Model.FilterTypes)
{
if (item.Selected)
{
<option value="#item.Value" selected="selected">#item.Text</option>
}
else
{
<option value="#item.Value">#item.Text</option>
}
}
I believe the reason is that the MVC binding engine doesn't know how to deal with Enum values. I think you need to create a "proxy" property for your view model. Something like this...
public enum MyEnum { a, b, c, d };
public MyEnum EnumVal { get; private set; }
public string EnumProxy
{
get { return EnumVal.ToString(); }
set { EnumVal = (MyEnum)Enum.Parse(typeof(MyEnum), value); }
}
Then construct the drop-down list using the Enum names:
Type t = typeof(MyEnum);
var ddList = Enum.GetNames(t).Select(
item => new SelectListItem() { Text = item, Value = item }
).ToArray();
Now you should be able to use DropDownListFor normally:
#Html.DropDownListFor(model => model.EnumProxy, ddList)
I'm not sure if there's a neater solution. This one should work though.
My DDL tutorial shows how to use enums. See See my tutorial Working with the DropDownList Box and jQuery and My blog Cascading DropDownList in ASP.Net MVC Part 2 of the tutorial explains: When the string argument (the property to bind) and the SelectList object have the same name, the selected value is not used.
Darin has a SO post on another common reason the selected value is not display. See MVC DropDownList SelectedValue not displaying correctly
I know others have asked this question, but I'm totally confused by this:
This displays the dropdown with no values selected:
<%= Html.DropDownList("items", new MultiSelectList(Model.AvailableItems,
"id", "name", Model.items), new { multiple = "multiple" })%>
This displays the dropdown with the values that I'm passing in (Model.items) selected properly like what I'd expect:
<%= Html.DropDownList("somethingelse", new MultiSelectList(Model.AvailableItems,
"id", "name", Model.items), new { multiple = "multiple" })%>
But the problem is that this item is now named "somethingelse" when i POST. I know I can hack around this but what's going?
Too little context provided in your question but I will try to show a full working example:
Model:
public class Item
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MyModel
{
public IEnumerable<int> SelectedItemIds { get; set; }
public IEnumerable<Item> AvailableItems {
get
{
return new[]
{
new Item { Id = 1, Name = "Item 1" },
new Item { Id = 2, Name = "Item 2" },
new Item { Id = 3, Name = "Item 3" },
};
}
}
}
Controller:
[HandleError]
public class HomeController : Controller
{
public ActionResult Index()
{
var model = new MyModel
{
SelectedItemIds = new[] { 2, 3 }
};
return View(model);
}
[HttpPost]
public ActionResult Index(IEnumerable<int> selectedItemIds)
{
var model = new MyModel
{
// Important: Don't ever try to modify the selectedItemIds here
// The Html helper will completely ignore it and use
// the POSTed values
SelectedItemIds = selectedItemIds
};
return View(model);
}
}
View:
<% using (Html.BeginForm()) { %>
<%= Html.ListBoxFor(x => x.SelectedItemIds,
new MultiSelectList(Model.AvailableItems, "Id", "Name")) %>
<input type="submit" value="GO" />
<% } %>
Notice that the Html.ListBoxFor is more adapted if you want to generate a multiple select. Obviously the AvailableItems property should be fetched from a repository.
The problem you have is using Model.Items as a parameter. The code
<%= Html.DropDownList("items", new MultiSelectList(Model.AvailableItems,
"id", "name", Model.items), new { multiple = "multiple" })%>
isn't actually working as you would expect. It's working because the name of the dropdown is "items". That's because there was a form param called "items" posted back to your action. That param gets stored in the action's ViewState (don't confuse with ViewData).
The Html.DropdownList() sees that there is a ViewState param named the same as you have named your dropdown and uses that ViewState param to work out the selected values. It completely ignores the Model.items that you passed in.
If anyone can explain the logic of not being able to override the default behavior then I'd love to hear it.
So, that's your first problem. To get around it all you have to do is to rename the dropdown to something else - exactly like you did in your second example. Now your second problem comes into play: the list of selected items must be a collection of simple objects (I think it actually needs to be an IEnumerable but I'm not 100% sure).
The DropDownList() method will try and match those selected values to the Value in your AvailableItems collection. If it can't do that it will try to match against the Text.
So, try this to see if it works
<%= Html.DropDownList("somethingelse", new MultiSelectList(Model.AvailableItems,
"id", "name", Model.items.Select(c=> c.name)), new { multiple = "multiple" })%>
Good luck
Actually, if you look at the MVC source code this behavior is baked into DropDownListFor by default (search for allowMultiple: false). The solution is to use ListBoxFor instead (you will see that as well in the MVC source code, allowMultiple: true), which makes a lot of sense as HTML wise, both render to
<select ...>
<option ...>
<option ...>
...
</select>
You don't have to use different properties on the model as suggested in the answers above this one, I got this working by simply switching to ListBoxFor instead (CSS takes it from there):
#Html.ListBoxFor(model => model.SelectedCategories,
new MultiSelectList(Model.Categories, Model.SelectedCategories),
new { multiple = "multiple" })
Works like a charm, even with POST and re-displaying the view on error.
I had the same problem, I used my own extention method to generate the html and problem solved
public static MvcHtmlString ListBoxMultiSelectFor<TModel, TProperty>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList,
object htmlAttributes)
{
return ListBoxMultiSelectFor(helper, expression, selectList, new RouteValueDictionary(htmlAttributes));
}
public static MvcHtmlString ListBoxMultiSelectFor<TModel, TProperty>(
this HtmlHelper<TModel> helper,
Expression<Func<TModel, TProperty>> expression,
IEnumerable<SelectListItem> selectList,
IDictionary<string, object> htmlAttributes)
{
string name = ExpressionHelper.GetExpressionText(expression);
TagBuilder selectTag = new TagBuilder("select");
selectTag.MergeAttributes(htmlAttributes);
selectTag.MergeAttribute("id", name, true);
selectTag.MergeAttribute("name", name, true);
foreach (SelectListItem item in selectList)
{
TagBuilder optionTag = new TagBuilder("option");
optionTag.MergeAttribute("value", item.Value);
if (item.Selected) optionTag.MergeAttribute("selected", "selected");
optionTag.InnerHtml = item.Text;
selectTag.InnerHtml += optionTag.ToString();
}
return new MvcHtmlString(selectTag.ToString());
}
I had a similar problem, when using ListBoxFor and a MultiSelectList assigned as a ViewBag property. #jero's answer helped me figure out that if there's a naming collision between the ViewBag field and the model field, then the selected values don't appear properly.
If I did the following, the items did not show up as selected.
//Controller
ViewBag.SelectionIds = new MultiSelectView(possibleValues, "Value", "Name", selectedValues);
//View
#Html.ListBoxFor(model => model.SelectionIds, (MultiSelectList)ViewBag.SelectionIds, new { #class = "form-control" })
If I changed it to the following, then it was fine.
//Controller
//Changed ViewBag.SelectionIds to ViewBag.SelectionIdList
ViewBag.SelectionIdList = new MultiSelectView(possibleValues, "Value", "Name", selectedValues);
//View
#Html.ListBoxFor(model => model.SelectionIds, (MultiSelectList)ViewBag.SelectionIdList, new { #class = "form-control" })
You can go to the to the value of "items" with this
<HttpPost()> _
Function Edit(ByVal crm_cliente As crm_cliente, ByVal form As FormCollection) As ActionResult
If ModelState.IsValid Then
Dim items As String
crm_cliente.usuario_modifico = "ejmorales"
crm_cliente.fecha_modifico = Date.Now
items = form("items")
that will get you the selected items as a string separate with commas (,)
Once again I'm confronted with a "This shouldn't be this ?*!# hard" situation.
Problem: I want to use a form in MVC for creation of an object. One of the elements of the object is a set of limited choices - a perfect candidate for a drop down list.
But if I use a SelectList in my model, and a drop down list in my View, and then try to post the Model back to my Create method, I get the error "Missing Method Exception:No Parameterless constructor for this object". Exploring the MVC source code, it appears that in order to bind to a model, the Binder has to be able to create it first, and it can't create a SelectList because there is no default constructor for it.
Here's the simplified code:
For the model:
public class DemoCreateViewModel
{
public SelectList Choice { get; set; }
}
For the controller:
//
// GET: /Demo/Create
public ActionResult Create()
{
DemoCreateViewModel data = new DemoCreateViewModel();
data.Choice = new SelectList(new string[] { "Choice1", "Choice2", "Choice3" });
ViewData.Model = data;
return View();
}
//
// POST: /Demo/Create
[HttpPost]
public ActionResult Create(DemoCreateViewModel form)
{
try
{
// TODO: Add insert logic here
return RedirectToAction("Index");
}
catch
{
return View();
}
}
And for the View:
<fieldset>
<legend>Fields</legend>
<%= Html.LabelFor(model => model.Choice) %>
<%= Html.DropDownListFor(model => model.Choice, Model.Choice) %>
<p>
<input type="submit" value="Create" />
</p>
</fieldset>
Now, I know I can MAKE this work by dropping back 10 yards and punting: bypass model binding and drop back to the FormCollection and validate and bind all the fields myself, but there's got to be a simpler way. I mean, this is about as simple a requirement as it gets. Is there a way to make this work within the MVC ModelBinding architecture? If so, what is it? And if not, how come?
Edit: Well, I have egg on my face, but maybe this will help someone else. I did some more experimenting and found a simple solution that seems to work.
Provide a simple value (string or integer, depending on what your select list value type is), and name that as the model element that you bind to. Then provide a second element as the select list of choices, and name it something else. So my model became:
public class DemoCreateViewModel
{
public string Choice { get; set; }
public SelectList Choices { get; set; }
}
And then the DropDownListFor statement in the View becomes:
<%= Html.DropDownListFor(model => model.Choice, Model.Choices) %>
When I do this, the submit button correctly binds the choice made in the form to the string Choice, and submits the model back to the second Create method.
Here is one approach:
#Html.DropDownListFor(model => model.Choice,
ViewBag.Choices as SelectList,
"-- Select an option--",
new { #class = "editor-textbox" })
Notice that I use ViewBag to contain my SelectList. This way when you post back, the client doesn't send the entire select list up to the server as part of the model.
In your controller code, you just need to set the view bag:
ViewBag.Choices = new SelectList(....
Consider creating a different view model for your post action without the SelectList property:
public class DemoCreateViewModelForUpdate
{
public string Choice { get; set; }
}
Then you can always map from the DemoCreateViewModelPost instance to an DemoCreateViewModel instance if the model state is invalid and you want to re-show the view. I tend to prefer everything needed by the view to be in my display view model class, so using a separate update only view model let's me keep things slim and trim for the trip back to the server.
In your view, you'd do:
#Html.DropDownListFor(m => m.Choice, Model.Choices)
as in the previous answer, so no unnecessary data would round trip.
How do I submit the ListBox selections to the ModelBinder?
<%=Html.Hidden("response.Index",index)%>
<%=Html.ListBox("response[index].ChoiceID",
new MultiSelectList(gc.choice,"ChoiceID","ChoiceText") )%>
'gc.choice' is a List
I can get the fisrt selected value to the model, but not the second selection presumably because I cannot change the index.
I have solved this in a slightly different way...
[Model]
public IEnumerable<string> SelectedStores { get; set; }
[View]
<%= Html.ListBox("SelectedStores",
(MultiSelectList)ViewData["Stores"],
new { size = "8" }) %>
[Controller]
ViewData["Stores"] =
new MultiSelectList(StoreItems, "Value", "Text", model.SelectedStores);
So the model has an IEnumberable that will be populated with the user-selections. The view displays the ListBox with the MultiSelectList and the controller passes in the SelectedStores from the model when it constructs the MultiSelectList.
I built a presentation model SamplePresentationModel class that has a MultiSelect member userList.
Then suppose IEnumerable<User> allUser is the list of options.
I use
View(new SamplePresentationModel(){ userList = new MultiSelectList(allUsers,
"UserId",
"UserName",
allUsers.Select(user => user.UserID))});
to pass the MultiSelection to the view.
Then in the view I can construct the listbox
<label for="userList">users:</label>
<%= Html.ListBox("usersList", Model.userList)%>
In the POST action I can capture the selections:
IEnumerable<int> selectedUserIDs = Request["usersList"].Split(new Char[] { ',' }).Select(idStr => int.Parse(idStr));
Don't know if this helps!