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

Меня это удручает. Давайте попробуем написать что-нибудь полезное и при этом показать вам, что можно и чего не нужно делать с достаточно новой технологией Microsoft под названием Blazor.

Не так давно мне пришлось помогать детишкам разобраться с программированием. Пацаны были маленькими, но глаза их были полны энтузиазма и постоянно слышался вопрос «Как?» и «Почему?». Кто-то из подопечных притащил в студию пару IoT реле, которые позволяли включать и выключать лампочки по сети. Конечно, для нас, Хабравчан, такая штука не очень интересна, но для подростков было как раз то что надо. Почему бы и нет, подумал я, и мы начали писать всякие реле с таймингами, которые позволяют анимировать гирлянды и всё такое. В конце концов, не хочу вас пугать, но конец года уже близится и скоро нам надо будет украшать ёлки и помещения.

Притащенные реле были куплены на алибабе и амазоне. Стоили они копейки и были в довольно большом ассортименте. Дети были рады тому, что первое реле отлично управлялось через HTTP путём посыла сформированных запросов. Такую штуку легко реализовать на любом языке программирования. Посему даже с яваскриптом вопросов не возникло, и все были рады.

Проблемы начались со вторым реле. В нём запросы надо было посылать в виде ASCII строки. А третье реле вообще требовало бинарного ввода на порту. Все программы по миганию гирляндами начали обрастать каким-то нездоровым количеством логики и превращались в костыли.

Было решено реализовать следующую программу:

  1. У нас есть список моделей различных реле.
  2. В списке мы указываем тип передачи данных и список команд, доступный для каждой модели.
  3. Создаём список устройств. Каждое устройство имеет отдельный IP-адрес, имя и название модели.
  4. На основе этой информации создаётся строка управления для каждой команды, каждого устройства, которая выглядит следующим образом:

    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 CoreHABRFIRSTDEDIC.
Доступно до 31 декабря 2021 г.

Комментарии (9)


  1. Enfriz
    25.10.2021 11:05
    +2

    Сам люблю Blazor, но угнетает плачевное состояние комьюнити. Библиотеки компонентов, например, или очень плохие (мало функций, плохая документация) или, блин, платные, за деньги. В опенсорсе, да.


    1. onatsko
      25.10.2021 12:39
      +3

      Radzen Blazor отличные, полностью фри, компоненты


      1. Nurked
        25.10.2021 18:40
        +2

        Добро пожаловать в мир Enterprise Microsoft. Где всем рулит Telerik. Эээх. Да. Весёлые были времена.


      1. Enfriz
        26.10.2021 14:20

        Да, но они страшненькие. Посмотрите на Bootstrap, Framework7, Vuetify — вот как должна выглядеть современная библиотека компонентов для веба и мобайл.

        Пока что лучшее, что я видел (визуально), это MudBlazor. Но там не разбирают Issue с багами, и вообще выглядит так, будто разработку забросили.


        1. Andriy1218
          27.10.2021 22:30
          +1

          AntDesignBlazor тоже не плох. Конечно не идеал, но выбор компонентов большой и выглядит неплохо.


    1. BMTLab
      02.11.2021 12:02
      +2

      Ну библиотеки компонентов скорее зло, чем польза. Особенно, когда библиотека берет на себя роль верстки, и/или, когда задача выходит за рамки библиотеки, то начинается смесь собственных решений, библиотеки и нативного кода. Проще и чище загрузить один из бесчисленных npm-пакетов под конкретную задачу и обернуть его.
      Больше удручает отсутствие грамотного коробочного шаблона проектирование, как MVVM для WPF. Если проект небольшой, то все прекрасно работает на параметрах и ручном обновлении компонента, но если проект большой, требует хранение состояния и имеет множество каналов для обновления своего состояния, то начинаются "макароны"


    1. winsky
      02.11.2021 12:02
      +2

      https://mudblazor.com/
      использую уже на третьем проекте, хватает с головой.
      ну либо вы странного хотите


      1. Enfriz
        02.11.2021 23:20

        Но... я ведь упомянул её в своём комментарии... Баг с которым я столкнулся, и который стоил мне нескольких часов работы. Оставил им issue 21 августа, до сих пор никакой реакции. В целом поддержка очень плохая.


  1. h4r7w3l1
    02.11.2021 12:02
    +1

    Для тех, кто никогда не работал с WebAssembly, рекомендую почитать информацию — здесь

    Ссылочку забыли