BlazingPizza: приложение на Blazor от начала и до конца. Часть 2. Добавляем компонент

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

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

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

q-pl95jcw5o2sdhgzzduaxzotke.png
После этого появится диалог с запросом на добавление файла в 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, не забудьте добавить нужные 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  Get()
        {
            throw new NotImplementedException();
        }
    }
}


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

nlp6ppjxbnbnhjiwxb1_qbtgwsw.png

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

ypi0hpgkoc9cv68k8zvi9zsthnu.png

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

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


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

// GET
public IEnumerable  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"
        },
    };
}


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

-cepf0dkn90kagmz-gul-dib42u.png

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


Видимая часть приложения находится в .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 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>("pizzas");


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

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 код, внутри которого будут непосредственно сами пиццы:


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

@foreach (var pizza in PizzaViewModels)
{
    
  • @pizza.Name @pizza.Description @pizza.GetFormattedBasePrice()
  • }


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

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

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

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


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

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

    g-4fm5pnokogbwv_c7vy3ru-5zk.png

    При рендеринге страницы возникло сообщение 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()}"); 


    oivachjfzw1mfopljcqmxwhurvy.png

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

    ki_ryxdxd-8xizlytgtgtb5lhpk.png

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

    @if (PizzaViewModels != null)
    {
        @foreach (var pizza in PizzaViewModels)
        {
            ……………………….. //здесь должен быть ваш код
        }
    }


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

    edsjztoc2aqa_wibm2mpdwvm2hy.png

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

    mkarf-kymgsseuioay24uwx0wui.png

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


    Поскольку мы уже познакомились с одним из событий жизни компонента OnInitializedAsync, логичным будет упомянуть и остальные:

    Заключение


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

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

    © Habrahabr.ru