Stilgar
Stilgar

Reputation: 23561

ASP.NET Blazor Required Validation with InputSelect

I am trying to get the Required attribute to work with InputSelect but validation doesn't work in Blazor Server. The form can be submitted without selection. Interestingly it works when the model property is nullable. Before .NET 5.0 it didn't work with nullable types because the InputSelect didn't support them. I however want a non-nullable required property because I want to reuse the dto from my API as a model and because it is logically wrong.

enter image description here

public class SomeModel
{
    [Required]
    public string SomeString { get; set; }

    [Required]
    public SomeEnum SomeEnum { get; set; }

    [Required]
    public SomeEnum? SomeNullableEnum { get; set; }

    [Required]
    public int SomeInt { get; set; }

    [Required]
    public int? SomeNullableInt { get; set; }
}

public enum SomeEnum
{
    A = 1,
    B = 2
}

The page

@page "/testrequired"
@using TestNET5BlazorServerApp.Data;


<EditForm Model="Model" OnValidSubmit="Submit">

    <DataAnnotationsValidator />
    <ValidationSummary />

    String:
    <br />
    <InputText @bind-Value="Model.SomeString" />
    <br />
    <br />
    Enum:
    <br />
    <InputSelect @bind-Value="Model.SomeEnum">
        <option value="">Select Enum</option>
        <option value="@SomeEnum.A">@SomeEnum.A</option>
        <option value="@SomeEnum.B">@SomeEnum.B</option>
    </InputSelect>
    <br />
    <br />

    Nullable Enum:
    <br />
    <InputSelect @bind-Value="Model.SomeNullableEnum">
        <option>Select Nullable Enum</option>
        <option value="@SomeEnum.A">@SomeEnum.A</option>
        <option value="@SomeEnum.B">@SomeEnum.B</option>
    </InputSelect>
    <br />
    <br />

    Int:
    <br />
    <InputSelect @bind-Value="Model.SomeInt">
        <option>Select Int</option>
        <option value="1">1</option>
        <option value="2">2</option>
    </InputSelect>
    <br />
    <br />

    Nullable Int:
    <br />
    <InputSelect @bind-Value="Model.SomeNullableInt">
        <option>Select Nullable Int</option>
        <option value="1">1</option>
        <option value="2">2</option>
    </InputSelect>
    <br />
    <br />

    <button type="submit">Save</button>
</EditForm>

@code 
{
    SomeModel Model = new Data.SomeModel();

    void Submit()
    {
        System.Diagnostics.Debug.WriteLine("Enum " + Model.SomeEnum);
        System.Diagnostics.Debug.WriteLine("Nullable Enum " + Model.SomeNullableEnum);
        System.Diagnostics.Debug.WriteLine("Int " + Model.SomeInt);
        System.Diagnostics.Debug.WriteLine("Nullable Int " + Model.SomeNullableInt);
    }
}

Upvotes: 7

Views: 10587

Answers (3)

willingdev
willingdev

Reputation: 9596

I had the problem if I use int for Id of ViewModel because the int default value is 0 not null. We must use string for Id property and @bind-Value to that string. Also for recognizing not selected any item in InputSelect add an option with value=""

<div class="form-floating mb-3">
    <InputSelect @bind-Value="vm.IndustryIdString" class="form-control" placeholder>
        <option value="">Select industry</option>
        @foreach (var industry in Industries)
        {
            <option value="@industry.Id">@industry.Title</option>
        }
    </InputSelect>
    <label>Industry</label>
    <ValidationMessage For="() => vm.IndustryIdString" class="text-danger" />
</div>

[SupplyParameterFromForm]
private AddCommodityVm vm { get; set; } = new();

public class AddCommodityVm
{
   [Display(Name = "Industry")]
   [Required(ErrorMessage = Constants.RequiredMsg)]
   public string? IndustryIdString { get; set; }
}

public class Constants
{
    public const string RequiredMsg = "The {0} field is required.";
}

Upvotes: 1

AggressiveMail
AggressiveMail

Reputation: 69

A quick and dirty workaround would be to use the Range attribute on the enum in your model. You must assign numeric values to your enum though and use the attribute based on them. It's definitely not the best solution, but this is what works for me temporarily. If anyone finds a better solution, please share it.

Example:

public class SomeModel
{
    [Required]
    public string SomeString { get; set; }

    [Required]
    [Range(1, int.MaxValue)]
    public SomeEnum SomeEnum { get; set; }

    [Required]
    public SomeEnum? SomeNullableEnum { get; set; }

    [Required]
    public int SomeInt { get; set; }

    [Required]
    public int? SomeNullableInt { get; set; }
}

public enum SomeEnum
{
    A = 1,
    B = 2
}

Upvotes: 4

enet
enet

Reputation: 45626

Not sure I understand you, but let's give it a try...

I however want a non-nullable required property

Do you mean, as for instance, that you want a property such as the following:

[Required]
public int SomeInt { get; set; }

be bound to an InputSelect component like this:

<InputSelect @bind-Value="Model.SomeInt">
    <option>Select Int</option>
    <option value="1">1</option>
    <option value="2">2</option>
</InputSelect>

And when the user hit the "Submit" button, a validation message should be displayed if the user did not select a value ?

If yes, this is my answer:

The InputSelect component, at least before .Net 5.0, can only bind to string and enum types.

If you want to make your InputSelect supports binding to an int, as in the case above, you should subclass it as follows...

public class InputSelectNumber<T> : InputSelect<T>
    {
        
        protected override bool TryParseValueFromString(string value, out T result, out string validationErrorMessage)
        {
            if (typeof(T) == typeof(int))
            {
                if (int.TryParse(value, out var resultInt))
                {
                    result = (T)(object)resultInt;
                    validationErrorMessage = null;
                    return true;
                }
                else
                {
                    result = default;
                    validationErrorMessage = "The chosen value is not a valid number.";
                    return false;
                }
            }
            else
            {
                return base.TryParseValueFromString(value, out result, out validationErrorMessage);
            }
        }
    }

Update

I can bind it, but it does not honor the required attribute, no validation is performed

Please, run the code below, enter a value for the name field, then press the "Submit" button. The form is "submitted". Now, select a country, and then select "Select your country:"... a validation message is displayed. Conclusion: Validation occurs only if a value was previously selected and then removed. Is this behavior by design or a bug, I don't know. I'm, however, of the opinion that this behavior is not related to Blazor. Need to check how a select element with a Required attribute behave in Razor Pages. Anyhow, I'll try to solve this bug, and if succeeded, I'll let you know...

@using System.ComponentModel.DataAnnotations;
 
<EditForm EditContext="@EditContext" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />

    <div class="form-group">
        <label for="name">Enter your Name: </label>
        <InputText Id="name" Class="form-control" @bind-Value="@comment.Name"></InputText>
        <ValidationMessage For="@(() => comment.Name)" />

    </div>
    <div class="form-group">
        <label for="body">Select your country: </label>

        <InputSelect @bind-Value="@comment.Country" >
            <option value="0">Select country...</option>
            @foreach (var country in Enum.GetValues(typeof(Country)))
            {
            
                <option value="@country">@country</option>
            }
        </InputSelect>
               
        <ValidationMessage For="@(() => comment.Country)" />
    </div>

    <p>
        <button type="submit">Submit</button>
    </p>
</EditForm>


@code
    {
    private EditContext EditContext;
    private Comment comment = new Comment();

    private void HandleValidSubmit()
    {
        Console.WriteLine("Submitting");
    }
    protected override void OnInitialized()
    {
        EditContext = new EditContext(comment);


        base.OnInitialized();
    }

    public enum Country
    {
        USA = 1,
        Britain,
        Germany,
        Israel

    }
    public class Comment
    {
        [Required]
        public string Name { get; set; }
        [Required]
        public Country Country { get; set; }
    }

}   

Upvotes: 3

Related Questions