ASP.NET MVC ViewModel with SelectList(s) best practice
Asked Answered
H

5

14

I noticed that in the NerdDinner application that if the ModelState is invalid for a dinner, it merely returns the view for the model:

        if (ModelState.IsValid) {
            ...
            return RedirectToAction("Details", new { id=dinner.DinnerID });
        }

        return View(dinner);

However, in my application the model (a view model in this situation) contains multiple SelectLists. These lists are not instantiated at this point because this view model was just populated from the form submission. What is the recommended way to repopulate this SelectLists before sending them back to the user?

This is what I want my controller to do:

public ActionResult Save(MyModel model)
{
    if (ModelState.IsValid)
    {
        businessClass.Save(model);
        return RedirectToAction("Index", "Home");
    }

    // This won't work because model has uninstantiated SelectLists
    return View("MyView", model);
}

I don't want to send the model to my business logic if the ModelState is invalid, but it doesn't seem to make sense to put SelectList population code in my controller. Should I create a public method in my business logic solely for doing this kind of stuff on my view model(s)?

Harrovian answered 25/10, 2011 at 17:41 Comment(0)
C
15

Personally I like to keep it simple:-

[HttpGet]
public Edit(int id) {
     EditForm form = new EditForm();
     // Populate from the db or whatever...
     PopulateEditPageSelectLists(form);
     return View(form);
}

[HttpPost]
public Edit(EditForm form) {
     if (ModelState.IsValid) {
         // Do stuff and redirect...
     }
     PopulateEditPageSelectLists(form);
     return View(form);
}

public void PopulateEditPageSelectLists(form) {
     // Get lookup data from the db or whatever.
}

If the logic to populate the select lists is all kinds crazy it might be worthwhile moving to a separate class or whatever it but as a first step this is the best place to start.

Clathrate answered 25/10, 2011 at 18:32 Comment(0)
R
5

You dont say how much reusability would you like. But personally, i like things "clear" (dont invading controller) and reausable as possible, and that in MVC means - filters.

Look at this :

public class SupplyLanguagesAttribute : System.Web.Mvc.ActionFilterAttribute
{
    public override void OnActionExecuting(System.Web.Mvc.ActionExecutingContext filterContext)
    {
        filterContext.Controller.ViewData["languagesList"] =
            someService.LoadLanguagesAsDictionary();

        base.OnActionExecuting(filterContext);
    }
}

then you just use it with every action method where you "might" need languages :

[SupplyLanguages]
public ActionResult DoSomething()
{
...
}

And then in view, you can use the data directly for DropDownList from ViewData, or you can even "wrap" this too (and avoid "magic strings" in views), with custom reusable DropDown :

public static MvcHtmlString LanguageDropDown(this HtmlHelper html, string name, object selectValue, bool defaultOption = false)
    {
        var languages = html.ViewData["languagesList"] as IDictionary<string,string>;

        if (languages == null || languages.Count() == 0)
            throw new ArgumentNullException("LanguageDropDown cannot operate without list of languages loaded in ViewData. Use SupplyLanguages filter.");

        var list = new SelectList(languages, "Key", "Value", selectValue);

        return SelectExtensions.DropDownList(html, name, list);
    }
Ruckman answered 26/10, 2011 at 7:46 Comment(3)
This looks interesting. I'll have to play around with it. The DI support for filters in MVC 3 should make this easier.Tracitracie
I like this method also, but because I have action methods that can take multiple types of models, this won't work. I'm using a custom model binder so I'm not using MVC in a typical fashion. This seems like it would work in most situations though. I'm just not sure about putting a bunch of attributes in my controllers.Harrovian
IMO - having to create your own html helper just to populate a select list is neither "clear", nor "reusable". Doesn't get any clearer than @John Foster's answer (use a method that returns the select list you want.)Garnes
T
1

My controllers populate the SelectLists on my Model if the ModelState is not valid.

Following Separation of Concerns, your business classes shouldn't know anything about the view model at all. If your view needs a list of employees your controller gets a list of employees from your business layer and creates the SelectList that your view needs.

Example

public ActionResult Save(MyModel model) 
{ 
    if (ModelState.IsValid) 
    { 
        businessClass.Save(model); 
        return RedirectToAction("Index", "Home"); 
    } 
    model.PossibleEmployees 
             = _employeeRepository.All().Select(e => 
                                                new SelectListItem{Text=e.Name, 
                                                                   Value=e.Id});
    return View("MyView", model); 
} 

Update

If your select list population code is determining WHICH options to present I think you probably should move that to a service in your business layer. If reusability is the big concern, rouen's answer looks like it has the most possibility for reuse.

Tracitracie answered 25/10, 2011 at 18:30 Comment(0)
E
0

I use to fill lists even when the model is invalid. One other possible solution is to have an action returning the json information and build the select via ajax. SOmetimes I've also resorted to static properties / cached collections. I guess it's always depending on the particular case.

PS: You can use a local Model in each action, so I can leave initialization inside the Model constructor. (often I override a base model with [NonAction] utilities as well).

For example, I have an Employee list used widely in your application.

I've added some utility method in a base controller to build up SelectListItems and the likes. Since each and every model inherits from the base, I've got them almost everywhere in the app. Of course the Collection is filled via a dedicated business objec.

Erinerina answered 25/10, 2011 at 19:50 Comment(0)
J
0

What I do is I have a static function in a class that returns a SelectList. The method accepts an Enum value which defines which SelectList to return. In the View the DropDownList or DropDownListFor functions call this function to get the SelectList.

The static function looks like this:

class HelperMethods
{
  enum LookupType {Users, Companies, States};

  public static SelectList CommonSelectList(LookupType type, int? filterValue = null)
    //filterValue can be used if the results need to be filtered in some way
    var db = new WhateverEntities();

    switch (type)
    {  
       case LookupType.Users:
         var list = db.Users.OrderBy(u => u.LastName).ToList()
         return new SelectList(list, "ID", "FullName")
         break;

       case LookupType.Companies
         var list = db.Companies.OrderBy(u => u.Name).ToList()
         return new SelectList(list, "ID", "Name")
         break;

       //and so on...
    }
  }
}

And the view contains this:

@Html.DropDownListFor(m => m.UserID, HelperMethods.CommonSelectList(LookupType.Users))

This way the Model and Controller does not need code to configure a SelectList to send over to the View. It makes it very easy to reuse a SelectList that has already been configured. Also, if a View needs to loop through a list of objects, then this same function can be used to get a list for that. This is the simplest and most convenient way I found of doing this.

Jaella answered 8/4, 2015 at 16:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.