Компоненты Razor ASP.NET Core

Приложения Blazor собираются с использованием компонентовRazor . Компонент — это автономная часть пользовательского интерфейса с логикой обработки, предназначенная для реализации динамического поведения. Компоненты могут быть вложенными, повторно используемыми, общими для проектов и использоваться в приложениях MVC и Razor Pages.

Классы компонентов

Компоненты реализуются в файлах компонентов Razor с расширением имени файла .razor с помощью комбинации разметки HTML и C#.

Синтаксис Razor

Компоненты используют синтаксис Razor. Компоненты широко используют две функции Razor: директивы и атрибуты директив. Это зарезервированные ключевые слова с префиксом @, которые отображаются в разметке Razor:

  • Директивы: изменение способа анализа разметки компонентов или функций. Например, директива @page указывает маршрутизируемый компонент с помощью шаблона маршрута. Достичь ее можно непосредственно с помощью пользовательского запроса в браузере по определенному URL-адресу.
  • Атрибуты директивы: изменение способа анализа или функционирования элемента компонента. Например, атрибут директивы @bind для элемента <input> привязывает данные к значению элемента.

Директивы и атрибуты директив, используемые в компонентах, подробно рассматриваются далее в этой статье и других статьях комплекта документации Blazor. Общие сведения о синтаксисе Razor см. в разделе Справочник по синтаксису Razor для ASP.NET Core.

Имена

Имя компонента должно начинаться с заглавной буквы.

  • ProductDetail.razor является допустимым;
  • productDetail.razor недопустим.

Ниже представлены некоторые общие соглашения об именовании Blazor, используемые в документации Blazor.

  • Пути к файлам компонентов используют регистр Pascal† и отображаются перед примерами кода компонента. Пути обозначают типичные расположения папок. Например, Pages/ProductDetail.razor обозначает, что у компонента ProductDetail есть имя файла ProductDetail.razor и он находится в папке Pages приложения.
  • Пути к файлам маршрутизируемых компонентов соответствуют своим URL-адресам, в которых пробелы между словами в шаблоне маршрута компонента заменяются дефисами. Например, компонент ProductDetail с шаблоном маршрута /product-detail (@page "/product-detail") запрашивается в браузере по относительному URL-адресу /product-detail.

†Регистр Pascal (верхний горбатый регистр) — это соглашение об именовании без пробелов и знаков препинания, где все слова, включая первое, пишутся с прописной буквы.

Маршрутизация

Маршрутизация в Blazor достигается путем предоставления шаблона маршрута каждому доступному компоненту в приложении с директивой @page. При компиляции файла Razor с директивой @page созданному классу предоставляется атрибут RouteAttribute, указывающий шаблон маршрута. Во время выполнения маршрутизатор ищет классы компонентов с RouteAttribute и преобразует для просмотра любой компонент, шаблон маршрута которого соответствует запрошенному URL-адресу.

Следующий компонент HelloWorld использует шаблон маршрута /hello-world. Преобразованная для просмотра веб-страница для компонента находится по относительному URL-адресу /hello-world. При локальном запуске приложения Blazor с помощью протокола, узла и порта по умолчанию компонент HelloWorld запрашивается в браузере по адресу https://localhost:5001/hello-world. Компоненты, создающие веб-страницы, обычно находятся в папке Pages, однако для хранения компонентов можно использовать любую папку, в том числе вложенные папки.

Pages/HelloWorld.razor:

@page "/hello-world"

<h1>Hello World!</h1>
@page "/hello-world"

<h1>Hello World!</h1>

Предыдущий компонент загружается в браузере по адресу /hello-world независимо от того, добавлен ли он в навигацию пользовательского интерфейса приложения. При необходимости компоненты можно добавить в компонент NavMenu, чтобы ссылка на компонент отображалась в структуре навигации приложения на основе пользовательского интерфейса.

Для предыдущего компонента HelloWorld добавьте следующий компонент NavLink в компонент NavMenu. Добавьте компонент NavLink в новый элемент списка (<li>...</li>) между тегами неупорядоченного списка (<ul>...</ul>).

Shared/NavMenu.razor:

<li class="nav-item px-3">
    <NavLink class="nav-link" href="hello-world">
        <span class="oi oi-list-rich" aria-hidden="true"></span> Hello World!
    </NavLink>
</li>

Дополнительные сведения, в том числе описания компонентов NavLink и NavMenu, см. в разделе Маршрутизация ASP.NET Core Blazor.

разметку

Пользовательский интерфейс компонента определяется с помощью синтаксиса Razor, который состоит из разметки Razor, C# и HTML. Во время компиляции приложения разметка HTML и логика отрисовки C# преобразуются в класс компонента. Имя создаваемого класса соответствует имени файла.

Элементы класса компонента определяются в одном или нескольких блоках @code. В блоках @code состояние компонента указывается и обрабатывается с помощью C#:

  • Инициализаторы свойств и полей.
  • Значения параметров из аргументов, переданных родительскими компонентами и параметрами маршрута.
  • Методы для обработки пользовательских событий, событий жизненного цикла и пользовательской логики компонента.

Элементы компонента используются в логике отрисовки с помощью выражений C#, начинающихся с символа @. Например, поле C# отрисовывается путем добавления @ к имени поля. Следующий компонент Markup вычисляет и отрисовывает следующее:

  • headingFontStyle для значения свойства CSS font-style элемента заголовка;
  • headingText для содержимого элемента заголовка.

Pages/Markup.razor:

@page "/markup"

<h1 style="font-style:@headingFontStyle">@headingText</h1>

@code {
    private string headingFontStyle = "italic";
    private string headingText = "Put on your new Blazor!";
}
@page "/markup"

<h1 style="font-style:@headingFontStyle">@headingText</h1>

@code {
    private string headingFontStyle = "italic";
    private string headingText = "Put on your new Blazor!";
}

Примечание

В примерах в составе документации Blazor указан модификатор доступа private для частных элементов. Частные элементы ограничены классом компонента. Однако C# принимает модификатор доступа private при отсутствии модификаторов доступа, поэтому явно помечать элементы как private в собственном коде необязательно. Дополнительные сведения о модификаторах доступа см. в статье Модификаторы доступа (руководство по программированию на C#).

Платформа Blazor обрабатывает компонент внутренним образом как дерево отрисовки, что сочетает в себе модель DOM и объектную модель каскадной таблицы стилей (CSSOM) компонента. После первоначальной отрисовки компонента повторно создается его дерево отрисовки в ответ на события. Затем Blazor сравнивает новое и прежнее дерево отрисовки и применяет все изменения в модели DOM браузера для отображения. Для получения дополнительной информации см. Отрисовка компонента Blazor ASP.NET Core.

Компоненты являются обычными классами C# и могут размещаться в любом месте внутри проекта. Компоненты, создающие веб-страницы, обычно находятся в папке Pages. Компоненты, не являющиеся страницами, часто находятся в папке Shared или пользовательской папке, добавленной в проект.

Вложенные компоненты

Компоненты могут включать другие компоненты, объявляя их с помощью синтаксиса HTML. Разметка для использования компонента выглядит как тег HTML с именем, соответствующем типу компонента.

Рассмотрим следующий компонент Heading, который может использоваться другими компонентами для отображения заголовка.

Shared/Heading.razor:

<h1 style="font-style:@headingFontStyle">Heading Example</h1>

@code {
    private string headingFontStyle = "italic";
}
<h1 style="font-style:@headingFontStyle">Heading Example</h1>

@code {
    private string headingFontStyle = "italic";
}

Следующая разметка в компоненте HeadingExample отрисовывает предыдущий компонент Heading в расположении, где отображается тег <Heading />.

Pages/HeadingExample.razor:

@page "/heading-example"

<Heading />
@page "/heading-example"

<Heading />

Если компонент содержит HTML-элемент с первой заглавной буквой, который не соответствует имени компонента в том же пространстве имен, выдается предупреждение о том, что элемент имеет непредвиденное имя. Добавление директивы @using для пространства имен компонента делает компонент доступным, что позволяет устранить это предупреждение. Дополнительные сведения см. в разделе Пространства имен.

Пример компонента Heading, приведенный в этом разделе, не содержит директивы @page, поэтому компонент Heading недоступен пользователю напрямую путем непосредственного запроса в браузере. Однако любой компонент с директивой @page можно вложить в другой компонент. Если компонент Heading был доступен напрямую, включая @page "/heading" в верхней части файла Razor, компонент будет отрисован для запросов в браузере как в /heading, так и в /heading-example.

Пространства имен

Как правило, пространство имен компонента является производным от корневого пространства имен приложения и расположения компонента (папки) в приложении. Если пространством имен корня приложения является BlazorSample, а компонент Counter находится в папке Pages:

  • Пространством имен компонента Counter является BlazorSample.Pages.
  • Полным именем компонента является BlazorSample.Pages.Counter.

При использовании пользовательских папок, содержащих компоненты, добавьте директиву @using в родительский компонент или в файл _Imports.razor приложения. В следующем примере становятся доступными компоненты в папке Components.

@using BlazorSample.Components

Примечание

Директивы @using в файле _Imports.razor применяются только к файлам Razor (.razor), но не к файлам C# (.cs).

На компоненты также можно ссылаться с помощью полных имен, для чего не требуется директива @using. В следующем примере папка Components приложения напрямую ссылается на компонент ProductDetail:

<BlazorSample.Components.ProductDetail />

Пространство имен компонента, созданного с помощью Razor, основано на следующем (в порядке приоритета).

  • Директива @namespace в разметке файла Razor (например, @namespace BlazorSample.CustomNamespace).
  • RootNamespace проекта в файле проекта (например, <RootNamespace>BlazorSample</RootNamespace>).
  • Имя проекта, полученное из имени файла проекта (.csproj), и путь из корневого каталога проекта к компоненту. Например, платформа разрешает {PROJECT ROOT}/Pages/Index.razor с помощью пространства имен проекта BlazorSample (BlazorSample.csproj) в пространство имен BlazorSample.Pages для компонента Index. {PROJECT ROOT} — корневой путь к проекту. Компоненты соответствуют правилам привязки имен C#. Для компонента Index в этом примере компонентами в области действия являются все компоненты:
    • в этой же папке Pages;
    • в корневой папке проекта, которая не задает другое пространство имен явным образом.

Следующие службы не поддерживаются:

  • Квалификация global::.
  • Импорт компонентов с инструкциями using, содержащими псевдонимы. Например, @using Foo = Bar не поддерживается.
  • Частичные имена. Например, нельзя добавить @using BlazorSample в компонент, а затем сослаться на компонент NavMenu в папке Shared приложения (Shared/NavMenu.razor) с помощью <Shared.NavMenu></Shared.NavMenu>.

Поддержка разделяемых классов

Компоненты формируются как разделяемые классы C#. Создать их можно одним из следующих способов:

  • Один файл содержит код C#, определенный в одном или нескольких блоках @code, разметке HTML и разметке Razor. Шаблоны проекта Blazor определяют свои компоненты с помощью этого однофайлового подхода.
  • HTML и разметка Razor помещаются в файл Razor (.razor). Код C# помещается в файл кода программной части, определенный как разделяемый класс (.cs).

Примечание

Таблица стилей компонента, определяющая характерные для компонента стили, — это отдельный файл (.css). Изоляция CSS Blazor описывается далее в статье Изоляция CSS в ASP.NET Core Blazor.

В следующем примере показан компонент Counter по умолчанию с блоком @code в приложении, созданном из шаблона проекта Blazor. Разметка и код C# находятся в одном файле. Это наиболее распространенный подход к разработке компонентов.

Pages/Counter.razor:

@page "/counter"

<h1>Counter</h1>

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

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

    private void IncrementCount()
    {
        currentCount++;
    }
}
@page "/counter"

<h1>Counter</h1>

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

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount = 0;

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

Следующий компонент Counter разделяет HTML и разметку Razor из кода C# с помощью файла кода программной части с разделяемым классом:

Pages/CounterPartialClass.razor:

@page "/counter-partial-class"

<h1>Counter</h1>

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

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

Pages/CounterPartialClass.razor.cs:

namespace BlazorSample.Pages
{
    public partial class CounterPartialClass
    {
        private int currentCount = 0;

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

Директивы @using в файле _Imports.razor применяются только к файлам Razor (.razor), но не к файлам C# (.cs). При необходимости добавьте в файл разделяемого класса пространства имен.

Типичные пространства имен, используемые компонентами:

using System.Net.Http;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.Web.Virtualization;
using Microsoft.JSInterop;
using System.Net.Http;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;

Типичные пространства имен также включают пространство имен приложения и пространство имен, соответствующее папке Shared приложения:

using BlazorSample;
using BlazorSample.Shared;

Указание базового класса

Директива @inherits используется для указания базового класса для компонента. В следующем примере показано, как компонент может наследовать базовый класс, чтобы предоставить свойства и методы компонента. Базовый класс BlazorRocksBase является производным от ComponentBase.

Pages/BlazorRocks.razor:

@page "/blazor-rocks"
@inherits BlazorRocksBase

<h1>@BlazorRocksText</h1>
@page "/blazor-rocks"
@inherits BlazorRocksBase

<h1>@BlazorRocksText</h1>

BlazorRocksBase.cs:

using Microsoft.AspNetCore.Components;

namespace BlazorSample
{
    public class BlazorRocksBase : ComponentBase
    {
        public string BlazorRocksText { get; set; } =
            "Blazor rocks the browser!";
    }
}
using Microsoft.AspNetCore.Components;

namespace BlazorSample
{
    public class BlazorRocksBase : ComponentBase
    {
        public string BlazorRocksText { get; set; } =
            "Blazor rocks the browser!";
    }
}

Параметры компонентов

Параметры компонентов передают данные компонентам и определяются с помощью открытых свойств C# в классе компонента с атрибутом [Parameter]. В следующем примере встроенный ссылочный тип (System.String) и определяемый пользователем ссылочный тип (PanelBody) передаются как параметры компонента.

PanelBody.cs:

public class PanelBody
{
    public string Text { get; set; }
    public string Style { get; set; }
}
public class PanelBody
{
    public string Text { get; set; }
    public string Style { get; set; }
}

Shared/ParameterChild.razor:

<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">@Title</div>
    <div class="card-body" style="font-style:@Body.Style">
        @Body.Text
    </div>
</div>

@code {
    [Parameter]
    public string Title { get; set; } = "Set By Child";

    [Parameter]
    public PanelBody Body { get; set; } =
        new()
        {
            Text = "Set by child.",
            Style = "normal"
        };
}
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">@Title</div>
    <div class="card-body" style="font-style:@Body.Style">
        @Body.Text
    </div>
</div>

@code {
    [Parameter]
    public string Title { get; set; } = "Set By Child";

    [Parameter]
    public PanelBody Body { get; set; } =
        new PanelBody()
        {
            Text = "Set by child.",
            Style = "normal"
        };
}

Предупреждение

Поддерживается указание начальных значений для параметров компонента, однако не следует создавать компонент, выполняющий запись в собственные параметры после первой отрисовки. Дополнительные сведения см. в разделе Перезаписанные параметры этой статьи.

Параметры Title и Body компонента ParameterChild задаются аргументами в HTML-теге, который отрисовывает экземпляр компонента. Следующий компонент ParameterParent отрисовывает два компонента ParameterChild:

  • Отрисовка первого компонента ParameterChild выполняется без указания аргументов параметров.
  • Второй компонент ParameterChild принимает значения для Title и Body от компонента ParameterParent, который использует явное выражение C# для задания значений свойств PanelBody.

Pages/ParameterParent.razor:

@page "/parameter-parent"

<h1>Child component (without attribute values)</h1>

<ParameterChild />

<h1>Child component (with attribute values)</h1>

<ParameterChild Title="Set by Parent"
                Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />
@page "/parameter-parent"

<h1>Child component (without attribute values)</h1>

<ParameterChild />

<h1>Child component (with attribute values)</h1>

<ParameterChild Title="Set by Parent"
                Body="@(new PanelBody() { Text = "Set by parent.", Style = "italic" })" />

Следующая отрисованная разметка HTML из компонента ParameterParent показывает значения компонента ParameterChild по умолчанию, если компонент ParameterParent не предоставляет значения параметров компонента. Если компонент ParameterParent предоставляет значения параметров компонента, они заменяют значения по умолчанию для компонента ParameterChild.

Примечание

Следует уточнить, что отрисованные классы стилей CSS не отображаются в следующей отрисованной разметке HTML.

<h1>Child component (without attribute values)</h1>

<div>
    <div>Set By Child</div>
    <div>Set by child.</div>
</div>

<h1>Child component (with attribute values)</h1>

<div>
    <div>Set by Parent</div>
    <div>Set by parent.</div>
</div>

Назначьте поле, свойство или результат метода C# параметру компонента как значение атрибута HTML с помощью зарезервированного символа @ Razor. Следующий компонент Title отображает четыре экземпляра предыдущего компонента ParameterParent2 и задает для их параметра ParameterChild следующие значения:

  • значение поля title;
  • результат метода C# GetTitle;
  • текущая локальная дата в длинном формате с параметром ToLongDateString, в котором используется неявное выражение C#;
  • свойство Title объекта panelData.

Pages/ParameterParent2.razor:

@page "/parameter-parent-2"

<ParameterChild Title="@title" />

<ParameterChild Title="@GetTitle()" />

<ParameterChild Title="@DateTime.Now.ToLongDateString()" />

<ParameterChild Title="@panelData.Title" />

@code {
    private string title = "From Parent field";
    private PanelData panelData = new();

    private string GetTitle()
    {
        return "From Parent method";
    }

    private class PanelData
    {
        public string Title { get; set; } = "From Parent object";
    }
}
@page "/parameter-parent-2"

<ParameterChild Title="@title" />

<ParameterChild Title="@GetTitle()" />

<ParameterChild Title="@DateTime.Now.ToLongDateString()" />

<ParameterChild Title="@panelData.Title" />

@code {
    private string title = "From Parent field";
    private PanelData panelData = new PanelData();

    private string GetTitle()
    {
        return "From Parent method";
    }

    private class PanelData
    {
        public string Title { get; set; } = "From Parent object";
    }
}

Примечание

При назначении элемента C# параметру компонента добавьте префикс к элементу с помощью символа @ и никогда не добавляйте префикс к атрибуту HTML параметра.

Верно:

<ParameterChild Title="@title" />

Неправильно:

<ParameterChild @Title="title" />

В отличие от Razor Pages (.cshtml), Blazor не может выполнять асинхронные операции в выражении Razor при рендеринге компонента. Это обусловлено тем, что Blazor предназначается для рендеринга интерактивных пользовательских интерфейсов. В интерактивном пользовательском интерфейсе на экране всегда должно что-то отображаться, поэтому нет смысла блокировать поток рендеринга. Вместо этого асинхронные операции выполняются во время одного из событий асинхронного жизненного цикла. После каждого события асинхронного жизненного цикла для компонента может выполняться повторный рендеринг. Следующий синтаксис Razor не поддерживается:

<ParameterChild Title="@await ..." />

Если приложение собрано, при выполнении кода из предыдущего примера происходит ошибка компилятора:

Оператор await можно использовать только в методах с модификатором async. Попробуйте пометить этот метод модификатором async и изменить тип его возвращаемого значения на Task.

Чтобы асинхронно получить значение для параметра Title в предыдущем примере, компонент может использовать событие жизненного цикла OnInitializedAsync, как показано в следующем примере:

<ParameterChild Title="@title" />

@code {
    private string title;
    
    protected override async Task OnInitializedAsync()
    {
        title = await ...;
    }
}

Для получения дополнительной информации см. Жизненный цикл компонента Razor ASP.NET Core.

Использование явного выражения Razor для сцепления текста с результатом выражения для назначения полученного значения параметру не поддерживается. Следующий пример направлен на сцепление текста Set by со значением свойства объекта. Страница Razor (.cshtml) поддерживает этот синтаксис, однако он недопустим для назначения параметру дочернего элемента Title в компоненте. Следующий синтаксис Razor не поддерживается:

<ParameterChild Title="Set by @(panelData.Title)" />

Если приложение собрано, при выполнении кода из предыдущего примера происходит ошибка компилятора:

Атрибуты компонента не поддерживают сложное содержимое (смешанный C# и разметка).

Чтобы можно было назначить составное значение, используйте метод, поле или свойство. В следующем примере Set by сцепляется со значением свойства объекта в методе C# GetTitle:

Pages/ParameterParent3.razor:

@page "/parameter-parent-3"

<ParameterChild Title="@GetTitle()" />

@code {
    private PanelData panelData = new();

    private string GetTitle() => $"Set by {panelData.Title}";

    private class PanelData
    {
        public string Title { get; set; } = "Parent";
    }
}
@page "/parameter-parent-3"

<ParameterChild Title="@GetTitle()" />

@code {
    private PanelData panelData = new PanelData();

    private string GetTitle() => $"Set by {panelData.Title}";

    private class PanelData
    {
        public string Title { get; set; } = "Parent";
    }
}

Для получения дополнительной информации см. Справочник по синтаксису Razor для ASP.NET Core.

Предупреждение

Поддерживается указание начальных значений для параметров компонента, однако не следует создавать компонент, выполняющий запись в собственные параметры после первой отрисовки. Дополнительные сведения см. в разделе Перезаписанные параметры этой статьи.

Параметры компонента должны объявляться как автосвойства. Это означает, что такие параметры не должны содержать настраиваемую логику в методах доступа get или set. Например, следующее свойство StartData является автосвойством:

[Parameter]
public DateTime StartData { get; set; }

Не размещайте настраиваемую логику в методе доступа get или set, так как параметры компонента можно использовать исключительно как канал для перемещения информации из родительского компонента в дочерний. Если метод доступа set свойства дочернего компонента содержит логику, которая вызывает повторную отрисовку родительского компонента, в итоге создается бесконечный цикл отрисовки.

Чтобы преобразовать полученное значение параметра, сделайте следующее:

  • Для представления предоставляемых необработанных данных оставьте для свойства параметра автосвойство.
  • Создайте другое свойство или метод, который предоставляет преобразованные данные на основе свойства параметра.

Переопределите OnParametersSetAsync, чтобы преобразовывать полученный параметр при каждом получении новых данных.

Запись начального значения в параметр компонента поддерживается, так как назначения начальных значений не мешают автоматической отрисовке компонентов Blazor. Следующее назначение текущего локального объекта DateTime с DateTime.Now свойству StartData является допустимым синтаксисом в компоненте:

[Parameter]
public DateTime StartData { get; set; } = DateTime.Now;

После первоначального назначения DateTime.Now не назначайте значение StartData в коде разработчика. Дополнительные сведения см. в разделе Перезаписанные параметры этой статьи.

Параметры маршрута

Компоненты могут задавать параметры маршрута в шаблоне маршрута директивы @page. Маршрутизатор Blazor использует параметры маршрута для заполнения соответствующих параметров компонента.

Поддерживаются необязательные параметры маршрута. В следующем примере необязательный параметр text назначает значение сегмента маршрута свойству Text компонента. Если сегмента нет, для Text устанавливается значение fantastic в OnInitializedметоде жизненного цикла .

Pages/RouteParameter.razor:

@page "/route-parameter/{text?}"

<h1>Blazor is @Text!</h1>

@code {
    [Parameter]
    public string Text { get; set; }

    protected override void OnInitialized()
    {
        Text = Text ?? "fantastic";
    }
}

Pages/RouteParameter.razor:

@page "/route-parameter"
@page "/route-parameter/{text}"

<h1>Blazor is @Text!</h1>

@code {
    [Parameter]
    public string Text { get; set; }

    protected override void OnInitialized()
    {
        Text = Text ?? "fantastic";
    }
}

Необязательные параметры маршрута не поддерживаются, поэтому в предыдущем примере применяются две директивы @page. Первая директива @page позволяет переходить к компоненту без параметра маршрута. Вторая директива @page принимает параметр маршрута {text} и присваивает значение свойству Text.

Сведения об универсальных параметрах маршрута ({*pageRoute}), которые захватывают пути в нескольких папках, см. в разделе Маршрутизация ASP.NET Core Blazor.

Перезаписанные параметры

Платформа Blazor обычно позволяет безопасно назначить параметр с родительского компонента на дочерний:

  • Параметры не перезаписываются неожиданно.
  • Побочные эффекты сводятся к минимуму. Например, дополнительные отрисовки избегаются, так как они могут создавать бесконечные циклы отрисовки.

Дочерний компонент получает новые значения параметров, которые могут перезаписать существующие значения при повторной отрисовке родительского компонента. Случайно перезаписанные значения параметров в дочернем компоненте часто возникают при разработке компонента с одним или несколькими параметрами, привязанными к данным, а также если разработчик выполняет запись данных непосредственно в параметр в дочернем элементе:

  • Дочерний компонент отрисовывается с одним или несколькими значениями параметров из родительского компонента.
  • Дочерний компонент выполняет запись непосредственно в значение параметра.
  • Родительский компонент отрисовывается повторно и перезаписывает значение параметра дочернего элемента.

Возможность перезаписи значений параметров также распространяется и на методы доступа свойства set дочернего компонента.

Важно!

Мы не рекомендуем создавать компоненты, которые напрямую выполняют запись в собственные параметры после первой отрисовки.

Рассмотрим следующий компонент Expander с ошибкой, который

  • преобразует дочернее содержимое;
  • переключает отображение дочернего содержимого с помощью параметра компонента (Expanded).
  • Компонент выполняет запись непосредственно в параметр Expanded, который демонстрирует проблему с перезаписанными параметрами. Его следует избегать.

После демонстрации неправильного подхода к этому сценарию с помощью следующего компонента Expander демонстрируется правильный подход с помощью компонента Expander. Следующие примеры можно разместить в локальном примере приложения для ознакомления с описанным выше поведением.

Shared/Expander.razor:

<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">
    <div class="card-body">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @Expanded)</h2>

        @if (Expanded)
        {
            <p class="card-text">@ChildContent</p>
        }
    </div>
</div>

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

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

    private void Toggle()
    {
        Expanded = !Expanded;
    }
}
<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">
    <div class="card-body">
        <h2 class="card-title">Toggle (<code>Expanded</code> = @Expanded)</h2>

        @if (Expanded)
        {
            <p class="card-text">@ChildContent</p>
        }
    </div>
</div>

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

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

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

Компонент Expander добавляется в следующий родительский компонент ExpanderExample, который может вызывать StateHasChanged:

Pages/ExpanderExample.razor:

@page "/expander-example"

<Expander Expanded="true">
    Expander 1 content
</Expander>

<Expander Expanded="true" />

<button @onclick="StateHasChanged">
    Call StateHasChanged
</button>
@page "/expander-example"

<Expander Expanded="true">
    Expander 1 content
</Expander>

<Expander Expanded="true" />

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

Изначально компоненты Expander работают независимо, когда их свойства Expanded переключаются. Дочерние компоненты сохраняют свои состояния, как и ожидалось. Когда в родительском элементе вызывается StateHasChanged, параметр Expanded первого дочернего компонента сбрасывается обратно к первоначальному значению (true). Значение Expanded второго компонента Expander не сбрасывается, так как во втором компоненте не отображается дочернее содержимое.

Чтобы сохранить состояние в предыдущем сценарии, используйте закрытое поле в компоненте Expander, чтобы сохранить состояние переключения.

Следующий измененный компонент Expander:

  • Принимает значение параметра компонента Expanded из родительского элемента.
  • Присваивает значение параметра компонента закрытому полю (expanded) в событии OnInitialized.
  • Использует закрытое поле для поддержания внутреннего состояния переключения, в котором описывается, как избегать непосредственной записи в параметре.

Примечание

Рекомендации в этом разделе распространяются на аналогичную логику методов доступа set параметров компонентов, что может приводить к аналогичным нежелательным побочным эффектам.

Shared/Expander.razor:

<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">
    <div class="card-body">
        <h2 class="card-title">Toggle (<code>expanded</code> = @expanded)</h2>

        @if (expanded)
        {
            <p class="card-text">@ChildContent</p>
        }
    </div>
</div>

@code {
    private bool expanded;

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

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

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

    private void Toggle()
    {
        expanded = !expanded;
    }
}
<div @onclick="Toggle" class="card bg-light mb-3" style="width:30rem">
    <div class="card-body">
        <h2 class="card-title">Toggle (<code>expanded</code> = @expanded)</h2>

        @if (expanded)
        {
            <p class="card-text">@ChildContent</p>
        }
    </div>
</div>

@code {
    private bool expanded;

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

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

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

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

Дополнительные сведения см. в статье Ошибка двусторонней привязки Blazor (dotnet/aspnetcore #24599).

Дочернее содержимое

Компоненты могут задавать содержимое другого компонента. Назначающий компонент предоставляет содержимое между открывающим и закрывающим тегами дочернего компонента.

В следующем примере компонент RenderFragmentChild имеет свойство ChildContent, представляющее сегмент пользовательского интерфейса для отрисовки в качестве RenderFragment. Расположение ChildContent в разметке Razor компонента совпадает с местом отрисовки содержимого в окончательном выводе HTML.

Shared/RenderFragmentChild.razor:

<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Child content</div>
    <div class="card-body">@ChildContent</div>
</div>

@code {
    [Parameter]
    public RenderFragment ChildContent { get; set; }
}
<div class="card w-25" style="margin-bottom:15px">
    <div class="card-header font-weight-bold">Child content</div>
    <div class="card-body">@ChildContent</div>
</div>

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

Важно!

Свойству, принимающему содержимое RenderFragment, по соглашению необходимо присвоить имя ChildContent.

Следующий компонент RenderFragmentParent предоставляет содержимое для отрисовки RenderFragmentChild путем размещения содержимого между открывающим и закрывающим тегами дочернего компонента.

Pages/RenderFragmentParent.razor:

@page "/render-fragment-parent"

<h1>Render child content</h1>

<RenderFragmentChild>
    Content of the child component is supplied
    by the parent component.
</RenderFragmentChild>
@page "/render-fragment-parent"

<h1>Render child content</h1>

<RenderFragmentChild>
    Content of the child component is supplied
    by the parent component.
</RenderFragmentChild>

В связи с тем, как Blazor выполняет отрисовку дочернего содержимого, для отрисовки компонентов в цикле for требуется задать локальную переменную индекса, если в содержимом дочернего компонента RenderFragmentChild используется переменная цикла приращения: Следующий пример можно добавить к предыдущему компоненту RenderFragmentParent:

<h1>Three children with an index variable</h1>

@for (int c = 0; c < 3; c++)
{
    var current = c;

    <RenderFragmentChild>
        Count: @current
    </RenderFragmentChild>
}

Кроме того, можно использовать цикл foreach с Enumerable.Range вместо цикла for. Следующий пример можно добавить к предыдущему компоненту RenderFragmentParent:

<h1>Second example of three children with an index variable</h1>

@foreach (var c in Enumerable.Range(0,3))
{
    <RenderFragmentChild>
        Count: @c
    </RenderFragmentChild>
}

Сведения о том, как использовать RenderFragment в качестве шаблона для пользовательского интерфейса компонента, см. в следующих статьях:

Сплаттинг атрибутов и произвольные параметры

Компоненты могут записывать и визуализировать дополнительные атрибуты в дополнение к объявленным параметрам компонента. Можно записать дополнительные атрибуты в словарь, а затем выполнить сплаттинг для элемента при отрисовке компонента с помощью атрибута директивы @attributes Razor. Этот сценарий полезен при определении компонента, который создает элемент разметки, поддерживающий разнообразные настройки. Например, может оказаться утомительным по отдельности определять атрибуты для <input>, поддерживающего много параметров.

Следующий компонент Splat:

  • Первый элемент <input> (id="useIndividualParams") использует индивидуальные параметры компонентов.
  • Второй элемент <input> (id="useAttributesDict") использует сплаттинг атрибутов.

Pages/Splat.razor:

@page "/splat"

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
@page "/splat"

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new Dictionary<string, object>()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}

Отрисованные элементы <input> на веб-странице идентичны:

<input id="useIndividualParams"
       maxlength="10"
       placeholder="Input placeholder text"
       required="required"
       size="50">

<input id="useAttributesDict"
       maxlength="10"
       placeholder="Input placeholder text"
       required="required"
       size="50">

Чтобы принять произвольные атрибуты, определите параметр компонента с помощью свойства CaptureUnmatchedValues, имеющего значение true:

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object> InputAttributes { get; set; }
}

Свойство CaptureUnmatchedValues в [Parameter] позволяет параметру соответствовать всем атрибутам, которые не соответствуют никакому другому параметру. Компонент может определять только один параметр с CaptureUnmatchedValues. Тип свойства, используемый с CaptureUnmatchedValues, должен быть назначаемым из Dictionary<string, object> с ключами строки. В этом сценарии также можно использовать IEnumerable<KeyValuePair<string, object>> или IReadOnlyDictionary<string, object>.

Расположение @attributes относительно положения атрибутов элемента имеет значение. Когда выполняется сплаттинг @attributes для элемента, атрибуты обрабатываются справа налево (от последнего к первому). Рассмотрим следующий пример родительского компонента, использующего дочерний компонент:

Shared/AttributeOrderChild1.razor:

<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}

Pages/AttributeOrderParent1.razor:

@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />
@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />

Атрибут extra компонента AttributeOrderChild1 стоит справа от @attributes. <div>, визуализируемый компонентом AttributeOrderParent1, содержит extra="5" при передаче через дополнительный атрибут, так как атрибуты обрабатываются справа налево (от последнего к первому):

<div extra="5" />

В следующем примере порядок extra и @attributes в <div> дочернего компонента изменен на противоположный:

Shared/AttributeOrderChild2.razor:

<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}

Pages/AttributeOrderParent2.razor:

@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />
@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />

<div> на отрисованной веб-странице родительского компонента содержит extra="10" при передаче через дополнительный атрибут:

<div extra="10" />

Запись ссылок на компоненты

Ссылки на компоненты предоставляют способ ссылаться на экземпляр компонента, чтобы можно было выполнять команды. Чтобы записать ссылку на компонент, сделайте следующее:

  • Добавьте к дочернему компоненту атрибут @ref.
  • Определите поле с тем же типом, что и у дочернего компонента.

При отрисовке компонента поле заполняется экземпляром компонента. Затем можно вызывать методы .NET в экземпляре.

Рассмотрим следующий компонент ReferenceChild, который регистрирует сообщение при вызове метода ChildMethod.

Shared/ReferenceChild.razor:

@using Microsoft.Extensions.Logging
@inject ILogger<ReferenceChild> logger

@code {
    public void ChildMethod(int value)
    {
        logger.LogInformation("Received {Value} in ChildMethod", value);
    }
}
@using Microsoft.Extensions.Logging
@inject ILogger<ReferenceChild> logger

@code {
    public void ChildMethod(int value)
    {
        logger.LogInformation("Received {Value} in ChildMethod", value);
    }
}

Ссылка на компонент заполняется только после отрисовки компонента, а ее выходные данные включают элемент ReferenceChild. Пока компонент не будет преобразован для просмотра, ссылка на него не используется.

Для управления ссылками на компоненты после завершения отрисовки компонента используйте методы OnAfterRender или OnAfterRenderAsync.

Чтобы использовать ссылочную переменную с обработчиком событий, используйте лямбда-выражение или назначьте делегат обработчика событий в методах OnAfterRender или OnAfterRenderAsync. Это гарантирует, что ссылочная переменная будет назначена до назначения обработчика событий.

В следующем лямбда-подходе используется предыдущий компонент ReferenceChild.

Pages/ReferenceParent1.razor:

@page "/reference-parent-1"

<button @onclick="@(() => childComponent.ChildMethod(5))">
    Call <code>ReferenceChild.ChildMethod</code> with an argument of 5
</button>

<ReferenceChild @ref="childComponent" />

@code {
    private ReferenceChild childComponent;
}
@page "/reference-parent-1"

<button @onclick="@(() => childComponent.ChildMethod(5))">
    Call <code>ReferenceChild.ChildMethod</code> with an argument of 5
</button>

<ReferenceChild @ref="childComponent" />

@code {
    private ReferenceChild childComponent;
}

В следующем подходе делегата используется предыдущий компонент ReferenceChild.

Pages/ReferenceParent2.razor:

@page "/reference-parent-2"

<button @onclick="callChildMethod">
    Call <code>ReferenceChild.ChildMethod</code> with an argument of 5
</button>

<ReferenceChild @ref="childComponent" />

@code {
    private ReferenceChild childComponent;
    private Action callChildMethod;

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            callChildMethod = CallChildMethod;
        }
    }

    private void CallChildMethod()
    {
        childComponent.ChildMethod(5);
    }
}
@page "/reference-parent-2"

<button @onclick="callChildMethod">
    Call <code>ReferenceChild.ChildMethod</code> with an argument of 5
</button>

<ReferenceChild @ref="childComponent" />

@code {
    private ReferenceChild childComponent;
    private Action callChildMethod;

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            callChildMethod = CallChildMethod;
        }
    }

    private void CallChildMethod()
    {
        childComponent.ChildMethod(5);
    }
}

Используйте коллекцию, чтобы ссылаться на компоненты в цикле. В следующем примере:

  • Компоненты добавляются в List<T>.
  • Для каждого компонента, который активирует соответствующий компонент ChildMethod по индексу компонента в List<T>, создается кнопка.

Pages/ReferenceParent3.razor использует предыдущий компонент ReferenceChild:

@page "/reference-parent-3"

<ul>
    @for (int i = 0; i < 5; i++)
    {
        var index = i;
        var v = r.Next(1000);

        <li>
            <ReferenceChild @ref="childComponent" />
            <button @onclick="@(() => callChildMethod(index, v))">
                Component index @index: Call <code>ReferenceChild.ChildMethod(@v)</code>
            </button>
        </li>
    }
</ul>

@code {
    private Random r = new();
    private List<ReferenceChild> components = new();
    private Action<int, int> callChildMethod;

    private ReferenceChild childComponent
    {
        set => components.Add(value);
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            callChildMethod = CallChildMethod;
        }
    }

    private void CallChildMethod(int index, int value)
    {
        components.ElementAt(index).ChildMethod(value);
    }
}
@page "/reference-parent-3"

<ul>
    @for (int i = 0; i < 5; i++)
    {
        var index = i;
        var v = r.Next(1000);

        <li>
            <ReferenceChild @ref="childComponent" />
            <button @onclick="@(() => callChildMethod(index, v))">
                Component index @index: Call <code>ReferenceChild.ChildMethod(@v)</code>
            </button>
        </li>
    }
</ul>

@code {
    private Random r = new Random();
    private List<ReferenceChild> components = new List<ReferenceChild>();
    private Action<int, int> callChildMethod;

    private ReferenceChild childComponent
    {
        set => components.Add(value);
    }

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            callChildMethod = CallChildMethod;
        }
    }

    private void CallChildMethod(int index, int value)
    {
        components.ElementAt(index).ChildMethod(value);
    }
}

При записи ссылок на компоненты используется синтаксис, аналогичный синтаксису записи ссылок на элементы. Запись ссылок на компоненты не является функцией взаимодействия JavaScript. Ссылки на компоненты не передаются в код JavaScript. Они используются только в коде .NET.

Важно!

Не используйте ссылки на компоненты для изменения состояния дочерних компонентов. Вместо этого используйте обычные декларативные параметры компонентов для передачи данных дочерним компонентам. Использование обычных параметров компонентов приводит к тому, что дочерние компоненты автоматически визуализируются в нужное время. Дополнительные сведения см. в разделе Параметры компонентов и статье Привязка к данным в ASP.NET Core Blazor.

Контекст синхронизации

Blazor использует контекст синхронизации (SynchronizationContext) для принудительного использования одного логического потока выполнения. Методы жизненного цикла компонента и обратные вызовы событий, сделанные Blazor, выполняются в этом контексте синхронизации.

Контекст синхронизации Blazor Server пытается эмулировать однопоточную среду таким образом, чтобы она точно соответствовала модели WebAssembly в браузере, которая является однопоточной. В любой момент времени работа выполняется только в одном потоке, что создает впечатление единого логического потока. Две операции не могут выполняться одновременно.

Избегайте блокирующих вызовов

Как правило, не следует вызывать следующие методы в компонентах. Следующие методы блокируют поток выполнения и, таким образом, блокируют возобновление работы приложения до тех пор, пока не завершится базовый Task:

Примечание

Примеры документации Blazor, в которых используются методы блокировки потоков, упомянутые в этом разделе, используют эти методы исключительно в целях демонстрации, а не в качестве рекомендаций по написанию кода. Например, несколько демонстраций кода компонента моделируют продолжительный процесс путем вызова метода Thread.Sleep.

Внешний вызов методов компонента для изменения состояния

Если компонент нужно изменить на основе внешнего события, такого как таймер или другое уведомление, используйте метод InvokeAsync, который выполняет отправку выполнения кода в контекст синхронизации Blazor. Например, рассмотрим следующую службу уведомителя, которая может уведомлять любой компонент, ожидающий передачи данных, об измененном состоянии. Метод Update можно вызывать из любого места в приложении.

Notifier.cs:

using System;
using System.Threading.Tasks;

public class Notifier
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}
using System;
using System.Threading.Tasks;

public class Notifier
{
    public async Task Update(string key, int value)
    {
        if (Notify != null)
        {
            await Notify.Invoke(key, value);
        }
    }

    public event Func<string, int, Task> Notify;
}

Зарегистрируйте службу Notifier:

  • В приложении Blazor WebAssembly зарегистрируйте службу как отдельную (singleton) в Program.Main:

    builder.Services.AddSingleton<Notifier>();
    
  • В приложении Blazor Server зарегистрируйте службу с заданной областью (scoped) в Startup.ConfigureServices:

    services.AddScoped<Notifier>();
    

Используйте службу Notifier для обновления компонента.

Pages/NotifierExample.razor:

@page "/notifier-example"
@inject Notifier Notifier
@implements IDisposable

<p>Last update: @lastNotification.key = @lastNotification.value</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}
@page "/notifier-example"
@inject Notifier Notifier
@implements IDisposable

<p>Last update: @lastNotification.key = @lastNotification.value</p>

@code {
    private (string key, int value) lastNotification;

    protected override void OnInitialized()
    {
        Notifier.Notify += OnNotify;
    }

    public async Task OnNotify(string key, int value)
    {
        await InvokeAsync(() =>
        {
            lastNotification = (key, value);
            StateHasChanged();
        });
    }

    public void Dispose()
    {
        Notifier.Notify -= OnNotify;
    }
}

В предшествующем примере:

  • Notifier вызывает метод OnNotify компонента вне контекста синхронизации Blazor. InvokeAsync используется для переключения на подходящий контекст и постановки отрисовки в очередь. Для получения дополнительной информации см. Отрисовка компонента Blazor ASP.NET Core.
  • Компонент реализует IDisposable. Для делегата OnNotify отменяется подписка в методе Dispose, который вызывается платформой при удалении компонента. Для получения дополнительной информации см. Жизненный цикл компонента Razor ASP.NET Core.

Использование @key для управления сохранением элементов и компонентов

При отрисовке списка элементов или компонентов и последующем изменении элементов или компонентов Blazor должен решить, какие из предыдущих элементов или компонентов можно оставить и как следует сопоставить с ними объекты модели. Обычно этот процесс выполняется автоматически, и его можно игнорировать, но в некоторых случаях может потребоваться управлять данным процессом.

Рассмотрим следующие компоненты Details иPeople:

  • Компонент Details получает данные (Data) из родительского компонента People, который отображается в элементе <input>. Пользователь может установить фокус на любой заданный отображаемый элемент <input> страницы при выборе одного из элементов <input>.
  • Компонент People создает список объектов Person для отображения с помощью компонента Details. Каждые три секунды в коллекцию добавляется новый пользователь.

В этой демонстрации можно выполнить следующие действия:

  • Выбрать <input> из нескольких отрисованных компонентов Details.
  • Изучить поведение фокуса страницы по мере автоматического роста коллекции пользователей.

Shared/Details.razor:

<input value="@Data" />

@code {
    [Parameter]
    public string Data { get; set; }
}
<input value="@Data" />

@code {
    [Parameter]
    public string Data { get; set; }
}

В следующем компоненте People каждая итерация добавления пользователя в OnTimerCallback заставляет Blazor перестраивать всю коллекцию. Фокус страницы остается на одном и том же положении указателя элементов <input>, поэтому при каждом добавлении пользователя фокус сдвигается. Сдвиг фокуса с выбранного пользователем элемента нежелателен. После демонстрации нежелательного поведения с помощью следующего компонента атрибут директивы @key используется для повышения удобства работы пользователя.

Pages/People.razor:

@page "/people"
@using System.Timers
@implements IDisposable

<ol>
    @foreach (var person in people)
    {
        <li>
            <Details Data="@person.Data" />
        </li>
    }
</ol>

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();

    public class Person
    {
        public string Data { get; set; }
    }
}
@page "/people"
@using System.Timers
@implements IDisposable

<ol>
    @foreach (var person in people)
    {
        <li>
            <Details Data="@person.Data" />
        </li>
    }
</ol>

@code {
    private Timer timer = new Timer(3000);

    public List<Person> people =
        new List<Person>()
        {
            { new Person { Data = "Person 1" } },
            { new Person { Data = "Person 2" } },
            { new Person { Data = "Person 3" } }
        };

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            people.Insert(0,
                new Person
                {
                    Data = $"INSERTED {DateTime.Now.ToString("hh:mm:ss tt")}"
                });
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();

    public class Person
    {
        public string Data { get; set; }
    }
}

Содержимое коллекции people изменяется при вставке, удалении или повторном упорядочении записей. Повторная отрисовка может привести к появлению видимых различий в поведении. При каждой вставке пользователя в коллекцию people фокус устанавливается на элемент, предшествующий элементу, который находится в фокусе в текущий момент. Фокус пользователя теряется.

Процесс сопоставления элементов или компонентов с коллекцией можно контролировать с помощью атрибута @key директивы. Использование @key гарантирует сохранение элементов или компонентов на основе значения ключа. Если фокус компонента Details в предыдущем примере установлен на элемент person, Blazor игнорирует повторную отрисовку компонентов Details, которые не изменились.

Чтобы изменить компонент People для использования атрибута @key директивы с коллекцией people, обновите элемент <Details> следующим образом:

<Details @key="person" Data="@person.Data" />

При изменении коллекции people связь между экземплярами Details и экземплярами person сохраняется. При вставке Person в начало коллекции один новый экземпляр Details вставляется на соответствующую позицию. Другие экземпляры остаются без изменений. Таким образом, по мере добавления пользователей в коллекцию установленный пользователем фокус не теряется.

Другие обновления коллекции при использовании атрибута @key директивы ведут себя точно так же:

  • Если экземпляр удаляется из коллекции, то из пользовательского интерфейса удаляется только соответствующий экземпляр компонента. Другие экземпляры остаются без изменений.
  • При переупорядочении записей коллекции соответствующие экземпляры компонентов сохраняются и переупорядочиваются в пользовательском интерфейсе.

Важно!

Ключи являются локальными для каждого компонента или элемента контейнера. Ключи не сравниваются глобально по всему документу.

Условия для использования @key

Как правило, @key имеет смысл использовать при отрисовке списка (например, в блоке foreach) и при наличии подходящего значения для определения @key.

Если объект не изменяется, @key можно также использовать для сохранения поддерева элемента или компонента, как показано в следующих примерах.

Пример 1:

<li @key="person">
    <input value="@person.Data" />
</li>

Пример 2.

<div @key="person">
    @* other HTML elements *@
</div>

При изменении экземпляра person атрибут @key директивы заставляет Blazor выполнить следующие действия:

  • Полностью отменить <li> или <div>, а также их потомков.
  • Перестроить поддерево в пользовательском интерфейсе с помощью новых элементов и компонентов.

Это позволяет гарантировать сохранение состояния пользовательского интерфейса при изменении коллекции в поддереве.

Условия для отказа от использования @key

Отрисовка с использованием @key подразумевает определенное снижение производительности. Это снижение производительности незначительно, но указывать @key следует только в том случае, если сохранение элементов или компонентов выгодно для приложения.

Даже если @key не используется, Blazor сохраняет экземпляры дочерних элементов и компонентов в максимально возможной степени. Единственным преимуществом использования @key является контроль над тем, как экземпляры модели сопоставляются с сохраненными экземплярами компонентов, вместо выбора сопоставления с помощью Blazor.

Значения, которые следует использовать для @key

Как правило, для @key имеет смысл указать одно из следующих значений:

  • Экземпляры объектов моделей. Например, в предыдущем примере использовался экземпляр Person (person). Это гарантирует сохранение на основе равенства ссылок на объекты.
  • Уникальные идентификаторы. Например, уникальные идентификаторы могут основываться на значениях первичного ключа типа int, string или Guid.

Убедитесь, что значения, используемые для @key, не конфликтуют. Если в одном родительском элементе обнаруживаются конфликтующие значения, Blazor выдает исключение, поскольку не может детерминированно сопоставлять старые элементы или компоненты с новыми. Используйте только уникальные значения, такие как экземпляры объекта или значения первичного ключа.

Применение атрибута

Атрибуты можно применять к компонентам с помощью директивы @attribute. В следующем примере атрибут [Authorize] применяется к классу компонента:

@page "/"
@attribute [Authorize]

Условные атрибуты элемента HTML

Свойства атрибута элемента HTML условно задаются в зависимости от значения .NET. Если значение равно false или null, свойство не задано. Если значение равно true, свойство задано.

В следующем примере IsCompleted определяет, задано ли свойство checked элемента <input>.

Pages/ConditionalAttribute.razor:

@page "/conditional-attribute"

<label>
    <input type="checkbox" checked="@IsCompleted" />
    Is Completed?
</label>

<button @onclick="@(() => IsCompleted = !IsCompleted)">
    Change IsCompleted
</button>

@code {
    [Parameter]
    public bool IsCompleted { get; set; }
}
@page "/conditional-attribute"

<label>
    <input type="checkbox" checked="@IsCompleted" />
    Is Completed?
</label>

<button @onclick="@(() => IsCompleted = !IsCompleted)">
    Change IsCompleted
</button>

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

Для получения дополнительной информации см. Справочник по синтаксису Razor для ASP.NET Core.

Предупреждение

Некоторые атрибуты HTML, такие как aria-pressed, работают неправильно, если типом .NET является bool. В этих случаях используйте тип string вместо bool.

Необработанный HTML-код

Строки обычно визуализируются с помощью текстовых узлов модели DOM, что означает, что любая разметка, которую они могут содержать, игнорируется и обрабатывается как текстовый литерал. Для отрисовки необработанного HTML-кода заключите HTML-содержимое в значение MarkupString. Это значение анализируется как HTML или SVG и вставляется в модель DOM.

Предупреждение

Отрисовка необработанного HTML-кода, созданного из любого ненадежного источника, является угрозой безопасности, и ее всегда следует избегать.

В следующем примере показано использование типа MarkupString для добавления блока статического HTML-содержимого в визуализируемые выходные данные компонента.

Pages/MarkupStringExample.razor:

@page "/markup-string-example"

@((MarkupString)myMarkup)

@code {
    private string myMarkup =
        "<p class=\"text-danger\">This is a dangerous <em>markup string</em>.</p>";
}
@page "/markup-string-example"

@((MarkupString)myMarkup)

@code {
    private string myMarkup =
        "<p class=\"text-danger\">This is a dangerous <em>markup string</em>.</p>";
}

Шаблоны Razor

Фрагменты отрисовки можно определить с помощью синтаксиса шаблона Razor для определения фрагмента пользовательского интерфейса. Шаблоны Razor используют следующий формат:

@<{HTML tag}>...</{HTML tag}>

В следующем примере показано, как указать значения RenderFragment и RenderFragment<TValue> и визуализировать шаблоны непосредственно в компоненте. Фрагменты отрисовки также могут передаваться в качестве аргументов в шаблонные компоненты.

Pages/RazorTemplate.razor:

@page "/razor-template"

@timeTemplate

@petTemplate(new Pet { Name = "Nutty Rex" })

@code {
    private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.</p>;
    private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet: @pet.Name</p>;

    private class Pet
    {
        public string Name { get; set; }
    }
}
@page "/razor-template"

@timeTemplate

@petTemplate(new Pet { Name = "Nutty Rex" })

@code {
    private RenderFragment timeTemplate = @<p>The time is @DateTime.Now.</p>;
    private RenderFragment<Pet> petTemplate = (pet) => @<p>Pet: @pet.Name</p>;

    private class Pet
    {
        public string Name { get; set; }
    }
}

Визуализированные выходные данные предыдущего кода:

<p>The time is 4/19/2021 8:54:46 AM.</p>
<p>Pet: Nutty Rex</p>

Статические ресурсы.

Blazor следует соглашению приложений ASP.NET Core для статических ресурсов. Статические ресурсы находятся в папке web root (wwwroot) проекта или папках в папке wwwroot.

Используйте базовый относительный путь (/) для ссылки на корневой веб-каталог статического ресурса. В следующем примере logo.png физически находится в папке {PROJECT ROOT}/wwwroot/images. {PROJECT ROOT} — корень проекта приложения.

<img alt="Company logo" src="/images/logo.png" />

Компоненты не поддерживают нотацию тильды с косой чертой (~/).

Сведения о настройке базового пути приложения см. в разделе Размещение и развертывание ASP.NET Core Blazor.

Вспомогательные функции тегов в компонентах не поддерживаются

Tag Helpers не поддерживаются в компонентах. Чтобы обеспечить функциональные возможности, аналогичные вспомогательным функциям тегов, в Blazor, создайте компонент с теми же функциональными возможностями, что и вспомогательная функция тега, и используйте его вместо нее.

Изображения SVG

Так как Blazor выполняет рендеринг HTML-кода, поддерживаемые браузером изображения, включая изображения SVG (.svg), поддерживаются при использовании тега <img>.

<img alt="Example image" src="image.svg" />

Аналогичным образом изображения SVG поддерживаются в правилах CSS файла таблицы стилей (.css).

.element-class {
    background-image: url("image.svg");
}

Однако встроенная разметка SVG не поддерживается во всех сценариях. Если поместить тег <svg> непосредственно в файл Razor (.razor), базовая отрисовка изображений будет доступной, но многие расширенные сценарии пока не поддерживаются. Например, теги <use> сейчас не учитываются, а с некоторыми тегами SVG невозможно использовать @bind. Дополнительные сведения см. в справке по SVG в Blazor (dotnet/aspnetcore#18271).

Поведение при отрисовке пробелов

Если директива @preservewhitespace не используется со значением true, лишние пробелы будут удаляться по умолчанию, если:

  • Они находятся в начале или конце элемента.
  • Они находятся в начале или конце параметра RenderFragment/RenderFragment<TValue> (например, дочернее содержимое, передаваемое другому компоненту).
  • Они находятся в начале или конце блока кода C#, например @if и @foreach.

При использовании правила CSS, такого как white-space: pre, удаление пробелов может повлиять на отображаемые выходные данные. Чтобы отключить эту оптимизацию производительности и сохранить пробелы, выполните одно из следующих действий.

  • Добавьте директиву @preservewhitespace true в начало Razor-файла (.razor), чтобы применить предпочтение к конкретному компоненту.
  • Добавьте директиву @preservewhitespace true внутрь файла _Imports.razor, чтобы применить предпочтение к подкаталогу или проекту целиком.

В большинстве случаев никаких действий не требуется, так как приложения, как правило, продолжают работать в обычном режиме (но быстрее). Если удаление пробелов приводит к возникновению проблемы отрисовки в конкретном компоненте, используйте @preservewhitespace true в таком компоненте, чтобы отключить эту оптимизацию.

Пробел сохраняется в исходной разметке компонента. Текст, состоящий только из пробелов, отображается в модели DOM браузера даже при отсутствии визуального эффекта.

Рассмотрим следующую разметку компонента:

<ul>
    @foreach (var item in Items)
    {
        <li>
            @item.Text
        </li>
    }
</ul>

В предыдущем примере отображается следующий ненужный пробел:

  • за пределами блока кода @foreach;
  • вокруг элемента <li>;
  • вокруг выходных данных @item.Text.

Список из 100 элементов приводит к более чем 400 областям пробелов. Ни один из лишних пробелов визуально не влияет на отрисованные выходные данные.

При отображении статического кода HTML для компонентов пробелы внутри тега не сохраняются. Например, просмотрите отрисованные выходные данные следующего тега <img> в Razor-файле компонента (.razor):

<img     alt="Example image"   src="img.png"     />

Пробелы из предыдущей разметки не сохраняются:

<img alt="Example image" src="img.png" />