SariDev
SariDev

Reputation: 71

How to dynamically switch child content in blazor pages

As I'm an absoulute beginner when it comes to web development, I started to look Blazor and learn how to use it to get an easy start in to web developlment and now struggle with a problem.

I have built a Master / Detail page and that page uses a master component (the list of employees) and 2 different detail component (employee readonly detail view and employee edit view).

The master detail page uses the following routes:

I tried to accomplish these goals:

  1. When a user clicks a list entry from the master component, this should be shown in the URL like https://localhost:44344/masterdetail/2 and than load the employee readonly detail view into the detail area

  2. When a user clicks the edit button located on the employee readonly detail view, the master detail page should switch to the employee edit view inside the detail area and show this in the URL like https://localhost:44344/masterdetail/2/edit

  3. When a user clicks the save button located on the employee edit view, the master detail page should switch to the employee readonly detail view inside the detail area and show this in the URL like https://localhost:44344/masterdetail/2

The problems that I have faced: When the user is in the readonly view and than clicks the edit button, my code is calling NavigationManager.NavigateTo($"/masterdetail/{Id}/edit"); which switches the URL in the address bar of the browser but does not invoke the OnParametersSet() lifecycle method of the master detail page. Blazor seems to reuse the instance if the [Parameter] Id has not changed it's value.

The same happens when the user is on /masterdetail/{Id}/edit route (entered via browser address bar) and than clicks the save button.

What I learned while researching the problem:

Here is a simplified sample that shows the problem:

  1. Create a new "Blazor App" project in Visual Studio
  2. Choose "Blazor Server App"
  3. Add a new .razor file and paste the code snippet in
  4. Have a look at the comments and the code
  5. Navigate to https://localhost:44344/masterdetail/ and try it yourself
@*Default route for this page when no entry is selected in the master list*@
@page "/masterdetail"
@*Route for this page when an entry is selected in the master list. The detail area should show a readonly view / component*@
@page "/masterdetail/{id:int}"
@*Route for this page when an entry is selected in the master list and the user clicked the edit button in the readonly view / component. The detail area should show a edit view / component*@
@page "/masterdetail/{id:int}/edit"
@using Microsoft.AspNetCore.Components

@inject NavigationManager NavigationManager

<h1>MyMasterDetailPage</h1>

<br />
<br />
<br />

<div>
    <h1>Master Area</h1>

    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <button @onclick=@(mouseEventArgs => ShowListItemDetails(1))>Item 1</button>
        </li>
        <li class="nav-item px-3">
            <button @onclick=@(mouseEventArgs => ShowListItemDetails(2))>Item 2</button>
        </li>
        <li class="nav-item px-3">
            <button @onclick=@(mouseEventArgs => ShowListItemDetails(3))>Item 3</button>
        </li>
    </ul>
</div>

<br />
<br />
<br />

<div>
    <h1>Detail Area</h1>
    @{
        if (_isInEditMode)
        {
            // In the real project a <EmployeeEditComponent></EmployeeEditComponent> is being used here instead of the h2
            <h2>Edit view for item no. @Id</h2>
            <h3>Imagine lots of editable fields here e.g. TextBoxes, DatePickers and so on...</h3>
            <button @onclick=@SaveChanges> save...</button>
        }
        else
        {
            // In the real project a <EmployeeDetailComponent></EmployeeDetailComponent> is being used here instead of the h2
            <h2>ReadOnly view for item no. @Id</h2>
            <h3>Imagine lots of NON editable fields here. Probably only labels...</h3>
            <button @onclick=@SwitchToEditMode> edit...</button>
        }
    }
</div>

@code {
    private bool _isInEditMode;

    [Parameter]
    public int Id { get; set; }

    protected override void OnParametersSet()
    {
        // This lifecycle method is not called if the [Parameter] has already been set as Blazor seems to reuse the instance if the [Parameter] Id has not changed it's value.
        // For example this method is not being called when navigating from /masterdetail/1 to /masterdetail/1/edit

        Console.WriteLine($"Navigation parameters have been set for URI: {NavigationManager.Uri}");
        _isInEditMode = NavigationManager.Uri.EndsWith("edit");
        base.OnParametersSet();
    }

    private void ShowListItemDetails(int id)
    {
        Console.WriteLine($"Showing readonly details of item no. {id}");
        NavigationManager.NavigateTo($"/masterdetail/{id}");
    }

    private void SwitchToEditMode()
    {
        Console.WriteLine("Switching to edit mode...");
        NavigationManager.NavigateTo($"/masterdetail/{Id}/edit");

        // Setting _isInEditMode = true here would work and update the UI correctly.
        // In the real project this method is part of the <EmployeeEditComponent></EmployeeEditComponent> and therefore has no access to _isInEditMode as it belongs to the <MyMasterDetailPage> component.
        // I know that I could create a public EventCallback<MouseEventArgs> OnClick { get; set; } in the <EmployeeEditComponent> and react to that event here in the <MyMasterDetailPage> component but is that really the right way to do this?

        //_isInEditMode = true;
    }

    private void SaveChanges()
    {
        Console.WriteLine("Saving changes made in edit mode and switching back to readonly mode...");
        NavigationManager.NavigateTo($"/masterdetail/{Id}");

        // Setting _isInEditMode = false here would work and update the UI correctly.
        // In the real project this method is part of the <EmployeeDetailComponent></EmployeeDetailComponent> and therefore has no access to _isInEditMode as it belongs to the <MyMasterDetailPage> component
        // I know that I could create a public EventCallback<MouseEventArgs> OnClick { get; set; } in the <EmployeeDetailComponent> and react to that event here in the <MyMasterDetailPage> component but is that really the right way to do this?

        //_isInEditMode = false;
    }
}

My setup:

What is the best practice / recommendation on how to switch child content inside a blazor page?

Upvotes: 4

Views: 3817

Answers (1)

SariDev
SariDev

Reputation: 71

I asked the question on the AspNetCore Github repo and got an answer.

https://github.com/aspnet/AspNetCore/issues/16653

As "mrpmorris" said, I changed the following lines

Before @page "/masterdetail/{id:int}/edit"

After @page "/masterdetail/{id:int}/{displayMode}"


Before -

After [Parameter]<br> public string DisplayMode { get; set; }


Before _isInEditMode = NavigationManager.Uri.EndsWith("edit");

After string.Equals(DisplayMode, "edit", StringComparison.InvariantCultureIgnoreCase);


and the website behaves as intended and that solves my problem :)

Upvotes: 3

Related Questions