简体   繁体   中英

How can I trigger/refresh my main .RAZOR page from all of its sub-components within that main .RAZOR page when an API call is complete?

I am working on an app that lets users search our database. When a user enters search terms, the app hits an API endpoint, and returns the data. I then display the data.

When the API returns the data, I have a scoped service:

services.AddScoped<AppState>();

That keeps each returned dataset for use throughout all components of the app.

As soon as the SearchResults.razor page loads, it grabs the results from my scoped service, then draws the rest of the page.

I need a "Loading" spinner to put in place of the data until the API returns the data, which could be a long time depending on the amount of data searched for.

My problem is that I cannot figure out what to use as a true/false "trigger" to use to know whether or not to show the data or loading spinner, or how to refresh the page once the API sends me it's data.

What I have below works only for the first initial search (from my Index.razor page), but does not work for any of the included "filters" components.

SearchResults.razor:

@page "/searchresults"
@layout PageTopComponents

<Header.razor></Header.razor>

<LeftMenu.razor>

    <FilterRazorComponent01.razor></FilterRazorComponent01.razor>

    <FilterRazorComponent02.razor></FilterRazorComponent02.razor>

    <FilterRazorComponent03.razor></FilterRazorComponent03.razor>

    <FilterRazorComponent04.razor></FilterRazorComponent04.razor>

</LeftMenu.razor>

<MainContentComponent.razor>

    // CONTENT HERE SHOULD BE VISIBLE WHEN DATA HAS ARRIVED, OTHERWISE IT SHOULD SHOW A "WAITING" SPINNER
    @if(API_Data_Received != null && API_Data_Received.count > 0){
        foreach(){
            // API Retrieved Data Here
        }
    } else {
        // Loading Spinner
    }

    <ContinueSearch.razor></ContinueSearch.razor>

    <Paginator.razor @ref="PaginatorComponentReference">

        <ChildContent>

            // THIS IS WHERE I DISPLAY ALL SEARCH DATA ...
            // CONTAINS: public Paginator PaginatorComponentReference;

        </ChildContent>

    </Paginator.razor>

</MainContentComponent.razor>

@code {
    // code here ...

    public async Task GetQueryStringValues()
    {
        Uri uri = navigationManager.ToAbsoluteUri(System.Net.WebUtility.UrlDecode(navigationManager.Uri));
        Dictionary<string, StringValues> queryStrings = QueryHelpers.ParseQuery(uri.Query);
    }
}

Paginator.razor:

<div> [ << ] [ < ] NAVIGATION [ > ] [ >> ] </div>

    @ChildContent // Is "ChildContent" in SearchResults.razor

<div> [ << ] [ < ] NAVIGATION [ > ] [ >> ] </div>

Most of my included .RAZOR components do some kind of "filtering" and use the following:

String href = "/searchresults" + // other parameters here ...
NavigationManager.NavigateTo(href);

Meaning, whenever I "filter", I always hit the SearchResults.razor page.

I believe I have tried some combination of await InvokeAsync(StateHasChanged); in all of the override-able methods:

  1. OnInitialized()
  2. OnInitializedAsync()
  3. OnParametersSet()
  4. OnParametersSetAsync()
  5. OnAfterRender()
  6. OnAfterRenderAsync()

But nothing seems to work after that first load of SearchResults.razor from my form entry in Index.razor .

What do I need to do to get this to work? It seems simple enough, but I just cannot figure it out.

The answer shows how to update the Blazor WeatherForecast application to demonstrate the state/notification pattern and how to use it in components. I've used the Weather Forecast application because there's not enough detail in your question to use your code as the basis for an answer, and the Weather Forecast application provides a good template to build on.

The starting point is a standard Blazor Server template project. Mine is called StackOverflow.Answers

Add a Loading.razor component. This will detect load state and display a rotator when the records are loading.

@if (this.IsLoaded)
{
    @this.ChildContent
}
else
{
    <div class="loader"></div>
}

@code {
    [Parameter] public RenderFragment ChildContent { get; set; }

    [Parameter] public bool IsLoaded { get; set; }
}

Add a component CSS file - Loading.razor.css - to format the rotator:

.page-loader {
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 1;
    width: 150px;
    height: 150px;
    margin: -75px 0 0 -75px;
    border: 16px solid #f3f3f3;
    border-radius: 50%;
    border-top: 16px solid #3498db;
    width: 120px;
    height: 120px;
    -webkit-animation: spin 2s linear infinite;
    animation: spin 2s linear infinite;
}

.loader {
    border: 16px solid #f3f3f3;
    /* Light grey */
    border-top: 16px solid #3498db;
    /* Blue */
    border-radius: 50%;
    width: 120px;
    height: 120px;
    animation: spin 2s linear infinite;
    margin-left: auto;
    margin-right: auto;
}

@-webkit-keyframes spin {
    0% {
        -webkit-transform: rotate(0deg);
    }

    100% {
        -webkit-transform: rotate(360deg);
    }
}

@keyframes spin {
    0% {
        transform: rotate(0deg);
    }

    100% {
        transform: rotate(360deg);
    }
}

I split the original service into separate data and view services (good design practice).

Update the WeatherForecastService . It's now the data service and all it needs to do is provide the data. In a real app this will interface with the data brokers to get real data.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace StackOverflow.Answers.Data
{
    public class WeatherForecastService
    {
        private static readonly string[] Summaries = new[]
        {
            "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
        };

        private List<WeatherForecast> recordsShort;
        private List<WeatherForecast> recordsLong;

        public WeatherForecastService()
        {
            recordsShort = GetForecastsShort;
            recordsLong = GetForecastsLong;
        }

        public async Task<List<WeatherForecast>> GetForecastsAsync(bool islong = false)
        {
            await Task.Delay(3000);
            return islong ? this.recordsLong : this.recordsShort;
        }

        public List<WeatherForecast> GetForecastsShort
        {
            get
            {
                var rng = new Random();
                return Enumerable.Range(1, 3).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = rng.Next(-20, 55),
                    Summary = Summaries[rng.Next(Summaries.Length)]
                }).ToList();
            }
        }

        public List<WeatherForecast> GetForecastsLong
        {
            get
            {
                var rng = new Random();
                return Enumerable.Range(1, 6).Select(index => new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = rng.Next(-20, 55),
                    Summary = Summaries[rng.Next(Summaries.Length)]
                }).ToList();
            }
        }
    }
}

Add a new WeatherForecastViewService class to Data folder. This is our view service. It holds our data and is the service the UI uses. It gets data from the data service and exposes a Records list and ListChanged event that is triggered whenever the list changes.

using System;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace StackOverflow.Answers.Data
{
    public class WeatherForecastViewService
    {
        public List<WeatherForecast> Records { get; set; }

        private WeatherForecastService weatherForecastService;

        public WeatherForecastViewService(WeatherForecastService weatherForecastService)
        {
            this.weatherForecastService = weatherForecastService;
        }

        public async Task GetForecastsAsync(bool islong = false)
        {
            this.Records = null;
            this.NotifyListChanged(this.Records, EventArgs.Empty);
            this.Records = await weatherForecastService.GetForecastsAsync(islong);
            this.NotifyListChanged(this.Records, EventArgs.Empty);
        }

        public event EventHandler<EventArgs> ListChanged;

        public void NotifyListChanged(object sender, EventArgs e)
            => ListChanged?.Invoke(sender, e);
    }
}

Add a new Component - WeatherForecastList.razor . This is the guts from Fetchdata . It:

  1. Uses the new Loading component.
  2. Uses the new WeatherForecastViewService.
  3. Uses the list directly from WeatherForecastViewService . It doesn't have it's own copy - all components use the same list.
  4. Wires into the view service ListChanged event and calls StateHasChanged whenever the evwnt is triggered.
@implements IDisposable
@using StackOverflow.Answers.Data

<h1>Weather forecast</h1>
<Loading IsLoaded="this.isLoaded" >
    <table class="table">
        <thead>
            <tr>
                <th>Date</th>
                <th>Temp. (C)</th>
                <th>Temp. (F)</th>
                <th>Summary</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var forecast in viewService.Records)
            {
                <tr>
                    <td>@forecast.Date.ToShortDateString()</td>
                    <td>@forecast.TemperatureC</td>
                    <td>@forecast.TemperatureF</td>
                    <td>@forecast.Summary</td>
                </tr>
            }
        </tbody>
    </table>
</Loading>

@code {

    [Inject] private WeatherForecastViewService viewService { get; set; }

    private bool isLoaded => viewService.Records is not null;

    protected override async Task OnInitializedAsync()
    {
        await GetForecastsAsync();
        this.viewService.ListChanged += this.OnListChanged;
    }

    private async Task GetForecastsAsync()
        =>  await viewService.GetForecastsAsync();

    private void OnListChanged(object sender, EventArgs e)
        => this.InvokeAsync(this.StateHasChanged);

    public void Dispose()
    {
        this.viewService.ListChanged -= this.OnListChanged;
    }
}

Update Startup Services for the new services.

    services.AddSingleton<WeatherForecastService>();
    services.AddScoped<WeatherForecastViewService>();

Update FetchData . It now uses the WeatherForecastList component. The button provides a mechanism to change the List and see the UI updates.

@page "/fetchdata"
@using StackOverflow.Answers.Data

<WeatherForecastList/>
<div class="m-2">
    <button class="btn btn-dark" @onclick="this.LoadRecords">Reload Records</button>
</div>
@code {

    [Inject] WeatherForecastViewService viewService { get; set; }

    private bool isLong = true;

    private async Task LoadRecords()
    {
        await this.viewService.GetForecastsAsync(isLong);
        this.isLong = !this.isLong;
    }
}

Hopefully I've got all the code right first time! I'm sure someone will point out any glaring errors, or improvements.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM