Когда я вижу, как кто-то учит кого-то языку программирования, то частенько замечаю тенденцию показывать новичкам примитивные примеры в виде ToDo list. Помимо того, что подобные примеры не учат ничему полезному в программировании, они очень однобоки и не позволяют оценить все плюсы и минусы какой-либо среды разработки.
Меня это удручает. Давайте попробуем написать что-нибудь полезное и при этом показать вам, что можно и чего не нужно делать с достаточно новой технологией Microsoft под названием Blazor.
Не так давно мне пришлось помогать детишкам разобраться с программированием. Пацаны были маленькими, но глаза их были полны энтузиазма и постоянно слышался вопрос «Как?» и «Почему?». Кто-то из подопечных притащил в студию пару IoT реле, которые позволяли включать и выключать лампочки по сети. Конечно, для нас, Хабравчан, такая штука не очень интересна, но для подростков было как раз то что надо. Почему бы и нет, подумал я, и мы начали писать всякие реле с таймингами, которые позволяют анимировать гирлянды и всё такое. В конце концов, не хочу вас пугать, но конец года уже близится и скоро нам надо будет украшать ёлки и помещения.
Притащенные реле были куплены на алибабе и амазоне. Стоили они копейки и были в довольно большом ассортименте. Дети были рады тому, что первое реле отлично управлялось через HTTP путём посыла сформированных запросов. Такую штуку легко реализовать на любом языке программирования. Посему даже с яваскриптом вопросов не возникло, и все были рады.
Проблемы начались со вторым реле. В нём запросы надо было посылать в виде ASCII строки. А третье реле вообще требовало бинарного ввода на порту. Все программы по миганию гирляндами начали обрастать каким-то нездоровым количеством логики и превращались в костыли.
Было решено реализовать следующую программу:
- У нас есть список моделей различных реле.
- В списке мы указываем тип передачи данных и список команд, доступный для каждой модели.
- Создаём список устройств. Каждое устройство имеет отдельный IP-адрес, имя и название модели.
- На основе этой информации создаётся строка управления для каждой команды, каждого устройства, которая выглядит следующим образом:
http://control-center/control/relay-lobby/port-1-turn-on http://control-center/control/relay-lobby/port-1-turn-off http://control-center/control/relay-lobby/port-2-turn-on http://control-center/control/relay-lobby/port-2-turn-off http://control-center/control/relay-lobby/port-3-turn-on http://control-center/control/relay-lobby/port-3-turn-off
При наличии подобной системы общения с реле, вызывать функции не представляет собой никакого труда. В том числе подобные вещи будет проще подключать к умным домам.
Ладно, отбираем самых продвинутых учеников и идём писать.
Работать будем с последней версией Blazor для .NET 6.
Создаём пустой проект и идём заниматься проектированием. В принципе, в ТЗ всё достаточно хорошо описано, есть база данных, в ней есть реле, тут всё просто. Для базы данных мы возьмёмся за Entity Framework Core и будем использовать Code-Frist подход (то есть сначала мы пишем код, после чего фреймворк генерирует на основании этого кода базу данных).
База данных
Модель доступна здесь.
public class DataModelContext : DbContext
{
public DataModelContext (DbContextOptions<DataModelContext> options)
: base(options)
{
}
public DbSet<Model> Models { get; set; }
public DbSet<Device> Devices { get; set; }
public DbSet<Command> Commands { get; set; }
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Model>().ToTable("Model");
modelBuilder.Entity<Device>().ToTable("Device");
modelBuilder.Entity<Command>().ToTable("Command");
}
}
public class Model
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public ICollection<Command> Commands { get; set; }
public ICollection<Device> Devices { get; set;}
}
public class Device
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Address { get; set; }
public int Port { get; set; }
public int ModelId {get;set;}
public Model Model {get;set;}
}
public class Command
{
public int ID { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public CommandType Type { get; set; }
public string Payload { get; set; }
public Model Model {get;set;}
}
public enum CommandType
{
AsciiString,
UtfString,
ByteArray,
Binary,
}
Если вы никогда не видели ничего подобного, то давайте остановимся и подтянем знания. Перед вами — Entity Framework. Это фреймворк, который позволяет ускорить разработку баз данных для приложений, написанных на платформе .NET. Приложения, в частности, написаны на языке C#. В основном потому что для того, чтобы вы могли добраться до данных, вам предложено использовать функцию языка под названием LINQ (Language-integrated query).
Чем всё это хорошо? Тем что вы можете заботиться о своём коде в первую очередь и не париться по поводу базы данных. EF Core позволяет подключить вашу программу к различным базам данных, и вам не нужно будет учить синтаксис. Всё можно сделать в самом языке.
Чем это плохо? EF Core позволяет подключить вашу программу к различным базам данных, и вам не нужно будет учить синтаксис. Всё можно сделать в самом языке. В интернете вы найдёте множество баталий по поводу того, что подобный подход может шибко отразиться на производительности. И действительно, Join в SQL может выглядеть очень красиво и передавать вам в ответ только те данные, которые вы запросили. Если вы напишете кривой запрос на Linq, то вы можете в одну строку увалить целый кластер.
Так что Linq — это палка о двух концах. Чрезвычайно быстрая разработка и прототипирование баз данных должны идти рука об руку с чётким пониманием того, как работает ваш запрос и что происходит на стороне сервера.
Сразу оговорюсь: теперь следовало бы написать бизнес-логику на основе этой базы данных. Но я решил, что делать это будут подопечные, и оставил эту задачу для них (в данном случае бизнес-логика вполне соответствует методам, предложенным самим Entity Framework, и нам ничего дополнительного писать не нужно).
Учтите, если вы где-нибудь, когда-нибудь, в каком-нибудь интервью скажете, что вы втянули модель Entity Framework напрямую в код, то вас отчислят из соискателей и отправят копать картошку на полях. Так что не вздумайте этого делать.
Далее нам понадобятся две вещи. Первое — это сам Blazor сайт, который позволит редактировать значения в этой таблице. Также нам понадобится контроллер, который позволит вызывать функции работы с сетью и отправлять команды на само устройство. Этот компонент будет написан отдельно, поскольку Blazor нам здесь не нужен.
Frontend
Для тех, кто здесь в первый раз, давайте подтянем теорию. Blazor — это относительно новый фреймворк для создания сайтов, разработанный компанией Microsoft на основе open-source лицензии.
Основной прикол Blazor заключается в том, что всю логику на сайте вы можете написать на C#, без использования Javascript (чем мы здесь и займёмся в показательных целях). Сайт будет скомпилирован в WebAssembly, и вы сможете выложить его на сервер. Для тех, кто никогда не работал с WebAssembly, рекомендую почитать информацию — здесь.
Что это значит для разработчиков? Сайт представляет из себя несколько файлов — пустой HTML, небольшой JavaScript, который будет управлять сайтом, и приличный кусок кода на C#, скомпилированный в WebAssembly файл. Подобная файловая структура не требует никакого специального сервера для работы. Вы можете выложить это всё в открытый доступ на статическом сервере, без какой-либо платформы Microsoft, и всё будет работать.
Но не всё в жизни так просто. Сайт, в котором кнопки нажимаются и всё крутится и вертится — это хорошо, но он ничем не лучше, чем спиннеры и фиджит-кьюбы. Всё взаимодействие с пользователем должно быть записано на сервере.
Идея номер один — сайт написан на Blazor и у вас есть отдельный сервер для API, который позволяет дёргать функции удалённо через Web.
Идея номер два — Blazor может и не делать всего этого WebAssembly. Можно скомпилировать ваш код в библиотеку .NET и запускать его, как в старом добром клиент-серверном приложении. Код будет выполняться на сервере, а клиент будет обновлять страницу на экране, отражая изменения.
Именно этим подходом мы и воспользуемся.
Тут надо заметить пару вещей.
В старые добрые времена ASP.NET мы всё делали так — клиент нажимает кнопку на сайте. Весь сайт, представляющий собою форму, отправляется на сервер со всеми данными на экране. Сервер этот сайт обрабатывает, меняет его и выплёвывает обратно на экран. Всё это занимает меньше 10-ти секунд, и все этому рады в 2002 году. Но у нас на дворе 2021, и это нам не подойдёт.
Blazor использует другую систему. Все элементы, доступные пользователю, вызывают код на Javascript, который обращается к серверу, выполняет нужную процедуру на сервере, после чего сервер отправляет вам список изменений, которые надо внести на странице. Всё это упаковывается и передаётся вашему клиенту, и клиентский Javascript производит обновление страницы.
Всё намного быстрее и работает за доли секунды. Нам такой подход будет удобнее, и им-то мы и воспользуемся.
Основной плюс Blazor заключается в том, что вам не нужно морочить себя тем, как передавать сообщения с сервера и на сервер. Всё сделано автоматически. Даже если вам захочется переключить своё приложение из Server-Side в Web Assembly, вам всего-то нужно поменять один параметр в конфигах.
Основной минус Blazor заключается именно в этом же. Как бы ни были прекрасны все эти ускорения, они не будут быстрее, чем статический контент из кеша. Не стоит писать каждый сайт на Blazor, его удел — сложные приложения с большим количеством кнопок, вертелок и тому подобного. Если вам вдруг приспичило написать бложик, то писать его надо не на Blazor.
Backend
Теперь перейдём к Backend, с которым мы будем работать. Как я уже сказал, у нас на руках есть код, который будет брать определённые последовательности символов и передавать их на реле. Этот код будет использовать .NET TCPClient для отправки данных. Обычно таким сайты не занимаются.
Для создания этого контроллера мы воспользуемся технологией ASP.NET Core MVC.
В принципе, сам код MVC предельно прост:
[Microsoft.AspNetCore.Mvc.Route("api/[controller]")]
[ApiController]
public class SendController : Controller
{
[Inject]
private IDbContextFactory<DataModelContext> Context { get; set; }
public SendController(IDbContextFactory<DataModelContext> dmc)
{
Context = dmc;
}
[HttpGet("{Device}/{Command}")]
public JsonResult Get(string device, string command)
{
try
{
CommandRunner c = new(Context.CreateDbContext());
var answer = c.Run(device, command);
return Json(new { result = "success", device = device, command=command, payload = answer });
}
catch (Exception ex)
{
return Json(new { result="error", message= ex.Message });
}
}
}
Мы просто определяем путь
[HttpGet("{Device}/{Command}")]
. Если пользователь заходит на сайт по этому адресу, то мы подразумеваем, что первая часть команды будет идентификатором устройства, а вторая часть — идентификатором команды, которую нужно послать.В случае успеха мы дадим подтверждение, а в случае какой-либо ошибки сообщим об этом клиенту.
Как вы видите, у нас также есть класс CommandRunner, который как раз и выполняет команды на сервере, отправляя пакеты на устройства. Давайте заглянем в этот класс.
Код
CommandRunner.Run
достаточно прост:public string Run(String Device, String Command)
{
var dev = DbContext.Devices.Include(p => p.Model).ThenInclude(p => p.Commands).First(p => p.Name.ToLower() == Device.ToLower());
var com = dev.Model.Commands.First(p => p.Name.ToLower() == Command.ToLower());
var answer = Executor.Say(com.Payload, com.Type, dev.Address, dev.Port);
return String.Join(", 0x", answer);
}
А код
Executor.Say
в свою очередь выполняет следующее:public static Byte[] Say(string What, CommandType Type, string Address, int Port)
{
Byte[] bt = Type switch
{
CommandType.AsciiString => System.Text.Encoding.ASCII.GetBytes(What),
CommandType.UtfString => System.Text.Encoding.UTF8.GetBytes(What),
CommandType.Binary => ProcessBinary(What, 8),
CommandType.ByteArray => ProcessBytes(What),
_ => Array.Empty<byte>()
};
using TcpClient t = new TcpClient(Address, Port);
var s = t.GetStream();
s.Write(bt, 0, bt.Length);
return bt;
}
Здесь мы преобразовываем строку символов в последовательность битов, байтов, ASCII или Unicode символов, в зависимости от того, что принимает на вход наше устройство.
Основной прикол платформы заключается вот в чём — пользователь копается в инструкции и находит коды, которые надо посылать на устройство. После чего пишет эти коды в текстовое поле, и мы посылаем их на само устройство.
Понятное дело, тут был удобный шанс подучить детей знать разницу между строками, бинарными данными и ASCII последовательностями.
Итогом этой исследовательской деятельности явились две функции, которые переводят бинарные и байтовые строки в бинарные и байтовые значения:
static Byte[] ProcessBytes(string What)
{
if (What.Length % 2 == 1) What += "0"; //If user sent us uneven byte count
List<Byte> ret = new(What.Length/2);
foreach (String ch in What.SplitInParts(2))
{
var d1 = Convert.ToByte(ch[0].ToString(), 16);
var d2 = Convert.ToByte(ch[1].ToString(), 16);
d1 *= 0x10;
d1 += d2;
ret.Add(d1);
}
return ret.ToArray();
}
static Byte[] ProcessBinary(string What, int WordLength)
{
List<Byte> ret = new(What.Length);
foreach (var ch in What.SplitInParts(WordLength))
{
ret.Add(Convert.ToByte(ch, 2));
}
return ret.ToArray();
}
После чего мы просто выплёвываем эту последовательность на адрес устройства и считаем свою задачу выполненной.
Итак, что же нужно знать o MVC фреймворке?
Это уже устоявшаяся технология разработки программного обеспечения от Microsoft. Ею можно пользоваться, не особо опасаясь нарваться на подводные мины (хотя не бойтесь, мин у нас будет достаточно).
Это только присказка, сказка впереди
Итак, у нас есть базовый концепт приложения.
Всё хорошо, вокруг прыгают пони и поют птички. Всё работает.
Ага, как бы не так.
Всё на самом деле не так-то просто. Во второй части вам предлагается поприсутствовать в мире безбожного Blazor и огромного количества ошибок, с которыми стоило бы разобраться, прежде чем радостно окунаться в мир кодинга.
А пока что вы можете полюбоваться исходными кодами здесь.
— 15% на все тарифы VDS (кроме тарифа Прогрев) — HABRFIRSTVDS
— 20% на выделенные серверы AMD Ryzen и Intel Core — HABRFIRSTDEDIC.
Доступно до 31 декабря 2021 г.
Комментарии (9)
h4r7w3l1
02.11.2021 12:02+1Для тех, кто никогда не работал с WebAssembly, рекомендую почитать информацию — здесь
Ссылочку забыли
Enfriz
Сам люблю Blazor, но угнетает плачевное состояние комьюнити. Библиотеки компонентов, например, или очень плохие (мало функций, плохая документация) или, блин, платные, за деньги. В опенсорсе, да.
onatsko
Radzen Blazor отличные, полностью фри, компоненты
Nurked
Добро пожаловать в мир Enterprise Microsoft. Где всем рулит Telerik. Эээх. Да. Весёлые были времена.
Enfriz
Да, но они страшненькие. Посмотрите на Bootstrap, Framework7, Vuetify — вот как должна выглядеть современная библиотека компонентов для веба и мобайл.
Пока что лучшее, что я видел (визуально), это MudBlazor. Но там не разбирают Issue с багами, и вообще выглядит так, будто разработку забросили.
Andriy1218
AntDesignBlazor тоже не плох. Конечно не идеал, но выбор компонентов большой и выглядит неплохо.
BMTLab
Ну библиотеки компонентов скорее зло, чем польза. Особенно, когда библиотека берет на себя роль верстки, и/или, когда задача выходит за рамки библиотеки, то начинается смесь собственных решений, библиотеки и нативного кода. Проще и чище загрузить один из бесчисленных npm-пакетов под конкретную задачу и обернуть его.
Больше удручает отсутствие грамотного коробочного шаблона проектирование, как MVVM для WPF. Если проект небольшой, то все прекрасно работает на параметрах и ручном обновлении компонента, но если проект большой, требует хранение состояния и имеет множество каналов для обновления своего состояния, то начинаются "макароны"
winsky
https://mudblazor.com/
использую уже на третьем проекте, хватает с головой.
ну либо вы странного хотите
Enfriz
Но... я ведь упомянул её в своём комментарии... Баг с которым я столкнулся, и который стоил мне нескольких часов работы. Оставил им issue 21 августа, до сих пор никакой реакции. В целом поддержка очень плохая.