I'm currently working on a project in which we have a number of similar models. We made them all inherit from the same base model and we also made a common controller (basic stuff like list, adding, editing etc.) handles the base model. It actually works and made things easy for us, but I have small problem with dropdowns.
Without going into much details, it would make things really easy for me if I could pass data from controller to view using an action. Something like this:
@Html.DropDownListFor(model => model.Xtypechild, Action("GetList", "XController"))
Where action GetList
returns IEnumerable<SelectListItem>
This way I could write the code just once and use it for a number of different controllers.
I know it sounds crazy, but if I tried use one of more conventional ways to do this I would end up with redundant code again and would lose some of the advantage we gained from inheriting controllers.
The only alternative I can see right now is to make GetList
return JSON with all the items and populate the dropdowns using Ajax/JS. I could also make an action that returns a partial view containing the dropdown, but I'm not sure if that's a good idea.
Thank you far all the input. In the end I went with the following solution:
We had a services class for handling database operations for our models. I just add a method that generates a IEnumerable<SelectListItem>
to the base services class. Now I pass the items for drop downs through ViewBag (though it forces me to cast them back to IEnumerable before use in the view). I assign them to ViewBag in the overridden constructor of the inherited controller.
I've best consult the original author of the code if this solution will be ok for our project, though he is on holiday right now. For now this will have to do. On the up side this method produced little code, so it should be easy to replace (if I have to do so).
The code evolved a little bit and now I have something like this in the view (note the int?
casting is there because the property was of type int and it wouldn't work otherwise):
Html.DropDownListFor(
model => model.XId,
new SelectList((List<X>)ViewBag.Xs, "Id", "Name", (Model != null ? (int?)Model.XId : null)),
" -- select -- ",
null)
And something like this inside of overridden constructor of the controller:
ViewBag.Xs = db.Xs.ToList();
It's definitelly not the best way to implement dropdowns in razor, but it was the best way to implement them in the project. The reason for this was: there was no time to make the project use ViewModels and override all the actions of base controller to handle them (as well as provide data for the dropdowns in those actions).
The code evolved even further. It's certainly not a good idea to execute queries in the controller constructor if the data will be used just in a handful of views. So now the constructor contains equivalent of:
ViewBag.Xs = (IQueryable<X>)db.Xs;
And the view:
Html.DropDownListFor(
model => model.XId,
new SelectList((IQueryable<X>)ViewBag.Xs, "Id", "Name", (Model != null ? (int?)Model.XId : null)),
" -- select -- ",
null)
This way the query is only executed when a view actually needs the data.
Items for drop down lists should definitely be in your ViewModel. I don't think it's wrong to create select lists in your derived ViewModels. It might seem like duplicate code, but at least it's clear what you do. You can create some nice helpers in your base controller which takes care of mapping entities to SelectListItem
s.
If you really want a generic solution, you could make a method in your base model:
public class BaseViewModel
{
// Other properties..
public IEnumerable<SelectListItem> GetListFromAction(string actionName, string controllerName)
{
// return the select list from the action.
}
}
And use this in DropDownListFor()
:
@Html.DropDownListFor(model => model.Xtypechild, Model.GetListFromAction("GetList", "XController"))
I certainly don't agree with having a method on the view model to populate the SelectList
, even if it is calling the controller. The reason for that is simple: it is the controller's responsibility to populate a model, not the other way around.
The best way to handle this is by having a method on your controller which can build lists for you. As it's a pretty common task, that suggests multiple controllers would need access to that functionality, and so creating your own BaseController
which all controllers derive from, is the way to go.
With that in mind, here's a BaseController
with a generic method I wrote to build any SelectList
:
BaseController:
public class BaseController : Controller
{
[NonAction]
public SelectList BuildSelectList<TSource>(IEnumerable<TSource> source,
Expression<Func<TSource, int>> valueKey, Expression<Func<TSource, string>> textKey,
object selectedValue = null)
{
var selectedValueKey = ((MemberExpression)(MemberExpression)valueKey.Body).Member.Name;
var selectedTextKey = ((MemberExpression)(MemberExpression)textKey.Body).Member.Name;
return new SelectList(source, selectedValueKey, selectedTextKey, selectedValue);
}
}
Note the use of the NonActionAttribute
marking the method. This prevents the method being invoked as an action. Below is a simple example of how to use this.
StudentViewModel:
public class StudentViewModel
{
public int Id { get; set; }
public string Name { get; set; }
[Display(Name = "Course")]
public int CourseId { get; set; }
public SelectList Courses { get; set; }
}
CourseId
represents the selected course.
Course:
public class Course
{
public int Id { get; set; }
public string Name { get; set; }
}
HomeController
// Notice how we're deriving from BaseController
public class HomeController : BaseController
{
public ActionResult Index()
{
var model = new StudentViewModel();
model.Id = 1;
model.Name = "John";
model.Courses = BuildSelectList(this.courses,
m => m.Id, m => m.Name);
return View(model);
}
[HttpPost]
public ActionResult Index(StudentViewModel model)
{
if (ModelState.IsValid)
{
return RedirectToAction("Success");
}
// The model wasn't valid, so repopulate the dropdown
model.Courses = BuildSelectList(this.courses, m => m.Id,
m => m.Name, model.CourseId);
return View(model);
}
}
You'd then display the dropdown in your view like so:
@Html.DropDownListFor(m => m.CourseId, Model.Courses)
public ActionResult Index()
{
var directors = new Collection
{
new Director {Id = 1, Name = "David O. Russell"},
new Director {Id = 2, Name = "Steven Spielberg"},
new Director {Id = 3, Name = "Ben Affleck"}
};
var selectList = new SelectList(directors, "Id", "Name");
var vm = new ViewModel {DirectorSelectList = selectList};
return View(vm);
}
@Html.DropDownListFor(viewModel => viewModel.DirectorId, directorList)
If you want to check example , please enter to link.
http://peternewhook.com/2013/02/asp-net-mvc-selectlist-selectedvalue-dropdownlistfor/
I have a BaseController
which inherits from Controller
and then put methods such as GenerateStateOptions
in the BaseController
.
It might look something like this:
public IEnumerable<SelectListItem> GenerateStateOptions()
{
var items = new List<SelectListItem>();
var states = New List<State>(); // get states from your DB or wherever
foreach(var state in states)
{
items.Add(new SelectListItem()); // set your properties for the SLI
}
return items;
}
Then in your Controller, you can do something like:
... // additional ActionMethod code here
model.States = GenerateStateOptions();