Search code examples
c#.netvalidationblazorblazor-editform

How to provide ValidationMessage For with property generated at runtime?


I'm generating an object at runtime to use as the EditForm model. Validation is working but I'm unsure how to set up the ValidationMessage component which requires an Expression<Func<object>>.

I want to provide it with a property via reflection. Something like this:

<ValidationMessage For="@(() => modelType.GetProperty("MyString").GetValue(model))" />

How can I get an Expression from an object property generated at runtime?

EDIT:

Here is my code:

<EditForm Model="@GeneratedModel" OnInvalidSubmit="@HandleInvalidSubmit" OnValidSubmit="@OnValidSubmit">
    <DataAnnotationsValidator />
    <input @bind="TestPropBind" type="text" />
    <ValidationMessage For="@ValidationFor" />
</EditForm>

@code
{
    private object GeneratedModel { get; set; }

    private string TestPropBind
    {
        get
        {
            PropertyInfo? propertyInfo = GeneratedModel.GetType().GetProperty("Test");
            MethodInfo? getMethod = propertyInfo.GetGetMethod();
            return getMethod.Invoke(GeneratedModel, new object?[0]) as string;
        }
        set
        {
            PropertyInfo? propertyInfo = GeneratedModel.GetType().GetProperty("Test");
            MethodInfo? setMethod = propertyInfo.GetSetMethod();
            setMethod.Invoke(GeneratedModel, new[] { value });
        }
    }

    protected override void OnInitialized()
    {
    //GeneratedModel created and instantiated here at runtime
    }
}

Solution

  • ValidationMessage is a fairly simple component. Internally it uses the For to build a FieldIdentifier object which it uses to lookup validation messages in the EditContext's Validation Message Store.

    You can shortcut the whole reflection/expression builder process by building your own ValidationMessage that takes a FieldIdentifier as a parameter:

    @foreach (var message in editContext.GetValidationMessages(Identifier))
    {
        <div class="validation-message" @attributes=this.AdditionalAttributes>
            @message
        </div>
    }
    
    @code {
        [CascadingParameter] private EditContext editContext { get; set; } = default!;
        [Parameter, EditorRequired] public FieldIdentifier Identifier { get; set; } = new FieldIdentifier();
        [Parameter(CaptureUnmatchedValues = true)] public IReadOnlyDictionary<string, object>? AdditionalAttributes { get; set; }
    
        protected override void OnInitialized()
        {
            ArgumentNullException.ThrowIfNull(editContext);
        }
    }
    

    Your demo page:

    @page "/"
    @using System.ComponentModel.DataAnnotations;
    @using System.Reflection;
    
    <PageTitle>Index</PageTitle>
    
    <EditForm Model=this.model OnValidSubmit=this.OnValidSubmit >
        <DataAnnotationsValidator/>
        <div class="mb-3">
            <span class="form-label">Value</span>
            <input class="form-control" @bind=this.model.Value />
            <MyValidationMessage Identifier=TestPropIdentifier  />
        </div>
        <div class="mb-3">
            <button class="btn btn-primary">Submit</button>
        </div>
    </EditForm>
    
    @code {
        private Model model = new();
    
        private FieldIdentifier TestPropIdentifier => new FieldIdentifier(model, "Value");
    
        private string TestPropBind
        {
            get
            {
                PropertyInfo? propertyInfo = model.GetType().GetProperty("Value");
                MethodInfo? getMethod = propertyInfo?.GetGetMethod();
               return getMethod?.Invoke(model, new object?[0]) as string ?? string.Empty ;
            }
            set
            {
                PropertyInfo? propertyInfo = model.GetType().GetProperty("Value");
                MethodInfo? setMethod = propertyInfo?.GetSetMethod();
                setMethod?.Invoke(model, new[] { value });
            }
        }
    
        private void OnValidSubmit()
        {
        }
    
        public class Model
        {
            [Required]
            public string? Value { get; set; }
        }
    }