Привет всем! Всем тем, кто хочет узнать о Blazor немного больше. Сегодня мы продолжим создание нашего сайта для пиццерии, а именно, создадим web api контроллер и попробуем отобразить данные которые поступают из него на компоненте Blazor.

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

Назовём его BasePizza и добавим в проект BlazingPizza.DomainModels. На мой взгляд добавление нового класса очень круто реализовано в Rider, выскакивает неблокирующий диалог, вводим имя класса и тут же можем выбрать что именно нам нужно создать:



После этого появится диалог с запросом на добавление файла в git, ответим утвердительно.

Содержимое класса:

public class BasePizza
{
  public int Id { get; set; }
    
    public string Name { get; set; }
    
    public decimal BasePrice { get; set; }
    
    public string Description { get; set; }
    
    public string ImageUrl { get; set; }
}

Он представляет из себя шаблон для какого-то типа пиццы, позже его можно будет сконфигурировать как нам удобно, изменить размер, добавить топинги и прочее. Название полей мне кажется говорит само за себя.

В проекте BlazingPizza.DomainPizza у нас будут расположены классы представляющие бизнес-домен нашего приложения. То есть им не должно и не будет известно ничего о том как хранятся наши данные ни о том как они отображаются. Только информация о бизнес объекте, то есть пицце.

Далее нам нужно что-бы эти данные каким-то образом попали на клиент. Для этого идём в проект BlazingPizza.Server и в папку Controllers добавляем PizzasController:

public class PizzasController : Controller
{
    // GET
    public IActionResult Index()
    {
        return View();
    }
}

Нам нужен метод который отдает нам список всех основ для пиццы.

Помимо добавления метода нужно сделать несколько несложных действий:

  1. Пометим контроллер атрибутом [ApiController] который дает некоторые преимущества, в частности автоматический возврат 400 кода если модель не прошла валидацию, без него — это обычный MVC контроллер, отдающий View.
  2. Добавим атрибут [Route(«pizzas»)]. Мы используем так называемый Attribute Routing, благодаря чему пути настраиваются декларативно с помощью атрибутов, второй вариант это так называемый Conventional Routing, основанный на определённых соглашениях. Что значит “pizzas” в нашем пути? Это значит что все запросы по пути http{s}://hostName/pizzas/{ещеЧтоТо}
    будут попадать в контроллер с этим атрибутом.
  3. Переименуем базовый класс из Controller в ControllerBase, поскольку нам не нужна лишняя MVC функциональность.

Ок, например мы сделали запрос localhost:5000/pizzas в надежде получить список всех пицц и ничего не произошло. Опять же, дело в соглашениях.

Если это был Get запрос, то у нас либо должен быть метод (Action в терминах Asp.Net ) помеченный атрибутом [HttpGet] либо, что ещё более очевидно просто метод с названием Get и всё! Всё остальное .Net и рефлексия сделают за нас.
И так переименуем единственный метод Index в Get. Тип возвращаемого значения поменяем на IEnumerable<BasePizza>, не забудьте добавить нужные using. Ну и временно вставим заглушку о том, что метод не реализован для того что-бы как-то скомпилировать код и убедиться, что ошибок нет.

В итоге PizzasController.cs будет выглядеть вот так:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using BlazingPizza.DomainModels;

namespace BlazingPizza.Server.Controllers
{
    [ApiController]
    [Route("pizzas")]
    public class PizzasController : ControllerBase
    {
        // GET
        public IEnumerable<BasePizza>  Get()
        {
            throw new NotImplementedException();
        }
    }
}

Прямо сейчас запустим приложение для отладки, кнопка с зеленым жучком.



и убедимся что роуты настроены правильно. Порт по котором нужно делать запросы можно увидеть на вкладке Console:



В нашем случае это 5000, если сделать запрос по пути localhost:5000/pizzas то мы попадём в Get action и словим NotImplementedException. То есть пока наш контроллер не делает ничего полезного, просто принимает запросы и валится с ошибкой.

Возвращаем данные из контроллера


Самое время заставить наш код сделать что-то полезное, например возвращать пиццы. Пока что, у нас не реализован слой данных, поэтому мы просто вернем пару пицц из нашего action. Для этого вернем массив состоящий из двух объектов BasePizza. Метод Get будет выглядить как на примере ниже:

// GET
public IEnumerable<BasePizza>  Get()
{
    return new[]
    {
        new BasePizza()
        {
            BasePrice = 500,
            Description = "Самая вкусная пицца которую вы пробовали",
            Id = 0,
            ImageUrl = "img/pizzas/pepperoni.jpg"
        },
        new BasePizza()
        {
            BasePrice = 400,
            Description = "Вот эта точно вкусная",
            Id = 1,
            ImageUrl = "img/pizzas/meaty.jpg"
        },
    };
}

Результат запроса в браузере будет таким:



Настроим главную страницу


Видимая часть приложения находится в .razor компонентах в проекте BlazingPizza.Client. Нас интересует Index.razor в папке Pages, откроем его и удалим все его содержимое которое нам досталось в наследство от дефолтного проекта. И начнем добавлять то, что нам действительно нужно.

1. Добавим: page "/" Эта директива служит для настройки клиентского роутинга и говорит о том что именно этот контрол будет загружаться по умолчанию, то есть, если мы просто перейдем по адресу приложения localhost:5000/ без всяких /Index, /Pizzas или ещё чего-то.

2. inject HttpClient HttpClient С помощью директиы inject добавим сервис типа HttpClient на нашу страницу и назовем объект тоже HttpClient. Объект типа HttpClient уже сконфигурирован для нас инфраструктурой Blazor благодаря чему мы можем просто делать нужные нам запросы. Данный вид инъекций назывется Property Injection, более привычное внедрение через конструктор не поддерживается и как следует из заявления разработчиков маловероятно что когда то появится, а нужно ли оно тут?

3. Добавим директиву

 @code{

 }

Она специально нужна для того, что бы размещать клиентский C# код, тот самый, который является заменой JavaScript. Внутри этого блока разместим коллекцию объектов типа BasePizzaViewModel

IEnumerable<BasePizzaViewModel> PizzaViewModels;

4. Как вы уже поняли BasePizzaViewModel не существует, самое время её создать, эта модель будет полностью аналогична доменной модели BasePizza за исключением того что у неё добавится expression body GetFormattedBasePrice возвращающий цену базовой пиццы в нужном нам формате. Модель добавим в корень проекта BlazingPizza.ViewModels в файл BasePizzaViewModel.cs:

public class BasePizzaViewModel
{
    public int Id { get; set; }
    
    public string Name { get; set; }
    
    public decimal BasePrice { get; set; }
    
    public string Description { get; set; }
    
    public string ImageUrl { get; set; }
    
    public string GetFormattedBasePrice() => BasePrice.ToString("0.00");
}

5. Вернемся к нашему Index.razor и блоку code, добавим код для получения всех доступных пицц. Данный код разместим в async методе OnInitializedAsync:

protected async override Task OnInitializedAsync() {
	
}

Данный метод вызывается после инициализации компонента и в момент вызова, все его параметры уже инициализированы родительским компонентом. В нем можно выполнять какие-то асинхронные операции, после выполнения которых требуется обновление состояния. Позже я расскажу об этом подробнее. Метод вызывается только однажды при создании компонента.

Добавим наконец получение пицц внутрь данного метода:

var queryResult = await HttpClient.GetJsonAsync<IEnumerable<BasePizza>>("pizzas");

pizzas – относительный путь который добавляется к базовому и уже установлен за нас Blazor. Как следует из сигнатуры метода данные запрашиваются get запросом и потом клиент пытается сериализовать их в IEnumerable<BasePizza>.

6. Поскольку мы получили данные не того типа который мы хотим отобразить в компоненте, нам нужно получить объекты типа BasePizzaViewModel, воспользуется для этого Linq и его методом Select который позволяет преобразовать объекты входящей коллекции в объекты того типа, который мы планируем использовать. Добавим в конец метода OnInitializedAsync:

PizzaViewModels = queryResult.Select(i => new BasePizzaViewModel()
{
    BasePrice = i.BasePrice,
    Description = i.Description,
    Id = i.Id,
    ImageUrl = i.ImageUrl,
    Name = i.Name
});

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

7. Над директивой code добавим html код, внутри которого будут непосредственно сами пиццы:

<div class="main">
    <ul class="pizza-cards">

    </ul>
</div>

Как видим, список ul с говорящим названием класса “pizza-cards” пока пуст, исправим эту оплошность:

@foreach (var pizza in PizzaViewModels)
{
    <li style="background-image: url('@pizza.ImageUrl')">
        <div class="pizza-info">
            <span class="title">@pizza.Name</span>
                @pizza.Description
            <span class="price">@pizza.GetFormattedBasePrice()</span>                    
        </div>
    </li>
}

Всё самое интересное здесь происходит внутри цикла foreach(var {item} in {items})
Это типичная Razor разметка которая позволяет нам использовать возможности C# на одной странице с обычным html кодом. Главное ставить перед ключевыми словами языка и переменными символ “@”.

Внутри цикла мы просто обращаемся к свойствам объекта pizza.

В конце, мы выводим отформатированную базовую цену пиццы с помощью метода GetFormattedBasePrice. Это кстати и является отличием доменной модели BasePizza от её ViewModel представления, поскольку в этом методе находится простейшая логика по отображению цены в нужном формате, которая нам не нужна на уровне сервисов, где мы как-то манипулируем ценой, но нигде ее не показываем.

Отображаем полученные данные в браузере


Мы получили все необходимые данные для отображения. Самое время запустить наше приложение и убедиться, что все работает. Нажимаем на кнопку DebugRider кнопка Run просто запускает приложение без возможности Debug).

И охо-хо, ничего-то и не работает:) Открываем консоль (F12) и видим что она вся красная, что-то явно пошло не так. Blazor не так уж и безнадёжен в отладке и весь call stack можно увидеть в Console, причем на мой взгляд это сделано даже лучше чем в том же Angular. Не нужно по косвенным признакам гадать, где произошла ошибка, достаточно просто посмотреть call stack:



При рендеринге страницы возникло сообщение NullReferenceException. Как такое могло произойти, ведь мы же инициализировали в методе OnInitializedAsync единственную используемую нами коллекцию.

Что бы понять чуть лучше, вставим вывод времени в нужных местах что бы посмотреть таймфрейм того что произошло:

  1. Console.WriteLine($"Time from markup block: {DateTime.Now.ToString()}:{DateTime.Now.Millisecond.ToString()}");
  2. Console.WriteLine($"Time from cycle: {DateTime.Now.ToString()}:{DateTime.Now.Millisecond.ToString()}");
  3. Console.WriteLine($"Time from code block, before await: {DateTime.Now.ToString()}:{DateTime.Now.Millisecond.ToString()}");
  4. Console.WriteLine($"Time from code block, after await: {DateTime.Now.ToString()}:{DateTime.Now.Millisecond.ToString()}"); 



На скриншоте ниже, в консоли — то, что происходило, в момент рендеринга страницы. Видно что страница начинает рендериться еще до завершения выполнения асинхронных методов.
При первом проходе PizzaViewModels ещё не был инициализирован и мы словили NullReferenceException. Потом, как и ожидалось после возврата Task-ом метода OnInitializedAsync статуса RanToCompletion произошёл ререндеринг контрола. Что примечательно, во время второго прохода мы попали в цикл, что видно по сообщениям в консоли. Но в этот момент UI уже не обновляется и мы не видим никаких видимых изменений.



На самом деле проблему очень легко решить, нужно просто перед выполнением цикла по коллекции которая заполняется асинхронно вставить проверку на null, тогда исключение в первый раз не возникнет и во время второго прохода мы увидим нужные нам данные.
@if (PizzaViewModels != null)
{
    @foreach (var pizza in PizzaViewModels)
    {
        ……………………….. //здесь должен быть ваш код
    }
}


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



Так гораздо лучше, но не хватает стилей и ресурсов, в частности картинок, замените содержимое папки wwwroot содержимым из папки "~/Articles/Part2/BlazingPizza.Client/wwwroot" репозитория(ссылка в конце статьи) и снова запустите проект, так уже гораздо лучше. Хотя по прежнему далеко от идеала:



События жизни компонента


Поскольку мы уже познакомились с одним из событий жизни компонента OnInitializedAsync, логичным будет упомянуть и остальные:
Методы инициализации
OnInitialized
Вызывается в момент, когда компонент уже инициализирован, а его параметры уже установлены родительским компонентом. В течении жизни компонента вызывается один раз после его инициализации
OnInitializedAsync
Асинхронная версия первого метода, после выполнения происходит повторный рендер компонента. Поэтому при написании кода нужно учитывать что некоторые объекты могут быть равны null.
Метод исполняющийся до установки значений параметров
SetParametersAsync
Устанавливает параметры, значения которых приходят из родительского компонента. Единственный аргумент функции ParameterView содержит набор значений всех параметров компонента.

Уже содержит неплохо реализованную базовую версию которая устанавливает значения всех свойств компонента помеченных атрибутами [Parameter] или [CascadingParameter] соответствующими значениями из ParameterView. В случае если соответствующий параметр отсутствует, значение свойства не меняется. В случае если вы не вызываете базовую реализацию, то вы можете интерпретировать пришедшие значения как угодно и даже ничего с ними не делать)
Методы выполняющиеся после установки значений
OnParametersSet
Вызывается когда компонент инициализирован и первый раз получил параметры от родительского компонента. Также вызывается при рендеринге родительского компонента, при этом работает несколько по другому

— Если параметр является примитивным типом то при повторном вызове передаются только изменённые значения.
— Если параметр комплексного типа, то фреймворк не проверяет какие значения конкретно изменились и считает что значение параметра все же менялось и передает его заново.
OnParametersSetAsync асинхронная версия OnParametersSet
Методы выполняющиеся после рендеринга компонента
OnAfterRender Вызывается после того как закончился рендеринг компонента. Можно использовать этот шаг для дополнительной инициализации компонента. Здесь можно задействовать сторонние JavaScript библиотеки для взаимодействия с DOM который в этот момент уже существует. Имеет единственный аргумент bool типа firstRender который равен true только при первом рендере компонента.
OnAfterRenderAsync
Асинхронная версия метода выше, даже если вы вернете Task из данного метода, дальше не какой работы запланировано не будет потому-что это последний по времени метод в рамках жизненного цикла компонента.
Методы вне цикла
ShouldRender
Метод который стоит особняком от других нужен для блокировки обновления UI, если вам это по каким-то причинам понадобится. Вызывается каждый раз при ререндеринге компонента. Как минимум один раз компонент будет отрендерен независимо от значения возвращаемого данным методом.
StateHasChanged
Вызов данного метода принудительно вызывает ререндеринг компонента, в случаях когда Blazor не в состоянии отследить изменения состояния компонента самостоятельно.
Dispose
Хорошо знакомый всем метод, который вызывается при удалении компонента из UI. Вызов StateHasChanged из Dispose не поддерживается. Для использования Dispose компонент должен реализоваывать интерфейс IDisposable, что можно сделать с помощью директивы @implements IDisposable

Заключение


В этой части мы научились получать данные из контроллера и отображать их пользователю.
В следующей части, мы приведём в порядок Layout и добавим слой доступа данных, что-бы отображать на главной странице реальные данные.

Ссылка на репозиторий данной серии статей.
Ссылка на оригинальный источник.