Search code examples
asp.net-coreblazorblazor-client-side

Blazor - razor page not updating after property from DI Service is changed


Using dotnet 3.1.100-preview2-014569

Ok consider the following example:

Create a new Blazor WebAssemply project from template, then add the following:

books.razor

@page "/books"
@inject BookService bookService

@if (bookService.isLoaned)
{
    <p><em>Book loaned</em></p>
}
else
{
    <p><em>Book returned</em></p>
}

BookService.cs

public class BookService
    {
        public int BookId { get; set; }

        public string Title { get; set; }

        public bool isLoaned { get; set; }
    }

Startup.cs

public void ConfigureServices(IServiceCollection services)
        {
            services.AddScoped<BookService>();
        }

NavMenu.razor

@inject BookService bookService;
<div class="top-row pl-4 navbar navbar-dark">
    <a class="navbar-brand" href="">BlazorBlank_PV2</a>
    <button class="navbar-toggler" @onclick="ToggleNavMenu">
        <span class="navbar-toggler-icon"></span>
    </button>
</div>

<div class="@NavMenuCssClass" @onclick="ToggleNavMenu">
    <ul class="nav flex-column">
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="" Match="NavLinkMatch.All">
                <span class="oi oi-home" aria-hidden="true"></span> Home
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="books" @onclick="LoanBookClicked">
                <span class="oi oi-plus" aria-hidden="true"></span>Loan Book
            </NavLink>
        </li>
        <li class="nav-item px-3">
            <NavLink class="nav-link" href="books" @onclick="ReturnBookClicked">
                <span class="oi oi-list-rich" aria-hidden="true"></span>Return Book
            </NavLink>
        </li>
    </ul>
</div>

@code {
    private bool collapseNavMenu = true;

    private string NavMenuCssClass => collapseNavMenu ? "collapse" : null;

    private void ToggleNavMenu()
    {
        collapseNavMenu = !collapseNavMenu;
    }

    private void LoanBookClicked()
    {
        bookService.isLoaned = true;       
    }
    private void ReturnBookClicked()
    {
        bookService.isLoaned = false;
    }


}

What happens: Not rendering issue

Expected behavior: When clicking on the menu item Loan Book the page should show Book Loaned or if I click Return Book it should say Book returned. But this only happens if I click to another page, like Home, and then back again.

How can I force the page to re-render/re-check updated values from BookService even when it's on the same page already?

Thanks!


Solution

  • Here's a new solution in which I implement the INotifyPropertyChanged intereface in the BookService class. Why use this interface ?

    • It can be applied to other properties as well

    • Notifying clients that a property value has changed is essential part of good services, and this is not limited to only call the StateHasChanged solely for this purpose.

    • With the INotifyPropertyChanged intereface implemented, I can pass event data to registered or subscribing objects.

    Important: Calling a method to update a property value like bookService.SetLoanedState(false) is a bad programming and an anti-patterns design. A property can have two accessors, get and set, and they should be used to get a value and to change a value. Methods have different roles...

    BookService.cs


    using System.ComponentModel;
    using System.Runtime.CompilerServices;
    
    
    
    public class BookService : INotifyPropertyChanged
        {
            private bool isLoaned;
            public event PropertyChangedEventHandler PropertyChanged;
    
            public int BookId { get; set; }
    
            public string Title { get; set; }
    
    
            public bool IsLoaned
            {
                get
                {
                    return this.isLoaned;
                }
    
                set
                {
                    if (value != this.isLoaned)
                    {
                        this.isLoaned = value;
                        NotifyPropertyChanged();
                    }
                }
            }
    
    
            private void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
            {
                if (PropertyChanged != null)
                {
                    PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
                }
            }
        }
    

    Books.razor

    @page "/books"
    @inject BookService bookService
    @using System.ComponentModel
    
    
    @if (bookService.IsLoaned)
    {
        <p><em>Book loaned</em></p>
    }
    else
    {
        <p><em>Book returned</em></p>
    }
    
    @code{
    
        protected override void OnInitialized()
        {
            bookService.PropertyChanged += PropertyHasChanged;
        }
        private void PropertyHasChanged(object sender, PropertyChangedEventArgs args)
        {
             StateHasChanged();
        }
    }
    

    NavMenu.razor

    @code {
    
        // Code removed for brevity
    
        private void LoanBookClicked()
        {
            bookService.IsLoaned = true;      
        }
        private void ReturnBookClicked()
        {
            bookService.IsLoaned = false;      
        }
    }
    

    Hope this works...