I have a .NET8 Blazor project that I am creating a Document Vault in.
Files can be uploaded via drag and drop and then downloaded or deleted. Everything works great until a document is uploaded or is deleted. The page and components update correctly after an upload or delete but all Blazor events stop working, i.e., clicking any item on the page no longer does anything.
Does anybody have any idea what could be causing this?
I am not seeing any errors in the browser console. Also, I am running the project in debug and no exceptions are being thrown.
It does not appear to be an issue with the JsInterop because delete does not even use JS.
Relevant code is below, focusing on the document upload.
BaseDocumentVaultPage.cs
public class BaseDocumentVaultPage : AuthedBasePage
{
[Inject]
public IStateContainer<DocumentsMetaDataVm> StateContainer { get; set; }
[Inject]
public IStateContainer<PopoverOptions> ProgressStateContainer { get; set; }
[Inject]
public ICurrentUserService CurrentUserService { get; set; }
[Inject]
public IDocumentVaultDataBroker DocumentVaultDataBroker { get; set; }
public async Task<DocumentsMetaDataVm> LoadDataFromApiAsync()
{
//Omitted, this appears to be working correctly
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await StateContainer.GetOrInitializeValueAsync(LoadDataFromApiAsync);
}
}
}
AdvisorDocumentVaultPage.razor
@page "/advisor/document-vault"
@layout BasePageLayout
@inherits BaseDocumentVaultPage
@rendermode InteractiveAuto
<div class="grid grid-cols-12 gap-6">
<FileUploadComponent
FileOwner="@FileOwnerEnum.Advisor"
DocumentsMetaData="@DocumentsMetaData"
OnUpload="@FilesUploadedAsync" />
</div>
<div class="grid grid-cols-12 gap-6">
<FileListComponent
CanEdit="true"
FileOwner="@FileOwnerEnum.Advisor"
DocumentsMetaData="@(DocumentsMetaData?.AdvisorDocuments ?? new List<DocumentMetaDataVm>())"
Title="Shared by Me"
OnFileDeleted="@OnDeleteAsync"
OnFileRetrieval="@OnRetrievalAsync" />
</div>
AdvisorDocumentVaultPage.razor.cs
public partial class AdvisorDocumentVaultPage
{
public DocumentsMetaDataVm DocumentsMetaData { get; set; } = new();
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
StateContainer.OnStateChange += StateContainer_OnStateChange;
StateContainer.OnInitialized += StateContainer_OnInitialized;
await SetPageDetailsAsync("Document Vault", new Dictionary<string, string?>()
{
{"Advisor", "/advisor"}
});
DocumentsMetaData = StateContainer.CurrentValue ?? new DocumentsMetaDataVm()
{
AdvisorDocuments = new List<DocumentMetaDataVm>(),
ClientDocuments = new List<DocumentMetaDataVm>()
};
}
public async Task StateContainer_OnInitialized(DocumentsMetaDataVm? arg)
{
DocumentsMetaData = StateContainer.CurrentValue ?? new DocumentsMetaDataVm()
{
AdvisorDocuments = new List<DocumentMetaDataVm>(),
ClientDocuments = new List<DocumentMetaDataVm>()
};
await InvokeAsync(StateHasChanged);
}
public async Task StateContainer_OnStateChange(DocumentsMetaDataVm? arg)
{
DocumentsMetaData = StateContainer.CurrentValue ?? new DocumentsMetaDataVm()
{
AdvisorDocuments = new List<DocumentMetaDataVm>(),
ClientDocuments = new List<DocumentMetaDataVm>()
};
ProgressStateContainer.UpdateValue(new PopoverOptions(false));
await InvokeAsync(StateHasChanged);
}
public async Task FilesUploadedAsync(IReadOnlyList<IBrowserFile> files)
{
try
{
ProgressStateContainer.UpdateValue(new PopoverOptions(true));
await DocumentVaultDataBroker.SaveDocumentAsync(UserContext.ClientId.GetValueOrDefault(),
UserContext.ToAdvisorRequest(), FileOwnerEnum.Advisor, files);
}
catch (Exception ex)
{
await DispatchExceptionAsync(ex);
}
finally
{
ProgressStateContainer.UpdateValue(new PopoverOptions(false));
await base.OnInitializedAsync();
}
}
}
FileUploadComponent.razor
<div class="col-span-12 xxl:col-span-6" >
<div class="box">
<div class="box-header">
<h5 class="box-title">File Upload</h5>
</div>
<div class="box-body" >
<div @ref="dropZoneElement" class="document-vault-drop-zone">
<div id="drag-and-drop-div">
Drag & Drop or
<InputFile OnChange="@OnChange" multiple @ref="inputFile" id="document-vault-file-input"/>
</div>
</div>
</div>
</div>
</div>
FileUploadComponent.razor.cs
public partial class FileUploadComponent
{
[Inject]
public IDocumentVaultJsInteropService JsInteropService { get; set; }
[Parameter]
public FileOwnerEnum? FileOwner { get; set; }
[Parameter]
public DocumentsMetaDataVm? DocumentsMetaData { get; set; }
[Parameter]
public EventCallback<IReadOnlyList<IBrowserFile>> OnUpload { get; set; }
private const int MaxAllowedFiles = 5;
public ElementReference? dropZoneElement;
public InputFile? inputFile;
public async Task OnChange(InputFileChangeEventArgs args)
{
if (args.FileCount > MaxAllowedFiles)
{
//todo: add a toast error
}
await OnUpload.InvokeAsync(args.GetMultipleFiles(MaxAllowedFiles));
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
if (inputFile != null && dropZoneElement != null)
{
await JsInteropService.InitializeFileDropZone(dropZoneElement, inputFile.Element);
}
}
}
}
implementation of IDocumentVaultDataBroker
public class DocumentVaultDataBroker(IMediator mediator, IMapper mapper, IStateContainer<DocumentsMetaDataVm> stateContainer, INotificationHandler notificationHandler) : IDocumentVaultDataBroker
{
public async Task SaveDocumentAsync(long clientId, AdvisorRequestContext context, FileOwnerEnum fileOwner, IReadOnlyList<IBrowserFile> documents,
CancellationToken cancellationToken = default)
{
try
{
var vms = new List<DocumentMetaDataVm>();
foreach (var file in documents)
{
await using var stream =
file.OpenReadStream(maxAllowedSize: 5242880, cancellationToken: cancellationToken);
using var ms = new MemoryStream();
await stream.CopyToAsync(ms, cancellationToken);
var documentBytes = ms.ToArray();
//Note, handler calls API
var result = await mediator.Send(new SaveDocumentCommand()
{
ClientId = clientId,
RequestContext = context,
FileOwner = fileOwner,
FileNameWithExtension = file.Name,
Document = documentBytes
}, cancellationToken);
vms.Add(mapper.Map<DocumentMetaDataVm>(result));
}
var documentsMetaData = stateContainer.CurrentValue.Clone();
if (documentsMetaData == null)
{
documentsMetaData = new DocumentsMetaDataVm()
{
AdvisorDocuments = new List<DocumentMetaDataVm>(),
ClientDocuments = new List<DocumentMetaDataVm>()
};
}
documentsMetaData.TotalBytesUsed.Bytes += vms.Sum(vm => vm.FileSizeInBytes ?? 0);
if (fileOwner == FileOwnerEnum.Advisor)
{
documentsMetaData.AdvisorDocuments.AddRange(vms);
}
else
{
documentsMetaData.ClientDocuments.AddRange(vms);
}
var message = documents.Count > 1
? $"{documents.Count} documents successfully uploaded."
: "Document successfully uploaded.";
stateContainer.UpdateValue(documentsMetaData, new SuccessNotificationDto(message));
}
catch (Exception e)
{
await notificationHandler.HandleExceptionAsync("Error while uploading document", e);
}
}
}
I have tried many things:
- Moved the JsInterop into a scoped service so that the module only needs loaded once
- Cloning the VMs before updating the state container
- Updating to use callbacks to page instead of injecting DataBroker in the components
- Reviewing the code with other developers trying to find what might be causing the issue. We don't have a clue what the issue could be.
Just cannot figure out why successfully uploading or deleting a document causes all blazor events stop working once the page updates.
Turned out to be a thread deadlock issue. There was a synchronous method in the underlying structure of the project doing a
and this was causing the deadlock. Was able to refactor to make the method async so it could just do: