Blazor

Недавно Microsoft представила .NET Core 3, а вместе с ним и Blazor - новый famework для написания Web-приложений. Прежде всего Blazor рассчитан на Fullstack-программистов, которые работали с ASP.NET MVC или/и ASP.NET Pages, но в отличие от данных Framework’ов в Blazor вся логика изменения UI реализована на всеми любимом C#. Теперь для “оживления” Web-сайта можно минимизировать использование js.

Как удалось реализовать это Microsoft?

Microsoft давно уже хотела “запихнуть” C# в Frontend. Ещё в далеком 2002 года Microsoft представила миру первую реализацию ASP.NET Web Forms. ASP.NET Web Forms была хороша, как и Win Forms, для своего времени. Но ASP.NET Web Forms не прошел проверку временем. Microsoft решила абстрагировать разработчиков Web Forms от самих Web-технологий. Таких как: HTTP, HTML, CS и т.д. В надежде, что миллионы разработчиков дескторных приложений для Windows перейдут на Web-приложения. Но с развитием Web’a ASP.NET Web Forms умер. И основные причины этому:

  • Тяжеловесное состояние страница - современные Web-приложения требовали динамического контента, и в ASP.NET Web Forms решили эту проблему путем передачи гигантских данных между клиентом и сервером при любом изменение страницы;
  • Отсутсвие четкого шаблона проектирования - за счет этого тестировать и отлаживать enterprise-приложения становилось тяжело, а иногда и просто не реально;
  • Ограниченный контроль на HTML версткой - создание пользовательских элементов управления было не из легких задач.

Понимая это Microsoft переосмыслила framework для написания Web-приложений. Первым делом убрали абстракцию, теперь разработчик четко понимал что он разрабатывает Web-приложения, а не десктопное приложения в браузере. Как только убрали абстракцию то появился полный контроль над HTML,CSS и js. И это всё было подано с MVC паттерном. Так появился ASP.NET MVC.

Отличная Web-технология, которая позволяет писать хорошие приложения. ASP.NET MVC включает в себя Razor - движок представлений. Язык разметки Razor позволяет генерировать HTML страницы со вставками C# кода. Но для того, чтобы создать динамически изменяемый контент нужно использовать js.

И совсем недавно Microsoft представила новый framework Blazor. Blazor основан на технологии WebAssembly , которая позволяет запускать код, написанный на любом языке, в браузере. Это происходит за счет интерпретации кода в байт-кода.

Microsoft разрабытвает несколько видов приложений на Blazor:

  • Blazor Client - это платформа для SPA-приложений, которая позволяет создавать интерактивные клиентские веб-приложения с помощью .NET. Это происходит благодаря загрузке .NET Framework’a и .NET Runtime на клиент (в настоящее время доступна только предварительная версия);
  • Blazor Server - предоставляет поддержку размещения компонентов Razor на сервере в приложении ASP.NET Core, а обновления пользовательского интерфейса передаются через подключение SignalR

Для того, чтобы пощупать Blazor я создам приложение для ведение дел ToDos. Для начало выберу новый шаблон проекта, который доступен в Visual Studio:

New Blazor template

В только что созданном проекте находятся примеры, которые Microsoft показывала на протяжение последнего пол года. Подробно я их не буду рассматривать.

А вот первое, что бросается в глаза это способ изменение настройки маршрутизации для приложения на Blazor в сравнение с настройка маршрутизации для приложения ASP.NET MVC. Для наглядности ниже я размещу код Configure :

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
     if (env.IsDevelopment())
     {
         app.UseDeveloperExceptionPage();
     }
     else
     {
         app.UseExceptionHandler("/Error");
     }

     app.UseStaticFiles();

     app.UseRouting();

     app.UseEndpoints(endpoints =>
     {
         endpoints.MapBlazorHub();
         endpoints.MapFallbackToPage("/_Host");
     });
}

Как видно из кода выше в Configure отсутствует настройка route, а вместо неё есть настройка endpoints. Данный подход взять из ASP.NET Pages. А за routing отвечает специальный компонент Router, который находится в App.razor:

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="@typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

Blazor как и Angular основан на компонентах, что упрощает повторное использование одно функционала в разных местах. Для меня, как для человека, который пришел в мир Web’a, из мира разработки десктопных и мобильных приложений на Windows, компонент - это UserControl из WPF или UWP. И с этой ассоциацией мне было легче понять, что такое компонент.

Основной компонент Route располагает (хостится) на обычной razor-странице. Ниже показан код _Host.cshtml:

@namespace ToDos.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>ToDos</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
</head>
<body>
    <app>
        @(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))
    </app>

    <script src="_framework/blazor.server.js"></script>
</body>
</html>

Единственное, что меня смутило так это, что рендер компонента происходит через HTML-хэлпер. Скорей, всего в дальнейшем Microsoft откажется от такого подхода в сторону tag-хэлперов, и вместо строчки :

@(await Html.RenderComponentAsync<App>(RenderMode.ServerPrerendered))

Будет строчка:

<Component name="App" mode="ServerPrerendered"/>

UPD: На сайту Microsoft можно увидеть текущею реализацию (https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1)

<component type="typeof(Counter)" render-mode="ServerPrerendered" param-IncrementAmount="10" />

JS скрипт _framework/blazor.server.js используется для создания связи между сервером и клиентом через SignalR.

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

public class ToDoItem
{
    public string Title { get; set; }

    public bool IsDone { get; set; }
}

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

public class ToDoList
{
    public List<ToDoItem> Items { get; set; } = new List<ToDoItem>();

    public void AddNewToDo(ToDoItem todo)
    {
        Items.Add(todo);
    }

    public Task<List<ToDoItem>> GetToDoItems()
    => Task.Run(() =>
    {
       if (!Items.Any())
       {
          for (int i = 0; i < 10; i++)
          {
              Items.Add(new ToDoItem
              {
                  Title = $"Task #{i}",
                  IsDone = false
              });
           }
        }

        return Items;
     });
 }

Для того, чтобы избавиться от зависимости реализации буду использовать принцип инверсии зависимостей. Для этого реализую интерфейс IToDoList:

public interface IToDoList
{
     void AddNewToDo(ToDoItem todo);

     Task<List<ToDoItem>> GetToDoItems();
}

Унаследую данный интерфейс классом ToDoList. И для того, чтобы система понимала экземпляр какого класса использовать при использование интерфейса IToDoList буду использовать встроенный контейнер внедрения зависимостей:

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();
    services.AddTransient<IToDoList, ToDoList>();
}

Теперь создадим новый компонент для нашего списка дел.

New Blazor template

Любой Blazor-компонент состоит из двух блоков:

  • HTML-разметка - отвечает за внешний вид компонента;
  • Блок @code{} - отвечает за логику компонента.

Для начало я отображу текущий список дел. Для этого изменю ToDo.razor как показано ниже:

@page "/todo"
@using Microsoft.AspNetCore.Components
@using ToDos.Data
@inject IToDoList ToDoList

@if (toDoItems == null)
{
    <h6>>ToDo list is empty!</h6>
}
else
{
    <table>
        @foreach (var toDoItem in toDoItems)
        {
            <tr class="form-control">
                <td>
                    <input type="checkbox" checked="@toDoItem.IsDone" />
                </td>
                <td>@toDoItem.Title</td>
            </tr>
        }
    </table>
}

@code{
    List<ToDoItem> toDoItems;
    protected override async Task OnInitializedAsync()
    {
        toDoItems = await ToDoList.GetToDoItems();
    }
}

При инициализации данного компонента , метод OnInitializedAsync, происходит загрузка задач с помощью нашего сервиса ToDoList, при этом сервис я указал как абстракцию,а реализацию находит сама платформа:

@inject IToDoList ToDoList

New Blazor template

Но отображать списка дел можно и с помощью обычных razor-страниц, для этого Blazor не нужен. Для того, чтобы воспользоваться всеми прелестями нового framework’a реализую возможность добавления новых задач. Для этого в компонент помещу input и при нажатие на Enter будет создаваться новая задача.

<input class="form-control" text="@title" placeholder="Add a new todo" />

Создам новое приватное поле в блоке @code title, и укажу его в качестве text для только что созданного элемента input.

Теперь нужно подписаться на событие нажатие кнопки на Enter. И здесь началось самое интересное потому, я не сразу понял как это сделать.

Изначально мой вариант выглядел так:

<input type="text" onkeypress=@KeyWasPressed />

@code {
  private void KeyWasPressed()
  {
    //TODO
  }
}

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

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

<input type="text" @onkeypress="KeyWasPressed" />

@code {
  private void KeyWasPressed()
  {
    //TODO
  }
}

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

<input class="form-control" text="@title" @onkeypress="OnKeyDown" placeholder="Add a new todo" />

@code{
string title;
public void OnKeyDown(KeyboardEventArgs aegArgs)
{
     if (aegArgs.Key == "Enter")
     {
        ToDoList.AddNewToDo(new ToDoItem { Title = title });
        title = string.Empty;
     }
}
}

Здесь меня ждал сюрприз. Текст, который вводит пользователь, не сохранялся.

Дело в том, что атрибут text=”@title” задавал значение свойству text элементу input при рендере страницы, но не обеспечивал связь между свойством элемента и полем блока @code.

Нужен был некий механизм привязки…

И да, данный механизм есть в Balzor. После не большего изменение, элемент input выглядит следующим образом:

<input class="form-control" @bind="@title" @onkeypress="OnKeyDown" placeholder="Add a new todo" />

Однако, после тестирование я выяснил, что значение поле title изменяется после потери фокуса на элементе input. Меня такой вариант не устраивал и после не большего пояска я нашел решение. После небольших исправлений код элемента input выглядит следующим образом:

<input class="form-control" @bind-value="@title" @bind-value:event="oninput" @onkeypress="OnKeyDown" placeholder="Add a new todo" />

Результат:

New Blazor template

Выглядит не плохо, с учетом того, что вся логика добавление новых задач реализована на C#.

Теперь во мне взыграл интерес: а что ещё я смогу сделать?

Я выделил для себя несколько функций, которые можно добавить к приложению:

  • Не отображать завершенные задачи;
  • Показывать общие число отображаемых задач;
  • Возможность выбора отображения: показывать все задачи или показывать не завершенные задачи.

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

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

<select class="form-control" @onchange="OnChangeSelect">
    <option selected>Choose...</option>
    <option value="true">Not show done tasks</option>
    <option value="false">Show all tasks</option>
</select>

@code{
bool? showNotDone;
public void OnChangeSelect(ChangeEventArgs args)
{
    var isNull = Boolean.TryParse(args.Value.ToString(), out var value);
    if (!isNull)
    {
        showNotDone = null;
        return;
    }
    showNotDone = value;
}
}

Свойства showNotDone будет отвечать за генерацию необходимого фильтра. Сам фильтр будет реализован с помощью метода:

public Func<ToDoItem, bool> FilterExpression()
{
    if (showNotDone == null || showNotDone.Value)
    {
       return x => !x.IsDone;
    }

    return x => !x.IsDone || x.IsDone;
}

И вот что получилось в конечно итоге:

New Blazor template

Вместо вывода:

Мне кажется, что Blazor - это отличный framework, для написания интерактивного пользовательского интерфейса. Он сочетает в себе всё лучшее, что есть в мире ASP.NET, но при этом избавляется от недостатков и проблем, которые могли бы возникнуть при использование js.

Написано 29 сентября 2019