Достоинство платформы Blazor в скорости разработки интерактивных веб-страниц. Технология позволяет значительно уменьшить объем написания JavaScript кода, и во многих случаях исключить затраты времени на создание скриптов JavaScript.
Особенно это касается рутинных стандартных операций: обработка событий onclick кнопок, обработка событий HTML DOM Events - onchange, onmouseover, onmouseout, onkeydown, onload и т. д., получение значений текстовых полей и многое другое. Необходимый посреднический сервер-клиент JavaScript код для приложения Blazor создается автоматически.
Основное кодирование в Blazor происходит на прикладном языке C#, при этом поддерживается полное взаимодействие с самостоятельно написанными JavaScript скриптами и сторонними JavaScript-библиотеками.
Приложение-чат Blazor создан по принципу стандартной функциональности веб-чатов. Несколько пользователей обмениваются сообщениями на открытой веб-странице приложения. Пользователи могут отправлять сообщения всей чат-группе или персональные сообщения только выбранным участникам. Приложение демонстрационное, во многом упрощенное, не имеет процедур идентификации пользователей, но предлагает вполне работоспособный принцип создания приложений-чатов на платформе Blazor. На базе данного примера можно создать рабочую версию чата или форума пользователей.
Веб-приложение чата хранит сообщения в базе данных, представляющую из себя текстовый файл json-формата. Данные пользователей фиксированы в виде константных свойств класса User.
Служба Singleton-класса DispatcherChat производит обновление страниц чата всех пользователей приложения. Singleton-класс создаётся и существует всегда в единственном экземпляре: независимо от количества открытых в браузерах веб-страниц приложения Blazor, все компоненты будут взаимодействовать с данными и событиями одного экземпляра.
Часть кода файла Program.cs:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor();
// Создание службы единственного класса, все компоненты
// приложения Blazor будут получать один и тот же экземпляр класса.
builder.Services.AddSingleton();
// Буквенное отображение вместо символов unicode для всех языков мира.
builder.Services.Configure(options =>
{
options.TextEncoderSettings = new TextEncoderSettings(UnicodeRanges.All);
});
var app = builder.Build();
В приложении чата класс DispatcherChat содержит определение события, которое генерируется после отправки сообщения пользователем. Тип события создан собственный и не имеет параметров, задача события простая: обновлять веб-страницу чата всех пользователей после добавления нового сообщения в чат.
// Глобальный Singleton класс обновления страниц чата.
public class DispatcherChat
{
// Тип события
public delegate void MessageEventHandler();
// Событие обновления интерфейса веб-страниц пользователей чата.
public event MessageEventHandler? Refresh;
// Метод вызова события.
public virtual void OnRefresh() => Refresh?.Invoke();
}
Модель-класс User, объявляет минимальный набор данных пользователя. Модель только описывает пользователя, инициализация свойств происходит в классе Users, в принципе представляющего таблицу базы данных.
Модель пользователя:
public class User
{
public readonly int Id;
public readonly string Name;
public readonly string Photo;
public User(int id, string name, string photo)
{
Id = id;
Name = name;
Photo = photo;
}
}
Класс Users имитирует таблицу базы данных. Здесь инициируются данные всех пользователей чата. Веб-приложения Blazor могут работать с любой базой данных и единственная цель использования объектов класса Users - это упрощение и уменьшение программного кода. Несмотря на то, что компоненты Blazor чат будут использовать различные экземпляры класса Users, инициализация у всех будет одинаковая. С некоторой степенью точности можно утверждать, что класс Users обеспечивает функциональность Singleton-класса.
Изменение количества инициализированных пользователей автоматически уменьшит или увеличит число участников чата. Распределение сообщений в новом составе также будет происходить автоматически.
Класс, исполняющий обязанности таблицы базы данных:
public class Users
{
public readonly List ListUsers;
// Конструктор класса инициализирует данные пользователей чата.
public Users()
{
ListUsers = new(){
new(1, "Витя", "chat-vitya.png"),
new(2, "Петя", "chat-petya.png"),
new(3, "Sarah", "chat-sarah.png"),
new(4, "Michel", "chat-michel.png"),
new(5, "Таня", "chat-tanya.png"),
};
}
}
Модель сообщения сформирована классом Message. Модель представляет данные одного сообщения (один элемент списка сообщений) в текстовом json-файле. После отправки сообщения экземпляр класса Message добавляется в список сообщений, который немедленно сохраняется в текстовом файле.
public class Message
{
public DateTime Date { get; set; }
public string? Letter { get; set; }
public int IdFrom { get; set; }
public int IdTo { get; set; }
}
Список сообщений создается классом ChatMessages. Класс имеет одно свойство автоматического типа. Примечание. По умолчанию System.Text.Json.JsonSerializer сериализует только открытые свойства класса. Для сериализации полей необходимо пометить их атрибутом [JsonInclude] или установить опции JsonSerializerOptions.IncludeFields = true.
public class ChatMessages
{
public List Messages { get; set; } = new();
}
Взаимодействие с json-базой данных происходит посредством объектов класса ReadUpdateDB. Класс осуществляет операции чтения, записи и удаления сообщений. Чтение и запись происходит в асинхронном режиме. Для простоты разбора кода в методах отсутствуют функциональность обеспечения параллельного доступа к файловой базе данных нескольких пользователей одновременно. Замена файловой базы на клиент-серверную СУБД успешно решает проблему параллельного доступа к списку сообщений.
Принцип чтения и записи: считывание сообщений в память, добавление нового сообщения и запись обновленных данных обратно в файл.
public class ReadUpdateDB
{
public async Task ReadFromDBAsync()
{
return await Task.Run(() =>
{
try
{
// Чтение сообщений из json-файла.
using FileStream fs = File.OpenRead(Constants.PathDB);
ChatMessages? messages = JsonSerializer.Deserialize(fs);
// Избегаем объектов null.
return messages ?? new();
}
catch
{
// При возможном повреждении json-файла, создаем
// пустой новый.
File.WriteAllText(Constants.PathDB, "{}");
return new ChatMessages();
}
});
}
public async Task WriteToDBAsync(ChatMessages messages)
{
await Task.Run(() =>
{
var serializerOptions = new JsonSerializerOptions
{
// Формирует вид, удобный для чтения и печати.
WriteIndented = true,
// Настройка кодировки символов для кириллицы.
// По умолчанию сериализатор выполняет escape - последовательность символов,
// отличных от ASCII.То есть он заменяет их uxxxx,
// где xxxx является кодом Юникода символа.
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All)
};
// Запись массива сообщений в json-файл.
using FileStream fs = File.OpenWrite(Constants.PathDB);
JsonSerializer.Serialize(fs, messages, typeof(ChatMessages), serializerOptions);
});
}
// Удаление всех сообщений из файловой базы данных.
public void DeleteMessages()
{
File.WriteAllText(Constants.PathDB, "{}");
}
}
После загрузки приложения сообщения считываются в память и выводятся на веб-страницы пользователей:
Пользователи создают сообщения и нажимают на одну из кнопок выбора получателей.
Обновленный массив сообщений записывается в файловую базу. (При использовании файловой базы данных возможны артефакты из-за отсутствия возможности параллельной записи в файл)
Веб-страница вывода сообщений представляет компонент Blazor адрес которой определён директивой @page. Компонент отвечает за взаимодействие с пользователями: считывает введенный в окно textarea текста сообщения, сохраняет его в базе и генерирует событие обновления интерфейса у всех пользователей. Для уменьшения программного кода веб-страница имеет вложенный компонент обработки сообщений.
Немного отступая от темы, можно заметить виртуозную работу синтаксиса разметки Razor, логика которого четко отделяет код языка C# от разметки HTML. Работу синтаксиса Razor можно увидеть в дополнительных компонентах ParseMessages и ItemMessage.
Программный код Blazor компонента веб-страницы чата ChatPage.razor:
@page "/chat/{id:int?}"
@* id - регистр букв не учитывается*@
@using BlazorServerChat.Data
@using BlazorServerChat.Models
@using BlazorServerChat.Pages.Components
@inject DispatcherChat _classChat
<PageTitle>
Пользователь: @(userCurrent == null ? "..." : userCurrent.Name)
</PageTitle>
<header>
<img src="assets/img/group_chat_96x96.png">
<h1 class="display-6">Комната чата</h1>
<p class="lead">Технологии Blazor</p>
</header>
<div class="row">
<div class="col-lg-8 offset-lg-2">
<section class="text-start">
<a href="">На главную ></a>
<h4 class="text-start text-secondary">
Пользователь: @(userCurrent == null ? "..." : userCurrent.Name)
</h4>
</section>
<section class="px-2 pt-1" >
@* Вложенный компонент распределения сообщений между пользователями *@
<ParseMessages Id=@Id
ListUsers=@users.ListUsers
Messages=@chatMessages.Messages />
</section>
<section class="text-start mt-3">
<label class="form-label">Ввод сообщения</label>
<textarea style="width: 100%;"
rows="3" maxlength="50"
@bind="@textMessage" autofocus></textarea>
@* Кнопки выбора получателя сообщения *@
<div class="text-center mt-1">
@foreach (var user in users.ListUsers)
{
<button class="btn btn-primary m-1"
@onclick="() => SendMessage(user.Id)"
type="button">
@user.Name
</button>
}
<button class="btn btn-success m-1"
@onclick="() => SendMessage(0)"
type="button">
Всем
</button>
<div class="mt-2">
@* Для удобства тестирования создана кнопка удаления всех сообщений *@
<button class="btn btn-danger"
@onclick="() => DeleteMessages()"
type="button">
Удалить все сообщения
</button>
</div>
</div>
</section>
</div>
</div>
@code {
// в url id - регистр букв не учитывается
[Parameter]
public int Id { get; set; }
// Переменная считывания введенного в textarea текста сообщения.
string? textMessage;
// Все пользователи чата.
private readonly Users users = new();
// Интерфейс чтения-записи сообщений.
private readonly ReadUpdateDB chat = new();
// Текущий пользователь
private User userCurrent = new(-1, "", "");
// Интерфейс хранения сообщений в памяти.
private ChatMessages chatMessages = new();
protected override async Task OnInitializedAsync()
{
// Избегаем объекта null.
userCurrent = users.ListUsers.Find(d => d.Id == Id) ?? new User(-1, "", "");
_classChat.Refresh += Update;
// Читаем все сообщения из базы.
chatMessages = await chat.ReadFromDBAsync();
}
// Обновление веб-страниц пользователей.
void Update()
{
InvokeAsync(async () =>
{
chatMessages = await chat.ReadFromDBAsync();
StateHasChanged();
});
}
// Отправка сообщения выбранному пользователю или всем при id=0.
private async Task SendMessage(int id)
{
User? u = users.ListUsers.Find(u => u.Id == Id);
if (textMessage == null || u == null) return;
Message m = new()
{
Date = DateTime.UtcNow,
Letter = textMessage,
IdFrom = Id,
IdTo = id
};
// Новое сообщение добавляем в начало списка,
// чтобы последнее сообщение было первым в окне сообщений.
chatMessages.Messages.Insert(0, m);
// Запишем новое сообщение в базу данных
await chat.WriteToDBAsync(chatMessages);
// Отправляем всем приложениям команду обновления интерфейса.
_classChat.OnRefresh();
// Очистка окна сообщения.
textMessage = null;
}
private void DeleteMessages()
{
// Удаление всех сообщений.
chat.DeleteMessages();
// Отправляем всем приложениям команду обновления интерфейса.
// Очистка окна сообщений.
_classChat.OnRefresh();
}
}
Веб-страница ChatPage.razor имеет вложенный компонент ParseMessages. В этом компоненте находится логика распределения сообщений. В итоге программный код обработки и вывода сообщений сжат до одной строчки:
В свою очередь, компонент ParseMessage также имеет вложенный компонент визуализации сообщений. Чтобы сосредоточить работу над логикой распределения сообщений визуальная часть сообщений выведена в дополнительный компонент.
Одной из главных целей системы компонентов Blazor является уменьшение количества кода выходной веб-страницы путем распределения логической специализации между вложенными компонентами. Такое построение структуры компонентов способствует повышению качества и читабельности программного кода каждого отдельного компонента. Примеры реализации различных компонентов есть на странице Компоненты Blazor данного сайта.
Программный код компонента ParseMessages:
@using BlazorServerChat.Models
@foreach (Message m in Messages)
{
// Избегаем объекта null.
User usercurrent =
ListUsers.Find(d => d.Id == Id) ?? new User(-1, "", "");
User userreceiver =
ListUsers.Find(d => d.Id == m.IdTo) ?? new User(-1, "", "");
User usersender =
ListUsers.Find(d => d.Id == m.IdFrom) ?? new User(-1, "", "");
// Если отправитель не текущий пользователь (не логично от самого себя принимать сообщения),
// но отправление предназначено для текущего пользователя
// или всей группе чата.
if ((m.IdTo == 0 || m.IdTo == Id) && m.IdFrom != Id)
{
string heading =
usersender.Name + "-> для " + usercurrent.Name + " [ " + m.Date + "]";
if (m.IdTo == 0) heading = usersender.Name + "-> всем [ " + m.Date + "]";
// Компонент визуализации сообщений.
<ItemMessage LightBG=true Heading=@heading Letter=@m.Letter />
}
// Если отправитель сообщения текущий пользователь.
if (m.IdFrom == Id)
{
string heading = "Я -> для всех [ " + m.Date + " ] ";
// Если сообщение определенному пользователю.
if (m.IdTo != 0) heading = "Я -> для " + userreceiver.Name + " [ " + m.Date + " ] ";
<ItemMessage LightBG=false Heading=@heading Letter=@m.Letter />
}
}
@code {
[Parameter]
public int Id { get; set; }
[Parameter]
public List<Message> Messages { get; set; } = new();
[Parameter]
public List<User> ListUsers { get; set; } = new();
}