3

I've discovered that my Blazor design for this particular context is unusual, and I'm having trouble figuring out how to address it.

This is my data class models:

Data class model holder of database-fetched data

[Keyless]
public class Specs
{
  [Column("id")]
  public string? ID { get; set; }

  [Column("name")]
  public string? Name { get; set; }

  [Column("value")]
  public string? Value { get; set; }

  [Column("rank")]
  public int Rank { get; set; }
}

Data class model holder of user-modified data

public class SpecsEntryHolder(string? id, string? n, string? val, int rank)
{
  public string? ID { get; set; } = id;

  public string? Name { get; set; } = n;

  public string? Value { get; set; } = val;

  public int Rank { get; set; } = rank;
}

Redefine those data class models as a List

public List<Specs> SpecsList { get; set; } = [];
public List<SpecsEntryHolder> SpecsEntryHolderList { get; set; } = [];

I cloned the data through this approach:

// SpecsList has already been fed with the database records.
SpecsEntryHolderList = SpecsList.Select(v => new SpecsEntryHolder(v.ID, v.Name, v.Value, v.Rank)).ToList();

In the Razor UI page:

<EditForm EditContext="editContext" OnValidSubmit="UpdateSomething" FormName="edit" Enhance>
    <DataAnnotationsValidator />
    @* other fields here... *@
    @foreach (var specs in SpecsEntryHolderList)
    {
        <div class="col-12 col-sm-6 col-lg-3 mb-3 d-flex flex-column align-items-start">
            <label for="someText" class="form-label text-nowrap fw-bold">@($"{specs.Name}")</label>
            <InputText id="@($"{specs.ID}")"
                @bind-Value="specs.Value"
                @onblur="FormatOnBlurSpecs"
                class="form-control form-control-sm" />
            <ValidationMessage
                For="() => specs.Value"
                class="text-danger" />
        </div>
    }
</EditForm>

Note: I have an already existing EditForm setup with <DataAnnotationsValidator /> that has other fields that are separated from this context. I excluded them because they're not part of this case. This note is to mention that I have existing validation for another field, and this should be in a single form because it falls in a single EditContext.

Here's what it looks like:

sample output

I was hoping this would work, but it doesn’t:

Version 1

private void FormatOnBlurSpecs(FocusEventArgs e)
{
    foreach (var spec in SpecsEntryHolderList)
    {
        var fieldIdentifier = editContext.Field(nameof(spec.Value));
        if (string.IsNullOrWhiteSpace(spec.Value))
        {
            messageStore!.Add(fieldIdentifier, $"{spec.Name} is required.");
            editContext!.NotifyValidationStateChanged();
        }
        messageStore?.Clear(fieldIdentifier);
        editContext!.NotifyValidationStateChanged();
    }
}

Version 2

private void FormatOnBlurSpecs(FocusEventArgs e)
{
    foreach (var spec in SpecsEntryHolderList)
    {
        var fieldIdentifier = new FieldIdentifier(spec, nameof(spec.Value));
        if (string.IsNullOrWhiteSpace(spec.Value))
        {
            messageStore!.Add(fieldIdentifier, $"{spec.Name} is required.");
            editContext!.NotifyValidationStateChanged();
        }
        messageStore?.Clear(fieldIdentifier);
        editContext!.NotifyValidationStateChanged();
    }
}

Version 3 (For testing)

This testing procedure is to verify that the fields generated by the foreach loop are properly validated to see whether they are empty or not. It also properly checks a certain field to see if it's empty. Take note that it's not limited to string.IsNullOrWhiteSpace(specs.Value).

private void FormatOnBlurSpecs(FocusEventArgs e)
{    
    foreach (var specs in SpecsEntryHolderList)
    {
        if (string.IsNullOrWhiteSpace(specs.Value))
        {
            Console.WriteLine($"Spec {specs.Name} (ID={specs.ID}) is empty!");
        }
        else
        {
            Console.WriteLine($"Spec {specs.Value} (ID={specs.ID})");
        }
    }
}

In the example above, I provided a minimal setup to simplify the process. However, in a real application, you should handle message validation for each text field, as they will likely not be the same. For example, each field has its own specific validation that's unique from other fields. However, the tricky part is that the field is generated by a foreach loop, so it's actually a single field.

I was trying to programmatically handle the validation for a certain field when it's empty (again, it's not limited to being an emptiness of the field) through this approach (if it is possible), but I can't achieve what I wanted. I already know that the basic implementation of built-in validation in Blazor is through a set of fields that corresponds to each text entry. The approach I'm trying to achieve is to provide a custom validation instead, with a reconceptualized field binding source. I was trying to figure out this limitation and its corresponding workaround since it is obviously a dynamic setup of InputText.

Here is my additional question, aside from the title question for this context:

  • Is there a workaround for this approach? If so, how can it be implemented properly?
12
  • Although I have already resolved the issue of not having a validation for a certain field that is empty, I'm still open to other workarounds where I could also handle it dynamically and granularly for a looped field. For example, each field has its own specific validation that is not similar to other fields. Yet, I've no idea how to do it. In the meantime, I'll leave it as is, since at least it worked. Commented Sep 13 at 7:01
  • 1
    Have you looked into using FluentValidation instead of data annotations? In my experience FluentValidation is way more powerful and customizable. Chris Sainty has also released a NuGet package to make it super simple to replace data annotations validation with Fluent in a Blazor app. Commented Sep 15 at 17:38
  • @Lex, if you believe that you can post an official answer related to FluentValidation and having a foreach loop for every text field, I will accept it instead of my own answer. My question is all about having a customizable validation message, even when using a loop. Commented Sep 16 at 0:53
  • @Lex, I also updated my question to put the important bits from my comment here. Commented Sep 16 at 1:00
  • 1
    @SolarOsQuiere, I updated my question for you to see what I've been through during my testing and trial-and-error process. Commented Sep 17 at 13:01

2 Answers 2

2

The simple answer to the question is to specify the [Required(ErrorMessage = "Error message here...")] annotation on the corresponding field, instead of programmatically defining where an invalid entry occurred. So, in my code snippet, it should look like this instead:

public class SpecsEntryHolder(string? id, string? n, string? val, int rank)
{
  public string? ID { get; set; } = id;

  public string? Name { get; set; } = n;

  [Required(ErrorMessage = "This field is required.")]
  public string? Value { get; set; } = val;

  public int Rank { get; set; } = rank;
}

To trigger validation upon focus changes on a specific field, especially if it's empty:

private void FormatOnBlurSpecs(FocusEventArgs e)
{
    editContext.NotifyValidationStateChanged();
}

Out of curiosity, I further investigated the behavior of the validation features that I've defined. I found out that specifying @onblur="FormatOnBlurSpecs" is redundant for this context. The simple solution to validate if it's empty is only the [Required(ErrorMessage = "Error message here...")] annotation. Therefore, there's no significant impact to the program of defining it with the FormatOnBlurSpecs method.


Furthermore, the sections "Data class model holder of user-modified data..." and "I cloned the data through this approach:..." are also redundant. I've found out that I can simply define it with SpecsList and Specs alone, without cloning the database-fetched records with another data class model.

P.S., I constantly update my answer to suppress the identified overheads, which I think is helpful to avoid introducing bad habits in programming.

Sign up to request clarification or add additional context in comments.

Comments

1

You can definitely do what you are after using FluentValidation. There's probably a few different ways you could do this, but my approach was to add one additional class that holds the collection of Specs objects which then allows the creation of a validator for it.

SpecsContainer.cs

public class SpecsContainer
{
    public List<Specs> SpecsList { get; set; }
}

SpecsValidator.cs

using FluentValidation;

public class SpecsValidator : AbstractValidator<Specs>
{
    public SpecsValidator()
    {
        this.RuleFor(x => x.Value)
            .Custom(
                (value, context) =>
                {
                    if (string.IsNullOrWhiteSpace(value))
                    {
                        context.AddFailure($"{context.InstanceToValidate.Name} is required.");
                    }
                });
    }
}

SpecsContainerValidator.cs

using FluentValidation;

public class SpecsContainerValidator : AbstractValidator<SpecsContainer>
{
    public SpecsContainerValidator()
    {
        this.RuleForEach(x => x.SpecsList).SetValidator(new SpecsValidator());
    }
}

In your Razor component create an instance of SpecsContainer and assign the list of Specs to its SpecsList property. When you define your EditForm you assign SpecsContainer as the model and then your validation should work and show a unique required message for each Specs entity. Note that this solution will require both FluentValidation and Blazored.FluentValidation NuGet packages.

Modified Razor

<EditForm Model="this.SpecsContainer"
          OnValidSubmit="this.UpdateSomething">
    <FluentValidationValidator />
    @foreach (var specs in this.SpecsContainer.SpecsList)
    {
        <div class="col-12 col-sm-6 col-lg-3 mb-3 d-flex flex-column align-items-start">
            <label for="@specs.ID" class="form-label text-nowrap fw-bold">@specs.Name</label>
            <InputText id="@specs.ID"
                       @bind-Value="specs.Value"
                       class="form-control form-control-sm" />
            <ValidationMessage
                For="() => specs.Value"
                class="text-danger" />
        </div>
    }
    <div class="row">
        <div class="col">
            <button type="submit">Submit</button>
        </div>
    </div>
</EditForm>

3 Comments

Just one follow-up question, does your concept perform the same behavior as <DataAnnotationsValidator />? I forgot to note that I have other validation that's already working fine with <DataAnnotationsValidator />. I'll try to mix them up, let's see what will happen.
Good question, I've never tried to mix the two. Honestly, FluentValidation is my go to validator just because of the flexibility and power it provides. I have all my validators defined together in one folder of my solution so everyone knows where to go to look for validation rules.
I found another issue with having two different instances of validator classes, such as <DataAnnotationsValidator /> and <FluentValidationValidator />. This introduces duplicate validation messages. I had to remove the <DataAnnotationsValidator /> component and handle the validation through a codebase control using editContext.Field(nameof(field)) with ValidationMessageStore instead. It now works exactly as I desire, but I need to work on another validation refinement.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.