The solution is a little bit longer and has quite a few moving parts.
I'd recommend implementing the fail-safe by an independent service. This service is consumed by your CreatePost
component and an UploadManager
component. Be aware that this solution is not perfect. The entire "canceling upload" and error handling parts are missing. Also, there is no queue implemented.
Adding progress to uploads
The default HttpContent
has no ability to be informed about an upload's progress. However, we can create a new implementation and add this functionality. I decided to use events for communications. The classes are copied and modified from this post.
This class FileTransferInfo
represents the state of a transfer while FileTransferingProgressChangedEventArgs
to copy the change any part of the application that might be interested in.
public class FileTransferingProgressChangedEventArgs : EventArgs
{
public String Filename { get; set; }
public Int64 BytesSent { get; init; }
public Int64 TotalBytes { get; init; }
public FileTransferInfo.States State { get; init; }
}
public class FileTransferInfo
{
public enum States
{
Init,
Pending,
Transfering,
PendingResponse,
Finished
}
public States State { get; private set; }
public Int64 BytesSent { get; private set; }
public String Filename { get; private set; }
public Int64 TotalSize { get; private set; }
public FileTransferInfo(String filename, Int64 totalSize)
{
Filename = filename;
TotalSize = totalSize;
BytesSent = 0;
State = States.Init;
}
private void SendEventHandler() => Progress?.Invoke(this,
new FileTransferingProgressChangedEventArgs
{
BytesSent = BytesSent,
State = State,
TotalBytes = TotalSize,
Filename = Filename
});
public event EventHandler<FileTransferingProgressChangedEventArgs> Progress;
public void Start()
{
State = States.Transfering;
SendEventHandler();
}
public void UpdateProgress(int length)
{
State = States.Transfering;
BytesSent += length;
SendEventHandler();
}
internal void UploadFinished()
{
State = States.PendingResponse;
//just in case
BytesSent = TotalSize;
SendEventHandler();
}
internal void ResponseReceived()
{
State = States.Finished;
SendEventHandler();
}
}
This class is used by the ProgressableStreamContent
which will be used by the HttpClient
as a specialized way to upload files.
//copied from https://stackoverflow.com/questions/35320238/how-to-display-upload-progress-using-c-sharp-httpclient-postasync
public class ProgressableStreamContent : HttpContent
{
private const int defaultBufferSize = 4096;
private readonly Stream _content;
private readonly Int32 _bufferSize;
private Boolean _contentConsumed;
private readonly FileTransferInfo _progressInfo;
public ProgressableStreamContent(Stream content, FileTransferInfo progressInfo) : this(content, defaultBufferSize, progressInfo) { }
public ProgressableStreamContent(Stream content, Int32 bufferSize, FileTransferInfo progressInfo)
{
if (bufferSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(bufferSize));
}
this._content = content ?? throw new ArgumentNullException(nameof(content));
this._bufferSize = bufferSize;
this._progressInfo = progressInfo;
}
protected async override Task SerializeToStreamAsync(Stream stream, TransportContext context)
{
PrepareContent();
var buffer = new Byte[this._bufferSize];
_progressInfo.Start();
using (_content)
{
while (true)
{
var length = await _content.ReadAsync(buffer, 0, buffer.Length);
if (length <= 0) break;
_progressInfo.UpdateProgress(length);
stream.Write(buffer, 0, length);
}
}
_progressInfo.UploadFinished();
}
protected override bool TryComputeLength(out long length)
{
length = _content.Length;
return true;
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
_content.Dispose();
}
base.Dispose(disposing);
}
private void PrepareContent()
{
if (_contentConsumed)
{
// If the content needs to be written to a target stream a 2nd time, then the stream must support
// seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target
// stream (e.g. a NetworkStream).
if (_content.CanSeek)
{
_content.Position = 0;
}
else
{
throw new InvalidOperationException("SR.net_http_content_stream_already_read");
}
}
_contentConsumed = true;
}
}
Upload Manager
The upload manager can initialize uploads and get the FileTransferInfo
of ongoing/finished uploads.
public class UploadManager
{
private readonly HttpClient _vimeoApiClient;
private List<FileTransferInfo> _transfers = new();
public IReadOnlyList<FileTransferInfo> Transfers => _transfers.AsReadOnly();
public UploadManager(HttpClient vimeoApiClient)
{
_vimeoApiClient = vimeoApiClient;
}
public void StartFileUpload(Stream stream, String fileName, Int64 fileSize)
{
FileTransferInfo uploaderInfo = new FileTransferInfo(fileName, fileSize);
uploaderInfo.Progress += UpdateFileProgressChanged;
_transfers.Add(uploaderInfo);
var singleFileContent = new ProgressableStreamContent(stream, uploaderInfo);
//read the docs if vimeo expected such an encoded content
var multipleFileContent = new MultipartFormDataContent();
multipleFileContent.Add(singleFileContent, "file", fileName);
Task.Run(async () =>
{
var result = await _vimeoApiClient.PostAsync("<YourURLHere>", multipleFileContent);
uploaderInfo.ResponseReceived();
uploaderInfo.Progress -= UpdateFileProgressChanged;
});
}
private void UpdateFileProgressChanged(Object sender, FileTransferingProgressChangedEventArgs args) => FileTransferChanged?.Invoke(sender, args);
public event EventHandler<FileTransferingProgressChangedEventArgs> FileTransferChanged;
}
The actual upload logic may be changed based on the needs of the API. This example shows how to use it with MultipartForm, where you can add more files or other content.
The event FileTransferChanged
could be much finer, if needed. For now, it's just like a flow-type heater.
The upload manager needs to be registered for DI.
public static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
....
builder.Services.AddSingleton( sp => new UploadManager(
new HttpClient {
BaseAddress = new Uri("<BaseURL>") }
));
await builder.Build().RunAsync();
}
The most improvements can be made here. The upload manager is the place for queueing and canceling.
Upload View
The upload view is pretty straight-forward and can easily be tailored to your needs.
@inject UploadManager UploadManager
@implements IDisposable
<h3>Uploads</h3>
<ul>
@foreach (var item in UploadManager.Transfers)
{
<li><span>@item.Filename</span> @(Math.Round( (100.0 * item.BytesSent / item.TotalSize),2 )) %</li>
}
</ul>
@code {
protected override void OnInitialized()
{
base.OnInitialized();
UploadManager.FileTransferChanged += TransferProgressChanged;
}
private async void TransferProgressChanged(Object sender, FileTransferingProgressChangedEventArgs args)
{
await InvokeAsync(StateHasChanged);
}
public void Dispose()
{
UploadManager.FileTransferChanged -= TransferProgressChanged;
}
}
** The NewPost component ```
With the UploadManager
as the "heavy lifter," the file upload becomes easy.
@page "/NewPost"
@inject UploadManager UploadManager
<EditForm Model="_model">
<InputText @bind-Value="_model.Name" class="form-control" />
<InputFile OnChange="UploadFile" />
</EditForm>
<UploadManagerView />
@code {
private void UploadFile(InputFileChangeEventArgs args)
{
// to make the demo easier, we assume that only one file is uploaded at a time
if (args.FileCount > 1) { return; }
UploadManager.StartFileUpload(args.File.OpenReadStream(/*SetMaxFileSizeHere*/), args.File.Name, args.File.Size);
}
}