Andrew Corrigan
Andrew Corrigan

Reputation: 1057

Server-side Blazor page issues with parameters and OnInitialisedAsync

I'm relatively new to Blazor, and I'm having issues with something that I would've thought would be rather simple - passing an integer as a route parameter. For reference, passing the parameter itself works perfectly (i.e. it appears in the address bar).

However, the issue is when I try to use that parameter on the new page - the idea is that the parent page is an accordion filled with various action items, and if you click the edit action on an item you're able to edit it in a new tab.

If I declare the page like this:

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

I get the associated error for converting from a string to an integer - which I expected. When I "fix" that error by declaring it like this:

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

I receive an error once the code enters the OnInitialisedAsync method - it's a null reference error on the BuildRenderTree for the page. For reference, this is my OnInitialisedAsync method:

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;

    }

Is there any way for me to set the page up based on that parameter - or do I have to wait for the page to be drawn on the client-side before I can use the parameter?

EDIT - The Task.Delay was just me experimenting with a potential fix I'd found on my searches.

As per requests for further information:

It's this exception. I've attempted switching to OnParameterSetAsync as opposed to OnInitialisedAsync but had no luck

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)

The full file is:

@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);
    }
}

If push comes to shove I could just render this up as a dialog box, but I'd still like to get it sorted as a page so that I know how to get it working for future reference.

Further Edit:

As per Nicola's suggestion, I altered the OnInitialisedAsync method to the following:

   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();
        }
    }

This has now worked provided protection around the HTML block against the correct variable.

To provide further context; here is the code for the accordion component that calls this page - essentially, the idea is for the user to be able to edit an accordion item, I'd figured doing the edit in a new tab would be better than blocking off the page with a large dialog box:

@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);
    }

}

Upvotes: 1

Views: 690

Answers (2)

Nicola Biada
Nicola Biada

Reputation: 2800

I think the problem is the itemModel (and hypothetically other properties/variables) that aren't declared when you try to access it.

You can protect you code with a check like this:

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

Hint: remember that is a good practice to load the data only once.
If you load the data inside the OnInitialized method the calls to the backend will be done twice.
The first time for the prerendering (you can disable it, but it's the standard in Blazor), the second for the final rendering.

So is better to add a

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

and create a method named LoadDataAsync with all the methods call.

Upvotes: 0

Marvin Klein
Marvin Klein

Reputation: 1746

The parameter has not being set yet. Put your code inside OnParameterSetAsync() instead.

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

I think your line with .First() results in this error because newsID is 0.

Upvotes: 0

Related Questions