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:
В только что созданном проекте находятся примеры, которые Microsoft показывала на протяжение последнего пол года. Подробно я их не буду рассматривать.
А вот первое, что бросается в глаза это способ изменение настройки маршрутизации для приложения на Blazor в сравнение с настройка маршрутизации для приложения ASP.NET MVC. Для наглядности ниже я размещу код Configure :
Как видно из кода выше в Configure отсутствует настройка route, а вместо неё есть настройка endpoints. Данный подход взять из ASP.NET Pages. А за routing отвечает специальный компонент Router, который находится в App.razor:
Blazor как и Angular основан на компонентах, что упрощает повторное использование одно функционала в разных местах. Для меня, как для человека, который пришел в мир Web’a, из мира разработки десктопных и мобильных приложений на Windows, компонент - это UserControl из WPF или UWP. И с этой ассоциацией мне было легче понять, что такое компонент.
Основной компонент Route располагает (хостится) на обычной razor-странице. Ниже показан код _Host.cshtml:
Единственное, что меня смутило так это, что рендер компонента происходит через HTML-хэлпер. Скорей, всего в дальнейшем Microsoft откажется от такого подхода в сторону tag-хэлперов, и вместо строчки :
Будет строчка:
UPD: На сайту Microsoft можно увидеть текущею реализацию (https://docs.microsoft.com/en-us/aspnet/core/blazor/components?view=aspnetcore-3.1)
JS скрипт _framework/blazor.server.js используется для создания связи между сервером и клиентом через SignalR.
Для того, чтобы манипулировать задачам нужно создать сущность, у которой будет название запланированной задачи и свойство показывающее завершенность. Нижне показан код только что созданной модели:
Так же нужно создать сервис, который будет возвращать список задач, для этого я создам класс ToDoList.cs:
Для того, чтобы избавиться от зависимости реализации буду использовать принцип инверсии зависимостей. Для этого реализую интерфейс 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>();
}
Теперь создадим новый компонент для нашего списка дел.
Любой 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
Но отображать списка дел можно и с помощью обычных 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 происходят с помощью директивы @. Но это не сработало.
При просмотре документации я обнаружил, что чтобы подписаться на событие нужно использовать следующий синтаксис:
Но мне по прежнему было необходимо отслеживать клавишу, которую нажал пользователь. И здесь мне помог 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" />
Результат:
Выглядит не плохо, с учетом того, что вся логика добавление новых задач реализована на 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;
}
И вот что получилось в конечно итоге:
Вместо вывода:
Мне кажется, что Blazor - это отличный framework, для написания интерактивного пользовательского интерфейса. Он сочетает в себе всё лучшее, что есть в мире ASP.NET, но при этом избавляется от недостатков и проблем, которые могли бы возникнуть при использование js.