MVC setting up Html.DropdownList on ModelState.IsValid = false

前端 未结 2 1581
说谎
说谎 2021-01-22 01:17

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 f

2条回答
  •  伪装坚强ぢ
    2021-01-22 02:17

    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().ToSelectList(Model);
          // read any attributes like [Required] from ViewData and ModelMetadata ...                
          var attributes = new Dictionary();    
       }
    
        @Html.DropDownListFor(m => m, allValues, attributes)
    
    
    

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

        public static IList ToSelectList(this IEnumerable list) {
            return ToSelectList(list, list.FirstOrDefault());
        }
    
        public static IList ToSelectList(this IEnumerable list, T selectedItem) {
            var items = new List();
            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.

    提交回复
    热议问题