Панель администратора (сленговое: админ-панель, админка) предназначена для управления данными сайта. Создание нового и редактирование текущего контента, а также анализ и отражение результатов эффективности работы сайта. У админ-панели много обязанностей, поэтому к ней предъявляются повышенные требования. Она должна быть удобной, информативной и всегда отражать актуальные данные.
Платформа Blazor предназначена для быстрой разработки интерактивных веб страниц. Скорость разработки повышается благодаря единому языку программирования для серверной и клиентской сторон. Общение сервера и клиента происходит в режиме реального времени на скорости интернет-соединения. Как вывод: приложения Blazor идеальны для использования в качестве панели администратора управления сайтам.
Речь идёт о модели размещения Blazor Server в которой основная работа приложения происходит на сервере и результаты отправляются на клиенты. Обновление страниц происходит в фоновом режиме, необходимыми порциями, без перезагрузки всего контента. Библиотека SignalR поддерживает постоянную связь браузера и приложения Blazor. Оповещения о редактируемых данных с панели администратора мгновенно отправляются на сервер.
На основе исходников описанных на страницах сайта компоненты Blazor и вызов функций JavaScript создан прототип, шаблон для интерактивной панели администратора. Blazor AdminPanel представляет собой типичную конфигурацию админки для отображения и редактирования данных: домашняя страница - общий свод, другие страницы - редактирование данных в базе.
Веб приложение панели администратора состоит из нескольких автономных компонентов. Для поддержания актуальной информации при изменениях в базе данных компоненты используют синглетные классы (singleton - класс, допускающий создание только одного экземпляра на протяжении работы приложения). Редактирование данных вызывает события обновления компонентов на всех открытых страницах, даже в разных браузерах. Такие авто-обновляемые компоненты помогают поддерживать визуальные данные на открытых страницах админ-панели всегда в актуальном состоянии. Редактировать данные должен администратор, просматривать могут и пользователи.
Приложение состоит из главной страницы отображения данных и страниц редактирования по группам. На главной странице AdminPanel выводятся все компоненты визуализации данных. Это общая страница, свод всех данных из базы. Данные разделены на группы: доходы и клиенты, технологии веб программирования, виды СУБД, география клиентов.
Каждая группа имеет свою страницу редактирования. Интерфейс работы с базой данных состоит из компонентов визуализации и таблицы редактирования данных. Данные сохраняются в базу при потере фокуса текстовым элементом <input ... @onblur="() => Table.Write()">. После записи изменений в базу результаты без задержки отображаются на всех открытых страницах приложения.
База данных приложения построена на файлах .json. У каждой логической группы свой файл. Файл представляет таблицу. Данные записываются в файл посредством Json сериализации объекта List<Rows>. Объект строк хранится в оперативной памяти, после редактирования данные строк записываются в файл. Формат Json и текстовый файл базы выбран для удобства тестирования веб приложения AdminPanel. В реальный проект можно подключить базу данных любой разновидности.
Модель базы построена на абстрактном классе Table. В данном классе заложена практически вся функциональность для чтения и обновления файлов баз данных формата .json. Table имеет событие генерируемое после записи обновленных данных в файл.
Как видно из листинга ниже в классе два абстрактных члена. Путь к файлам базы и инициализацию данных каждый производный класс переопределяет для себя индивидуально.
Листинг абстрактного класса:
public abstract class Table
{
// Путь к базе.
public abstract string PathTable { get; }
// Методы инициализации данных.
public abstract void Init();
// Делегат события обновления таблицы базы данных.
public delegate void UpdateEventHandler(object sender);
// Событие обновления таблицы базы данных.
public event UpdateEventHandler UpdateEvent;
// База в памяти. Строки таблицы.
// Читается из файла и сохраняется в оперативной памяти.
public List Rows { get; set; } = new List();
public Table()
{
// Первоначальное заполнение таблицы в памяти.
Read();
}
// Запись в файл базы данных.
public void Write()
{
// Перед записью пересчитаем процентное содержание.
// У кого это есть. Другим это не помешает.
ComputePercents();
// Запись базы в файл
var serializerOptions = new JsonSerializerOptions
{
// Формирует вид, привлекательный для чтения и печати.
WriteIndented = true,
// Настройка кодировки символов для кириллицы.
// По умолчанию сериализатор выполняет escape - последовательность символов,
// отличных от ASCII.То есть он заменяет их на uxxxx,
// где xxxx является кодом Юникода символа.
Encoder = JavaScriptEncoder.Create(UnicodeRanges.All, UnicodeRanges.Cyrillic)
};
string s = JsonSerializer.Serialize(Rows, serializerOptions);
using var sw = new StreamWriter(PathTable);
sw.Write(s);
sw.Close();
// После записи в файл базы,
// обновляем данные строк таблиц в оперативной памяти.
Read();
// Событие обновления после записи.
UpdateEvent?.Invoke(this);
}
// Чтение из базы в память
public void Read()
{
using var streamReader = new StreamReader(PathTable);
Rows = JsonSerializer.Deserialize>(streamReader.ReadToEnd());
streamReader.Close();
}
// Вычисляет проценты для кого необходимо.
private void ComputePercents()
{
// Распределение процентов
int _100Percents = 0;
foreach (var i in Rows)
{
_100Percents += i.NumCustomers;
}
foreach (var i in Rows)
{
double percent = ((double)i.NumCustomers / (double)_100Percents) * 100.0;
i.Percent = Math.Round(percent, 2);
}
}
}
Унификация данных достигается посредством универсального класса строки таблицы. Одновременно все параметры строки не используются ни в одной таблице, каждый производный класс имеет свой набор данных. Но использование единственного класса строки и общего абстрактного класса позволили значительно уменьшить количество кода. Производные же классы заполняют свои таблицы только необходимыми значениями, "ненужные" свойства просто игнорируются.
Листинг универсального класса:
// Универсальный шаблон строки для таблиц данных.
public class Row
{
// Название параметра
public string Name { get; set; }
// Количество клиентов
public int NumCustomers { get; set; }
// Процентное содержание
public double Percent { get; set; }
// Двузначный код страны
public string ISO { get; set; }
// Название месяца
public string Month { get; set; }
// Доход
public int Earnings { get; set; }
}
Как упоминалось выше, база данных состоит из 4-х таблиц и, соответственно, из такого же количества классов производных от Table. Данные таблиц универсальные и подойдут для административных панелей различных сайтов.
Список и характеристика классов-таблиц:
EarningsTable
Доходы и клиенты. Данные: общий доход, средний за месяц, кол-во клиентов, рост дохода по сравнению с прошлым годом. Диаграммы доходов и регистрации клиентов.
WebProgTable
Веб программирование. Данные: виды технологий программирования, процентное содержание каждого вида. Визуализирует соотношения компонент progressbar.
DBMSTable
Виды баз данных. Данные: виды СУБД, процентное содержание каждого вида. Визуализирует соотношения компонент progressbar.
RegionsTable
География клиентов. Данные: кол-во клиентов в регионах, процентное соотношение клиентов в каждом регионе. На графической карте мира выделяются цветом регионы клиентов.
Программный код классов классов WebProgTable, DBMSTable, RegionsTable одинаковый и может быть показан на примере одного класса. Общая функциональность сосредоточена в родительском классе.
Листинг класса-потомка:
public class WebProgTable : Table
{
// Путь к своей таблице
public override string PathTable => Constants.PathTechnologyTable;
// Инициализация своих данных
public override void Init()
{
var r = new Random();
Rows.Add( new Row()
{
Name = "PHP",
NumCustomers = r.Next(10000, 40000)
}
);
...
Write();
}
}
Обход нарушения унификации
В классе таблицы доходов EarningsTable присутствуют свойства итоговых значений принадлежащих только классу доходов. Итоговые данные вычисляются для всей таблицы доходов, но не для каждой строки. Эти данные вычисляются на ходу и хранятся только в оперативной памяти. Наличием итоговых значений этот класс отличается от своих собратьев.
Итоговые данные вычисляются каждый раз после записи обновленных данных в файл. Событие обновления данных в родительском классе возникает после записи в файл, когда итоговые данные еще не подсчитаны. В силу этого, программный код класса EarningsTable заменяет событие и метод записи родителя своими версиями.
Листинг класса-таблицы Доходы-клиенты:
public class EarningsTable : Table
{
// Итоговые данные.
public int TotalEarning { get; set; }
public int TotalCustomers { get; set; }
public int MonthAverage { get; set; }
public int EarningsGrowth { get; set; }
// Скрываем событие родителя своей реализацией.
// Событие обновления итогов.
new public event UpdateEventHandler UpdateEvent;
// Значение для сравнения с текущим годом.
public const int LastYearEarnings = 1800000;
// Путь к файлу базы данных.
public override string PathTable => Constants.PathEarningsTable;
// Инициализация общих итоговых данных.
public EarningsTable() : base()
{
Total();
}
// Только для инициализации данных.
public override void Init()
{
var r = new Random();
Rows.Clear();
for (int i = 0; i < 12; i++)
{
DateTimeFormatInfo dtfi = CultureInfo.GetCultureInfo("ru-RU").DateTimeFormat;
string month = dtfi.GetMonthName(i + 1);
Row item = new Row
{
Month = month,
Earnings = r.Next(80000, 260001),
NumCustomers = r.Next(300, 900)
};
Rows.Add(item);
}
Write();
}
// Скрываем родительский метод своей реализацией.
// После записи родителем данных в файл, подсчитаем итоги.
// Теперь событие гарантировано после подсчёта итоговых данных.
new public void Write()
{
base.Write();
Total();
// Скрыто родительское событие,
// поскольку оно происходит
// до подсчёта итогов.
UpdateEvent?.Invoke(this);
}
// Вычисление итоговых значений.
public void Total()
{
TotalEarning = 0;
TotalCustomers = 0;
foreach (var i in Rows)
{
TotalEarning += i.Earnings;
TotalCustomers += i.NumCustomers;
}
MonthAverage = TotalEarning / Rows.Count;
EarningsGrowth = (int)(TotalEarning / (double)LastYearEarnings * 100.0 - 100.0);
}
}
При редактировании базы данных, компоненты, визуализирующие информацию, должны отражать последние изменения. Для админов и пользователей комфортней, если обновление будет происходить в режиме реального времени, без перезагрузки страницы. Приложения Blazor легко реализуют эту возможность на языке C#.
В шаблоне AdminPanel выполняется автообновление компонентов при изменении информации в базе данных. AdminPanel настроена так, что автоматически обновляет компоненты даже если страницы приложения открыты в разных браузерах. Для этого эффекта приложение использует службы классов-одиночек (singleton). Службы добавляются в методе настройки служб файла startup.cs.
Настройка служб приложения:
public void ConfigureServices(IServiceCollection services)
{
...
// Служба одного экземпляра класса на приложение
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
}
Синглетные службы гарантируют автоматическое обновление компонентов при редактировании базы данных на одной странице. Если открыто несколько вкладок браузера или страницы открыты в разных браузерах синглетных служб недостаточно для тотальных перерисовок компонентов. Для этого в классах таблиц организовано событие обновления данных. Оно генерируется после записи изменений в файл.
Программный код организации события. Полностью листинг класса см. выше.
public abstract class Table
{
...
// Событие обновления таблицы базы данных.
public event UpdateEventHandler UpdateEvent;
// База данных в памяти.
public List Rows { get; set; } = new List();
...
// Запись в файл базы данных.
public void Write()
{
...
using var sw = new StreamWriter(PathTable);
sw.Write(s);
sw.Close();
// Обновляем данные в оперативной памяти.
// Именно они отражаются на визуальных компонентах.
Read();
// Генерация события для обновления компонентов
// после записи файл и чтения в Rows.
UpdateEvent?.Invoke(this);
}
}
В качестве примера опишем код самообновляемого компонента с JavaScript диаграммой. Компонент показывает анимированный график доходов, извлекая данные из класса-таблицы EarningsTable (Доходы и клиенты). Параметр компонента Models.EarningsTable earningsTable представляет синглтон (класс-одиночка) получаемый от родительского компонента. После редактирования базы, на любой странице приложения AdminPanel, генерируется событие, перерисовывающее данный компонент.
Другие компоненты приложения построены подобным образом. Неважно в каком месте происходит запись новых данных, класс-singleton распространяет это событие по всем родственным компонентам.
@using Microsoft.JSInterop
@inject IJSRuntime JSRuntime
@* Обязательно для вызова метода Dispose, иначе метод вызываться не будет *@
@implements IDisposable
<div class="card shadow">
<div class="card-header d-flex justify-content-between align-items-center">
<h6 class="text-secondary font-weight-bold m-0">Доходы</h6>
</div>
<div class="card-body">
<div class="chart-area"><canvas id="chartEarnings"></canvas></div>
</div>
</div>
@code {
// Родительский компонент в этот параметр укажет объект-синглтон.
[Parameter]
public Models.EarningsTable earningsTable { get; set; }
// После того как все элементы DOM веб страницы загружены.
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
earningsTable.UpdateEvent += UpdateDB;
// Активируем диаграмму
await JSRuntime.InvokeVoidAsync("startChartEarnings");
// Обновляем график свежими данными.
await Task.Run(() => UpdateChart());
}
}
private async Task UpdateChart()
{
int[] data = earningsTable.Rows.Select(r => r.Earnings).ToArray();
await JSRuntime.InvokeVoidAsync("changeEarningsData", data);
}
public async void UpdateDB(object sender)
{
await UpdateChart();
}
// Очистка перед удалением компонента.
public void Dispose()
{
earningsTable.UpdateEvent -= UpdateDB;
}
}