MVC setting up Html.DropdownList on ModelState.IsV

2019-08-12 03:31发布

问题:

This is something that has always puzzled me as to the best way round, while keeping maintainable code. The below code sets up a list of months and years for a payment gateway form, before assigning these to a variable of type List<SelectListItem>.

Intial Action

PayNowViewModel paymentGateway = new PayNowViewModel();
List<SelectListItem> paymentGatewayMonthsList = new List<SelectListItem>();
List<SelectListItem> paymentGatewayYearsList = new List<SelectListItem>();

for (int i = 1; i <= 12; i++)
{
    SelectListItem selectListItem = new SelectListItem();
    selectListItem.Value = i.ToString();
    selectListItem.Text = i.ToString("00");

    paymentGatewayMonthsList.Add(selectListItem);
}

int year = DateTime.Now.Year;
for (int i = year; i <= year + 10; i++)
{
    SelectListItem selectListItem = new SelectListItem();
    selectListItem.Value = i.ToString();
    selectListItem.Text = i.ToString("00");

    paymentGatewayYearsList.Add(selectListItem);
}

paymentGateway.ExpiryMonth = paymentGatewayMonthsList;
paymentGateway.ExpiryYear = paymentGatewayYearsList;

return View(paymentGateway);

It's a fair bit of code, and I find myself repeating this code, in similar formats to re-setup the dropdown lists options should the ModelState.IsValid be false and I want to return back to the view for the user to correct there mistakes.

HttpPost Action - Code

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ConfirmPayment(PayNowViewModel paymentGatewayForm, FormCollection form)
{
    if (ModelState.IsValid)
    {
        // Post processing actions...
        return View();
    }
    else
    {
        for (int i = 1; i <= 12; i++)
        {
            SelectListItem selectListItem = new SelectListItem();
            selectListItem.Value = i.ToString();
            selectListItem.Text = i.ToString("00");

            paymentGatewayMonthsList.Add(selectListItem);
        }

        int year = DateTime.Now.Year;
        for (int i = year; i <= year + 10; i++)
        {
            SelectListItem selectListItem = new SelectListItem();
            selectListItem.Value = i.ToString();
            selectListItem.Text = i.ToString("00");

            paymentGatewayYearsList.Add(selectListItem);
        }

        form.ExpiryMonth = paymentGatewayMonthsList;
        form.ExpiryYear = paymentGatewayYearsList;

        return View("MakePayment", form);
    }
}

What's the best way to centralise this dropdown setup code so its only in one place? At present you'll see a large proportion (the for loops), is exactly repeated twice. A base controller with function? Or is it better to re-setup like the above?

Any advice appreciated! Mike.

回答1:

Add a private method to your controller (the following code assumes your ExpiryMonth and ExpiryYear properties are IEnumerable<SelectListItem> which is all that the DropDownListFor() method requires)

private void ConfigureViewModel(PayNowViewModel model)
{
  model.ExpiryMonth = Enumerable.Range(1, 12).Select(m => new SelectListItem
  {
    Value = m.ToString(),
    Text = m.ToString("00")
  });
  model.ExpiryYear = Enumerable.Range(DateTime.Today.Year, 10).Select(y => new SelectListItem
  {
    Value = y.ToString(),
    Text = y.ToString("00")
  });
}

and then in the GET method

public ActionResult ConfirmPayment()
{
  PayNowViewModel model = new PayNowViewModel();
  ConfigureViewModel(model);
  return View(model);
}

and in the POST method

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult ConfirmPayment(PayNowViewModel model)
{
  if (!ModelState.IsValid)
  {
    ConfigureViewModel(model);
    return View(model);
  }
  .... // save and redirect (should not be returning the view here)
}


回答2:

If the set of your dropdown options is fixed (or recompilation is OK after the potential options change), you can use an enum to store your options.

public enum Month {
    // if the dropdown is not required, add default value 0
    Optional = 0, 
    [Display(Name = @"Month_January")]
    January = 1,
    [Display(Name = @"Month_February")]
    February = 2,
    // etc ..
}

To render this as a dropdown use an EditorTemplate Enum.cshtml:

  @model Enum
  @{
      var enumType = ViewData.ModelMetadata.ModelType;
      var allValues = Enum.GetValues(enumType).Cast<object>().ToSelectList(Model);
      // read any attributes like [Required] from ViewData and ModelMetadata ...                
      var attributes = new Dictionary<string, object>();    
   }

    @Html.DropDownListFor(m => m, allValues, attributes)

The ToSelectList extension method loops over all enum values and converts them to SelectListItems:

    public static IList<SelectListItem> ToSelectList<T>(this IEnumerable<T> list) {
        return ToSelectList<T>(list, list.FirstOrDefault());
    }

    public static IList<SelectListItem> ToSelectList<T>(this IEnumerable<T> list, T selectedItem) {
        var items = new List<SelectListItem>();
        var displayAttributeType = typeof(DisplayAttribute);

        foreach (var item in list) {
            string displayName;

            // multi-language: 
            // assume item is an enum value
            var field = item.GetType().GetField(item.ToString());
            try {
                // read [Display(Name = @"someKey")] attribute
                var attrs = (DisplayAttribute)field.GetCustomAttributes(displayAttributeType, false).First();
                // lookup translation for someKey in the Resource file
                displayName =  Resources.ResourceManager.GetString(attrs.Name);
            } catch {
                // no attribute -> display enum value name
                displayName = item.ToString();
            }

            // keep selected value after postback:
            // assume selectedItem is the Model passed from MVC
            var isSelected = false;
            if (selectedItem != null) {
                isSelected = (selectedItem.ToString() == item.ToString());
            }

            items.Add(new SelectListItem {
                Selected = isSelected,
                Text = displayName,
                Value = item.ToString()
            });
        }

        return items;
    }     

To support multiple languages, add translations for the display name keys, e.g. "Month_January", to the Resource file.

Now that the setup code has been abstracted away using some reflection magic, creating a new viewmodel is a breeze :>

public class PayNowViewModel {
    // SelectListItems are only generated if this gets rendered
    public Month ExpiryMonth { get; set; }
}

// Intial Action
var paymentGateway = new PayNowViewModel();
return View(paymentGateway);

// Razor View: call the EditorTemplate 
@Html.EditorFor(m => m.ExpiryMonth)

Note that in the EditorTemplate, Model is passed as the selected item to ToSelectList. After postback, Model will hold the currently selected value. Therefore it stays selected, even if you just return the model after an error in the controller:

// HttpPost Action
if (!ModelState.IsValid) {
    return View("MakePayment", paymentGatewayForm);
}

Took us some time to come up with this solution, credits go to the Saratiba team.