I was wondering what is the best approach to having a select list on a form which contains values from a database without duplicating any code.
What I thought would make sense would be to load this data in the controller and pass it to a view model, so I can use SelectListFor<>
or whatever in order to generate the list. However this means that I have to duplicate all the list loading in both the GET and the POST methods. The other way I can see would be to pass the database context into the view model constructor and have it load the list , but this then presents two more issues:
1) Should the view model know about the database context?
2) I then cannot use model binding by accepting the view model type as a method argument because it does not have a no-argument constructor (if I create a no-argument constructor then it won't have the lists if I want to redisplay the view containing the form).
Is there a better way to do this? This seems like it must be a fairly common scenario and any advice would be appreciated.
We typically implement our lookups through a ReferenceDataRepository that gets used within the controllers in the same way as any other repository interaction. This repository will usually recieve a high number of calls for predominantly static readonly data so we may implement a derived CachedReferenceDataRepository over this using an abstraction of your caching scheme of choice (Session, AppFabric etc).
Why not get you db or repository or business rule - whatever you call it send back an IDictionary???
This example assumes you have a list of users, you will send back an Key with their ID and the Value with lets say first name + last name:
Then use this inside the view....
<%= Html.DropDownListFor(model => model.UserID, new SelectList(Model.AvailableUsers, "Key", "Value",Model.UserID))%>
model.UserID = Key
Model.AvailableUsers = IDictionary<int,string>
I create my lists in some helper code sometimes then I lookup those values using this helper... so there is one centralized class (usually static) that will generate these "Users"...
Pass these users onto the view directly or alternatively a ViewModel as in your case- which is what I recommend
NOTE: You would not hookup your data context with the List/ Model Binding, that makes things too complex. Just take in the UserID as the selected user from the list then in your post handle apporpriately...
ViewModel:
public class UsersViewModel
{
public int UserID { get; set; }
public IDictionary<int,string> AvailableUsers{ get; set; }
}
In your post...
[HttpPost]
[ValidateAntiForgeryToken]
[DemandAuthorization(RoleNames.AdminUser, AlwaysAllowLocalRequests = true)]
public ActionResult AllUsers(int UserID)
{
try
{
//save to db or whatever...
return RedirectToAction("Index", "Users");
}
catch (RulesException ex)
{
ex.CopyTo(ModelState); //custom validation copied to model state
}
var _users= _service.GetUsers();
return View("AllUsers", new UsersViewModel
{
UserID = UserID,
AvailableUsers = _users
});
}
Well, in my opinion, you have to have context or repository object in your viewmodel to keep dry in this scenario. Furthermore, it is also correct that your viewmodel should not know about your database. To handle this issue you can have viewmodel's constructor accept an interface like
public Interface IUserRepository
{
public IEnumerable<User> GetAll();
}
and you can have your view model like
public class CreateVM
{
private IUserRepository _repo;
public CreateVM(IUserRepository repo)
{
_repo = repo;
}
public IEnumerable<SelectListItem> AvailableUsers
{
get{
return _repo.GetAll().Where(x=>x.isAvailable).Select(x=>new SelectListinItem{Text = x.UserName, Value = x.UserID});
}
}
}
Last piece of the puzzle is your DI setup. tell ur IOC container to inject IUserRepository in constructor of your viewmodel whenever it is instantiated. I don't know if DI can work when modelbinder creates an instance of your view model but it it does you are there at least in theory. your viewmodel does not know your repository but only an interface and your list is created at single point so you are dry too.
The biggest problem I see with passing IRepository
to ViewModel is that this can easily cause performance issues - select n+1's are so natural in this case, that it is hard to avoid them. You want to have as little rountrips to DB as possible for the request, and having IRepository
passed around all those multi-level ViewModels just doesn't help that.
Instead, you can introduce ViewModel factory which is responsible for creating ViewModels of desired type. MyViewModelFactory
would depend on IRepository
, and would create MyViewModel
which is just plain DTO.