.NET 8 Blazor文档库操作后页面事件失效问题求助
我正在基于.NET 8开发Blazor文档库项目,支持文件拖拽上传、下载及删除功能。操作前所有功能正常,上传或删除后页面和组件能正确更新,但所有Blazor事件完全停止响应(比如点击页面元素无任何反应)。浏览器控制台无错误输出,调试模式下也未抛出异常,删除操作未使用JS,因此问题似乎与JsInterop无关。
核心代码
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); } } } }
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); } } }
已尝试的排查措施
- 将JsInterop改为作用域服务,确保模块仅加载一次
- 更新状态容器前克隆VM对象
- 改用页面回调替代组件中注入DataBroker
- 与其他开发者一同审查代码,仍未找到问题原因
排查思路
检查事件重复订阅问题:
FilesUploadedAsync的finally块中调用了base.OnInitializedAsync(),这会重复订阅StateContainer的OnStateChange和OnInitialized事件。多次订阅会导致状态变更时重复执行逻辑,可能引发UI线程死锁或组件状态混乱。建议移除该调用,改为直接调用StateContainer.GetOrInitializeValueAsync(LoadDataFromApiAsync)刷新数据。验证StateHasChanged调用逻辑:确认
StateContainer的状态变更事件是否在UI线程触发。如果事件在非UI线程执行,即使调用InvokeAsync(StateHasChanged)也可能导致UI更新异常。可以检查StateContainer的UpdateValue实现,确保事件触发在UI线程。排查组件生命周期冲突:页面使用
@rendermode InteractiveAuto,上传后调用base.OnInitializedAsync()可能触发组件重新初始化,导致原有组件实例被替换、事件绑定失效。尝试在上传完成后直接刷新状态容器数据,而非重新初始化页面。检查StateContainer线程安全性:如果
StateContainer在多线程环境下被修改,可能导致UI状态不一致。检查UpdateValue方法是否有线程安全处理(如锁机制),确保状态变更操作在单线程执行。测试移除ProgressStateContainer更新:暂时注释掉
ProgressStateContainer.UpdateValue调用,确认是否是进度弹窗的状态更新打乱了Blazor的UI渲染队列,导致事件失效。重置InputFile组件状态:上传完成后,InputFile组件未重置可能导致DOM元素状态异常。可以在
FilesUploadedAsync完成后,通过JsInterop重置InputFile的value,或调用inputFile?.Reset()(若支持)。
内容的提问来源于stack exchange,提问作者foolish_humans

