Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How can I validate a collection of objects? #199

Open
kingua opened this issue Oct 6, 2023 · 2 comments
Open

How can I validate a collection of objects? #199

kingua opened this issue Oct 6, 2023 · 2 comments
Labels
Question Question about this project Triage Issue needs to be triaged

Comments

@kingua
Copy link

kingua commented Oct 6, 2023

I'm trying to loop through and validate a collection of objects, but I'm getting the following error. It seems that the validator doesn't have the context of the object to validate. What am I doing wrong / how can I accomplish this?

System.InvalidOperationException: No pending ValidationResult found

Here is the component markup:

<div id="@HtmlIdBase">
    <Alert Id="@($"{HtmlIdBase}-error-alert")" AlertType="@AlertType" Message="@AlertMessage" />
    @if (IsLoading)
    {
        <LoadingIndicator HtmlIdBase="@HtmlIdBase" />
    }
    else
    {
        <div class="container-fluid">
            <TelerikForm @attributes="@Helpers.SetBlazorComponentHtmlIdAttribute($"{HtmlIdBase}")" Model="@Model" Columns="@FormLayoutColumnsCount" ColumnSpacing="25px" OnValidSubmit="@UploadEquipmentAttachments">
                <FormValidation>
                    <FluentValidationValidator @ref="FluentValidationValidator"></FluentValidationValidator>
                </FormValidation>
                <FormItems>
                    <FormItem>
                        <Template>
                            <label class="k-label k-form-label" for="@($"{HtmlIdBase}-description-textbox")">@Constants.FieldLabels.Description</label>
                            <TelerikTextBox @bind-Value="_description"/>
                        </Template>
                    </FormItem>
                    <TelerikFileSelect Multiple="true" OnSelect="@HandleAttachment" Accept="@AcceptedTypes()" AllowedExtensions="@_allowedMimeTypes.Keys.ToList()" />
                    <ValidationMessage For="@(() => FirstFileContent)" class="k-form-error k-invalid-msg"></ValidationMessage>
                </FormItems>
                <FormButtons>
                    <TelerikButton id="@($"{HtmlIdBase}-cancel-button")" ButtonType="@ButtonType.Button" OnClick="@NavigateToEquipmentSummary" Class="button-cancel">@Constants.ButtonLabels.Cancel</TelerikButton>
                    <TelerikButton id="@($"{HtmlIdBase}-upload-button")" ButtonType="@ButtonType.Submit" ThemeColor="@ThemeConstants.Button.ThemeColor.Primary" Class="button-save">@Constants.ButtonLabels.Upload</TelerikButton>
                </FormButtons>
            </TelerikForm>
        </div>
    }
</div>

Below is the codebehind in its entirety for context, but the relevant method is UploadEquipmentAttachments():

public partial class EquipmentAttachmentImageOrVideoUploadForm : ProjectFormComponent<EquipmentAttachmentImageOrVideoUploadForm>
{
    public const string HtmlIdBase = "equipment-attachment-image-or-video-upload";

    [Inject] private IEquipmentAttachmentService EquipmentAttachmentService { get; set; } = null!;

    protected override ILogger TaskRunnerLogger => Logger;

    [Parameter] public int EquipmentId { get; set; }

    public List<AttachmentUpload> Model = new();

    private readonly Dictionary<string, string> _allowedMimeTypes = MimeType.ImageAndVideo;
    private List<string> _validationMessages = new();
    private string _description = string.Empty;
    private byte[]? FirstFileContent => Model.Count > 0 ? Model[0].FileContent : null;

    public async Task HandleAttachment(FileSelectEventArgs args)
    {
        foreach (var file in args.Files)
        {
            var attachment = new AttachmentUpload();

            var byteArray = new byte[file.Size];
            await using var ms = new MemoryStream(byteArray);
            await file.Stream.CopyToAsync(ms);

            // NOTE: The Telerik Control limits file extensions to our allowed types, so this should never be null
            if (_allowedMimeTypes.TryGetValue(file.Extension, out var type))
            {
                attachment.ContentType = type;
            }

            attachment.FileContent = byteArray;
            attachment.FileName = file.Name;
            attachment.Description = _description?.Trim();

            Model.Add(attachment);
        }
    }

    private string AcceptedTypes()
    {
        var sb = new StringBuilder();

        foreach (var pair in _allowedMimeTypes)
        {
            if (sb.Length > 0) sb.Append(',');

            sb.Append(pair.Key);
            sb.Append(',');
            sb.Append(pair.Value);
        }

        var asString = sb.ToString();
        return asString;
    }

    public async Task UploadEquipmentAttachments()
    {
        if (ProjectId is null)
            return;

        await TryExecuteAsync(async _ =>
        {
            IsLoading = true;

            var numberOfValidAttachments = 0;

            foreach (var attachment in Model)
            {
                var validationResult = await FluentValidationValidator!.ValidateAsync(strategy =>
                    strategy.IncludeRuleSets(ValidatorRuleSets.ClientRules));

                if (validationResult)
                {
                    numberOfValidAttachments++;
                }
                else
                {
                    _validationMessages.Add($"Attachment: {attachment.FileName} is invalid.  Please remove.{Environment.NewLine}");
                }
            }

            if (numberOfValidAttachments != Model.Count)
            {
                AlertType = Alert.AlertTypes.Warning;
                AlertMessage = _validationMessages.ToString();
            }
            else
            {
                var serviceResults =
                        await EquipmentAttachmentService.UploadEquipmentAttachments(ProjectId.Value, EquipmentId, Model);

                var results = serviceResults as ServiceResult<bool>[] ?? serviceResults.ToArray();
                var allResultsSuccessful = results.All(sr => sr.Status == OperationResult.Success);

                if (allResultsSuccessful)
                {
                    NavigateToEquipmentSummary();
                }
                else
                {
                    foreach (var result in results)
                    {
                        HandleServiceResult(result,
                            onError: message =>
                            {
                                AlertType = Alert.AlertTypes.Warning;
                                AlertMessage += message + Environment.NewLine;
                            });
                    }
                }
            }

            IsLoading = false;
        }, (ex, _) => throw ex);
    }

    public void NavigateToEquipmentSummary()
    {
        NavigationManager.NavigateTo(
            Constants.Routes.Projects.EquipmentDetail(JobNumber!, EquipmentId));
    }
}

@kingua kingua added Question Question about this project Triage Issue needs to be triaged labels Oct 6, 2023
@kingua
Copy link
Author

kingua commented Oct 6, 2023

I think I figured this out:

public class AttachmentUploadValidator : AbstractValidator<AttachmentUpload>
{
    public AttachmentUploadValidator()
    {
        RuleLevelCascadeMode = CascadeMode.Stop;

        RuleSet(ValidatorRuleSets.ClientRules, () =>
        {
            RuleFor(attachment => attachment.AttachmentType)
                .NotEmpty()
                .When(attachment => MimeType.EquipmentInformation.ContainsValue(attachment.ContentType));

            RuleFor(attachment => attachment.FileContent).NotEmpty()
                .WithMessage("'File' must be selected");
        });
    }
}

**public class AttachmentUploadListValidator : AbstractValidator<List<AttachmentUpload>>
{
    public AttachmentUploadListValidator()
    {
        RuleForEach(x => x).SetValidator(new AttachmentUploadValidator());
    }
}**

@kingua
Copy link
Author

kingua commented Oct 6, 2023

The above seems to properly validate, but now I don't get the actual validation error message returned as validationResult is just a bool. Is there a way to get the error messages so I can do something like the following?

            foreach (var attachment in Model)
            {
                var validationResult = await FluentValidationValidator!.ValidateAsync(strategy =>
                    strategy.IncludeRuleSets(ValidatorRuleSets.ClientRules));

                if (validationResult)
                {
                    numberOfValidAttachments++;
                }
                else
                {
                    var errorMessages = string.Join(", ", validationResult.Errors.Select(e => e.ErrorMessage));
                    validationMessages.Add($"Attachment: {attachment.FileName} is invalid for the following reasons: {errorMessages}.");
                }
            }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question Question about this project Triage Issue needs to be triaged
Projects
None yet
Development

No branches or pull requests

1 participant