4

Please note that although this is closely related to this question, the answer there is specific to one known model property, whereas I want this to work for any property, so the component is reusable.

I want to use the Blazor <ValidationMessage> tag within a component. However, when I do this, the validation message isn't shown.

For example, the following component (FormRowText.razor) creates a row (in the Bootstrap grid sense) containing an <input type="text /> for a named property on a model...

<div class="form-group row">
  <label for="@PropertyName" class="col-lg-2 col-form-label">@(Caption ?? PropertyName)</label>
  <div class="col-lg-10 input-group">
    <div class="input-group-prepend">
      <span class="input-group-text">
        <i class="far fa-@Icon"></i>
      </span>
    </div>
    <input class="form-control" Value="@Value" type="text"
           id="@PropertyName" name="@PropertyName" @onchange="@OnChanged" />
  </div>
</div>

@code {

  [Parameter]
  public string PropertyName { get; set; }

  [Parameter]
  public string Value { get; set; }

  [Parameter]
  public EventCallback<string> ValueChanged { get; set; }

  [Parameter]
  public string Caption { get; set; }

  [Parameter]
  public string Icon { get; set; }

  private async Task OnChanged(ChangeEventArgs cea) =>
    await ValueChanged.InvokeAsync(cea.Value?.ToString() ?? "");

}

It is used like this...

<EditForm ...>
  <FormRowText PropertyName="@nameof(Customer.Email)" @bind-Value="_customer.Email" Icon="at" />
</EditForm>

If I add a <ValidationMessage> after this markup, it works fine, but if I add it inside the component (which is where I want it to be), it doesn't work.

I saw this answer from @enet, which shows how to do this for a specific, known model property. However, my component is designed to work with any model property, and I'm not sure how to modify it.

I tried adding a parameter for the model (with @typeparam T at the top of the file)...

[Parameter]
public T Model { get; set; }

...and adding the following inside the HTML...

<ValidationMessage For="() => PropertyName" />

However this doesn't work.

Any ideas?

1 Answer 1

6

You can cascade the EditContext to your component and use it to notify that the current field has changed

FormRowText.razor.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;    

public partial class FormRowText
    {
        [CascadingParameter] EditContext CascadedEditContext { get; set; }

        [Parameter]
        public string PropertyName { get; set; }

        [Parameter]
        public string Value { get; set; }

        [Parameter]
        public EventCallback<string> ValueChanged { get; set; }

        [Parameter]
        public string Caption { get; set; }

        [Parameter]
        public string Icon { get; set; }

        [Parameter] public Expression<Func<string>> ValueExpression { get; set; }
        private async Task OnChanged(ChangeEventArgs cea)
        {
            await ValueChanged.InvokeAsync(cea.Value?.ToString() ?? "");
            var identifier = CascadedEditContext.Field(PropertyName);
            CascadedEditContext.NotifyFieldChanged(identifier);
        }
         
    }

FormRowText.razor

<div class="form-group row">
    <label for="@PropertyName" class="col-lg-2 col-form-label">@(Caption ?? PropertyName)</label>
    <div class="col-lg-10 input-group">
        <div class="input-group-prepend">
            <span class="input-group-text">
                <i class="far fa-@Icon"></i>
            </span>
        </div>
        <input class="form-control" value="@Value" type="text" id="@PropertyName" name="@PropertyName" 
               @onchange="@OnChanged" />
        <ValidationMessage For="@ValueExpression" />

    </div>
</div>

You'll also need

public class Customer
{
    [Required]
    public string Name { get; set; }
    [Required]
    [DataType(DataType.EmailAddress)]
    [EmailAddress]
    public string Email { get; set; }
}

Usage

@page "/"

@using System.ComponentModel.DataAnnotations;
@using Microsoft.AspNetCore.Components.Forms;

<EditForm  EditContext="EditContext" OnValidSubmit="HandleValidSubmit">
    <DataAnnotationsValidator />

    <div class="form-group">
        <label for="name">Name: </label>
         <InputText @bind-Value="@customer.Name"></InputText>
        <ValidationMessage For="@(() => customer.Name)" />
    </div>
    <div class="form-group">
        <FormRowText @bind-Value="customer.Email" Caption="Email" 
             PropertyName="@nameof(Customer.Email)" Icon="at"></FormRowText>
    </div>
    <p>
        <button type="submit" class="btn btn-primary">Save</button>
    </p>
</EditForm>

@code{
    private Customer customer = new Customer();
    private EditContext EditContext;

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

    public async Task HandleValidSubmit()
    {
        await Task.Delay(1);

        Console.WriteLine("Saving...");   
    }
}

Another version

As you're using the EditForm component, it'd be a good idea to use the Forms components, such as the InputText component and such like.

The following code snippet describes how to implement the FormRowText component by sub-classing the InputText components, and adding new properties. Note that the PropertyName is not required for the functioning of the component

FormRowText2.razor

@inherits InputText

<div class="form-group row">
    <label for="@PropertyName" class="col-lg-2 col-form-label">@(Caption ?? PropertyName)</label>
    <div class="col-lg-10 input-group">
        <div class="input-group-prepend">
            <span class="input-group-text">
                <i class="far fa-@Icon"></i>
            </span>
        </div>
        <input class="form-control" @bind="CurrentValue" type="text" id="@PropertyName" name="@PropertyName" />
        <ValidationMessage For="@ValueExpression" />
     </div>
</div>

@code{
    [Parameter]
    public string Caption { get; set; }

    [Parameter]
    public string Icon { get; set; }

    [Parameter]
    public string PropertyName { get; set; }
    
}
Sign up to request clarification or add additional context in comments.

3 Comments

Thanks, that's exactly what I needed! Do you have a blog? If not - you should! You've been a great help to me a few times, I'd like to see more of your thoughts and ideas.
No, I don't have a blog. Actually I'm not a programmer at all. I do that for fun. Right now I'm volunteering in charity association that provides hot meals to the poor. See in my profile a link to donate ;) This link can navigate you to all my answers related to Blazor: stackoverflow.com/tags/blazor/topusers
You're not a programmer? I do this for a living and don't know as much as you do! Hats off to you for your צדקה work. Good to know that there are people like you around. כל הכבוד to you, will certainly donate!

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.