Pelkas
Pelkas

Reputation: 33

Date validator in Blazor Server Side

I have a simple input model for my blazor server side component. I want to use the build in validation for two DateTime properties.

[DataType(DataType.Date)]
public DateTime? FromDate { get; set; }
[DataType(DataType.Date)]
public DateTime? ToDate { get; set; }

How can I only accept ToDate > FromDate?

Upvotes: 3

Views: 1898

Answers (1)

Dimitris Maragkos
Dimitris Maragkos

Reputation: 11322

Solution using custom ValidationAttributes:

DateMustBeAfterAttribute.cs:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class DateMustBeAfterAttribute : ValidationAttribute
{
    public DateMustBeAfterAttribute(string targetPropertyName)
        => TargetPropertyName = targetPropertyName;

    public string TargetPropertyName { get; }

    public string GetErrorMessage(string propertyName) =>
        $"'{propertyName}' must be after '{TargetPropertyName}'.";

    protected override ValidationResult? IsValid(
        object? value, ValidationContext validationContext)
    {
        var targetValue = validationContext.ObjectInstance
            .GetType()
            .GetProperty(TargetPropertyName)
            ?.GetValue(validationContext.ObjectInstance, null);
        
        if ((DateTime?)value < (DateTime?)targetValue)
        {
            var propertyName = validationContext.MemberName ?? string.Empty;
            return new ValidationResult(GetErrorMessage(propertyName), new[] { propertyName });
        }

        return ValidationResult.Success;
    }
}

DateMustBeBeforeAttribute.cs:

[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
public class DateMustBeBeforeAttribute : ValidationAttribute
{
    public DateMustBeBeforeAttribute(string targetPropertyName)
        => TargetPropertyName = targetPropertyName;

    public string TargetPropertyName { get; }

    public string GetErrorMessage(string propertyName) =>
        $"'{propertyName}' must be before '{TargetPropertyName}'.";

    protected override ValidationResult? IsValid(
        object? value, ValidationContext validationContext)
    {
        var targetValue = validationContext.ObjectInstance
            .GetType()
            .GetProperty(TargetPropertyName)
            ?.GetValue(validationContext.ObjectInstance, null);

        if ((DateTime?)value > (DateTime?)targetValue)
        {
            var propertyName = validationContext.MemberName ?? string.Empty;
            return new ValidationResult(GetErrorMessage(propertyName), new[] { propertyName });
        }

        return ValidationResult.Success;
    }
}

Usage:

public class DateTimeModel
{
    [Required]
    [DateMustBeBefore(nameof(ToDate))]
    [DataType(DataType.Date)]
    public DateTime? FromDate { get; set; }

    [Required]
    [DateMustBeAfter(nameof(FromDate))]
    [DataType(DataType.Date)]   
    public DateTime? ToDate { get; set; }
}

The fields are linked so we need to notify EditContext when any one of them changes to re-validate the other.

Example EditForm:

<EditForm EditContext="editContext" OnInvalidSubmit="@HandleValidSubmit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <label>
            From Date:
            <InputDate TValue="DateTime?"
                       Value="dateTimeModel.FromDate"
                       ValueChanged="HandleFromDateChanged"
                       ValueExpression="() => dateTimeModel.FromDate" />
        </label>
        <ValidationMessage For="@(() => dateTimeModel.FromDate)" />
    </p>

    <p>
        <label>
            To Date:
            <InputDate TValue="DateTime?"
                       Value="dateTimeModel.ToDate"
                       ValueChanged="HandleToDateChanged"
                       ValueExpression="() => dateTimeModel.ToDate" />
        </label>
        <ValidationMessage For="@(() => dateTimeModel.ToDate)" />
    </p>

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

@code {
    private EditContext? editContext;
    private DateTimeModel dateTimeModel = new();

    protected override void OnInitialized()
    {
        editContext = new EditContext(dateTimeModel);
    }

    private void HandleFromDateChanged(DateTime? fromDate)
    {
        dateTimeModel.FromDate = fromDate;

        if (editContext != null && dateTimeModel.ToDate != null)
        {
            FieldIdentifier toDateField = editContext.Field(nameof(DateTimeModel.ToDate));
            editContext.NotifyFieldChanged(toDateField);
        }
    }

    private void HandleToDateChanged(DateTime? toDate)
    {
        dateTimeModel.ToDate = toDate;

        if (editContext != null && dateTimeModel.FromDate != null)
        {
            FieldIdentifier fromDateField = editContext.Field(nameof(DateTimeModel.FromDate));
            editContext.NotifyFieldChanged(fromDateField);
        }
    }

    private void HandleValidSubmit()
    {
    }
}

Blazor fiddle example

GitHub repository with demo

Solution using IValidatableObject:

To do more complex validation checks, your model can inherit from IValidatableObject interface and implement the Validate method:

public class ExampleModel : IValidatableObject
{
    [DataType(DataType.Date)]
    public DateTime? FromDate { get; set; }

    [DataType(DataType.Date)]
    public DateTime? ToDate { get; set; }

    public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
    {
        if (ToDate < FromDate)
        {
            yield return new ValidationResult("ToDate must be after FromDate", new[] { nameof(ToDate) });
        }
    }
}

Blazor fiddle example

Upvotes: 3

Related Questions