Several views from my project have the same dropdownlist...
So, in the ViewModel from that view I have :
public IEnumerable Fo
The first question is if the options-list belongs to the ViewModel. A year or two ago I did the same, but what I see recently more and more as a "best practice" is that people add the list to the ViewBag/ViewData not to the ViewModel. That's an option and I tend to do the same for a one-shot drop-down list, but it doesn't answer the code-reuse question you are facing. For that I see two different approaches (and two more that I rule out).
Shared editor template. Create an editor template for the type that's represented by the dropdown. In this case - because we don't have the list of possible options in the ViewModel or the ViewBag - the template has to reach out for the options to the server. That's possible by adding an action method (that returns json) to a controller class. Either to a shared "LookupsController" (possibly an ApiController) or to the controller that the list-items' type belongs to.
Partial view. The drop down values belong to some type. The Controller of that type could have an action method that returns a partial view.
The benefit of the first one is that a nice @Html.EditorFor
call will do the job. But I don't like the ajax dependency. Partly for that reason I would prefer the partial view.
And there is a third one: child action, but I don't see that a good pattern here. You can google what's the difference between child actions and partial views, for this case child action is the wrong choice. I also wouldn't recommend helper methods. I believe they are not designed for this use case either.
If your DropDownList is exactly the same the approach I would use is:
1) In your Base Controller or in a Helper class, you can create a method that returns a SelectList
. That method should receive a nullabe int to get the select list with a value pre selected.
2) It is wise to cache the information you list in the DDL, to not query the database too often.
So, for (1):
public SelectList GetMyDDLData(int? selectedValue){
var data = fooRepository.GetAll().Select(x => new { Value = x.Id, Text = x.Name });
return new SelectList(data, "Id","Name", selectedValue);
}
In the view model:
var myVM = new MyVM();
myVM.DDLData = this.GetMyDDLData(null) // if it is in your BaseController.
myVM.DDLData = YourHelperClass.GetMyDDLData(null) // if it is in a helper static class
In your views:
@Html.DropDownListFor(x => x.FooProp, Model.DDLData, "Select one...")
For number (2):
private IEnumerable<YourModel> GetMyData()
{
var dataItems = HttpContext.Cache["someKey"] as IEnumerable<YourModel>;
if (dataItems == null)
{
// nothing in the cache => we perform some expensive query to fetch the result
dataItems = fooRepository.GetAll().Select(x => new YourModel(){ Value = x.Id, Text = x.Name };
// and we cache it so that the next time we don't need to perform the query
HttpContext.Cache["someKey"] = dataItems ;
}
return dataItems;
}
The "someKey"
could be something specific and static is this data is the same to all users, or you can do "someKey" + User.Id
if the data is specific to one user.
If your repository is an abstractin layer (not directly EntityFramework) you can place this code there.
I use an IModelEnricher
combined with Automapper and attributes that define relationships between a type of list and select list provider. I return an entity etc using a specific ActionResult that then automaps my entity to a ViewModel and enriches with data required for select lists (and any additional data required). Also keeping the select list data as part of your ViewModel keeps your controller, model, and view responsibilities clear.
Defining a ViewModel ernicher means that anywhere that ViewModel is used it can use the same enricher to get its properties. So you can return the ViewModel in multiple places and it will just get populated with the correct data.
In my case this looks something like this in the controller:
public virtual ActionResult Edit(int id)
{
return AutoMappedEnrichedView<PersonEditModel>(_personRepository.Find(id));
}
[HttpPost]
public virtual ActionResult Edit(PersonEditModel person)
{
if (ModelState.IsValid){
//This is simplified (probably don't use Automapper to go VM-->Entity)
var insertPerson = Mapper.Map<PersonEditModel , Person>(person);
_personRepository.InsertOrUpdate(insertPerson);
_requirementRepository.Save();
return RedirectToAction(Actions.Index());
}
return EnrichedView(person);
}
This sort of ViewModel:
public class PersonEditModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public int FavouriteTeam { get; set; }
public IEnumerable<SelectListItem> Teams {get;set;}
}
With this sort of Enricher:
public class PersonEditModelEnricher :
IModelEnricher<PersonEditModel>
{
private readonly ISelectListService _selectListService;
public PersonEditModelEnricher(ISelectListService selectListService)
{
_selectListService = selectListService;
}
public PersonEditModelEnrich(PersonEditModel model)
{
model.Teams = new SelectList(_selectListService.AllTeams(), "Value", "Text")
return model;
}
}
One other option is to decorate the ViewModel with attributes that define how the data is located to populate the select list. Like:
public class PersonEditModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
public int FavouriteTeam { get; set; }
[LoadSelectListData("Teams")]
public IEnumerable<SelectListItem> Teams {get;set;}
}
Now you can decorate an appropriate method in your select service with an attribute like:
[ProvideSelectData("Teams")]
public IEnumerable Teams()
{
return _teamRepository.All.ToSelectList(a => a.Name, a => a.TeamId);
}
Then for simple models with no complex enrichment just the generic enrichment process can handle it. If you want to do anything more complex you can define an enricher and it will be used if it exists.
Other options could be a convention over configuration approach where the Enricher looks at property name and type e.g. IEnumerable<SelectListItem> PossibleFirstDivisionTeams
{get;set;} then matches this if it exists with a select list provider name in a class that say implements a marker interface ISelectListProvider
. We went the attribute based one and just created Enums representing the various lists E.g. SelectList.AllFirstDivisionTeams
. Could also try interfaces on ViewModel that just have a property collection for a selectlist. I don't really like interfaces on my ViewModels so we never did this
It all really depends on the scale of your application and how frequently same type of select list data is required across multiple models. Any specific questions or points you need clarified let me know
See this question. Also this blog post and this. Also this question on Automapper forum
Have an interface with all your properties that need to be automatically populated:
public interface ISelectFields
{
public IEnumerable<SelectListItem> FooDdl { get; set; }
}
Now all your view models that want to have those properties, implement that interface:
public class MyVM : ISelectFields
{
public IEnumerable<SelectListItem> FooDdl { get; set; }
}
Have a BaseController
, override OnResultExecuting
, find the ViewModel
that is passed in and inject the properties to the interface:
public class BaseController : Controller
{
protected override void OnResultExecuting(ResultExecutingContext filterContext)
{
var viewResult = filterContext.Result as ViewResult;
if (viewResult != null)
{
var viewModel = viewResult.Model as ISelectFields;
if (viewModel != null)
{
viewModel.FooDdl = fooRepository.GetAll().ToSelectList(x => x.Id, x => x.Name)
}
}
base.OnResultExecuting(filterContext);
}
}
Now your controllers are very simple, everything is strongly typed, you are sticking with the DRY principle and you can just forget about populating that property, it will always be available in your views as long as your controllers inherit from the BaseController
and your ViewModels
implement the interface.
public class HomeController : BaseController
{
public ActionResult Index()
{
MyVM vm = new MyVM();
return View(vm); //you will have FooDdl available in your views
}
}
You could put that fetch in the default (null) constructor of MyVM if you don't need to vary its content.
Or you could use a PartialView that you render into the views that need t.
Why not use the advantages of RenderAction:
@(Html.RenderAction("ControllerForCommonElements", "CommonDdl"))
Create a controller, and an action that returns the Ddl and and just reference it in the views.
See some tips here on how you could use it
This way you can also cache this result. Actually the guys building StackOverflow talked about the pros of using this combined with different caching rules for different elements (i.e. if the ddl does not need to be 100% up to date you could cache it for a minute or so) in a podcast a while ago.