MVC Server-side Validation of RadioButton and Drop

2020-05-08 08:22发布

问题:

Using ASP.NET Core 2.2 Razor Pages, I'm exploring binding radio buttons and dropdownlists to the page model.

Plenty of people are asking about client-side validation to "get it to work".

My question is: when I look at this code. Is there any server-side check being done by the binding engine?

@foreach (var gender in Model.Genders)
{
    <input type="radio" asp-for="Gender" value="@gender" id="Gender@(gender)" /> @gender
}

@Html.DropDownListFor(x => x.Country, new List<SelectListItem>
{
    new SelectListItem() {Text = "Canada", Value="CA"},
    new SelectListItem() {Text = "USA", Value="US"},
    new SelectListItem() {Text = "Mexico", Value="MX"}
})  

What's preventing someone from posting gender "bababa" and country "xxx", which could cause undefined behaviors in my code and database?

I'd be surprised if the above code is doing such validation (correct me if I'm wrong), and I couldn't find posts asking about that because everyone is asking about client-side validation.

What's the recommend approach here?

回答1:

Server side and client side validations are important, you always need implement server side validations, maybe your client validations could be omited but never the server side validations, the code that you posted not perform any server side validation



回答2:

Came up with my own elegant solution since I found nothing out there.

With the helper class below, I'll declare my model with this

[BindProperty]
public InputList Gender { get; set; } = new InputList(new[] { "Man", "Woman" });

[BindProperty]
public InputList Country { get; set; } = new InputList(new NameValueCollection()
{
    { "", "--Select--" },
    { "CA", "Canada" },
    { "US", "USA" },
    { "MX", "Mexico" }
});

Insert radio buttons and a dropdown list on my page

@foreach (var item in Model.Gender.ListItems)
{
    <input type="radio" asp-for="Gender.Value" value="@item.Value" id="Gender@(item.Value)" /><label for="Gender@(item.Value)" style="padding-right:15px;"> @item.Text </label>
}
<span asp-validation-for="Gender" class="text-danger"></span>

@Html.DropDownListFor(x => x.Country.Value, Model.Country.ListItems)
<span asp-validation-for="Country" class="text-danger"></span>

And voilà! Validation works both on the client-side and server-side, ensuring posted value is valid.

Of course, can move "Man" and "Woman" into constants, and can move the list of countries into a separate class that generates it once for the whole application.

Here's the InputList helper class.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.Rendering;

namespace EmergenceGuardian.WebsiteTools.Web
{
    /// <summary>
    /// Represents a list of items to display as radio buttons or drop down list that can be bound to a web page and validated.
    /// </summary>
    [InputListValidation]
    public class InputList
    {
        /// <summary>
        /// Initializes a new instance of InputList with specified list of items that will be used for both the value and text.
        /// </summary>
        /// <param name="values">A list of string values reprenting valid values.</param>
        /// <param name="required">Whether this field is required.</param>
        public InputList(IEnumerable<string> values, bool required = true)
        {
            Required = required;
            foreach (var item in values)
            {
                ListItems.Add(new SelectListItem(item, item));
            }
        }

        /// <summary>
        /// Initializes a new instance of InputList with specified list of SelectListItem objects.
        /// </summary>
        /// <param name="values">A list of SelectListItem objects representing display text and valid values.</param>
        /// <param name="required">Whether this field is required.</param>
        public InputList(IEnumerable<SelectListItem> values, bool required = true)
        {
            Required = required;
            ListItems.AddRange(values);
        }

        /// <summary>
        /// Initializes a new instance of InputList with a NameValueCollection allowing quick collection initializer.
        /// </summary>
        /// <param name="values">The NameValueCollection containing display texts and valid values.</param>
        /// <param name="required">Whether this field is required.</param>
        public InputList(NameValueCollection values, bool required = true)
        {
            Required = required;
            foreach (var key in values.AllKeys)
            {
                ListItems.Add(new SelectListItem(values[key], key));
            }
        }

        /// <summary>
        /// Gets or sets whether this field is required.
        /// </summary>
        public bool Required { get; set; }
        /// <summary>
        /// Gets or sets the list of display text and valid values, used for display and validation.
        /// </summary>
        public List<SelectListItem> ListItems { get; set; } = new List<SelectListItem>();
        /// <summary>
        /// Gets or sets the user input value. This value can be bound to the UI and validated by InputListValidation.
        /// </summary>
        public string Value { get; set; }
    }

    /// <summary>
    /// Validates an InputList class to ensure Value is contained in ListItems.
    /// </summary>
    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
    sealed public class InputListValidationAttribute : ValidationAttribute
    {
        private const string DefaultErrorMessage = "Selected value is invalid.";
        private const string DefaultRequiredErrorMessage = "The {0} field is required.";

        public InputListValidationAttribute()
        {
        }

        /// <summary>
        /// Validates whether InputList.Value contains a valid value.
        /// </summary>
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var input = value as InputList;
            if (input != null)
            {
                if (string.IsNullOrEmpty(input.Value))
                {
                    if (input.Required)
                    {
                        return new ValidationResult(string.Format(ErrorMessage ?? DefaultRequiredErrorMessage, validationContext.MemberName));
                    }
                }
                else if (input.ListItems?.Any(x => x.Value == input.Value) == false)
                {
                    return new ValidationResult(ErrorMessage ?? DefaultErrorMessage);
                }

            }
            return ValidationResult.Success;
        }
    }
}