This is code from the Contoso University online example:
Controller:
[HttpGet]
public ActionResult Edit(int id)
{
Department department = departmentService.GetById(id);
PopulateAdministratorDropDownList(department.PersonID);
return View(department);
}
// POST: /Department/Edit/5
[HttpPost]
public ActionResult Edit(Department department)
{
try
{
if (ModelState.IsValid)
{
departmentService.Update(department);
return RedirectToAction("Index");
}
}
catch (DataException)
{
//Log the error (add a variable name after DataException)
ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem
persists, see your system administrator.");
}
PopulateAdministratorDropDownList(department.PersonID);
return View(department);
}
private void PopulateAdministratorDropDownList(object selectedAdministrator = null)
{
var administrators = instructorService.GetAll().OrderBy(i => i.LastName);
ViewBag.PersonID = new SelectList(administrators, "PersonID", "FullName",
selectedAdministrator);
}
View:
<div class="editor-field">
#Html.DropDownList("PersonID", String.Empty)
#Html.ValidationMessageFor(model => model.PersonID)
</div>
My question is: if in the View we are not accessing the ViewBag.PersonID (we just create a DropDownList, that generates an html select list with the ID="PersonID", without any default selected value), how the hell the ViewBag.PersonID property is binded to that DropDownList? What's happening behind the scenes? This looks like magic!!!
The second question is when Posting the data, I think the controller searches in the view for any html form field whose ID matches a property in the model and that's how we get the selected Department.PersonID on the postback, even if the view code doesn't reference the model (somtheing like model => model.PersonID) right?
Behind the scenes:
The view is calling Html.DropdownList(this HtmlHelper htmlHelper, string name, string optionLabel) which eventually ends up calling SelectExtensions.SelectInternal(htmlHelper, metadata, optionLabel, expression, selectList, allowMultiple, htmlAttributes1) This checks selectList1 for null, and if if it is, it calls SelectExtensions.GetSelectData(htmlHelper, name) which does the magic part of checking the View data for a key matching the name you passed in.
Posting:
Your assumptions here are pretty much right, but the in addition to form fields the framework will also check the query string and route data and any other IValueProvider that is plugged in to the pipe line.
Related
I have the following view model
public class ProjectVM
{
....
[Display(Name = "Category")]
[Required(ErrorMessage = "Please select a category")]
public int CategoryID { get; set; }
public IEnumerable<SelectListItem> CategoryList { get; set; }
....
}
and the following controller method to create a new Project and assign a Category
public ActionResult Create()
{
ProjectVM model = new ProjectVM
{
CategoryList = new SelectList(db.Categories, "ID", "Name")
}
return View(model);
}
public ActionResult Create(ProjectVM model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Save and redirect
}
and in the view
#model ProjectVM
....
#using (Html.BeginForm())
{
....
#Html.LabelFor(m => m.CategoryID)
#Html.DropDownListFor(m => m.CategoryID, Model.CategoryList, "-Please select-")
#Html.ValidationMessageFor(m => m.CategoryID)
....
<input type="submit" value="Create" />
}
The view displays correctly but when submitting the form, I get the following error message
InvalidOperationException: The ViewData item that has the key 'CategoryID' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'.
The same error occurs using the #Html.DropDownList() method, and if I pass the SelectList using a ViewBag or ViewData.
The error means that the value of CategoryList is null (and as a result the DropDownListFor() method expects that the first parameter is of type IEnumerable<SelectListItem>).
You are not generating an input for each property of each SelectListItem in CategoryList (and nor should you) so no values for the SelectList are posted to the controller method, and therefore the value of model.CategoryList in the POST method is null. If you return the view, you must first reassign the value of CategoryList, just as you did in the GET method.
public ActionResult Create(ProjectVM model)
{
if (!ModelState.IsValid)
{
model.CategoryList = new SelectList(db.Categories, "ID", "Name"); // add this
return View(model);
}
// Save and redirect
}
To explain the inner workings (the source code can be seen here)
Each overload of DropDownList() and DropDownListFor() eventually calls the following method
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata,
string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple,
IDictionary<string, object> htmlAttributes)
which checks if the selectList (the second parameter of #Html.DropDownListFor()) is null
// If we got a null selectList, try to use ViewData to get the list of items.
if (selectList == null)
{
selectList = htmlHelper.GetSelectData(name);
usedViewData = true;
}
which in turn calls
private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
which evaluates the the first parameter of #Html.DropDownListFor() (in this case CategoryID)
....
o = htmlHelper.ViewData.Eval(name);
....
IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
if (selectList == null)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
MvcResources.HtmlHelper_WrongSelectDataType,
name, o.GetType().FullName, "IEnumerable<SelectListItem>"));
}
Because property CategoryID is typeof int, it cannot be cast to IEnumerable<SelectListItem> and the exception is thrown (which is defined in the MvcResources.resx file as)
<data name="HtmlHelper_WrongSelectDataType" xml:space="preserve">
<value>The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.</value>
</data>
according to stephens (user3559349) answer, this can be useful:
#Html.DropDownListFor(m => m.CategoryID, Model.CategoryList ?? new List<SelectListItem>(), "-Please select-")
or in ProjectVM:
public class ProjectVM
{
public ProjectVM()
{
CategoryList = new List<SelectListItem>();
}
...
}
Most Likely Caused some sort of error redirecting to your page and you not initializing your model's drop down lists again.
Make sure that you initialize your drop downs in either the model's constructor or every time before you send said model to the page.
Otherwise you will need to maintain the state of the drop down lists either through the view bag or through the hidden value helpers.
OK, the poster's canned answer neatly explained why the error occurred, but not how to get it to work. I'm not sure that's really an answer, but it did point me in the right direction.
I ran into the same issue and found a slick way to resolve it. I'll try to capture that here. Disclaimer - I work on web pages once a year or so and really don't know what I'm doing most of the time. This answer should in no way be considered an "expert" answer, but it does the job with little work...
Given that I have some data object (most likely a Data Transfer Object) that I want to use a drop-down list to supply valid values for a field, like so:
public class MyDataObject
{
public int id;
public string StrValue;
}
Then the ViewModel looks like this:
public class MyDataObjectVM
{
public int id;
public string StrValue;
public List<SectListItem> strValues;
}
The real problem here, as #Stephen so eloquently described above, is the select list isn't populated on the POST method in the controller. So your controller methods would look like this:
// GET
public ActionResult Create()
{
var dataObjectVM = GetNewMyDataObjectVM();
return View(dataObjectVM); // I use T4MVC, don't you?
}
private MyDataObjectVM GetNewMyDataObjectVM(MyDataObjectVM model = null)
{
return new MyDataObjectVM
{
int id = model?.Id ?? 0,
string StrValue = model?.StrValue ?? "",
var strValues = new List<SelectListItem>
{
new SelectListItem {Text = "Select", Value = ""},
new SelectListITem {Text = "Item1", Value = "Item1"},
new SelectListItem {Text = "Item2", Value = "Item2"}
};
};
}
// POST
public ActionResult Create(FormCollection formValues)
{
var dataObject = new MyDataObject();
try
{
UpdateModel(dataObject, formValues);
AddObjectToObjectStore(dataObject);
return RedirectToAction(Actions.Index);
}
catch (Exception ex)
{
// fill in the drop-down list for the view model
var dataObjectVM = GetNewMyDataObjectVM();
ModelState.AddModelError("", ex.Message);
return View(dataObjectVM);
)
}
There you have it. This is NOT working code, I copy/pasted and edited to make it simple, but you get the idea. If the data members in both the original data model and the derived view model have the same name, UpdateModel() does an awesome job of filling in just the right data for you from the FormCollection values.
I'm posting this here so I can find the answer when I inevitably run into this issue again -- hopefully it will help someone else out as well.
I had the same problem, I was getting an invalid ModelState when I tried to post the form. For me, this was caused by setting CategoryId to int, when I changed it to string the ModelState was valid and the Create method worked as expected.
In my case the first ID in my list was zero, once I changed the ID to start from 1, it worked.
I have the following view model
public class ProjectVM
{
....
[Display(Name = "Category")]
[Required(ErrorMessage = "Please select a category")]
public int CategoryID { get; set; }
public IEnumerable<SelectListItem> CategoryList { get; set; }
....
}
and the following controller method to create a new Project and assign a Category
public ActionResult Create()
{
ProjectVM model = new ProjectVM
{
CategoryList = new SelectList(db.Categories, "ID", "Name")
}
return View(model);
}
public ActionResult Create(ProjectVM model)
{
if (!ModelState.IsValid)
{
return View(model);
}
// Save and redirect
}
and in the view
#model ProjectVM
....
#using (Html.BeginForm())
{
....
#Html.LabelFor(m => m.CategoryID)
#Html.DropDownListFor(m => m.CategoryID, Model.CategoryList, "-Please select-")
#Html.ValidationMessageFor(m => m.CategoryID)
....
<input type="submit" value="Create" />
}
The view displays correctly but when submitting the form, I get the following error message
InvalidOperationException: The ViewData item that has the key 'CategoryID' is of type 'System.Int32' but must be of type 'IEnumerable<SelectListItem>'.
The same error occurs using the #Html.DropDownList() method, and if I pass the SelectList using a ViewBag or ViewData.
The error means that the value of CategoryList is null (and as a result the DropDownListFor() method expects that the first parameter is of type IEnumerable<SelectListItem>).
You are not generating an input for each property of each SelectListItem in CategoryList (and nor should you) so no values for the SelectList are posted to the controller method, and therefore the value of model.CategoryList in the POST method is null. If you return the view, you must first reassign the value of CategoryList, just as you did in the GET method.
public ActionResult Create(ProjectVM model)
{
if (!ModelState.IsValid)
{
model.CategoryList = new SelectList(db.Categories, "ID", "Name"); // add this
return View(model);
}
// Save and redirect
}
To explain the inner workings (the source code can be seen here)
Each overload of DropDownList() and DropDownListFor() eventually calls the following method
private static MvcHtmlString SelectInternal(this HtmlHelper htmlHelper, ModelMetadata metadata,
string optionLabel, string name, IEnumerable<SelectListItem> selectList, bool allowMultiple,
IDictionary<string, object> htmlAttributes)
which checks if the selectList (the second parameter of #Html.DropDownListFor()) is null
// If we got a null selectList, try to use ViewData to get the list of items.
if (selectList == null)
{
selectList = htmlHelper.GetSelectData(name);
usedViewData = true;
}
which in turn calls
private static IEnumerable<SelectListItem> GetSelectData(this HtmlHelper htmlHelper, string name)
which evaluates the the first parameter of #Html.DropDownListFor() (in this case CategoryID)
....
o = htmlHelper.ViewData.Eval(name);
....
IEnumerable<SelectListItem> selectList = o as IEnumerable<SelectListItem>;
if (selectList == null)
{
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
MvcResources.HtmlHelper_WrongSelectDataType,
name, o.GetType().FullName, "IEnumerable<SelectListItem>"));
}
Because property CategoryID is typeof int, it cannot be cast to IEnumerable<SelectListItem> and the exception is thrown (which is defined in the MvcResources.resx file as)
<data name="HtmlHelper_WrongSelectDataType" xml:space="preserve">
<value>The ViewData item that has the key '{0}' is of type '{1}' but must be of type '{2}'.</value>
</data>
according to stephens (user3559349) answer, this can be useful:
#Html.DropDownListFor(m => m.CategoryID, Model.CategoryList ?? new List<SelectListItem>(), "-Please select-")
or in ProjectVM:
public class ProjectVM
{
public ProjectVM()
{
CategoryList = new List<SelectListItem>();
}
...
}
Most Likely Caused some sort of error redirecting to your page and you not initializing your model's drop down lists again.
Make sure that you initialize your drop downs in either the model's constructor or every time before you send said model to the page.
Otherwise you will need to maintain the state of the drop down lists either through the view bag or through the hidden value helpers.
OK, the poster's canned answer neatly explained why the error occurred, but not how to get it to work. I'm not sure that's really an answer, but it did point me in the right direction.
I ran into the same issue and found a slick way to resolve it. I'll try to capture that here. Disclaimer - I work on web pages once a year or so and really don't know what I'm doing most of the time. This answer should in no way be considered an "expert" answer, but it does the job with little work...
Given that I have some data object (most likely a Data Transfer Object) that I want to use a drop-down list to supply valid values for a field, like so:
public class MyDataObject
{
public int id;
public string StrValue;
}
Then the ViewModel looks like this:
public class MyDataObjectVM
{
public int id;
public string StrValue;
public List<SectListItem> strValues;
}
The real problem here, as #Stephen so eloquently described above, is the select list isn't populated on the POST method in the controller. So your controller methods would look like this:
// GET
public ActionResult Create()
{
var dataObjectVM = GetNewMyDataObjectVM();
return View(dataObjectVM); // I use T4MVC, don't you?
}
private MyDataObjectVM GetNewMyDataObjectVM(MyDataObjectVM model = null)
{
return new MyDataObjectVM
{
int id = model?.Id ?? 0,
string StrValue = model?.StrValue ?? "",
var strValues = new List<SelectListItem>
{
new SelectListItem {Text = "Select", Value = ""},
new SelectListITem {Text = "Item1", Value = "Item1"},
new SelectListItem {Text = "Item2", Value = "Item2"}
};
};
}
// POST
public ActionResult Create(FormCollection formValues)
{
var dataObject = new MyDataObject();
try
{
UpdateModel(dataObject, formValues);
AddObjectToObjectStore(dataObject);
return RedirectToAction(Actions.Index);
}
catch (Exception ex)
{
// fill in the drop-down list for the view model
var dataObjectVM = GetNewMyDataObjectVM();
ModelState.AddModelError("", ex.Message);
return View(dataObjectVM);
)
}
There you have it. This is NOT working code, I copy/pasted and edited to make it simple, but you get the idea. If the data members in both the original data model and the derived view model have the same name, UpdateModel() does an awesome job of filling in just the right data for you from the FormCollection values.
I'm posting this here so I can find the answer when I inevitably run into this issue again -- hopefully it will help someone else out as well.
I had the same problem, I was getting an invalid ModelState when I tried to post the form. For me, this was caused by setting CategoryId to int, when I changed it to string the ModelState was valid and the Create method worked as expected.
In my case the first ID in my list was zero, once I changed the ID to start from 1, it worked.
This may sound simple but i am stuck up at a very strange problem.
I have created dropdown's in my application and i am not able to fetch the selected value On Edit.
Here is my View:
<div class="editor-field">
#Html.DropDownListFor(model => model.Hobbyhome.HobbyHomeAddressList.First().ProvincialState.ProvincialStateID, new SelectList(ViewBag.State, "ProvincialStateID", "ProvincialStateName"), "-----Select State-----", new { #id = "State" })<font color="red">*</font>
</div>
i have written a function in controller to fetch the value and i am also able to fetch the value but when it goes in View it doesnot show me the value
i have created another dropdown like this:
#Html.DropDownListFor(model => model.Hobbydetail.Hobbymaster.HobbyId, new SelectList(ViewBag.Hobby, "HobbyId", "HobbyName"), "----Select Hobby----", new { #id = "Hobby" })<font color="red">*</font>
And the strange thing is i can see the value of the second dropdown but not for first One
What i think is maybe because i am using an list.First() in the first dropdown and so it isgiving this problem bcoz On edit view page it only shows me "-----Select State--" as value for first dropdown.
Please Help me
To get the "selected value object" in you edit view :
#Html.DropDownListFor(model => model.Hobbydetail.Hobbymaster.HobbyId,
new SelectList(ViewBag.Hobby, "HobbyId", "HobbyName", Model.Hobbydetail.Hobbymaster.HobbyId),
"----Select Hobby----", new { #id = "Hobby" })
To have all elements in your DropDown, be sure that your ViewBag.Hobby is initialized and filled in your controller before showing Edit view!
First() method returns the first element of a sequence. It will throw you an exception if the source sequence is empty.
Why you need to use First() there ? I think you are doing it in the wrong way. your first paramenter of DropDownListFor should be a variable which holds the selected value, in this purticular overload you are using.
public static MvcHtmlString DropDownList(
this HtmlHelper htmlHelper,
string name,
IEnumerable<SelectListItem> selectList,
IDictionary<string, Object> htmlAttributes
)
name : The name of the form field to return.
You should be using the second approach
I would make a clean ViewModel to handle this. This makes my Views Clean.
public class AddHobbyViewModel
{
public int SelectedHobbyId { set;get;}
public IEnumerable<Hobby> Hobbys { set;get;}
//other relevant properties for the View.
}
In the GET action method, Instead of returning the data in ViewBag, I would return it in the ViewModel.
public ActionResult Add()
{
AddHobbyViewModel model=new AddHobbyViewModel ();
model.Hobbys=db.Hobbies(); // get all available hobbies.
return View(model);
}
And in the View,
#Html.DropDownListFor(model => SelectedHobbyId , new SelectList(Hobbys , "HobbyId", "HobbyName"), "----Select Hobby----", new { #id = "Hobby" })
Now you will have the selected value available in your httppost mehod in the SelectedHobbyId property
[HttpPost]
public ActionResult Add(AddHobbyViewModel model)
{
// check model.SelectedHobbyId here
}
In asp.net mvc3 how to keep dropdown list selected item after postback.
Do something Like this :
[HttpPost]
public ActionResult Create(FormCollection collection)
{ if (TryUpdateModel(yourmodel))
{ //your logic
return RedirectToAction("Index");
}
int selectedvalue = Convert.ToInt32(collection["selectedValue"]);
ViewData["dropdownlist"] = new SelectList(getAllEvents.ToList(), "EventID", "Name", selectedvalue);// your dropdownlist
return View();
}
And in the View:
<%: Html.DropDownListFor(model => model.ProductID, (SelectList)ViewData["dropdownlist"])%>
Even easier, you can include the name(s) of your dropdowns in your ActionResult input parameters. Your dropdowns should be in form tags. When the ActionResult is posted to, ASP.Net will iterate through querystrings, form values and cookies. As long as you include your dropdown names, the selected values will be preserved.
Here I have a form with 3 dropdowns that posts to an ActionResult. The dropdown names are (non-case sensitive): ReportName, Year, and Month.
//MAKE SURE TO ACCEPT THE VALUES FOR REPORTNAME, YEAR, AND MONTH SO THAT THEY PERSIST IN THE DROPDOWNS EVEN AFTER POST!!!!
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult ReportSelection(string reportName, string year, string month)
{
PopulateFilterDrowdowns();
return View("NameOfMyView");
}
MVC does not use ViewState, which means you will need to manage the value persistence yourself. Typically this is done through your model. So, given that you have a view model, e.g.:
public class MyViewModel { }
And your controller:
public class MyController : Controller
{
public ActionResult Something()
{
return View(new MyViewModel());
}
public ActionResult Something(MyViewModel model)
{
if (!ModelState.IsValid)
return View(model);
return RedirectToAction("Index");
}
}
Now, when you pass the model back to the view with data (probably incorrect - failed validation), when you use your DropDownListFor method, just pass in the value:
#Model.DropDownListFor(m => m.Whatever, new SelectList(...))
... etc.
MVC's model binding will take care of the reading of the data into your model, you just need to ensure you pass that back to the view to show the same value again.
Assuming the selected item is part of the post, the controller now knows what it is. Simply have an entry in the ViewData dictionary indicating which item should be selected (null on get or if nothing was selected). In the view, check the value and if it's not null, select the appropriate option.
Use HttpRequestBase object.
In the view, this should work:
#Html.DropDownList("mydropdown", ViewBag.Itens as IEnumerable<SelectListItem>, new { value = Request["mydropdown"] })
If you are building the drop down list data source in the controller Action Method you can send the selected value to it
Controller:
public ActionResult Index( int serviceid=0)
{
// build the drop down list data source
List<Service> services = db.Service.ToList();
services.Insert(0, new Service() { ServiceID = 0, ServiceName = "All" });
// serviceid is the selected value you want to maintain
ViewBag.ServicesList = new SelectList(services, "ServiceID", "ServiceName",serviceid);
if (serviceid == 0)
{
//do something
}
else
{
// do another thing
}
return View();
}
View:
//ServiceList is coming from ViewBag
#Html.DropDownList("ServicesList", null, htmlAttributes: new { #class = "form-control" })
I have a UserController and an Edit.aspx. There is one field that is my primary key so i dont want to allow users to edit this field.
The issue is that if i remove the
<%= Html.TextBox("Email", Model.Email) %>
then when the asp.net-mvc magic calls my Controller code:
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, tblMailingList user_)
{
try
{
repo.UpdateUser(user_);
return RedirectToAction("Index");
}
catch
{
return View();
the email field of the tblMailingList is null. The issue is that i need this as the lookup in the table to retrieve the current record and obviously if its null i get an exception.
When i put the textbox back for this field, it works fine. It seems crazy that i would have to have a textbox and allow editing to pass this field over to the controller. i tried putting it in a label and it still shows up as null in the controller.
any suggestions?
My first question would be why are you doing the lookup on the Email field and not the Id field?
You can pass parameters in your Form declaration to be passed through to your Controller.
<% using (Html.BeginForm(
"MethodName", "Controller", FormMethod.Post,
new { id = Model.Id, email = Model.Email)) { %>
I'm not sure if I got the method declaration correct so please check.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, string email, tblMailingList user_)
{
try
{
repo.UpdateUser(user_);
return RedirectToAction("Index");
}
catch
{
return View();
I would recommend updating slightly differently as your tblMailingList user will not be valid to be updated in your Repository.
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(int id, FormCollection form)
{
tblMailingList user = repo.GetUser(id); // get the user using the id
// so we can update in the same
// context
UpdateModel(user); // this will automatically update
// your user model with values
// from the form
try
{
repo.UpdateUser(user);
return RedirectToAction("Index");
}
catch
{
return View();
If you just want a field that could passed to controller which needs to be invisible in the form, Html.HiddenField could work for your case.
Do I get wrong?