Reputation: 471
I'm working on a Blazor application using .NET 5 to upload PNG images. The functionality involves displaying file information (size, name, etc.) after selecting files, and subsequently submitting them.
Everything works as expected, except when I try to submit one file after another. I encounter an exception: "There is no file with ID 1. The file list may have changed."
Here's a simplified version of the code:
<button @onclick="Submit">Télécharger</button>
@code {
IList<string> imageDataUrls = new List<string>();
List<IBrowserFile> list = new List<IBrowserFile>();
void OnInputFileChange(InputFileChangeEventArgs e)
{
var maxAllowedFiles = 3;
list.AddRange(e.GetMultipleFiles(maxAllowedFiles));
}
async Task Submit()
{
var format = "image/png";
foreach (var imageFile in list)
{
var resizedImageFile = await imageFile.RequestImageFileAsync(format,
100, 100);
var buffer = new byte[resizedImageFile.Size];
await resizedImageFile.OpenReadStream().ReadAsync(buffer);
var imageDataUrl =
$"data:{format};base64,{Convert.ToBase64String(buffer)}";
imageDataUrls.Add(imageDataUrl);
}
}
}
The issue arises when attempting to submit multiple files sequentially. I receive an error related to changing file IDs, specifically "There is no file with ID 1. The file list may have changed."
I'm seeking guidance on how to handle this exception when submitting files one after another and how to avoid these ID-related issues in the Blazor application. Any assistance would be greatly appreciated.
Upvotes: 5
Views: 11651
Reputation: 786
The issue you are describing is an inherent limitation of the InputFile component, as stated in the docs:
File selection isn't cumulative when using an InputFile component or its underlying HTML , so you can't add files to an existing file selection. The component always replaces the user's initial file selection, so file references from prior selections aren't available.
While the previous answers posted here do work, they are problematic, since they load the entire contents of the selected files into memory (as ntasm says, "nasty workaround").
Fortunately there is another simple, efficient solution: dynamically add a new InputFile component every time OnInputFileChange is called, hiding the previous ones.
Here is your code with the necessary additions:
@page "/"
<h3>Upload PNG images</h3>
<p>
@for (int i = 0; i <= currentFileSelection; i++) { //++++++++++
<InputFile OnChange="@OnInputFileChange" multiple hidden="@(i != currentFileSelection)"/> //<<<<<<<<<<<<<<<
} //++++++++++
</p>
@if (imageDataUrls.Count > 0)
{
<h4>Images</h4>
<div class="card" style="width:30rem;">
<div class="card-body">
@foreach (var imageDataUrl in imageDataUrls)
{
<img class="rounded m-1" src="@imageDataUrl" />
}
</div>
</div>
}
@if (list.Count() > 0)
{
<div class="card" style="width:30rem;">
<div class="card-body">
@foreach (var file in list)
{
<div>@file.Name - @file.ContentType - @file.Size</div>
}
</div>
</div>
}
<button @onclick="Submit">Télécharger</button>
@code {
IList<string> imageDataUrls = new List<string>();
List<IBrowserFile> list = new List<IBrowserFile>();
int currentFileSelection = 0; //++++++++++
void OnInputFileChange(InputFileChangeEventArgs e)
{
var maxAllowedFiles = 3;
list.AddRange(e.GetMultipleFiles(maxAllowedFiles));
currentFileSelection++; //++++++++++
}
async Task Submit()
{
var format = "image/png";
foreach (var imageFile in list)
{
var resizedImageFile = await imageFile.RequestImageFileAsync(format,
100, 100);
var buffer = new byte[resizedImageFile.Size];
await resizedImageFile.OpenReadStream().ReadAsync(buffer);
var imageDataUrl =
$"data:{format};base64,{Convert.ToBase64String(buffer)}";
imageDataUrls.Add(imageDataUrl);
}
}
}
When Submit() is invoked, all the IBrowserFiles in the list have their references intact, so no error occurs.
Upvotes: 1
Reputation: 2372
Nasty workaround. Would not recommend with large and many files. Especially in server side
Here is same issue on BlazorInputFile github How to upload multiple files on form submit
private async Task Save()
{
foreach (var addedFile in _addedFiles)
{
using var stream = new MemoryStream(addedFile);
var fileContent = new StreamContent(stream);
var response = await new WebClient.PostAsync($"file", fileContent);
}
}
private List<byte[]> _addedFiles = new List<byte[]>();
private async Task SingleUpload(InputFileChangeEventArgs arg)
{
long maxFileSize = 1024 * 1024 * 15;
var data = arg.File.OpenReadStream(maxFileSize);
using var memoryString = new MemoryStream();
await data.CopyToAsync(memoryString);
_addedFiles.Add(memoryString.GetBuffer());
model.Files.Add(new FileModel(arg.File.Name));
}
Upvotes: 1
Reputation: 475
Once you leave the OnInputFileChange
handler method, you will lose the stream to the files. So you have to Read the stream and save it somewhere before you leave. Something like this worked for me. Must be done in the OnInputFileChange
event handler
List<byte[]> list = new List<byte[]>();
void OnInputFileChange(InputFileChangeEventArgs e)
{
var maxAllowedFiles = 3;
foreach (var imageFile in e.GetMultipleFiles(maxAllowedFiles))
{
var resizedImageFile = await imageFile.RequestImageFileAsync(format,100, 100);
var buffer = new byte[resizedImageFile.Size];
await resizedImageFile.OpenReadStream().ReadAsync(buffer);
list.Add(buffer);
}
}
async Task Submit()
{
var format = "image/png";
foreach (var buffer in list)
{
var imageDataUrl = $"data:{format};base64,{Convert.ToBase64String(buffer)}";
imageDataUrls.Add(imageDataUrl);
}
}
Upvotes: 0
Reputation: 2601
I don't have a really good answer to why your solution is not working, but it is easy to fix it. It seems like you have only access to file content when you are in the event-handling method.
As a workaround, create a "ViewModel" class for your image presentation.
public class FileViewModel
{
/// helpful in combination with @key to make list rendering more efficient
public Guid Id { get; set; }
public String Name { get; set; }
public String ContentType { get; set; }
public Int64 Size { get; set; }
public Byte[] Content { get; set; }
public String ImageDataUrl { get; set; }
public FileViewModel()
{
Id = Guid.NewGuid();
}
}
Merge your code from Submit
with OnInputFileChange
. During the event handling, create a ViewModel instance for each file and save the instance's content.
List<FileViewModel> list = new List<FileViewModel>();
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
var maxAllowedFiles = 3;
var format = "image/png";
foreach (var imageFile in e.GetMultipleFiles(maxAllowedFiles))
{
var resizedImageFile = await imageFile.RequestImageFileAsync(format,
100, 100);
var buffer = new byte[resizedImageFile.Size];
await resizedImageFile.OpenReadStream().ReadAsync(buffer);
var imageDataUrl =
$"data:{format};base64,{Convert.ToBase64String(buffer)}";
list.Add(new FileViewModel
{
Content = buffer,
ImageDataUrl = imageDataUrl,
ContentType = imageFile.ContentType,
Name = imageFile.Name,
Size = imageFile.Size,
});
}
}
I added a remove button to showcase a manipulation of the collection after the content is read.
@if (list.Count > 0)
{
<h4>Images</h4>
<div class="card" style="width:30rem;">
<div class="card-body">
@foreach (var file in list)
{
<img class="rounded m-1" @key="file.Id" src="@file.ImageDataUrl" />
<button type="button" @onclick="() => RemoveImage(file)">Remove</button>
}
</div>
</div>
}
@code {
private void RemoveImage(FileViewModel file)
{
list.Remove(file);
}
}
Using the detour, you now have full access to the data from your ViewModel instance. You can use these data to submit your file to your service then. In the example, I log the file name and length of the byte array as a proof of concept.
async Task Submit()
{
foreach (var item in list)
{
Console.WriteLine($"file {item.Name} | size {item.Content.Length}");
}
}
@page "/"
<h3>Upload PNG images</h3>
<p>
<InputFile OnChange="@OnInputFileChange" multiple />
</p>
@if (list.Count > 0)
{
<h4>Images</h4>
<div class="card" style="width:30rem;">
<div class="card-body">
@foreach (var file in list)
{
<img class="rounded m-1" @key="file.Id" src="@file.ImageDataUrl" />
<button type="button" @onclick="() => RemoveImage(file)">Remove</button>
}
</div>
</div>
}
@if (list.Count() > 0)
{
<div class="card" style="width:30rem;">
<div class="card-body">
@foreach (var file in list)
{
<div>@file.Name - @file.ContentType - @file.Size</div>
}
</div>
</div>
}
<button @onclick="Submit">Télécharger</button>
@code {
List<FileViewModel> list = new List<FileViewModel>();
private async Task OnInputFileChange(InputFileChangeEventArgs e)
{
var maxAllowedFiles = 3;
var format = "image/png";
foreach (var imageFile in e.GetMultipleFiles(maxAllowedFiles))
{
var resizedImageFile = await imageFile.RequestImageFileAsync(format,
100, 100);
var buffer = new byte[resizedImageFile.Size];
await resizedImageFile.OpenReadStream().ReadAsync(buffer);
var imageDataUrl =
$"data:{format};base64,{Convert.ToBase64String(buffer)}";
list.Add(new FileViewModel
{
Content = buffer,
ImageDataUrl = imageDataUrl,
ContentType = imageFile.ContentType,
Name = imageFile.Name,
Size = imageFile.Size,
});
}
}
private void RemoveImage(FileViewModel file)
{
list.Remove(file);
}
async Task Submit()
{
foreach (var item in list)
{
Console.WriteLine($"file {item.Name} | size {item.Content.Length}");
}
}
}
Upvotes: 4