Unikaj zastępowania parametrów w programie ASP.NET Core Blazor

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.

Autor: Robert Haken

W tym artykule wyjaśniono, jak uniknąć zastępowania parametrów w Blazor aplikacjach podczas rerendering.

Zastępowane parametry

Platforma Blazor generalnie stosuje bezpieczne przypisywanie parametrów z elementów nadrzędnych do podrzędnych:

  • Parametry nie są zastępowane nieoczekiwanie.
  • Efekty uboczne są minimalizowane. Na przykład unika się dodatkowego renderowania, ponieważ może to tworzyć nieskończone pętle renderowania.

Składnik podrzędny otrzymuje nowe wartości parametrów, które mogą zastąpić istniejące wartości, gdy składnik nadrzędny będzie ponownie renderowany. Przypadkowe zastąpienie wartości parametrów w składniku podrzędnym często występuje, gdy deweloper opracowuje składnik z jednym lub wieloma parametrami związanymi z danymi i bezpośrednio zapisuje wartość w parametrze składnika podrzędnego:

  • Składnik podrzędny jest renderowany z co najmniej jedną wartością parametru ze składnika nadrzędnego.
  • Składnik podrzędny bezpośrednio zapisuje wartość parametru.
  • Składnik nadrzędny jest renderowany ponownie i zastępuje wartość parametru składnika podrzędnego.

Możliwość zastąpienia wartości parametrów obejmuje też metody dostępu set właściwości składnika podrzędnego.

Ważne

Zalecamy ogólnie, aby nie tworzyć składników, które bezpośrednio zapisują wartości w swoich własnych parametrach po pierwszym renderowaniu składnika.

Rozważmy następujący składnik ShowMoreExpander, który:

  • Renderuje tytuł.
  • Wyświetla zawartość podrzędną po wybraniu.
  • Umożliwia ustawienie stanu początkowego za pomocą parametru składnika (InitiallyExpanded).

Poniższy składnik ShowMoreExpander przedstawia zastąpiony parametr. Następnie pokazano zmodyfikowany składnik ShowMoreExpander, aby przedstawić poprawne podejście dla tego scenariusza. Poniższe przykłady można umieścić w lokalnej aplikacji przykładowej, aby zobaczyć opisane zachowania.

ShowMoreExpander.razor:

<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

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

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

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

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

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

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

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

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}
<div @onclick="ShowMore" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @InitiallyExpanded)</h2>
    </div>
    @if (InitiallyExpanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool InitiallyExpanded { get; set; }

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

    private void ShowMore()
    {
        InitiallyExpanded = true;
    }
}

Składnik ShowMoreExpander jest dodawany do następującego składnika nadrzędnego Expanders, który może wywołać metodę StateHasChanged:

Expanders.razor:

@page "/expanders"

<PageTitle>Expanders</PageTitle>

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>
@page "/expanders"

<PageTitle>Expanders</PageTitle>

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>
@page "/expanders"

<PageTitle>Expanders</PageTitle>

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>
@page "/expanders"

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>
@page "/expanders"

<h1>Expanders Example</h1>

<ShowMoreExpander InitiallyExpanded="false">
    Expander 1 content
</ShowMoreExpander>

<ShowMoreExpander InitiallyExpanded="false" />

<button @onclick="StateHasChanged">Call StateHasChanged</button>

ShowMoreExpander Początkowo składniki zachowują się niezależnie po ustawieniu ich InitiallyExpanded właściwości. Składniki podrzędne utrzymują swoje stany w oczekiwany sposób.

Gdy metoda StateHasChanged jest wywoływana w składniku nadrzędnym, platforma Blazor ponownie renderuje składniki podrzędne, jeśli ich parametry mogły się zmienić:

  • W przypadku grupy typów parametrów, które są jawnie sprawdzane przez platformę Blazor, platforma Blazor ponownie renderuje składnik podrzędny, jeśli wykryje zmianę któregokolwiek z parametrów.
  • W przypadku niesprawdzanych typów parametrów platforma Blazor ponownie renderuje składnik podrzędny niezależnie od tego, czy parametry uległy zmianie. Zawartość podrzędna należy do tej kategorii typów parametrów, ponieważ ma ona typ RenderFragment, który jest delegatem odwołującym się do innych modyfikowalnych obiektów.

W przypadku składnika Expanders:

  • Pierwszy ShowMoreExpander składnik ustawia zawartość podrzędną w potencjalnie modyfikowalnym RenderFragmentelemencie , więc wywołanie elementu StateHasChanged w składniku nadrzędnym automatycznie rerenders składnik i potencjalnie zastępuje wartość InitiallyExpanded do początkowej falsewartości .
  • Drugi składnik ShowMoreExpander nie ustawia zawartości podrzędnej. Dlatego nie istnieje potencjalnie modyfikowalny element RenderFragment. Wywołanie metody StateHasChanged w składniku nadrzędnym nie powoduje automatycznego ponownego renderowania składnika podrzędnego, więc wartość InitiallyExpanded składnika nie jest zastępowana.

Aby zachować stan w poprzednim scenariuszu, użyj pola prywatnego w składniku ShowMoreExpander , aby zachować jego stan.

Poniższy zmieniony składnik ShowMoreExpander:

  • Przyjmuje wartość parametru składnika InitiallyExpanded od składnika nadrzędnego.
  • Przypisuje wartość parametru składnika do pola prywatnego (expanded) w zdarzeniu OnInitialized.
  • Używa pola prywatnego w celu zachowania swojego wewnętrznego stanu przełączenia, co pokazuje, jak uniknąć bezpośredniego zapisywania wartości w parametrze.

Uwaga

Porady w tej sekcji obejmują podobną logikę w metodach dostępu set dla parametrów składników, co może skutkować podobnymi niepożądanymi efektami ubocznymi.

ShowMoreExpander.razor:

<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

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

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

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

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

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

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

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

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

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

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

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

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}
<div @onclick="Expand" class="card bg-light mb-3" style="width:30rem">
    <div class="card-header">
        <h2 class="card-title">Show more (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    private bool expanded;

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

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

    protected override void OnInitialized()
    {
        expanded = InitiallyExpanded;
    }

    private void Expand()
    {
        expanded = true;
    }
}

Uwaga

Poprawiona ShowMoreExpander wartość nie odzwierciedla zmian parametru InitiallyExpanded po zainicjowaniu (OnInitialized). W niektórych scenariuszach zainicjowany składnik może odbierać nowe wartości parametrów. Może się to zdarzyć na przykład w widoku szczegółów wzorca, w którym ten sam składnik jest używany do renderowania różnych widoków szczegółów lub gdy /item/{id} parametr trasy zmieni się w celu wyświetlenia innego elementu.

Rozważ następujący ToggleExpander składnik, który:

  • Umożliwia zmianę stanu zarówno z wewnątrz, jak i na zewnątrz.
  • Obsługuje nowe wartości parametrów, nawet jeśli to samo wystąpienie składnika jest ponownie używane.

ToggleExpander.razor:

<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

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

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async void Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

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

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async void Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

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

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async void Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

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

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async void Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}
<div class="card bg-light mb-3" style="width:30rem">
    <div @onclick="Toggle" class="card-header">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @expanded)</h2>
    </div>
    @if (expanded)
    {
        <div class="card-body">
            <p class="card-text">@ChildContent</p>
        </div>
    }
</div>

@code {
    [Parameter]
    public bool Expanded { get; set; }

    [Parameter]
    public EventCallback<bool> ExpandedChanged { get; set; }

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

    private bool expanded;

    protected override void OnParametersSet()
    {
        expanded = Expanded;
    }

    private async void Toggle()
    {
        expanded = !expanded;
        await ExpandedChanged.InvokeAsync(expanded);
    }
}

Składnik ToggleExpander powinien być używany ze składnią @bind-Expanded="{field}" powiązania, umożliwiając dwukierunkową synchronizację parametru.

ExpandersToggle.razor:

@page "/expanders-toggle"

<PageTitle>Expanders Toggle</PageTitle>

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
@page "/expanders-toggle"

<PageTitle>Expanders Toggle</PageTitle>

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
@page "/expanders-toggle"

<PageTitle>Expanders Toggle</PageTitle>

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
@page "/expanders-toggle"

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}
@page "/expanders-toggle"

<h1>Expanders Toggle</h1>

<ToggleExpander @bind-Expanded="expanded">
    Expander content
</ToggleExpander>

<button @onclick="Toggle">Toggle</button>

<button @onclick="StateHasChanged">Call StateHasChanged</button>

@code {
    private bool expanded;

    private void Toggle()
    {
        expanded = !expanded;
    }
}

Aby uzyskać więcej informacji na temat powiązania nadrzędny-podrzędny, zobacz następujące zasoby:

Aby uzyskać więcej informacji na temat wykrywania zmian, w tym informacji na temat dokładnych typów sprawdzaniaBlazor, zobacz ASP.NET Renderowanie składników CoreRazor.