0

I am trying to validate two fields in a class that must total less than a third value. If Property1 + Property2 is greater than X then show an error.

I've created a custom attribute that will throw the error on the changed field if the total is greater than X. If you then reduce the value on this same field the validation error will be removed. However if you reduce the total by reducing the value in the other field than the validation error will still be shown.

Ideally, I'd be able to force Validation on Property1 to be refreshed if I update Property2 and vice-versa. However as far as I can tell, it will only do this is the value is changed from the original value at the close of the set block.

I'm using C# with a Blazor Server App and I've been doing the validation within the classes as opposed to resorting to JavaScript or TypeScript.

I've created a simple example below to replicate the issue. When the two boxes combined total more than ten a validation error is shown against the box which pushes it over 10.

At page load:

enter image description here

Changed the first value to 8, raising the total to 12:

enter image description here

Now reduced the second value to 1, reducing the total to 9:

enter image description here

If rather than reduce the second box to 1, I were to change it to 3, the total would still be over 10 and that box would also show its validation summary. Then if I changed either box to a 1, the validation error would disappear only for that one box.

Could anyone please give guidance on how to resolve this? Ideally I'd like a way of forcing the validation to fire on the both boxes if either were changed. It may be a case that being new to Blazor, I'm approaching this at an entirely wrong angle.

using Sample.ValidationAttributes;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;

namespace Sample.ViewModels
{
    public class SampleClass
    {
        [Required]
        [DisplayName("Sample A")]
        [LessThan10(ErrorMessage = "Sample A + Sample B Must be less than 10")]
        public int SampleIntA { get; set; }

        [Required]
        [DisplayName("Sample B")]
        [LessThan10(ErrorMessage = "Sample A + Sample B Must be less than 10")]
        public int SampleIntB { get; set; }

    }
}
namespace Sample.ValidationAttributes
{
    using Sample.ViewModels;
    using System.ComponentModel.DataAnnotations;

    public class LessThan10Attribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            if (((SampleClass)validationContext.ObjectInstance).SampleIntA + ((SampleClass)validationContext.ObjectInstance).SampleIntB > 10)
            {
                return new ValidationResult(ErrorMessage);
            }

            return ValidationResult.Success;
        }
    }
}
using Sample.ViewModels;

namespace Sample.Data
{
    public class SampleService
    {
        public SampleClass GetSampleClass()
        {
            return new SampleClass()
            {
                SampleIntA = 3,
                SampleIntB = 4
            };
        }
    }
}
@page "/SamplePage"
@using Sample.Data;
@using Sample.ViewModels;

@inject SampleService sampleService

<h3>SamplePage</h3>

<EditForm Model="@sampleClass" >
    <DataAnnotationsValidator />
    <ValidationSummary />

    <InputNumber id="SampleIntA" @bind-Value="sampleClass.SampleIntA" class="form-control" autocomplete="off" />
    <ValidationMessage For="() => sampleClass.SampleIntA" class="help-block text-danger" />

    <InputNumber id="SampleIntB" @bind-Value="sampleClass.SampleIntB" class="form-control" autocomplete="off" />
    <ValidationMessage For="() => sampleClass.SampleIntB" class="help-block text-danger" />

</EditForm>

@code {
    private SampleClass sampleClass;

    protected override async Task OnInitializedAsync()
    {
        sampleClass = sampleService.GetSampleClass();
    }
}

Jay
  • 878
  • 3
  • 12
  • 22

2 Answers2

0

Your validator only works on current field won't affect other fields,

You may try as below:

Model:

public class SampleClass
    {
        [Required]
        [DisplayName("Sample A")]
       
        public int SampleIntA { get; set; }

        [Required]
        [DisplayName("Sample B")]
        
        public int SampleIntB { get; set; }

       

    }

Custom Vlidation:

 public class CustomValidation : ComponentBase
    {
        private ValidationMessageStore? messageStore;

        [CascadingParameter]
        private EditContext? CurrentEditContext { get; set; }

        protected override void OnInitialized()
        {
            if (CurrentEditContext is null)
            {
                throw new InvalidOperationException(
                    $"{nameof(CustomValidation)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. " +
                    $"For example, you can use {nameof(CustomValidation)} " +
                    $"inside an {nameof(EditForm)}.");
            }

            messageStore = new(CurrentEditContext);

            CurrentEditContext.OnValidationRequested += (s, e) =>
                messageStore?.Clear();
            
        }

        public void DisplayErrors(Dictionary<string, List<string>> errors)
        {
            if (CurrentEditContext is not null)
            {
                foreach (var err in errors)
                {
                    messageStore?.Add(CurrentEditContext.Field(err.Key), err.Value);
                }

                CurrentEditContext.NotifyValidationStateChanged();
            }
        }

        public void ClearErrors()
        {
            messageStore?.Clear();
            CurrentEditContext?.NotifyValidationStateChanged();
        }
    }

Razor component:

EditForm  EditContext="@EditContext">

    <CustomValidation @ref="customValidation" />

    <ValidationSummary />

    <InputNumber id="SampleIntA" @bind-Value="sampleClass.SampleIntA"   class="form-control" autocomplete="off" />
    <ValidationMessage For="() => sampleClass.SampleIntA" class="help-block text-danger" />

    <InputNumber id="SampleIntB" @bind-Value="sampleClass.SampleIntB"   class="form-control" autocomplete="off" />
    <ValidationMessage For="() => sampleClass.SampleIntB" class="help-block text-danger" />

</EditForm>

@code {
    private SampleClass sampleClass;
    private EditContext EditContext;
    private CustomValidation? customValidation;
    [Parameter]
    public EventCallback<ChangeEventArgs> ValueChanged { get; set; }


    protected override void OnInitialized()
    {
        sampleClass = new SampleClass(){SampleIntA=3,SampleIntB=4};

        EditContext = new EditContext(sampleClass);
        EditContext.OnFieldChanged += EditContext_OnFieldChanged;

        base.OnInitialized();

    }
    private void EditContext_OnFieldChanged(object sender, FieldChangedEventArgs e)
    {
       
        customValidation?.ClearErrors();
        var errors = new Dictionary<string, List<string>>();

        if (sampleClass.SampleIntA + sampleClass.SampleIntB > 10)
        {
            errors.Add(nameof(sampleClass.SampleIntA),
                new() { "Sample A + Sample B Must be less than 10" });
            errors.Add(nameof(sampleClass.SampleIntB),
            new() { "Sample A + Sample B Must be less than 10" });
        }

        if (errors.Any())
        {
            customValidation?.DisplayErrors(errors);
        }
        else
        {
            
        }
    }   

}

Result:

enter image description here

Ruikai Feng
  • 6,823
  • 1
  • 2
  • 11
0

My approach to this would be to build a composite control with a built in validator so we encapsulate all the logic in one place.

Here's my composite control. Note it captures the cascaded EditContext and has two binds.

@using System.Linq.Expressions;

<div class="row">
    <div class="col-12 col-sm-4 col-lg-2 mb-3">
        <label class="form-label">Field 1</label>
        <InputNumber TValue="int" class="form-control" Value="ValueA" ValueChanged="this.OnASet" ValueExpression="ValueAExpression" />
    </div>
    <div class="col-12 col-sm-4 col-lg-2">
        <label class="form-label">Field 2</label>
        <InputNumber TValue="int" class="form-control" Value="ValueB" ValueChanged="this.OnBSet" ValueExpression="ValueBExpression" />
    </div>
</div>

<div class="col-12">
    <ValidationMessage For="ValueAExpression" />
</div>

@code {
    [CascadingParameter] private EditContext? editContext { get; set; }
    [Parameter] public int ValueA { get; set; }
    [Parameter] public EventCallback<int> ValueAChanged { get; set; }
    [Parameter] public Expression<Func<int>>? ValueAExpression { get; set; }
    [Parameter] public int ValueB { get; set; }
    [Parameter] public EventCallback<int> ValueBChanged { get; set; }
    [Parameter] public Expression<Func<int>>? ValueBExpression { get; set; }

    private ValidationMessageStore? _messageStore;
    private int a;
    private int b;
    private bool _isValid;

    protected override void OnInitialized()
    {
        ArgumentNullException.ThrowIfNull(editContext);

        _messageStore = new(editContext);
        a = this.ValueA;
        b = this.ValueB;
        this.editContext.OnValidationRequested += this.Validate;
    }

    private async Task OnASet(int value)
    {
        a = value;
        this.CheckValidation();
        await ValueAChanged.InvokeAsync(value);
    }

    private async Task OnBSet(int value)
    {
        b = value;
        this.CheckValidation();
        await ValueBChanged.InvokeAsync(value);
    }

    private void Validate(object? sender, ValidationRequestedEventArgs e)
    {
        this.CheckValidation();
    }

    private void CheckValidation()
    {
        if (ValueAExpression is null || ValueBExpression is null)
            return;

        var isValid = a + b <= 10;

        if (isValid && !_isValid)
            _messageStore?.Clear();

        if (!isValid && _isValid)
        {
            var fiA = FieldIdentifier.Create(ValueAExpression);
            _messageStore?.Add(fiA, "Value A and B must total less that 10");
            var fiB = FieldIdentifier.Create(ValueBExpression);
            _messageStore?.Add(fiB, "Value B and A must total less that 10");
            this.editContext?.NotifyValidationStateChanged();
        }

        if (_isValid != isValid)
            this.editContext?.NotifyValidationStateChanged();

        _isValid = isValid;
    }
}

And then the demo page:

@page "/"
@using System.ComponentModel.DataAnnotations;

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<EditForm EditContext="_editContext">
    <Component @bind-ValueA="_model.SampleIntA" @bind-ValueB="_model.SampleIntB" />
    <div class="bg-light border border-1 m-2 p-2 ">
        <ValidationSummary/>
    </div>
</EditForm>

<div class="bg-dark text-white m-2 p-2">
    <pre>A = @_model.SampleIntA</pre>
    <pre>B = @_model.SampleIntB</pre>
</div>

@code {
    private Model _model = new();
    private EditContext? _editContext;

    protected override void OnInitialized()
    {
        _editContext = new EditContext(_model);
    }

    public class Model
    {
        [Required]
        public int SampleIntA { get; set; }

        [Required]
        public int SampleIntB { get; set; }
    }
}
MrC aka Shaun Curtis
  • 19,075
  • 3
  • 13
  • 31