- PVSM.RU - https://www.pvsm.ru -
Привет! Таки да, скоро выходит net core 3.0 и там будет шаблон проекта с Blazor как один из дефолтных. Название у фреймворка, по-моему, похоже на название какого-нибудь покемона. Блазор вступает в бой! Решил я значит глянуть что за зверь такой и с чем его едят поэтому сделал на нем Todo лист. Ну и на Vue.js тоже, для сравнения с сабжем потому что по моему они похожи система компонентов в обоих и реактивность и вот это все. Больше тудушек богу тудушек! По факту это Гайд для юных, не окрепших умов которым лень TypeScript или JavaScript учить а кнопочки и инпуты на сайте сделать хочется. Как в том меме -«Технарь хотел написать книгу но получилась инструкция». Кому интересны мои похождения в фронт энде или узнать что за Blazor такой добро пожаловать под кат.
Была когда-то у Майкрософт идея работы C# в браузере и звали эту идею Силверлайт. Не взлетело. Эти ваши тырнеты были тогда другие как собственно и браузеры. Почему я думаю что сейчас взлетит? Потому что сейчас веб ассембли есть во всех современных браузерах по дефолту. Нет необходимости в установке отдельного расширения. Другая проблема размеры приложения. Если на Vue.js SPA вести 1.7 мегабайт, то точно такое же на Blazor 21 мегабайт. Сейчас интернет быстрее и надежнее стал чем во времена Сильверлайта да и скачивать приложение надо один раз, а дальше там кеш и все дела. Вообще Blazor мне показался очень похожим на Vue.js. И так, как дань уважения Silverligtht, WPF и UWP да и просто потому что у шарпистов так принято я решил использовать паттерн MVVM для своего проекта. Так для справки — Я вообще бекэндшик и мне Blazor понравился. Слабонервных предупреждаю — Дизайн и верстка в моих примерах ужасные, а в проекте с Vue.js опытный фронтэндшик может узреть много говнокода. Ну и с орфографией и пунктуацией дела тоже так себе.
Пример Todo на Vue + Vuex — gitlab.com/VictorWinbringer/vuetodo [1]
Привер Todo на Blazor — gitlab.com/VictorWinbringer/blazorexample [2]
Мне лично первый вариант больше нравиться и его можно использовать во всех тех случаях когда вам не нужно беспокоиться о конверсии пользователей. Например это какая-то внутренняя информационная система компании или специализированное B2B решение потому что Blazor долго скачивается в первый раз. Если ваши пользователи постоянно заходят в ваше приложение, то они не заметят никакой разницы с JS версией. Если пользователь заходит по рекламной ссылке просто глянуть что там за сайт какой-то скорее всего он не будет долго ждать пока сайт загрузиться и просто уйдет. В этом случае лучше использовать второй вариант размещения т.e. Server Side Blazor.
Скачайте net core 3.0 dotnet.microsoft.com/download/dotnet-core/3.0 [3]
Выполните в терминале команду которая загрузить вам необходимые шаблоны.
dotnet new -i Microsoft.AspNetCore.Blazor.Templates
Для создания Server Side
dotnet new blazorserverside -o MyWebApp
Для Client Side файлики которого будет раздавать сервер asp.net core
dotnet new blazorhosted -o MyWebApp
Если вам захотелось экзотики и вдруг решили не использовать в качестве сервера asp.net core а что-то другое (А оно вам надо вообще?) можете создать только клиент без сервера вот такой командой.
dotnet new blazor -o MyWebApp
Поддерживается односторонняя привязка и двусторонняя. Таки да, не надо никаких OnPropertichanged как в WPF. При изменении Вью Модели разметка меняется автоматически.
<label>One way binding:</label>
<br />
<input type="text" value=@Text />
<br />
<label>Two way binding:</label>
<br />
<input type="text" @bind=@Text />
<br />
<label>Two way binding и смена события при которов будет меняться поле Text на событие oninput:</label>
<br />
<input type="text" @bind=@Text @bind:event="oninput" />
//ViewModel
@code{
string Text;
async Task InpuValueChanged()
{
Console.WriteLine("Input value changed");
}
}
И так, тут у нас есть ViewModel (анонимная) у которой есть поле Text.
В первом инпуте через «value=@Text» мы сделали одностороннюю привязку. Теперь когда мы изменим Text в коде тут же изменится текст внутри input. Только вот чтобы мы не печатали в нашем инпуте это никак не повлияет на нашу VM. Во втором input через "@bind=@Text" мы сделали двухстороннюю привязку. Теперь если мы напишем что-то новое в нашем input тут же поменяется наша VM, и обратное тоже верно т.е. если мы поменяем поле Text в коде то наш input тут же отобразит новое значение. Тут есть одно НО — по дефолту изменения привязаны к событию onchange нашего input поэтому VM поменяться только тогда когда мы завершим ввод. В третьем input "@bind:event=«oninput»" мы изменили событие для передачи данных VM на oninput теперь каждый раз когда мы печатаем какой-нибудь символ новое значение тут же передается нашей VM. Так же для DateTime можно указать формат например так.
<input @bind=@Today @bind:format="yyyy-MM-dd" />
Можно ее делать анонимкой тогда ее нужно помешать внутри блока "@code {}"
@page "/todo"
<p> Привет @UserName </p>
@code{
public string UserName{get; set;}
}
или можно вынести ее в отдельный файл. Тогда ее надо наследовать от ComponentBase и в начале страницы указать ссылку на нашу VM c помошью "@inherits"
Например
TodoViewModel.cs:
public class TodoViewModel: ComponentBase{
public string UserName{get; set;}
}
Todo.razor:
@page "/todo"
@inherits MyWebApp.ViewModels.TodoViewModel
<p> Привет @UserName </p>
Маршруты на которые будет реагировать страница указываются в ее начале с помощью "@page". Причем их может быть несколько. Будет выбран первый точно соответствующий в порядке сверху вниз. Например
@page "/todo"
@page "/todo/delete"
<h1> Hello!</h1>
Эта страница будет открываться по адресу "/todo" или «todo/delete»
В общем-то сюда обычно помещают одинаковые для нескольких страниц вещи. Вроде сайдбара и прочего.
Для того чтобы использовать лайаут во первых, нужно его создать. Он должен наследоваться от LayotComponentBase с помощью "@inherits". Например
@inherits LayoutComponentBase
<div class="sidebar">
<NavMenu />
</div>
<div class="main">
<div class="top-row px-4">
<a href="http://blazor.net" target="_blank" class="ml-md-auto">About</a>
</div>
<div class="content px-4">
@Body
</div>
</div>
Во вторых его нужно импортировать. Для этого в директории со страницами которые его будут использовать нужно создать файл _imports.razor потом добавить в этот файл строчку "@layout"
@layout MainLayout
@using System
В третьих можно у страницы указать какой именно лайаут она использует напрямую
@layout MainLayout
@page "/todo"
@inherits BlazorApp.Client.Presentation.TodoViewModel
<h3>Todo</h3>
Вообще _imports.razor и using в нем действуют на все страницы которые находятся с ним в одной папке.
Во первых — указать параметр и его тип в фигурных скобках в нашем маршруте (регистра независим). Поддерживаются стандартные типы дотнет. Таки да, опциональных параметров нет т.е. значение нужно передавать всегда.
Само значение можно получить создав у нашей ViewModel свойство с именем таким же как у параметра и с атрибутом [Parameter] БТВ — забегая в перед — данные и события в дочерние компоненты из родительских передаются тоже с помощью атрибута [Parameter] так же есть каскадные параметры. Они передаются от родительского компонента всем его дочерним компонентам и их дочерним компонентам. Они используются в основном для стилей а стили лучше все же просто делать в CSS поэтому ну его нафиг.
@page "/todo/delete/{id:guid}"
<h1> Hello!</h1>
@code{
[Parameter]
public Guid Id { get; set; }
}
Все регистрируется в Startap.cs как в обычном asp.net core приложении. Тут ничего нового. А вот внедрение зависимостей для нашей VM таки происходит через публичные свойства а не через кноструктор. Свойство просто нужно декорировать отрибутом [Inject]
public class DeleteTodoViewModel : ComponentBase
{
[Parameter]
private Guid Id { get; set; }
[Inject]
public ICommandDispatcher CommandDispatcher { get; set; }
По умолчанию есть уже подключенных 3 сервиса. HttpClient — ну вы знаете зачем он. IJSRuntime — вызов JS кода из C#. IUriHelper — с помощью не его можно делать переадресацию на другие страницы.
TodoTableComponent.razor:
//1)
<table class="table table-hover">
<thead>
<th>Задача выполнена</th>
<th>Название</th>
<th>Дата создания</th>
<th>Действия</th>
</thead>
<tbody>
//2)
@foreach (var item in Items)
{
//3)
<tr @onclick=@(()=>ClickRow(item.Id)) class="@(item.Id == Current?"table-primary":null)">
<td><input type="checkbox" checked="@item.IsComplite" disabled="disabled" /></td>
<td>@item.Name</td>
<td>@item.Created.ToString("dd.MM.yyyy HH:mm:ss")</td>
<td><a href="/todo/delete/@item.Id" class="btn btn-danger">Удалить</a></td>
</tr>
}
</tbody>
</table>
@code {
//4)
[Parameter]
private List<BlazorApp.Client.Presentation.TodoDto> Items { get; set; }
[Parameter]
private EventCallback<UIMouseEventArgs> OnClick { get; set; }
[Parameter]
private Guid Current { get; set; }
private async Task ClickRow(Guid id)
{
//5
await OnClick.InvokeAsync(CreateArgs(id));
}
private ClickTodoEventArgs CreateArgs(Guid id)
{
return new ClickTodoEventArgs { Id = id };
}
//6)
public class ClickTodoEventArgs : UIMouseEventArgs
{
public Guid Id { get; set; }
}
}
@onclick=@(()=>ClickRow(item.Id))
Привязывает событие нажатия на строку к методу ClickRow нашей ViewModel
Пример использования:
<TodoTableComponent Items=@Items OnClick=@Select Current=@(Selected?.Id??Guid.Empty)></TodoTableComponent>
Наша ViewModel aka VM — DeleteTodoViewModel.cs:
public class DeleteTodoViewModel : ComponentBase
{
//1)
[Parameter]
private Guid Id { get; set; }
//2)
[Inject]
public ICommandDispatcher CommandDispatcher { get; set; }
[Inject]
public IQueryDispatcher QueryDispatcher { get; set; }
[Inject]
public IUriHelper UriHelper { get; set; }
//3)
public TodoDto Todo { get; set; }
protected override async Task OnInitAsync()
{
var todo = await QueryDispatcher.Execute<GetById,TodoItem>(new GetById(Id));
if (todo != null)
Todo = new TodoDto { Id = todo.Id, IsComplite = todo.IsComplite, Name = todo.Name, Created = todo.Created };
await base.OnInitAsync();
}
//4)
public async Task Delete()
{
if (Todo != null)
await CommandDispatcher.Execute(new Remove(Todo.Id));
Todo = null;
//5)
UriHelper.NavigateTo("/todo");
}
}
Наша View — DeleteTodo.razor:
//1)
@page "/todo/delete/{id:guid}"
@using BlazorApp.Client.TodoModule.Presentation
@using BlazorApp.Client.Shared;
//2)
@layout MainLayout
//3)
@inherits DeleteTodoViewModel
<h3>Удалить Todo </h3>
@if (Todo != null)
{
<div class="row">
<div class="col">
<input type="checkbox" checked=@Todo.IsComplite disabled="disabled" />
<br />
<label>@Todo.Name</label>
<br />
//4)
<button class="btn btn-danger" onclick=@Delete>Удалить</button>
</div>
</div>
}
else
{
<p><em>Такой Todo не найден</em></p>
}
TodoViewModel.cs:
public class TodoViewModel : ComponentBase
{
[Inject]
public ICommandDispatcher CommandDispatcher { get; set; }
[Inject]
public IQueryDispatcher QueryDispatcher { get; set; }
//1)
[Required(ErrorMessage = "Введите название Todo")]
public string NewTodo { get; set; }
public List<TodoDto> Items { get; set; }
public TodoDto Selected { get; set; }
protected override async Task OnInitAsync()
{
await LoadTodos();
await base.OnInitAsync();
}
public async Task Create()
{
await CommandDispatcher.Execute(new Add(NewTodo));
await LoadTodos();
NewTodo = string.Empty;
}
//2)
public async Task Select(UIMouseEventArgs args)
{
//3)
var e = args as TodoTableComponent.ClickTodoEventArgs;
if (e == null)
return;
var todo = await QueryDispatcher.Execute<GetById, TodoItem>(new GetById(e.Id));
if (todo == null)
{
Selected = null;
return;
}
Selected = new TodoDto { Id = todo.Id, IsComplite = todo.IsComplite, Name = todo.Name, Created = todo.Created };
}
public void CanselEdit()
{
Selected = null;
}
public async Task Update()
{
await CommandDispatcher.Execute(new Update(Selected.Id, Selected.Name, Selected.IsComplite));
Selected = null;
await LoadTodos();
}
private async Task LoadTodos()
{
var todos = await QueryDispatcher.Execute<GetAll, List<TodoItem>>(new GetAll());
Items = todos.Select(t => new TodoDto { Id = t.Id, IsComplite = t.IsComplite, Name = t.Name, Created = t.Created })
.ToList();
}
}
Todo.razor:
@layout MainLayout
@page "/todo"
@inherits BlazorApp.Client.Presentation.TodoViewModel
<h3>Todo</h3>
<h4>Список</h4>
<div class="row">
<div class="col">
@if (Items == null)
{
<p><em>Загрузка...</em></p>
}
else if (Items.Count == 0)
{
<p><em>Нет задач для отображения. Пожалуйсте добавте какую нибудь.</em></p>
}
else
{
//1)
<TodoTableComponent Items=@Items OnClick=@Select Current=@(Selected?.Id??Guid.Empty)></TodoTableComponent>
}
</div>
</div>
<br />
<h4>Создать Todo</h4>
<div class="row">
<div class="col">
@if (Items != null)
{
//2)
<EditForm name="addForm" Model=@this OnValidSubmit=@Create>
//3)
<DataAnnotationsValidator />
//4)
<ValidationSummary />
<div class="form-group">
//5)
<InputText @bind-Value=@NewTodo />
//6)
<ValidationMessage For="@(() => this. NewTodo)" />
//7)
<button type="submit" class="btn btn-primary">Создать</button>
</div>
</EditForm>
}
</div>
</div>
<br />
<h4>Редактировать Todo</h4>
<div class="row">
<div class="col">
@if (Items != null)
{
@if (Selected != null)
{
<EditForm name="editForm" Model=@Selected OnValidSubmit=@Update>
<DataAnnotationsValidator />
<ValidationSummary />
<div class="form-group">
<InputCheckbox @bind-Value=@Selected.IsComplite />
<InputText @bind-Value=@Selected.Name />
<button type="submit" class="btn btn-primary">Сохранить</button>
<button type="reset" class="btn btn-warning" @onclick=@CanselEdit>Отмена</button>
</div>
</EditForm>
}
else
{
<p><em>Кликните на задаче чтобы ее редактировать</em></p>
}
}
</div>
</div>
Тут мы регистрируем наши сервисы
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
//Добавляем LocalStorage и SessionStorage как синглтоны чтобы сохранять данные на
//стороне клиента в браузере
// Тут нужно подключить черед Nuget пакет Blazor.Extensions.Storage
services.AddStorage();
services.AddSingleton<ITodoRepository, TodoRepository>();
services.AddSingleton<ICommandDispatcher, CommandDispatcher>();
services.AddSingleton<IQueryDispatcher, QueryDispatcher>();
services.AddSingleton<IQueryHandler<GetAll, List<TodoItem>>, GetAllHandler>();
services.AddSingleton<IQueryHandler<GetById, TodoItem>, GetByIdHandler>();
services.AddSingleton<ICommandHandler<Add>, AddHandler>();
services.AddSingleton<ICommandHandler<Remove>, RemoveHandler>();
services.AddSingleton<ICommandHandler<Update>, UpdateHandler>();
}
public void Configure(IComponentsApplicationBuilder app)
{
//Указываем что корневым компонентом нашего приложения будет App.razor
// и его содержимое будет помещаться внутри тега <app></app>
app.AddComponent<App>("app");
}
}
Эта статься была написана чтобы разыграть аппетит и подтолкнуть к дальнейшему изучению Blazor. Надеюсь поставленной цели я достиг. Ну а чтобы изучить его получше рекомендую почитать официальное руководство от Майкрософт docs.microsoft.com/ru-ru/aspnet/core/blazor/?view=aspnetcore-3.0 [6]
Автор: Тимур Давлатов
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/c-2/326741
Ссылки в тексте:
[1] gitlab.com/VictorWinbringer/vuetodo: https://gitlab.com/VictorWinbringer/vuetodo
[2] gitlab.com/VictorWinbringer/blazorexample: https://gitlab.com/VictorWinbringer/blazorexample
[3] dotnet.microsoft.com/download/dotnet-core/3.0: https://dotnet.microsoft.com/download/dotnet-core/3.0
[4] docs.microsoft.com/ru-ru/aspnet/core/blazor/components?view=aspnetcore-3.0#event-handling: https://docs.microsoft.com/ru-ru/aspnet/core/blazor/components?view=aspnetcore-3.0#event-handling
[5] localhost/todo/delete/ae434aae44: http://localhost/todo/delete/ae434aae44
[6] docs.microsoft.com/ru-ru/aspnet/core/blazor/?view=aspnetcore-3.0: https://docs.microsoft.com/ru-ru/aspnet/core/blazor/?view=aspnetcore-3.0
[7] Источник: https://habr.com/ru/post/463197/?utm_source=habrahabr&utm_medium=rss&utm_campaign=463197
Нажмите здесь для печати.