带有参数和 OnInitialisedAsync 的服务器端 Blazor 页面问题

Server-side Blazor page issues with parameters and OnInitialisedAsync

我对 Blazor 比较陌生,我遇到了一些我认为相当简单的问题 - 将整数作为路由参数传递。作为参考,传递参数本身非常有效(即它出现在地址栏中)。

但是,问题是当我尝试在新页面上使用该参数时 - 想法是父页面是一个充满各种操作项目的手风琴,如果您单击项目上的编辑操作,您可以在新标签页中对其进行编辑。

如果我这样声明页面:

@page "/news/edit/{newsID}"

我收到了从字符串转换为整数的相关错误 - 这是我所预料的。当我通过这样声明来“修复”该错误时:

@page "/news/edit/{newsID:int}"

代码进入 OnInitialisedAsync 方法后我收到一个错误 - 这是页面 BuildRenderTree 上的空引用错误。作为参考,这是我的 OnInitialisedAsync 方法:

protected override async Task OnInitializedAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(10));

        var authState = await authenticationStateTask;
        var user = authState.User;
        userName = user.Identity.Name;
        FACT_FranklinContextProcedures procedures = context.Procedures;

        categories = await procedures.spGetNewsCategoriesAsync();
        users = await procedures.spGetUsersAsync();

        List<spGetNewsItemResult> results = await procedures.spGetNewsItemAsync(newsID);
        newsItem = results.First();

        itemModel = new SaveNewsItemModel(newsItem.headline, newsItem.category, newsItem.content, newsItem.id);

        if (newsItem.requestedStartDate != null)
        {
            itemModel.RequestDates(newsItem.requestedStartDate.Value, newsItem.requestedFinishDate.Value);
        }

        if (newsItem.requester != null)
        {
            itemModel.AssignRequester(newsItem.requester.GetValueOrDefault());
        }

        var fileList = await procedures.spGetNewsItemFilesAsync(newsID);

        if (fileList.Count > 0)
        {
            foreach (var file in fileList)
            {
                itemModel.AddFile(file.fileID, file.newsItem, file.id, file.fileName, file.mimeType, context);
            }
        }

        MaxSize = 15 * 1024 * 1024;

    }

我有什么方法可以根据该参数设置页面 - 还是我必须等待页面在客户端绘制后才能使用该参数?

编辑 - Task.Delay 只是我在尝试我在搜索中发现的潜在修复方法。

根据要求提供更多信息:

就是这个例外。我尝试切换到 OnParameterSetAsync 而不是 OnInitialisedAsync 但没有成功

System.NullReferenceException
  HResult=0x80004003
  Message=Object reference not set to an instance of an object.
  Source=FIS2withSyncfusion
  StackTrace:
   at FIS2withSyncfusion.Pages.News.EditNews.BuildRenderTree(RenderTreeBuilder __builder)

完整文件为:

@page "/news/edit/{newsID:int}"
@inject NavigationManager NavManager;
@inject FACT_FranklinContext context;
@inject IFileService fileService;
@inject IHttpClientFactory ClientFactory;
@inject IJSRuntime js;
@using FIS2withSyncfusion.Models;
@using FIS2withSyncfusion.Utility;
@using Syncfusion.Blazor.RichTextEditor;
@using System.Collections.Generic;
@using System.Threading.Tasks;
@using Newtonsoft.Json;

<div class="card">
    <div class="card-header bg-secondary p-2">
        <h6 class="text-white m-0">Edit a News Item</h6>
    </div>
    <div class="card-body">
        <div class="form-group row">
            <label for="title" class="col-sm-2 col-form-label">Title/Heading</label>
            <div class="col-sm-10">
                <input type="text" class="form-control" id="title" @bind-value="@(itemModel.Title)" placeholder="Enter Title/Heading" maxlength="100" />
            </div>
        </div>
        <div class="form-group row">
            <label for="category" class="col-sm-2 col-form-label">Category</label>
            <div class="col-sm-10">
                <SfDropDownList TItem="spGetNewsCategoriesResult" TValue="int" Placeholder="Select a category" @ref="sfDropDown" DataSource="categories" @bind-Value="@(itemModel.Category)">
                    <DropDownListFieldSettings Text="name" Value="id" />
                </SfDropDownList>
            </div>
        </div>
        <div class="form-group row">
            <label for="category" class="col-sm-2 col-form-label">On Behalf Of</label>
            <div class="col-sm-10">
                <SfDropDownList TItem="spGetUsersResult" TValue="int" Placeholder="Select a user" DataSource="users" @ref="userDropDown" @bind-Value="@(itemModel.requester)">
                    <DropDownListFieldSettings Text="userName" Value="id" />
                </SfDropDownList>
            </div>
        </div>
        <div class="form-group row">
            <div class="input-group mb-3">
                <div class="input-group-prepend">
                    <div class="input-group-text">
                        <input type="checkbox" aria-label="Request Specific Dates?" id="requestDates" @bind-value="isChecked" />
                    </div>
                </div>
                <label class="form-check-label" for="requestDates">Suggest Dates This Should Be Active?</label>
            </div>
        </div>
        @if (isChecked)
        {
            <div class="row">
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text">
                            Active From:
                        </span>
                    </div>
                    <SfDateTimePicker TValue="DateTime" Min="DateTime.Now" @bind-Value="activeFrom"></SfDateTimePicker>
                </div>
            </div>
            <div class="row">
                <div class="input-group mb-3">
                    <div class="input-group-prepend">
                        <span class="input-group-text">
                            Active Until:
                        </span>
                    </div>
                    <SfDateTimePicker TValue="DateTime" Min="DateTime.Now" @bind-Value="activeTo"></SfDateTimePicker>
                </div>
            </div>
        }
        <div class="form-group row">
            <SfUploader @ref="uploader" MaxFileSize="@MaxSize" Files="uploadedFiles" AllowedExtensions=".doc, .docx, .pdf, .xls, .xlsx, .ppt, .pptx, .jpg, .jpeg, .bmp, .png" AllowMultiple="true" AutoUpload="true">
                <UploaderEvents ValueChange="OnFileUpload" OnRemove="OnFileRemove" OnClear="OnClearFiles" />
            </SfUploader>
        </div>
        <div class="form-group row" style="height:fit-content;">
            <SfRichTextEditor @bind-Value="@(itemModel.Content)" Height="200px" EnableResize="true">
                <RichTextEditorPasteCleanupSettings KeepFormat="true" DeniedAttributes="@DeniedAttributes.ToArray()" />
                <RichTextEditorToolbarSettings Items="Tools" Type="ToolbarType.Expand" />
            </SfRichTextEditor>
        </div>
    </div>
    <div class="card-footer">
        <div class="btn-cls">
            <button type="button" class="btn btn-primary" @onclick="OnSave">Save</button>
            <button type="button" class="btn btn-secondary">Cancel</button>
        </div>
    </div>
</div>
@if (ShowDialog)
{
    <Dialog Title="Edit A News Item" message="@Message" OKText="@OKText" cancelText="@CancelText" OnClose="OnDialogClose">
    </Dialog>
}

@code {
    [Parameter]
    public int newsID { get; set; }

    [CascadingParameter]
    Task<AuthenticationState> authenticationStateTask { get; set; }

    public string userName { get; set; }

    private int MaxSize { get; set; }

    int count { get; set; }

    private bool ShowDialog { get; set; } = false;
    private string Message { get; set; } = "";
    private string OKText { get; set; } = "";
    private string CancelText { get; set; } = "";

    public DateTime activeTo { get; set; }
    public DateTime activeFrom { get; set; }

    private bool isChecked { get; set; }

    List<string> DeniedAttributes = new List<string>() {
        "id", "title", "style"
    };

    List<UploaderUploadedFiles> uploadedFiles = new List<UploaderUploadedFiles>();

    List<spGetNewsCategoriesResult> categories = new List<spGetNewsCategoriesResult>();

    List<spGetUsersResult> users = new List<spGetUsersResult>();

    SfDropDownList<int, spGetNewsCategoriesResult> sfDropDown;

    SfDropDownList<int, spGetUsersResult> userDropDown;
    spGetNewsItemResult newsItem = new spGetNewsItemResult();

    SaveNewsItemModel itemModel;

    List<Syncfusion.Blazor.Inputs.FileInfo> Files = new List<Syncfusion.Blazor.Inputs.FileInfo>();
    SfUploader uploader;
    List<System.IO.FileInfo> files = new List<System.IO.FileInfo>();

    protected override async Task OnInitializedAsync()
    {
        await Task.Delay(TimeSpan.FromSeconds(10));

        var authState = await authenticationStateTask;
        var user = authState.User;
        userName = user.Identity.Name;
        FACT_FranklinContextProcedures procedures = context.Procedures;

        categories = await procedures.spGetNewsCategoriesAsync();
        users = await procedures.spGetUsersAsync();

        List<spGetNewsItemResult> results = await procedures.spGetNewsItemAsync(newsID);
        newsItem = results.First();

        itemModel = new SaveNewsItemModel(newsItem.headline, newsItem.category, newsItem.content, newsItem.id);

        if (newsItem.requestedStartDate != null)
        {
            itemModel.RequestDates(newsItem.requestedStartDate.Value, newsItem.requestedFinishDate.Value);
        }

        if (newsItem.requester != null)
        {
            itemModel.AssignRequester(newsItem.requester.GetValueOrDefault());
        }

        var fileList = await procedures.spGetNewsItemFilesAsync(newsID);

        if (fileList.Count > 0)
        {
            foreach (var file in fileList)
            {
                itemModel.AddFile(file.fileID, file.newsItem, file.id, file.fileName, file.mimeType, context);
            }
        }

        MaxSize = 15 * 1024 * 1024;

    }

    private List<ToolbarItemModel> Tools = new List<ToolbarItemModel>() {
        new ToolbarItemModel()
        {
            Command = ToolbarCommand.Bold
        },
        new ToolbarItemModel()
        {
            Command = ToolbarCommand.Italic
        },
        new ToolbarItemModel()
        {
            Command= ToolbarCommand.Underline
        },
        new ToolbarItemModel()
        {
            Command= ToolbarCommand.Separator
        },
        new ToolbarItemModel()
        {
            Command = ToolbarCommand.Undo
        },
        new ToolbarItemModel()
        {
            Command = ToolbarCommand.Redo
        },
        new ToolbarItemModel()
        {
            Command= ToolbarCommand.Separator
        },
        new ToolbarItemModel()
        {
            Command = ToolbarCommand.OrderedList
        },
        new ToolbarItemModel()
        {
            Command = ToolbarCommand.UnorderedList
        }
    };

    private async Task OnFileUpload(UploadChangeEventArgs args)
    {
        foreach (var file in args.Files)
        {
            var fileName = file.FileInfo.Name;
            using (var ms = file.Stream)
            {
                System.IO.FileInfo fileInfo = new System.IO.FileInfo(fileName);
                files.Add(fileInfo);
                var bytes = ms.ToArray();
                await fileService.SaveFileToTempAsync(bytes, fileName);
                itemModel.AddFile(fileName, fileInfo.Extension, context);
            }
        }
    }

    private async Task OnClearFiles(ClearingEventArgs args)
    {
        foreach (var file in args.FilesData)
        {
            var fileName = file.Name;
            System.IO.FileInfo fileInfo = new System.IO.FileInfo(fileName);
            itemModel.RemoveFile(fileName, fileInfo.Extension, context);
            fileService.DeleteTempFile(fileName);
        }
    }

    private async Task OnFileRemove(RemovingEventArgs args)
    {
        foreach (var file in args.FilesData)
        {
            var fileName = file.Name;
            System.IO.FileInfo fileInfo = new System.IO.FileInfo(fileName);
            itemModel.RemoveFile(fileName, fileInfo.Extension, context);
            fileService.DeleteTempFile(fileName);
        }
    }

    private async Task OnSave()
    {

        if (isChecked)
        {
            itemModel.RequestDates(activeFrom, activeTo);
        }
        else
        {
            itemModel.RevokeDates();
        }

        var procedures = context.Procedures;

        var addedFiles = await procedures.spEditNewsItemAsync(JsonConvert.SerializeObject(itemModel), userName);

        if (addedFiles.Count > 0)
        {
            foreach (var file in addedFiles)
            {
                await fileService.MoveTempToNewsAsync(file.fileName, file.newsID, file.fileID);
            }
        }

        Message = "This has been successfully saved and is now pending review; pressing OK will refresh the page.";
        OKText = "OK";
        ShowDialog = true;
    }

    private async Task OnDialogClose(bool r)
    {
        ShowDialog = false;
        NavManager.NavigateTo(NavManager.Uri, true);
    }
}

如果到了紧要关头,我可以将它呈现为一个对话框,但我仍然希望将它作为一个页面进行排序,以便我知道如何让它工作以供将来参考。

进一步编辑:

根据 Nicola 的建议,我将 OnInitialisedAsync 方法更改为以下内容:

   protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var authState = await authenticationStateTask;
            var user = authState.User;
            userName = user.Identity.Name;
            FACT_FranklinContextProcedures procedures = context.Procedures;

            categories = await procedures.spGetNewsCategoriesAsync();
            users = await procedures.spGetUsersAsync();

            List<spGetNewsItemResult> results = await procedures.spGetNewsItemAsync(newsID);
            newsItem = results.First();

            itemModel = new SaveNewsItemModel(newsItem.headline, newsItem.category, newsItem.content, newsItem.id);

            if (newsItem.requestedStartDate != null)
            {
                itemModel.RequestDates(newsItem.requestedStartDate.Value, newsItem.requestedFinishDate.Value);
            }

            if (newsItem.requester != null)
            {
                itemModel.AssignRequester(newsItem.requester.GetValueOrDefault());
            }

            var fileList = await procedures.spGetNewsItemFilesAsync(newsID);

            if (fileList.Count > 0)
            {
                foreach (var file in fileList)
                {
                    itemModel.AddFile(file.fileID, file.newsItem, file.id, file.fileName, file.mimeType, context);
                }
            }

            MaxSize = 15 * 1024 * 1024;

            StateHasChanged();
        }
    }

这已经起作用了,提供了围绕 HTML 块的保护以防止正确的变量。

提供进一步的背景;这是调用此页面的手风琴组件的代码 - 本质上,这个想法是让用户能够编辑手风琴项目,我认为在新选项卡中进行编辑比用一个大对话框:

@page "/news/approvals"
@inject NavigationManager NavManager;
@inject FACT_FranklinContext context;
@inject IFileService fileService;
@inject IHttpClientFactory ClientFactory;
@inject IJSRuntime js;
@using FIS2withSyncfusion.Models;
@using FIS2withSyncfusion.Utility;
@using Syncfusion.Blazor.RichTextEditor;
@using System.Collections.Generic;
@using System.Threading.Tasks;
@using Newtonsoft.Json;

<div class="card">
    <div class="card-header bg-secondary p-2">
        <h6 class="text-white m-0">News Items Pending Approval</h6>
    </div>
    <div class="card-body">
        <SfAccordion @bind-ExpandedIndices="expandedIndicesArray">
            <AccordionItems>
                @foreach (var category in categories)
                {
                    <AccordionItem>
                        <HeaderTemplate>
                            <h4>@(category)</h4>
                        </HeaderTemplate>
                        <ContentTemplate>
                            <SfAccordion>
                                <AccordionItems>
                                    @foreach (var item in newsResults.Where(x => x.category == category))
                                    {
                                        <AccordionItem>
                                            <HeaderTemplate>
                                                <h5>@(item.headline) -- Created by @(item.createdBy)</h5>
                                            </HeaderTemplate>
                                            <ContentTemplate>
                                                <div>
                                                    @((MarkupString)item.content)
                                                </div>
                                                @if (item.files != null && item.files > 0)
                                                {
                                                    <h5>Files:</h5>
                                                    <ul>
                                                        @foreach (var file in newsFiles)
                                                        {
                                                            if (file.newsItem == item.id)
                                                            {
                                                                <li>
                                                                    <a id="@GetIDForFile(file.newsItem, file.fileID)" @onclick="eventArgs => HandleFileClick(eventArgs, file.newsItem, file.fileID, file.mimeType, file.fileName)">@(file.fileName)</a>
                                                                </li>
                                                            }
                                                        }
                                                    </ul>
                                                }
                                                <div class="row">
                                                    <button type="button" class="btn btn-primary" @onclick="eventArgs => OpenApproveDialog(eventArgs, item.id)">Approve</button>
                                                    <button type="button" class="btn btn-secondary" @onclick="eventArgs => OnEditClick(eventArgs, item.id)">Edit</button>
                                                    <button type="button" class="btn btn-secondary" @onclick="eventArgs => OnDeleteClick(eventArgs, item.id)">Delete</button>
                                                </div>
                                            </ContentTemplate>
                                        </AccordionItem>
                                    }
                                </AccordionItems>
                            </SfAccordion>
                        </ContentTemplate>
                    </AccordionItem>
                }
            </AccordionItems>
        </SfAccordion>
    </div>
</div>

@if (showDateDlg)
{
    <NewsApproveForm OnClose="ApproveNewsItem">
    </NewsApproveForm>
}

@if (showDeleteDlg)
{
    <Dialog Title="Delete News Item" message="@Message" OKText="@OKText" cancelText="@CancelText" OnClose="OnDialogClose">
    </Dialog>
}

@code {

    [CascadingParameter]
    Task<AuthenticationState> authenticationStateTask { get; set; }

    public string userName { get; set; }
    private bool showDateDlg { get; set; } = false;
    private bool showDeleteDlg { get; set; } = false;
    private int selectedNewsID { get; set; }

    List<spGetUnnapprovedNewsResult> newsResults = new List<spGetUnnapprovedNewsResult>();
    List<string> categories = new List<string>();
    List<int> expandedIndices = new List<int>();
    int[] expandedIndicesArray;
    List<spGetNewsFilesResult> newsFiles = new List<spGetNewsFilesResult>();

    private string Message { get; set; } = "Are you sure you wish to delete this news item? Once deleted the page will refresh.";
    private string OKText { get; set; } = "OK";
    private string CancelText { get; set; } = "Cancel";

    private string GetIDForFile(int newsItem, Guid FileID)
    {
        return $"{newsItem}-{FileID}";
    }

    private void OpenApproveDialog(MouseEventArgs eventArgs, int newsID)
    {
        showDateDlg = true;
        selectedNewsID = newsID;
    }

    private async Task OnEditClick(MouseEventArgs eventArgs, int newsID)
    {
        await js.InvokeAsync<object>("open", $"/news/edit/{newsID}", "_blank");
    }

    private async Task OnDeleteClick(MouseEventArgs eventArgs, int newsID)
    {
        showDeleteDlg = true;
        selectedNewsID = newsID;
    }

    private async Task ApproveNewsItem(ApproveNewsModel newsModel)
    {
        if (newsModel.IsApproved)
        {
            FACT_FranklinContextProcedures procedures = context.Procedures;
            await procedures.spApproveNewsItemAsync(selectedNewsID, newsModel.expiryDate, newsModel.startDate, userName);
        }
        selectedNewsID = -1;
        showDateDlg = false;

        NavManager.NavigateTo(NavManager.Uri, true);
    }

    protected async Task HandleFileClick(MouseEventArgs eventArgs, int newsID, Guid fileID, string mimeType, string name)
    {
        var content = await fileService.GetNewsFileAsync(newsID, fileID);

        DownloadUtility.SaveAs(js, name, content, mimeType);
    }

    protected override async Task OnInitializedAsync()
    {
        var authState = await authenticationStateTask;
        var user = authState.User;
        userName = user.Identity.Name;

        FACT_FranklinContextProcedures procedures = context.Procedures;
        newsResults = await procedures.spGetUnnapprovedNewsAsync();
        newsFiles = await procedures.spGetNewsFilesAsync(false);

        int count = 0;
        foreach (var item in newsResults)
        {
            if (!categories.Contains(item.category))
            {
                categories.Add(item.category);
                expandedIndices.Add(count);
                count++;
            }
        }

        categories.Sort();
        expandedIndicesArray = expandedIndices.ToArray();
    }

    private async Task OnDialogClose(bool r)
    {
        if (r)
        {
            var procedures = context.Procedures;
            await procedures.spDeleteNewsItemAsync(selectedNewsID, userName);
        }

        selectedNewsID = -1;
        showDeleteDlg = false;
        NavManager.NavigateTo(NavManager.Uri, true);
    }

}

参数尚未设置。将您的代码放在 OnParameterSetAsync() 中。

List<spGetNewsItemResult> results = await procedures.spGetNewsItemAsync(newsID);
        newsItem = results.First();

我认为你的行 .First() 导致了这个错误,因为 newsID 是 0。

我认为问题在于 itemModel(假设还有其他 properties/variables)在您尝试访问它时未声明。

您可以通过这样的检查来保护您的代码:

@if (itemModel == null)
{
    <div>loading data...</div>
}
else
{
<div class="card">
...
}

提示:请记住,只加载一次数据是一个好习惯。
如果您在 OnInitialized 方法中加载数据,对后端的调用将完成两次。
第一次用于预渲染(你可以禁用它,但它是 Blazor 中的标准),第二次用于最终渲染。

所以最好加一个

protected override async Task OnAfterRenderAsync(bool firstRender)
        {
            if (firstRender)
            {
                await LoadDataAsync();
                StateHasChanged();
            }
        }

并创建一个名为 LoadDataAsync 的方法,并调用所有方法。