BlazorInputFile icon indicating copy to clipboard operation
BlazorInputFile copied to clipboard

Upload action of InputFile submits parent EditForm

Open nemtajo opened this issue 6 years ago • 1 comments

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.

nemtajo avatar Dec 26 '19 22:12 nemtajo

Any updates on this?

The input triggers the EditForms OnValidSubmit Method.

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

Moerty avatar Jun 12 '20 16:06 Moerty