SteveSandersonMS / BlazorInputFile

A file input component for Blazor applications
251 stars 75 forks source link

Upload action of InputFile submits parent EditForm #21

Open nemtajo opened 4 years ago

nemtajo commented 4 years ago

Hi Steve, first, thank you for this great component!

I wrapped InputFile in a reusable component CustomInputFile that supports upload/download/delete functionality by calling methods on a back-end FileController, and notifies the parent component about changes through OnFileUploaded EventCallback.


@inject HttpClient HttpClient
@using System.IO
@using BlazorInputFile
@using TuServicioWebsite.Shared.Utils
@inject NavigationManager UriHelper

<InputFile OnChange="HandleFileSelected" UnmatchedParameters="UnmatchedParameters" />
@if (selectedFile != null)
{
    isLoading = selectedFile.Data.Position > 0;

<div class="file-row">
    <div>
        <h2>@selectedFile.Name</h2>
        Size: <strong>@savedFileSize</strong>;
        Last update: <strong>@selectedFile.LastModified.ToShortDateString()</strong>;
        Type: <strong>@selectedFile.Type</strong>
    </div>

    <!-- Upload button -->
    <button @onclick="() => LoadFile(selectedFile)" disabled="@isLoading">
        @if (!isLoading)
        {
            <span>Subir</span>
        }
        else
        {
            <span>Loaded @((100.0 * selectedFile.Data.Position / selectedFile.Size).ToString("0"))%</span>
        }
    </button>
    @if (isLoading && (savedFileId == Guid.Empty))
    {
        <button @onclick="() => ClearInputFileFields()">Delete</button>
    }
    @if (savedFileId != Guid.Empty)
    {
        <button @onclick="() => DownloadFile()">Download</button>
        <button @onclick="() => DeleteFile()">Delete</button>
    }
</div>
}

@code {

    IFileListEntry selectedFile;
    bool isLoading;
    [Parameter] public EventCallback<Tuple<Guid, string, string, string>> OnFileUploaded { get; set; }
    [Parameter(CaptureUnmatchedValues = true)] public Dictionary<string, object> UnmatchedParameters { get; set; }

    Guid savedFileId { get; set; } = Guid.Empty;
    string savedFileName { get; set; } = "";
    string savedFileType { get; set; } = "";
    string savedFileSize { get; set; } = "";

    void HandleFileSelected(IFileListEntry[] files)
    {
        selectedFile = files.FirstOrDefault();
    }

    async Task LoadFile(IFileListEntry file)
    {
        // So the UI updates to show progress
        file.OnDataRead += (sender, eventArgs) => InvokeAsync(StateHasChanged);

        // Load content into .NET memory stream
        // Alternatively it could be saved to disk, or parsed in memory, or similar
        var ms = new MemoryStream();
        await file.Data.CopyToAsync(ms);

        //Send file content to back-end
        var content = new MultipartFormDataContent {
                { new ByteArrayContent(ms.GetBuffer()), "\"upload\"", file.Name }
            };
        HttpResponseMessage response = await HttpClient.PostAsync("api/File/Upload", content);
        //Notify parent component
        string guidText = await response.Content.ReadAsStringAsync();
        savedFileId = new Guid(guidText);
        savedFileName = file.Name;
        savedFileSize = UiHelper.ConvertSizeInBytesToHumanReadableDescription(file.Size);
        savedFileType = file.Type;
        await OnFileUploaded.InvokeAsync(new Tuple<Guid, string, string, string>(savedFileId, savedFileName, savedFileType, savedFileSize));
    }

    async void DownloadFile()
    {
        if (savedFileId != Guid.Empty)
            UriHelper.NavigateTo($"api/File/Download/{savedFileId}", forceLoad: true);
    }

    async void DeleteFile()
    {
        if (savedFileId != Guid.Empty)
        {
            await HttpClient.PostJsonAsync($"api/File/Delete/{savedFileId}", savedFileId);
            await ClearInputFileFields();

        }
    }
    async Task ClearInputFileFields()
    {
        savedFileId = Guid.Empty;
        savedFileName = "";
        savedFileType = "";
        selectedFile = null;
        isLoading = false;
        await OnFileUploaded.InvokeAsync(new Tuple<Guid, string, string, string>(savedFileId, savedFileName, savedFileType, savedFileSize));
    }
}

When using it in EditForm of the parent .razor page, on finished upload, EditForm is also submitted. Here is the code of parent component:

<EditForm id="editFormDocumentation" Model="@pd" OnValidSubmit="@(async () => await CreateProjectDataModel())">
            <DataAnnotationsValidator />
            <ValidationSummary />
           <div class="form-group row">
                <label for="projectCode" class="col-form-label col-sm-4">Project code</label>
                <InputText id="projectCode" class="form-control col-sm-8" @bind-Value="@pd.ProjectCode" />
                <ValidationMessage For="@(() => pd.ProjectCode)" />
            </div>
            <div class="form-group row">
                <label for="projectName" class="col-form-label col-sm-4">Project name</label>
                <InputText id="projectName" class="form-control col-sm-8" @bind-Value="@pd.ProjectName" />
                <ValidationMessage For="@(() => pd.ProjectName)" />
            </div>
            <div class="form-group row">
                <label for="dbc" class="col-sm-4 col-form-label">DBC file</label>
                <div id="dbc" class="col-sm-8">
                    <CustomInputFile OnFileUploaded="@((args) => {
                                                       pd.DbcFileId = args.Item1;
                                                       pd.DbcFileName = args.Item2;
                                                       StateHasChanged();
                                                   })" />
                    <ValidationMessage For="@(() => pd.DbcFileId)" />
                </div>
            </div>
            <div class="form-group row">
                <label for="terms" class="col-sm-4 col-form-label">Reference terms</label>
                <div id="terms" class="col-sm-8">
                    <CustomInputFile OnFileUploaded="@((args) => {
                                                       pd.ReferenceTermsFileId = args.Item1;
                                                       pd.ReferenceTermsFileName = args.Item2;
                                                       StateHasChanged();
                                                   })" />
                    <ValidationMessage For="@(() => pd.ReferenceTermsFileId)" />
                </div>
            </div>
            <div class="form-group row">
                <div class="col-sm-4">
                    <input type="submit" class="btn btn-danger" @onclick="@Cancel" style="width:220px;" value="Cancel" />
                </div>
                <div class="col-sm-8">
                    <input type="submit" class="btn btn-success" @onclick="@SubmitData" style="width:220px;" value="Save" />
                </div>
            </div>
        </EditForm>

Code behind the parent page is very simple


    ProjectDataModel pd = new ProjectDataModel();
...
    private async Task SubmitData()
    {
        System.Console.WriteLine("Window 1: Data submitted.");
    }

    async Task CreateProjectDataModel()
    {
        if (pd != null && !string.IsNullOrEmpty(paramProjectCode))
        {
            await HttpClient.SendJsonAsync(HttpMethod.Put, "api/ProjectsData/Edit", pd);
        }
        else
        {
            pd = await HttpClient.SendJsonAsync<ProjectDataModel>(HttpMethod.Post, "api/ProjectsData/Create", pd);
        }
        UriHelper.NavigateTo("/Dashboard/fetch");
    }

    void Cancel()
    {
        UriHelper.NavigateTo("/Dashboard/fetch");
    }
public class ProjectDataModel
    {
        [Required]
        [Key]
        public string ProjectCode { get; set; }
        [Required]
        public string ProjectName { get; set; }
        public Guid DbcFileId { get; set; }
        public string DbcFileName {get; set; }
        public Guid ReferenceTermsFileId { get; set; }
        public string ReferenceTermsFileName { get; set; }
}

Because ProjectCode and ProjectName properties are specified, when I upload a file (in my case Dbc file), the validation passes and method CreateProjectDataModel gets called before I upload the second file, ReferenceTerms.

My question is, how do I make InputFile not to call CreateProjectDataModel of parent EditForm ? I still want to have validation on the file info(DbcFileId !=Guid.Empty or regex validation on file name).

File presence is not necessary for the EditForm model to be valid. File can be uploaded later, after saving ProjectDataModel.

Moerty commented 4 years ago

Any updates on this?

The input triggers the EditForms OnValidSubmit Method.

Edit: I add @onclick:preventDefault to my button and it works.