ASP.NET Core Blazor kaskadowych wartości i parametrów

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

W tym artykule wyjaśniono, jak przepływać dane ze składnika przodka Razor do składników malejących.

Wartości kaskadowe i parametry zapewniają wygodny sposób przepływu danych w dół hierarchii składników ze składnika przodka do dowolnej liczby składników malejących. W przeciwieństwie do parametrów składnika wartości kaskadowe i parametry nie wymagają przypisania atrybutów dla każdego składnika malejącego, w którym są używane dane. Kaskadowe wartości i parametry umożliwiają również składnikom koordynowanie ze sobą między hierarchią składników.

Uwaga

Przykłady kodu w tym artykule przyjmują typy odwołań dopuszczających wartość null (NRTs) i statyczną analizę stanu null kompilatora platformy .NET, które są obsługiwane w programie ASP.NET Core na platformie .NET 6 lub nowszym. W przypadku określania wartości docelowej ASP.NET Core 5.0 lub starszej usuń oznaczenie typu null (?) z CascadingType?typów , , @ActiveTab?RenderFragment?ITab?, TabSet?, i string? w przykładach artykułu.

Wartości kaskadowe na poziomie głównym

Wartości kaskadowe na poziomie głównym można zarejestrować dla całej hierarchii składników. Obsługiwane są nazwane wartości kaskadowe i subskrypcje dla powiadomień o aktualizacji.

Poniższa klasa jest używana w przykładach tej sekcji.

Dalek.cs:

// "Dalek" ©Terry Nation https://www.imdb.com/name/nm0622334/
// "Doctor Who" ©BBC https://www.bbc.co.uk/programmes/b006q2x0

namespace BlazorSample;

public class Dalek
{
    public int Units { get; set; }
}

Następujące rejestracje są wykonywane w pliku aplikacji Program za pomocą polecenia AddCascadingValue:

  • Dalek z wartością właściwości dla Units jest rejestrowana jako stała wartość kaskadowa.
  • Druga Dalek rejestracja z inną wartością właściwości dla Units ma nazwę "AlphaGroup".
builder.Services.AddCascadingValue(sp => new Dalek { Units = 123 });
builder.Services.AddCascadingValue("AlphaGroup", sp => new Dalek { Units = 456 });

Daleks Poniższy składnik wyświetla kaskadowe wartości.

Daleks.razor:

@page "/daleks"

<PageTitle>Daleks</PageTitle>

<h1>Root-level Cascading Value Example</h1>

<ul>
    <li>Dalek Units: @Dalek?.Units</li>
    <li>Alpha Group Dalek Units: @AlphaGroupDalek?.Units</li>
</ul>

<p>
    Dalek© <a href="https://www.imdb.com/name/nm0622334/">Terry Nation</a><br>
    Doctor Who© <a href="https://www.bbc.co.uk/programmes/b006q2x0">BBC</a>
</p>

@code {
    [CascadingParameter]
    public Dalek? Dalek { get; set; }

    [CascadingParameter(Name = "AlphaGroup")]
    public Dalek? AlphaGroupDalek { get; set; }
}

W poniższym przykładzie Dalek jest rejestrowana jako wartość kaskadowa przy użyciu metody CascadingValueSource<T>, gdzie <T> jest typem. Flaga isFixed wskazuje, czy wartość jest stała. Jeśli wartość false, wszyscy adresaci są subskrybowani na potrzeby powiadomień o aktualizacji, które są wydawane przez wywołanie metody NotifyChangedAsync. Subskrypcje tworzą obciążenie i zmniejszają wydajność, więc ustaw wartość isFixed na true wartość , jeśli wartość nie ulegnie zmianie.

builder.Services.AddCascadingValue(sp =>
{
    var dalek = new Dalek { Units = 789 };
    var source = new CascadingValueSource<Dalek>(dalek, isFixed: false);
    return source;
});

Ostrzeżenie

Zarejestrowanie typu składnika jako wartości kaskadowej na poziomie głównym nie powoduje zarejestrowania dodatkowych usług dla typu lub zezwolenia na aktywację usługi w składniku.

Traktuj wymagane usługi oddzielnie od kaskadowych wartości, rejestrując je oddzielnie od typu kaskadowego.

Unikaj używania metody , AddCascadingValue aby zarejestrować typ składnika jako wartość kaskadową. Zamiast tego zawijaj element <Router>...</Router> w składniku Routes (Components/Routes.razor) za pomocą składnika i zastosuj globalne interaktywne renderowanie po stronie serwera (interakcyjne SSR). Aby zapoznać się z przykładem, zobacz sekcję CascadingValue składników .

CascadingValue cm6long

Składnik nadrzędny udostępnia kaskadową wartość przy użyciu Blazor składnika platformy CascadingValue , który opakowuje poddrzewo hierarchii składników i dostarcza jedną wartość do wszystkich składników w ramach jego poddrzewa.

W poniższym przykładzie pokazano przepływ informacji o motywie w dół hierarchii składników, aby udostępnić klasę stylu CSS przyciskom w składnikach podrzędnych.

ThemeInfo Poniższa klasa języka C# określa informacje o motywie.

Uwaga

W przypadku przykładów w tej sekcji przestrzeń nazw aplikacji to BlazorSample. Podczas eksperymentowania z kodem we własnej przykładowej aplikacji zmień przestrzeń nazw aplikacji na przestrzeń nazw przykładowej aplikacji.

ThemeInfo.cs:

namespace BlazorSample;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses;

public class ThemeInfo
{
    public string? ButtonClass { get; set; }
}
namespace BlazorSample.UIThemeClasses
{
    public class ThemeInfo
    {
        public string ButtonClass { get; set; }
    }
}
namespace BlazorSample.UIThemeClasses
{
    public class ThemeInfo
    {
        public string ButtonClass { get; set; }
    }
}

Poniższy składnik układu określa informacje o motywie (ThemeInfo) jako wartość kaskadową dla wszystkich składników tworzących treść Body układu właściwości. ButtonClass ma przypisaną wartość btn-successtypu , która jest stylem przycisku Bootstrap. Każdy składnik malejący w hierarchii składników może używać ButtonClass właściwości za pośrednictwem ThemeInfo wartości kaskadowej.

MainLayout.razor:

@inherits LayoutComponentBase

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://learn.microsoft.com/aspnet/core/" target="_blank">About</a>
        </div>

        <CascadingValue Value="@theme">
            <article class="content px-4">
                @Body
            </article>
        </CascadingValue>
    </main>
</div>

<div id="blazor-error-ui" data-nosnippet>
    An unhandled error has occurred.
    <a href="" class="reload">Reload</a>
    <a class="dismiss">🗙</a>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <div class="top-row px-4">
            <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
        </div>

        <CascadingValue Value="@theme">
            <article class="content px-4">
                @Body
            </article>
        </CascadingValue>
    </main>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <main>
        <CascadingValue Value="@theme">
            <div class="content px-4">
                @Body
            </div>
        </CascadingValue>
    </main>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="page">
    <div class="sidebar">
        <NavMenu />
    </div>

    <div class="main">
        <CascadingValue Value="@theme">
            <div class="content px-4">
                @Body
            </div>
        </CascadingValue>
    </div>
</div>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };
}
@inherits LayoutComponentBase
@using BlazorSample.UIThemeClasses

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <CascadingValue Value="theme">
        <div class="content px-4">
            @Body
        </div>
    </CascadingValue>
</div>

@code {
    private ThemeInfo theme = new ThemeInfo { ButtonClass = "btn-success" };
}

Blazor Usługa Web Apps udostępnia alternatywne podejścia do kaskadowych wartości, które mają zastosowanie w szerszym zakresie do aplikacji niż ich wyposażenie za pośrednictwem pojedynczego pliku układu:

  • Zawijaj znaczniki Routes składnika w CascadingValue składniku, aby określić dane jako kaskadową wartość dla wszystkich składników aplikacji.

    W poniższym przykładzie Routes dane kaskadowe ThemeInfo ze składnika.

    Routes.razor:

    <CascadingValue Value="theme">
        <Router ...>
            ...
        </Router>
    </CascadingValue>
    
    @code {
        private ThemeInfo theme = new() { ButtonClass = "btn-success" };
    }
    

    Uwaga

    Zawijanie Routes wystąpienia składnika w składniku App (Components/App.razor) ze składnikiem CascadingValue nie jest obsługiwane.

  • Określ wartość kaskadową na poziomie głównym jako usługę, wywołując AddCascadingValue metodę rozszerzenia w konstruktorze kolekcji usług.

    Poniższy przykład kaskadowo ThemeInfo wyświetla dane z Program pliku.

    Program.cs

    builder.Services.AddCascadingValue(sp => 
        new ThemeInfo() { ButtonClass = "btn-primary" });
    

Aby uzyskać więcej informacji, zobacz następujące sekcje tego artykułu:

Atrybut [CascadingParameter]

Aby użyć wartości kaskadowych, składniki malejące deklarują parametry kaskadowe przy użyciu atrybutu [CascadingParameter]. Wartości kaskadowe są powiązane z parametrami kaskadowymi według typu. Kaskadowe wartości tego samego typu są omówione w sekcji Kaskada wielu wartości w dalszej części tego artykułu.

Poniższy składnik wiąże ThemeInfo wartość kaskadową z parametrem kaskadowym, opcjonalnie przy użyciu tej samej nazwy ThemeInfo. Parametr służy do ustawiania klasy CSS dla Increment Counter (Themed) przycisku.

ThemedCounter.razor:

@page "/themed-counter"

<PageTitle>Themed Counter</PageTitle>

<h1>Themed Counter Example</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button 
        class="btn @(ThemeInfo is not null ? ThemeInfo.ButtonClass : string.Empty)" 
        @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo? ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/themed-counter"
@using BlazorSample.UIThemeClasses

<h1>Themed Counter</h1>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">
        Increment Counter (Unthemed)
    </button>
</p>

<p>
    <button class="btn @ThemeInfo.ButtonClass" @onclick="IncrementCount">
        Increment Counter (Themed)
    </button>
</p>

@code {
    private int currentCount = 0;

    [CascadingParameter]
    protected ThemeInfo ThemeInfo { get; set; }

    private void IncrementCount()
    {
        currentCount++;
    }
}

Podobnie jak w przypadku zwykłego parametru składnika, składniki akceptujące parametr kaskadowy są rerenderowane po zmianie wartości kaskadowej. Na przykład skonfigurowanie innego wystąpienia motywu powoduje ThemedCounter , że składnik z CascadingValue sekcji składnika ma wartość rerender.

MainLayout.razor:

<main>
    <div class="top-row px-4">
        <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
    </div>

    <CascadingValue Value="theme">
        <article class="content px-4">
            @Body
        </article>
    </CascadingValue>
    <button @onclick="ChangeToDarkTheme">Dark mode</button>
</main>

@code {
    private ThemeInfo theme = new() { ButtonClass = "btn-success" };

    private void ChangeToDarkTheme()
    {
        theme = new() { ButtonClass = "btn-secondary" };
    }
}

CascadingValue<TValue>.IsFixed Może służyć do wskazania, że parametr kaskadowy nie zmienia się po zainicjowaniu.

Kaskadowe wartości/parametry i granice trybu renderowania

Parametry kaskadowe nie przekazują danych w granicach trybu renderowania:

  • Sesje interakcyjne są uruchamiane w innym kontekście niż strony korzystające ze statycznego renderowania po stronie serwera (statyczne SSR). Nie ma potrzeby, aby serwer tworzący stronę był nawet tą samą maszyną, która hostuje późniejszą sesję serwera interakcyjnego, w tym składników zestawu WebAssembly, na których serwer jest inną maszyną do klienta. Zaletą renderowania statycznego po stronie serwera (statycznego renderowania SSR) jest uzyskanie pełnej wydajności czystego bezstanowego renderowania HTML.

  • Stan przekraczania granicy między renderowaniem statycznym i interaktywnym musi być serializowalny. Składniki są dowolnymi obiektami odwołującymi się do rozległego łańcucha innych obiektów, w tym modułu renderującego, kontenera DI i każdego wystąpienia usługi DI. Należy jawnie spowodować serializacji stanu ze statycznego przewodnika SSR, aby udostępnić go w kolejnych składnikach renderowanych interakcyjnie. Przyjęto dwa podejścia:

    • Blazor Za pośrednictwem platformy parametry przekazywane przez statyczny przewodnik SSR do interakcyjnej granicy renderowania są serializowane automatycznie, jeśli są JSone serializowane w trybie on-serializable lub zgłaszany jest błąd.
    • Stan przechowywany w pliku PersistentComponentState jest serializowany i odzyskiwane automatycznie, jeśli jest JSon serializowany lub zgłaszany jest błąd.

Parametry kaskadowe nie JSsą serializowalne, ponieważ typowe wzorce użycia dla parametrów kaskadowych są nieco podobne do usług DI. Często istnieją warianty specyficzne dla platformy dotyczące parametrów kaskadowych, więc byłoby nieprzydatne dla deweloperów, jeśli platforma powstrzymała deweloperów od posiadania wersji specyficznych dla serwera lub wersji specyficznych dla zestawu WebAssembly. Ponadto wiele kaskadowych wartości parametrów w ogóle nie można serializować, więc niepraktyczne byłoby zaktualizowanie istniejących aplikacji, gdyby trzeba było przestać używać wszystkich nieserializowalnych wartości parametrów kaskadowych.

Rekomendacje:

  • Jeśli musisz udostępnić stan wszystkim składnikom interaktywnym jako parametr kaskadowy, zalecamy użycie wartości kaskadowych na poziomie głównym. Dostępny jest wzorzec fabryki, a aplikacja może emitować zaktualizowane wartości po uruchomieniu aplikacji. Wartości kaskadowe na poziomie głównym są dostępne dla wszystkich składników, w tym składników interaktywnych, ponieważ są przetwarzane jako usługi DI.

  • W przypadku autorów bibliotek składników można utworzyć metodę rozszerzenia dla użytkowników bibliotek podobnych do następujących:

    builder.Services.AddLibraryCascadingParameters();
    

    Poinstruuj deweloperów, aby wywołali metodę rozszerzenia. Jest to rozsądna alternatywa dla poinstruowania ich, aby dodać <RootComponent> składnik w swoim MainLayout składniku.

Kaskadowe wiele wartości

Aby kaskadowo utworzyć wiele wartości tego samego typu w ramach tego samego poddrzewa, podaj unikatowy Name ciąg dla każdego CascadingValue składnika i odpowiadających im [CascadingParameter] atrybutów.

W poniższym przykładzie dwa CascadingValue składniki kaskadowo używają różnych wystąpień programu CascadingType:

<CascadingValue Value="parentCascadeParameter1" Name="CascadeParam1">
    <CascadingValue Value="ParentCascadeParameter2" Name="CascadeParam2">
        ...
    </CascadingValue>
</CascadingValue>

@code {
    private CascadingType? parentCascadeParameter1;

    [Parameter]
    public CascadingType? ParentCascadeParameter2 { get; set; }
}

W składniku potomnym parametry kaskadowe otrzymują swoje kaskadowe wartości ze składnika przodka przez :Name

@code {
    [CascadingParameter(Name = "CascadeParam1")]
    protected CascadingType? ChildCascadeParameter1 { get; set; }

    [CascadingParameter(Name = "CascadeParam2")]
    protected CascadingType? ChildCascadeParameter2 { get; set; }
}

Przekazywanie danych w hierarchii składników

Parametry kaskadowe umożliwiają również składnikom przekazywanie danych w hierarchii składników. Rozważmy poniższy przykład zestawu kart interfejsu użytkownika, w którym składnik zestawu kart utrzymuje serię poszczególnych kart.

Uwaga

W przypadku przykładów w tej sekcji przestrzeń nazw aplikacji to BlazorSample. Podczas eksperymentowania z kodem we własnej przykładowej aplikacji zmień przestrzeń nazw na przestrzeń nazw przykładowej aplikacji.

Utwórz interfejs, który tabulatory ITab implementują w folderze o nazwie UIInterfaces.

UIInterfaces/ITab.cs:

using Microsoft.AspNetCore.Components;

namespace BlazorSample.UIInterfaces;

public interface ITab
{
    RenderFragment ChildContent { get; }
}

Uwaga

Aby uzyskać więcej informacji na temat RenderFragmentprogramu , zobacz składniki ASP.NET CoreRazor.

Poniższy TabSet składnik obsługuje zestaw kart. Składniki zestawu Tab kart, które są tworzone w dalszej części tej sekcji, podaj elementy listy (<li>...</li>) dla listy (<ul>...</ul>).

Składniki podrzędne Tab nie są jawnie przekazywane jako parametry do elementu TabSet. Zamiast tego składniki podrzędne Tab są częścią zawartości podrzędnej elementu TabSet. Jednak nadal potrzebuje odwołania do każdego Tab składnika, TabSet aby mógł renderować nagłówki i aktywną kartę. Aby umożliwić tę koordynację bez konieczności dodatkowego kodu, TabSet składnik może podać się jako wartość kaskadową, która jest następnie pobierana przez składniki malejąceTab.

TabSet.razor:

@using BlazorSample.UIInterfaces

<!-- Display the tab headers -->

<CascadingValue Value="this">
    <ul class="nav nav-tabs">
        @ChildContent
    </ul>
</CascadingValue>

<!-- Display body for only the active tab -->

<div class="nav-tabs-body p-4">
    @ActiveTab?.ChildContent
</div>

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

    public ITab? ActiveTab { get; private set; }

    public void AddTab(ITab tab)
    {
        if (ActiveTab is null)
        {
            SetActiveTab(tab);
        }
    }

    public void SetActiveTab(ITab tab)
    {
        if (ActiveTab != tab)
        {
            ActiveTab = tab;
            StateHasChanged();
        }
    }
}

Tab Składniki malejące przechwytują element zawierający TabSet jako parametr kaskadowy. Składniki Tab dodają się do TabSet współrzędnych i, aby ustawić aktywną kartę.

Tab.razor:

@using BlazorSample.UIInterfaces
@implements ITab

<li>
    <a @onclick="ActivateTab" class="nav-link @TitleCssClass" role="button">
        @Title
    </a>
</li>

@code {
    [CascadingParameter]
    public TabSet? ContainerTabSet { get; set; }

    [Parameter]
    public string? Title { get; set; }

    [Parameter]
    public RenderFragment? ChildContent { get; set; }

    private string? TitleCssClass => 
        ContainerTabSet?.ActiveTab == this ? "active" : null;

    protected override void OnInitialized()
    {
        ContainerTabSet?.AddTab(this);
    }

    private void ActivateTab()
    {
        ContainerTabSet?.SetActiveTab(this);
    }
}

ExampleTabSet Poniższy składnik używa TabSet składnika, który zawiera trzy Tab składniki.

ExampleTabSet.razor:

@page "/example-tab-set"

<TabSet>
    <Tab Title="First tab">
        <h4>Greetings from the first tab!</h4>

        <label>
            <input type="checkbox" @bind="showThirdTab" />
            Toggle third tab
        </label>
    </Tab>

    <Tab Title="Second tab">
        <h4>Hello from the second tab!</h4>
    </Tab>

    @if (showThirdTab)
    {
        <Tab Title="Third tab">
            <h4>Welcome to the disappearing third tab!</h4>
            <p>Toggle this tab from the first tab.</p>
        </Tab>
    }
</TabSet>

@code {
    private bool showThirdTab;
}

Dodatkowe zasoby