Search code examples
blazorfluentvalidation

FluentEditForm says that there is no data, despite data entered being there


My code, below, is a fluenteditform form. After I submit, it says that the fields still need to be filled out even though I filled them out. What is wrong with my code?

@page "/movieform" 
@using Assignment10.Entities
@using System.ComponentModel.DataAnnotations

<h3>MovieForm</h3>

<FluentCard>
    <FluentEditForm FormName="MovieForm" Model="@movie">
        <DataAnnotationsValidator />
        <FluentValidationSummary />
        <FluentGrid>
            <FluentGridItem xs="12">
                <FluentTextField Name="MovieNameField" Id="movieNameField" @bind-Value="movie.MovieName" Label="Name: " Required/>
                <ValidationMessage For="@(() => movie.MovieName)" />
            </FluentGridItem>
            <FluentGridItem xs="12">
                <FluentTextField Name="MoviePublisherField" Id="moviePublisherField" @bind-Value="movie.Publisher" Label="Publisher: " Required/>
                <ValidationMessage For="@(() => movie.Publisher)" />

            </FluentGridItem>
            <FluentGridItem xs="12" >
                <FluentTextField Name="MovieDescriptionField" Id="movieDescriptionField" @bind-Value="movie.MovieDescription" Label="Description: " Required/>
                <ValidationMessage For="@(() => movie.MovieDescription)" />
            </FluentGridItem>
            <FluentGridItem xs="12" >
                <FluentButton Type="ButtonType.Submit" Appearance="Appearance.Accent">Submit</FluentButton>
            </FluentGridItem>
        </FluentGrid>
    </FluentEditForm>
</FluentCard>

@code {
    private Movie movie = new Movie(); 

}

Solution

  • you use <DataAnnotationsValidator /> - that's the reason your validator haven't receive data - it try to pass data to DataAnnotations infrastructure.

    In my project I don't use libraries. Instead I use custom implementation of FluentValidationValidator component, and code looks like that:

    <EditForm Model="Model" OnValidSubmit="OnValidSubmit">
        <FluentValidationValidator TValidator="MyModelValidator" />
        <ValidationSummary />
    
        // your inputs, with validation messages, like below:
        <ValidationMessage For="@(() => Model.Thought)" />
    </EditForm>
    
    @code{
        private EditContext editContext;
        private ValidationMessageStore messageStore;
    
        protected override void OnInitialized()
        {
            Model = new MyModel();
    
            editContext = new EditContext(Model);
            messageStore = new(editContext);
    
            base.OnInitialized();
        }
    
        private async Task OnValidSubmit()
        {
            // your save logic here
        }
    }
    

    And component itself (code found on stackoverflow):

    public class FluentValidationValidator<TValidator> : ComponentBase where TValidator : IValidator, new()
    {
        private readonly static char[] separators = new[] { '.', '[' };
        private TValidator validator;
    
        [CascadingParameter]
        private EditContext EditContext { get; set; }
    
        protected override void OnInitialized()
        {
            validator = new TValidator();
            var messages = new ValidationMessageStore(EditContext);
    
            /* Re-validate when any field changes or when the entire form   requests validation.*/
            EditContext.OnFieldChanged += (sender, eventArgs)
                => ValidateModel((EditContext)sender!, messages, false);
    
            EditContext.OnValidationRequested += (sender, eventArgs)
                => ValidateModel((EditContext)sender!, messages, true);
        }
    
        private void ValidateModel(EditContext editContext, ValidationMessageStore messages, bool submit)
        {
            if (submit)
                editContext.Properties["submitted"] = true;
    
            if (!editContext.Properties.TryGetValue("submitted", out _))
                return;
    
            var context = new ValidationContext<object>(editContext.Model);
            var validationResult = validator.Validate(context);
            messages.Clear();
            foreach (var error in validationResult.Errors)
            {
                var fieldIdentifier = ToFieldIdentifier(editContext, error.PropertyName);
                messages.Add(fieldIdentifier, error.ErrorMessage);
            }
            editContext.NotifyValidationStateChanged();
        }
    
        private static FieldIdentifier ToFieldIdentifier(EditContext editContext, string propertyPath)
        {
            var obj = editContext.Model;
    
            while (true)
            {
                var nextTokenEnd = propertyPath.IndexOfAny(separators);
                if (nextTokenEnd < 0)
                {
                    return new FieldIdentifier(obj, propertyPath);
                }
    
                var nextToken = propertyPath.Substring(0, nextTokenEnd);
                propertyPath = propertyPath.Substring(nextTokenEnd + 1);
    
                object? newObj;
                if (nextToken.EndsWith("]"))
                {
                    nextToken = nextToken.Substring(0, nextToken.Length - 1);
                    var prop = obj.GetType().GetProperty("Item");
                    var indexerType = prop!.GetIndexParameters()[0].ParameterType;
                    var indexerValue = Convert.ChangeType(nextToken, indexerType);
                    newObj = prop.GetValue(obj, [indexerValue]);
                }
                else
                {
                    var prop = obj.GetType().GetProperty(nextToken);
                    if (prop == null)
                    {
                        throw new InvalidOperationException($"Could not find property named {nextToken} in object of type {obj.GetType().FullName}.");
                    }
                    newObj = prop.GetValue(obj);
                }
    
                if (newObj == null)
                {
                    return new FieldIdentifier(obj, nextToken);
                }
    
                obj = newObj;
            }
        }
    }
    

    It works for me on several projects. Pros is you can extend component and add some custom logic, if necessary. Cons is you need to support it in your project. So if you decide to not support, you can try to use something from this link.

    Hope it helpful.