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.