0

I found this great post by Chris Sainty: Creating Bespoke Input Components for Blazor from Scratch. It is exactly what I need, but not with string, but with uploaded files IBrowserFile. So I have adapted and extended the example for me. The customized component displays the new files and saves it in my model, but in the CSS the status unfortunately stays on class="modified invalid". I must be missing a small detail here. What is it? Thanks in advance for any hints.

Here is my code reduced to the essentials.

Selection.razor

    @page "/selection"
    @inherits ParentComponent<SelectionTestModel>
    <PageComponent @ref="Page" Model="Model" StatusCode="StatusCode" PageType="PageType.Touch">
        <PageBody>
            <EditForm Model="Model" OnValidSubmit="Save">
                <DataAnnotationsValidator />
    
                <DocumentComponent @ref="DocumentUpload" @bind-Documents="Model.Files" />
    
            </EditForm>
        </PageBody>
    </PageComponent>
    @code {
        private DocumentComponent DocumentUpload;
    }

SelectionTestModel.cs

public class SelectionTestModel
{
    public int? KeyID { get; set; }
    /* ... */
    [System.ComponentModel.DisplayName("Document")]
    [System.ComponentModel.DataAnnotations.Display(Name = "Document")]
    [System.ComponentModel.DataAnnotations.Range(2, 2, ErrorMessage = "You have to bring exactly two files!")]
    public List<DocumentModel> Files { get; set; } = new List<DocumentModel>();
}

DocumentModel

public class DocumentModel
{
    public int? Id { get; set; }
    public string Reference { get; set; }

    public string Name { get; set; }
    public long Size { get; set; }

    public string ContentType { get; set; }
    public string Content { get; set; } /*file as base64 string*/
}

DocumentComponent.razor

@using System.Linq.Expressions

<div class="dropzone rounded @_dropClass @_validClass">
     <InputFile id="inputDrop" multiple
               ondragover="event.preventDefault()"
               ondragstart="event.dataTransfer.setData('', event.target.id)"
               accept="@AllowedFileTypes"
               OnChange="OnInputFileChange"
               @ondragenter="HandleDragEnter"
               @ondragleave="HandleDragLeave" />
    @*...*@
</div>

@code {
    [CascadingParameter] public EditContext EditContext { get; set; }
    [Parameter] public List<DocumentModel> Documents { get; set; } = new List<DocumentModel>();
    [Parameter] public EventCallback<List<DocumentModel>> DocumentsChanged { get; set; }
    [Parameter] public Expression<Func<List<DocumentModel>>> DocumentsExpression { get; set; }

    /*...*/    
    public List<string> AllowedFileTypes { get; set; } = new List<string> { ".pdf", /*...*/ };
    private FieldIdentifier _fieldIdentifier;
    private string _validClass => EditContext?.FieldCssClass(_fieldIdentifier) ?? null;

    protected override void OnInitialized()
    {
        base.OnInitialized();

        _fieldIdentifier = FieldIdentifier.Create(DocumentsExpression);
    }

    private async Task OnInputFileChange(InputFileChangeEventArgs e)
    {
        // validation: do we accept the file (content type, amount of files, size)
        if (e.FileCount == 1) // keep it simple for this example
        {
            // read from IBrowserFile and return DocumentModel in memory only
            Documents.Add(await SaveFile(e.File));

            await DocumentsChanged.InvokeAsync(Documents);
            EditContext?.NotifyFieldChanged(_fieldIdentifier);
        }
    }

    /*...*/
}

How does it behave in the browser (Chrome)

After loading the page everything looks as expected. DocumentComponent - 0 file

After that I upload a single file. So I have one file and I expect two. The validation turns red and I get "modified invalid". So far everything is great. DocumentComponent - 1 file

Finally I drag another file into the component and get two files. I can also see this in the model. But unfortunately the class attribute "modified valid" is not set. DocumentComponent - 2 files

Thanks again for any advice

skyfrog
  • 107
  • 10

1 Answers1

0

I dug too deep in the wrong direction and didn't see the obvious.

The problem is that there is an attribute set in the model that does not throw an error, but also cannot validate. The Range attribute is not for lists and therefore the model could never validate. With an own attribute I could work around this.

SelectionTestModel.cs

    [Library.Validation.Attribute.ListRange(2, 2)]
    public List<DocumentModel> Files { get; set; } = new List<DocumentModel>();

ListRangeAttribute.cs

namespace Library.Validation.Attribute
{
    public class ListRangeAttribute : ValidationAttribute
    {
        public int Minimum { get; set; }
        public int Maximum { get; set; }

        public ListRangeAttribute(int minimum = 0, int maximum = int.MaxValue)
        {
            Minimum = minimum > 0 ? minimum : 0;
            Maximum = maximum;
        }

        public string GetErrorMessage(string displayName) { /* ... */ }

        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var list = value as IList;
            if (list == null)
            {
                throw new InvalidOperationException($"Attribute {nameof(ListRangeAttribute)} must be on a property of type {nameof(IList)}.");
            }

            if ((list?.Count ?? 0) < Minimum || (list?.Count ?? int.MaxValue) > Maximum)
            {
                return new ValidationResult(GetErrorMessage(validationContext.DisplayName), new[] { validationContext.MemberName });
            }

            return ValidationResult.Success;
        }
    }
}

I hope this post can help others.

Remaining: Now I am left with a new mystery. Why does the validation text disappear after a save button click, which could not be saved due to an invalid state of the model!?

skyfrog
  • 107
  • 10