Начало статьи по быстрой разработке гипермедиа-ориентированного веб-приложения с HTMX 2.0.
Введение
Представляю вашему вниманию мою первую обучающую статью по разработке приложений, ориентированных на гипермедиа. В этой статье я буду называть их просто гипермедийными приложениями. Моя статья посвящена разработке современного интерактивного приложения с помощью библиотеки Htmx.js на устаревшей платформе.
Мы будем использовать устаревшую серверную платформу ASP.NET MVC 5. Она была выпущена в 2013 году и хотя уже давно прекращена ее поддержка, многие старые проекты, созданные на этой платформе, поддерживаются разработчиками и прекрасно работают до сих пор. Проблема возникает когда такой старый сайт или приложение нужно сделать современным, адаптивным и интерактивным. Вот здесь и место гипермедийным системам. Не нужно уродовать прекрасную монолитную архитектуру старых веб приложения добавлением каких-либо фронтенд-фреймворков. Это напрасная трата вашего времени и сил. С помощью гипермедийных систем вы можете легко вдохнуть в ваше старое приложение вторую жизнь.
Я имею скромный опыт успешной модернизации большого старого веб приложения путем внедрения в него гипермедийных систем. В результате устаревшее приложение стало работать и выглядеть как современное. При этом было сэкономлено значительное количество сил и времени. Никакой фронтенд-фреймворк мной так и не был использован за ненадобностью. Я прошу с пониманием и не слишком критично отнестись к моей статье. Это моя первая статья в таком формате. Я думаю, что мой небольшой опыт модернизации старого приложения может оказаться чрезвычайно полезным некоторым разработчикам, которые работают со старыми веб приложениями. Тем более что на новых версиях серверной платформы ASP.NET Core работать с гипермедийными системами гораздо проще и удобнее.
Приемы работы с гипермедиа системами будут показаны конечно же не на производственном коде, а на примере простой настольной игры. Настольная игра в качестве примера сделает ваше знакомство с гипермедиа легким и увлекательным. Хотя приложение может показаться объемным, но мои примеры будут подробными, максимально упрощенными и сопровождаться пояснениями. Вы без труда можете в них разобраться и потом использовать в своих приложениях.
Аудитория
Эта статья рассчитана прежде всего на разработчиков, желающих познакомится с гипер медийными системами HTMX. Требуется прежде всего знание языка программирования C#, знание самой платформы разработки ASP.NET любой версии не обязательно, но приветствуется.
Прочитав эту статью и проработав несколько примеров из нее, вы познакомитесь с технологией разработки современных интерактивных приложений - гипермедиа. Вы освоите и научитесь создавать базовые гипермедийные элементы. А потому, что в статье используется устаревшая платформа, вы без труда сможете применить полученные знания в своих старых легаси проектах.
Гипермедийные системы
Для начала я в этой статье приведу определение гипермедийным системам от первоисточника, из книги Карсона Гросса.
Мы определяем гипермедиа-систему как систему, которая соответствует сетевой архитектуре RESTful в первоначальном понимании этого термина Роем Филдингом.
~ Карсон Гросс Hypermedia-разработка. Htmx и Hyperview
Рой Филдинг (Roy Thomas Fielding) — американский инженер, один из основных авторов спецификации HTTP и родоначальник архитектурного стиля Representational State Transfer (REST).
Карсон Гросс (Carson Gross) — разработчик библиотеки htmx.js и автор книги по гипермедийным системам «Hypermedia‑разработка. Htmx и Hyperview».
Именно на это замечательное определение мы и будем опираться в этой статье. Итак, гипертекст — это текст, сформированный с помощью языка разметки (например, HTML) с расчётом на использование гиперссылок. Гипермедиа же является логическим и техническим развитием гипертекста. Гипермедиа — это гипертекст, в который включены графика, звук, видео, данные и ссылки для создания совокупности нелинейной среды информации.
А в целом гипермедийная система — это объединение гипермедийного элемента на веб‑странице, серверного кода поддержки этого гипермедийного элемента и сетевой коммуникации между ними.
Рассмотрим это на примерах. Возьмем простой тег якоря, встроенный в более крупный HTML‑документ. Этот элемент является гиперссылкой на другой документ.
Листинг: Простая гиперссылка
<a href="http://www.yandex.ru/">
Яндекс
</a>
Это пример простейшего гипермедийного элемента, который распознает и обрабатывает браузер. Браузер можно назвать гипермедиа клиентом, а сервер — гипермедиа сервером. Далее посмотрим на реализацию простой гипермедийной кнопки на базе HTML с использованием библиотеки Htmx.js.
Это пример простейшего гипермедийного элемента, который распознает и обрабатывает браузер. Браузер можно назвать гипермедиа клиентом, а сервер — гипермедиа сервером. Далее посмотрим на реализацию простой гипермедийной кнопки на базе HTML с использованием библиотеки Htmx.js.
Листинг: Простая гипермедийная кнопка
<button hx-get="/counter" hx-target="#counter">
Прочитать счетчик
</button>
Эта кнопка на базе Htmx.js обменивается гипермедиа с сервером точно так же, как и гиперссылка. Библиотека Htmx.js добавляет тегу <button> этой кнопки функциональность. Эта кнопка, к примеру, отправляет на указанный в атрибуте hx-get адрес GET-запрос, а ответ от этого запроса вставляет внутрь элемента с тегом counter. Такая кнопка — это яркий пример гипермедийного элемента.
Конечно, есть и другие, похожие на Htmx.js библиотеки. Да, были и другие неудачные попытки оживить HTML разными путями. Но только теперь эта библиотека предоставляет удачный вариант развития гипермедиа и становится все более популярной.
Создание пробного приложения
Вначале создадим пробное гипермедийное приложение в среде разработки Visual Studio 2022. Я не буду подробно рассматривать процесс установки этой среды разработки, пакета разработки и процесс создания проекта. Эту информацию можно найти в свободном доступе в сети Интернет, а материал статьи прежде всего фокусируется на работе с гипер медийными приложениями. Мы просто создадим пробное приложение с одной гипермедийной кнопкой для того чтобы вас познакомить с гипер медийными системами.
Для начала создадим новый проект в среде разработки Visual Studio 2022. Выберите пункт меню «Создать» в главном меню «Файл» — далее подпункт «Проект...». Откроется окно «Создание проекта». Отфильтруйте список проектов по параметрам «C#», «Windows» и «Веб» и выберите из списка проектов тип проекта «Веб‑приложение ASP.NET (.NET Framework)».
Дайте название новому проекту и решению HelloHypermedia и выберите платформу .NET Framework 4.8. Нажмите кнопку “Далее” чтобы продолжить.
Возможно вы не сможете найти в списке доступных для выбора вариантов устаревший проект платформы .NET Framework. Тогда вамследует дополнительно установить в установщике Visual Studio Installer компонент «Шаблоны проектов и элементов.NET Framework».
На следующей странице выберите шаблон «MVC», а остальные параметры оставьте без изменения и нажмите кнопку «Создать».
Среда разработки создаст для нас новый проект, наполненный стартовыми образцами. Мы не будем в этом примере удалять стартовые образцы. Мы фокусируемся прежде всего на знакомстве с первой гипермедийной системой.
Первый гипермедийный элемент мы будем добавлять на главную страницу приложения. Найдите в папке View и в подпапке Home файл Index.cshtml и замените его содержимое начальным наполнением из листинга ниже.
Листинг: Визуальное представление Views/Home/Index.cshtml
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Введение в гипермедиа</title>
</head>
<body>
Пустая страница
</body>
</html>
Мы изменили главную страницу приложения для того, чтобы на нем просто отображался заголовок. Пока это просто пустая страница.
Первая гипермедийная система
С помощью гипермедийной системы мы можем интерактивно запросить данные с контроллера и обновить отображаемую страницу в браузере. Давайте добавим первую гипер медийную систему, которая будет отображать приветствие на странице.
Вначале нам необходимо добавить на страницу импорт библиотеки Htmx.js в визуальное представление. Затем нужно добавить гипермедийной системы. Приведите файл визуального представления Index.cshtml к виду на следующем листинге. В листинге комментариями выделены блоки, которые необходимо добавить.
Листинг: Визуальное представление Views/Home/Index.cshtml с гипермедиа кнопкой
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Введение в гипермедиа</title>
<script src="https://unpkg.com/htmx.org@2.0.1"></script> // добавить
</head>
<body>
<h1>Первое гипермедийное приложение</h1> // заменить
<button hx-get="/home/hello"> // добавить
Привет
</button>
</body>
</html>
Этой строкой <script src="https://unpkg.com/htmx.org@2.0.1"></script> мы добавляем в визуальное представление библиотеку поддержки гипермедийных систем Htmx с сервера unpkg.com.
Ниже мы добавляем кнопку гипермедийной системы, которая будет осуществлять GET-запрос по указанному адресу и вставлять в себя результат выполнения запроса.
В контроллер Home нужно добавить новый метод гипермедийной системы Hello, который в качестве результата должен передавать сообщение и текущее время. Откройте в папке Controllers файл HomeController.cs и произведите в нем изменения как в следующем листинге. Удалите лишние методы и добавьте вместо них один новый с именем Hello().
Листинг: Класса HomeController с методом гипермедийной системы
using System.Web.Mvc;
namespace HelloHypermedia.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
public string Hello() // Удалить другие и добавить этот метод
{
return "Привет, гипермедиа!";
}
}
}
Этот метод в качестве результата выполнения возвращает текстовую строку с приветственным сообщением. При нажатии на кнопку эта строка будет отображена внутри кнопки. Текст кнопки будет заменен.
Запустите приложение на выполнение и кликните по кнопке. Вы должны увидеть представленный на следующих скриншотах результат. В верхней части страница до нажатия кнопки, а в нижней части — после нажатия.
Всеместе кнопка в визуальном представлении, метод действия в контроллере и средство коммуникации между ними — это гипермедийная система. После этого успешного знакомства с гипер медийными системами можно перейти к созданию веб приложения.
Гипермедийное приложение
Моя цель заключается в демонстрации гипермедиа в действии, поэтому я опускаю некоторые детали, которые не будут подробно рассмотрены в этой статье.
Итак, в этой статье, как мной ранее было объявлено, мы создадим настольную онлайн игру «Шпион». Наша игра будет случайным образом распределять роли игроков — мирный или шпион. Мы подразумеваем, что игроки будут сидеть кругом за одним столом и будут взаимодействовать с игрой через открытое в браузерах телефонов веб‑приложение. Так как в таких играх не требуется навороченный интерфейс пользователя, то гипермедиа системы отлично подойдут для такого рода приложений.
План
Необходимы следующие возможности нашей настольной игры:
Главная страница, где отображается приветствие настольной игры и форма регистрации в игре;
Страница ожидания всех участвующих в игре игроков с кнопкой запуска игры;
Страница игры с отображением информации о своих ролях в игре и с формой голосования за шпиона;
Страницы отображения результатов игры — победы или проигрыша.
Далее в следующих разделах этой главы мы создадим новый проект настольной игры, настроим его и будем постепенно его наращивать, добавляя новые игровые возможности.
Проект
Создайте новый проект в среде разработки Visual Studio 2022 по аналогии с предыдущем примере, но введите название проекта SpyOnlineGame. Также выберите из списка проектов тип «Веб‑приложение ASP.NET (.NET Framework)» и сделайте все по аналогии. После того, как среда разработки опять создаст для нас новый проект со стартовыми образцами, можно перейти непосредственно к созданию веб приложения.
В этой статье мы будем использовать только такие, наполненные стартовыми образцами проекты ради уменьшения объема статьи и упрощения примера.
Подготовка проекта
Откройте веб настройки проекта. Щелкните правой кнопкой мыши по проекту — «Свойства» — вкладка «Веб». Измените стартовую страницу и установите порт 44 300, как на следующем скриншоте.
Мы настроили стартовую страницу, адрес и порт, на котором будет запускаться наше веб-приложение в режиме отладки.
Откройте проект и замените содержимое файла контроллера Home содержимым, представленным в листинге ниже. Удалите неиспользуемые методы действия, оставьте только начальный метод действия Index. Пусть при начальном запуске приложения будет показано визуальное представление по-умолчанию.
Листинг: Замена содержимого файла HomeController.cs
using System.Web.Mvc;
namespace SpyOnlineGame.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
}
Нужно заменить содержимое этого визуального представления по-умолчанию. На листинге ниже представлено содержимое файла Index.cshtml в папке Views/Home. Необходимо привести этот в файл к виду, представленному в следующем листинге. Пусть пока отображает только стилизованную приветственную информацию о игре.
Листинг: Новое содержимое визуального представления Index.cshtml
@{
ViewBag.Title = "Главная";
}
<div class="my-4 p-5 bg-danger text-light rounded">
<h1 class="text-center">Игра "Шпион"</h1>
<p>
Онлайн версия настольной игры "Шпион"
</p>
</div>
Дополнительно удалите ненужные визуальные представления в папке Views/Home About.cshtml, Contact.cshtml, а в папке Views/Shared Error.cshtml.
В этой статье мы по-максимуму будем использовать стартовый шаблон. Поэтому мы будем только немного редактировать изначальные файлы-образцы стартового проекта. Отредактируйте изначальный шаблон Views/Shared/_Layout.cshtml. Этот шаблон будут использовать все визуальные представления нашего приложения. Измените этот шаблон так, как на следующем листинге.
Листинг: Измененный шаблон _Layout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - Настольная игра "Шпион"</title> // Добавить
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
</head>
<body>
<header> // Заменить
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-dark
bg-danger"> // Изменить
<div class="container">
@Html.ActionLink("Настольная игра \"Шпион\"", "Index", "Home",
null, new { @class = "navbar-brand" }) // Изменить
<button type="button"
class="navbar-toggler" data-bs-toggle="collapse"
data-bs-target=".navbar-collapse" title="Переключить навигацию"
aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse d-sm-inline-flex
justify-content-between">
</div>
</div>
</nav>
</header>
<div class="container"> // Добавить
<main role="main" class="pb-5 pt-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer bg-danger text-light"> // Заменить
<div class="container py-3">
<p>© @DateTime.Now.Year - Настольная игра "Шпион"</p>
</div>
</footer>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("scripts", required: false)
</body>
</html>
Комментариями отмечены строки, в которых нужно произвести изменения. В этой статьем мы будем использовать сразу стилизованные визуальные представления в целях уменьшения объема статьи.
И наконец, замените содержимое файла каскадных таблиц стилей Content/Site.css, чтобы стилизовать нашу настольную онлайн игру оригинальным образом.
Листинг: Измененная каскадная таблица стилей Site.css
html {
position: relative;
min-height: 100%;
}
body {
margin-bottom: 80px;
}
.footer {
position: absolute;
bottom: 0;
width: 100%;
}
.validation-summary-errors {
font-weight: bold;
color: #f00;
}
.input-validation-error {
border: 1px solid #f00;
background-color: #fee;
}
После запуска приложения откроется окно браузера и в нем будет показана главная страница с приветственной информацией о нашей настольной онлайн игре «Шпион». Ниже на скриншоте показана стартовая страница запущенного в режиме отладки приложения.
Страница с правилами игры
Участникам игры наверняка захочется прочитать правила игры. Мы создадим отдельную страницу, на которой будем отображать правила игры.
Добавим еще один метод действия в контроллер, назначение которого — отображать правила игры. Добавьте показанный на листинге ниже дополнительный метод действия.
Листинг: Дополненный еще одним методом главный контроллер
using Microsoft.AspNetCore.Mvc;
namespace SpyOnlineGame.Controllers;
public class HomeController : Controller
{
public IActionResult Index()
{
return View();
}
public IActionResult Rules() // Добавить
{
return View();
}
}
Добавим новое визуальное представление для отображения правил игры. Добавьте визуальное представление с названием Rules.cshtml в папку Views/Home.
Нажмите правой кнопкой мыши на папку Views/Home и в появившемся меню выберите пункт «Добавить» — «Вид». В открывшемся окне выберите «Представление MVC 5» и нажмите кнопку «Добавить». А в окне «Добавление представления» введите только новое имя визуального представления — Rules, как показано на следующем скриншоте. После нажатия кнопки «Добавить» будет создано новое визуальное представление.
Наполните это визуальное представление правилами игры, как в следующем листинге.
Листинг: Содержимое визуального представления с правилами Rules.cshtml
@{
ViewBag.Title = "Правила игры";
}
<h4>Правила</h4>
<p>Среди нескольких игроков затаился случайно выбранный 1 шпион.</p>
<p>Загадано одно случайное место, про которое не знает шпион, но знают мирные игроки.</p>
<p>Сначала один случайный игрок задает вопрос про место другому любому игроку про это место так, чтобы не догадался шпион. Потом тот - следующему любому и так по кругу.</p>
<p>Цель вопросов - определить кто шпион, при этом не выдать загаданное место, так как шпион про него ничего не знает. Ответы на вопросы должны показать, что вы не шпион, при этом вы не должны выдать загаданное место шпиону.</p>
<p>Есть несколько попыток угадать кто шпион. Если мирные не угадают, то побеждает шпион.</p>
<h5 class="mt-2">Варианты мест:</h5>
<ol class="list-group list-group-numbered">
<li class="list-group-item">Банк</li>
<li class="list-group-item">Казино</li>
<li class="list-group-item">Больница</li>
<li class="list-group-item">Офис</li>
<li class="list-group-item">Казино</li>
</ol>
<h5 class="mt-2">Примеры вопросов:</h5>
<ol class="list-group list-group-numbered">
<li class="list-group-item">Хорошо ли там кормят?</li>
<li class="list-group-item">Тяжело ли туда попасть?</li>
<li class="list-group-item">Ты рад был бы туда попасть?</li>
</ol>
Для того, чтобы участники могли ознакомится с этими правилами, изменим пункт главного меню так, чтобы он теперь ссылался на страницу правил игры.
Листинг: Шаблон страниц Views/Shared/_Layout.cshtml с новым пунктом меню
…
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm
navbar-dark bg-danger">
<div class="container">
@Html.ActionLink("Настольная игра \"Шпион\"", "Index", "Home",
null, new { @class = "navbar-brand" })
<button type="button" class="navbar-toggler"
data-bs-toggle="collapse" data-bs-target=".navbar-collapse"
title="Переключить навигацию"
aria-controls="navbarSupportedContent"
aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse d-sm-inline-flex
justify-content-between">
<ul class="navbar-nav flex-grow-1">
<li>@Html.ActionLink("Правила", "Rules", "Home", null,
new { @class = "nav-link" })</li> // Добавить
</ul>
</div>
</div>
</nav>
</header>
…
И добавим кнопку перехода на страницу с правилами игры на главную страницу игры.
Листинг: Главная страниц Views/Home/Index.cshtml со кнопкой
…
<div class="my-4 p-5 bg-danger text-light rounded">
<h1 class="text-center">Игра "Шпион"</h1>
<p>
Онлайн версия настольной игры "Шпион"
</p>
@Html.ActionLink("Правила игры", "Rules", null,
new { @class ="btn btn-primary mt-2"}) // Добавить
</div>
…
Для краткости остальной код не показан. Ниже на скриншоте вы можете увидеть, какой результат должен получиться при запуска в режиме отладки приложения.
Далее нам нужно добавить на главную страницу форму регистрации участников игры. Начнем с добавления модели данных.
Модель данных
В MVC М обозначает модель, и это является самой важной частью MVC приложения. Папка Models будет наполняться моделями веб приложения. Модель, которая часто упоминается как доменная модель, содержит C# объекты (известные как доменные объекты), которые составляют суть нашего приложения, и методы, которые позволяют нам манипулировать ими. Это наиболее важная часть любого веб приложения.
Нам не нужна сложная доменная модель для настольной онлайн игры SpyOnlineGame. Мы создадим лишь несколько моделей нашего приложения, которые позволят пользователям зарегистрироваться в игре, ожидать регистрации других участников, а затем играть в нее.
По MVC соглашению классы, которые составляют модель, помещаются в папку Models. Щелкните правой кнопкой мыши по Models в окне Обозреватель решений и выберите Добавить, за которым следует Класс…, из всплывающего меню. Назовите файл Player.cs и нажмите кнопку Add, чтобы создать класс. Измените содержимое этого класса в соответствии с содержимым листинга ниже.
Листинг: Содержимое модели Player
namespace SpyOnlineGame.Models
{
public class Player
{
public int Id { get; set; }
public string Name { get; set; }
}
}
Эта модель будет хранить информацию о каждом зарегистрированном участнике игры. Регистрироваться пользователи смогут в форме регистрации на главной странице приложения.
Форма ввода данных
Для формы регистрации участников игры создадим отдельную веб модель регистрации участников. Создайте новую папку «Web». В этой папке будут находиться все веб модели и сервисы. В этой папке создайте еще одну папку с названием «Models». И уже в эту папку добавьте новый класс RegistrationWebModel. Этот класс — это обычная веб модель визуального представления, выполняющая функцию приема данных с формы. Наполните этот класс содержимым следующего листинга.
Листинг: Содержимое веб модели RegistrationWebModel
using System.ComponentModel.DataAnnotations;
using SpyOnlineGame.Models;
namespace SpyOnlineGame.Web.Models
{
public class RegistrationWebModel
{
[Display(Name = "Ваше имя:")]
public string Name { get; set; }
public Player Map()
{
return new Player
{
Name = Name,
};
}
}
}
Теперь добавим форму ввода данных на главную страницу Index.cshtml. Перед этим добавьте импорт пространства имен веб моделей в конфигурационный файл Views/Web.config, как на следующем листинге.
Листинг: Изменения в конфигурационном файле Web.config
…
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=5.2.9.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" />
<pages pageBaseType="System.Web.Mvc.WebViewPage">
<namespaces>
…
<add namespace="SpyOnlineGame" />
<add namespace="SpyOnlineGame.Models" /> // Добавить
<add namespace="SpyOnlineGame.Web.Models" /> // Добавить
</namespaces>
</pages>
</system.web.webPages.razor>
…
Добавьте новое содержимое в этот файл визуального представления главной страницы, которое показано в листинге ниже.
Листинг: Форма ввода данных на главной странице Index.cshtml
@model RegistrationWebModel
@{
ViewBag.Title = "Главная";
}
<div class="my-4 p-5 bg-danger rounded">
<h1 class="text-center">Игра "Шпион"</h1>
<p>
Онлайн версия настольной игры "Шпион"
</p>
<a asp-action="Rules" class="btn btn-dark">Правила игры</a>
</div>
<div class="p-2 bg-light border rounded"> // Добавить весь блок
@using (Html.BeginForm("Registration", "Home", FormMethod.Post))
{
<h5>Регистрация нового участника игры</h5>
<div class=”form-group”>
@Html.LabelFor(m => m.Name, new { @class="form-label" })
@Html.TextBoxFor(m => m.Name, new { @class="form-control" })
</div class=”my-2”>
<input class="btn btn-primary" type="submit"
value="Зарегистрироваться" />
}
</div>
Выражение @model в самом верху страницы определяет тип объекта, который это визуальное представление получает из метода действия контроллера. Мы описали элементы label и input для свойства Name веб модели регистрации нового участника игры с помощью вспомогательных методов LabelFor() и TextBoxFor().
Методом BeginForm() задается отрисовка формы ввода данных, а в параметрах этого метода задается настройка формы. Сейчас она будет отрисована для отправки данных формы в метод действия Registration POST-запросом. Такого метода действия еще нет, нужно его добавить в контроллер.
Получение данных с формы
После клика по кнопке отправки данных — этот элемент input с атрибутом типа submit — браузер соберет данные с формы и отправит их на сервер. Добавьте метод получения данных регистрации пользователя с формы главной страницы приложения через POST‑запрос. Ниже в листинге показан код метода Registration(), который нужно добавить в контроллер Home.
Листинг: Новый метод действия для получения данных с формы в контроллере
using System.Web.Mvc;
using SpyOnlineGame.Web.Models; // Добавить
namespace SpyOnlineGame.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost] // Добавить
public ActionResult Registration(RegistrationWebModel model) // Добавить
{
return View("Index");
}
public ActionResult Rules()
{
return View();
}
}
}
Мы добавили атрибут HttpPost в этот метод для указания того, что этот метод обрабатывает только входящие POST-запросы. Этот метод действия должен сохранить данные о новом участнике в игре и перенаправить на страницу ожидания всех игроков.
Создайте новую папку Data в проекте и добавьте в него новый класс PlayersRepository. Он будет ответственен за хранение данных о зарегистрированных участниках в памяти. Наполните новый файл содержимым, которое представлено в следующем листинге.
Листинг: Хранитель данных об участниках в памяти
using System.Collections.Generic;
using System.Linq;
using SpyOnlineGame.Models;
namespace SpyOnlineGame.Data
{
public static class PlayersRepository
{
private static List<Player> _players = new List<Player>();
private static int _lastId = 1;
public static IEnumerable<Player> All => _players;
public static Player GetById(int id)
{
return All.FirstOrDefault(p => p.Id == id);
}
public static int Add(Player player)
{
player.Id = _lastId++;
_players.Add(player);
return player.Id;
}
public static void Remove(int id)
{
var deleted = GetById(id);
if (deleted is null) return;
_players.Remove(deleted);
}
}
}
Каждому добавленному участнику в хранилище автоматически присваивается уникальный ключевой идентификатор, который будет использоваться в других частях приложения. Такой идентификатор однозначно идентифицирует каждого участника игры. Сам класс PlayersRepository и его члены снабжены ключевым словом static для облегчения сохранения и получения данных в разных частях приложения.
Сохранение сведений о регистрациях участников игры
Для сохранения сведений о зарегистрированном участнике нужно обновить метод действия в главном контроллере. Проведите изменения в контроллере, как на листинге ниже.
Листинг: Новый метод действия для получения данных с формы в контроллере
using System.Web.Mvc;
using SpyOnlineGame.Data; // Добавить
using SpyOnlineGame.Web.Models;
namespace SpyOnlineGame.Controllers
{
public class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
[HttpPost]
public ActionResult Registration(RegistrationWebModel model)
{
var player = model.Map(); // Добавить
var id = PlayersRepository.Add(player); // Добавить
return RedirectToAction("Index", "Wait", new { id }); // Заменить
}
public ActionResult Rules()
{
return View();
}
}
}
Эта новая версия метода действия Registration с атрибутом HttpPost получает данные с формы HTML в виде объекта и производит сохранение этого объекта в репозиторий. Вместо возвращения представления мы теперь используем метод RedirectToAction, чтобы произвести перенаправление зарегистрированного участника на страницу ожидания участников - это подготовительный перед началом игры. Такой подход очень часто используется в веб-приложениях и прост для понимания: заполнение формы - получение данных с формы - перенаправление.
Итак мы добавили перенаправление на контроллер ожидания всех игроков Wait и его метод действия Index, но не добавили сам контроллер. Следует следующим шагом создать новый контроллер, но перед этим необходимо обсудить одно соглашение.
Суть соглашения в том, что данные в визуальные представления следует передавать и получать без использования примитивных типов. Это делает немного чистыми как визуальное представление, так и метод действия контроллера. Потому перед добавлением контроллера добавим новую веб модель ожидания участников игры. Создайте в папке Web новую папку Models и в эту папку добавьте новый класс WaitWebModel. Этот класс будет ответственен за передачу данных в визуальные представления. Наполните этот класс содержимым из листинга ниже.
Листинг: Веб модель WaitWebModel
using System;
using System.Collections.Generic;
using SpyOnlineGame.Models;
namespace SpyOnlineGame.Web.Models
{
public class WaitWebModel
{
public int Id { get; set; }
public Player Current { get; set; }
public IEnumerable<Player> All { get; set; } = Array.Empty<Player>();
}
}
Данная веб модель будет хранить в себе данные для отрисовки списка зарегистрированных участников и данных самого зарегистрированного участника. После регистрации участнику присваивается уникальный идентификатор.
После этого давайте добавим уже этот контроллер. Добавьте в папку Controllers новый пустой контроллер и дайте ему название WaitController. Наполните этот новый контроллер содержимым листинга, представленного далее.
Листинг: Контроллер ожидания участников Wait
using System.Linq;
using System.Web.Mvc;
using SpyOnlineGame.Data;
using SpyOnlineGame.Web.Models;
namespace SpyOnlineGame.Controllers
{
public class WaitController : Controller
{
public ActionResult Index(int id)
{
var all = PlayersRepository.All.ToArray();
var player = PlayersRepository.GetById(id);
if (player is null) return new HttpNotFoundResult();
var model = new WaitWebModel
{
Id = id,
Current = player,
All = all,
};
return View(model);
}
}
}
В этом контроллере подготавливаются данные для показа участнику, который ждет других игроков. Ему следует показать его имя и список всех зарегистрированных участников. Данные по текущему участнику мы можем получить по идентификатору, который был передан с метода действия по регистрации участника. Визуальное представление, которое будет отображено зарегистрированному участнику, следует добавить следующим.
Добавьте в папку Views новую папку Wait. Добавьте в эту папку новое пустое визуальное представление. Наполните это представление новым содержимым.
Листинг: Первоначальное наполнение страницы ожидания Wait/Index.cshtml
@model WaitWebModel
@{
ViewBag.Title = "Ожидание";
Layout = ~/Views/Shared/_GameLayout.sshtml
}
<h4>@ViewBag.Title</h4>
<p>Ожидание окончания регистрации всех участников игры.</p>
<div class="my-1">
<label>Ваше имя:</label>
<p><strong>@Model.Current.Name</strong></p>
</div>
<div class="my-1">
<p>Все зарегистрированные игроки:</p>
<table class="table table-striped table-bordered">
<thead>
<tr><th>Id</th><th>Имя</th></tr>
</thead>
<tbody>
@foreach (var each in Model.All)
{
<tr>
<th scope="row" class="align-middle">@each.Id</th>
<td class="align-middle">@each.Name</td>
</tr>
}
</tbody>
</table>
</div>
В этом представлении мы используем отличный от других представлений шаблон страниц с названием _GameLayout.cshtml. Создайте этот шаблон в папке Views/Shared и полностью замените его содержимое представленным на следующем листинге.
Листинг: Шаблон для игровых страниц _GameLayout.cshtml
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@ViewBag.Title - Настольная игра "Шпион"</title>
@Styles.Render("~/Content/css")
@Scripts.Render("~/bundles/modernizr")
@Scripts.Render("~/bundles/htmx")
</head>
<body>
<header>
<nav class="navbar navbar-dark bg-danger">
<div class="container">
<span class="navbar-brand">Настольная игра "Шпион"</span>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-5 pt-3">
@RenderBody()
</main>
</div>
@Scripts.Render("~/bundles/jquery")
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("scripts", required: false)
</body>
</html>
На основе этого шаблона будут отрисованы только игровые страницы ожидания регистрации игроков и страница игры.
Вы можете запустить приложение и проверить работоспособность регистрации пользователя. После клика по кнопке регистрации в игре производится перенаправление на другую страницу браузера, где отображаются данные о зарегистрированных участниках игры.
Реакция на ввод данных
Нам нужно исправить один недостаток на странице ожидания всех игроков. Если второй участник зарегистрируется в игре, то первый участник игры его не увидит в списке. Первый участник может увидеть второго участника только после обновления своей страницы. На скрине ниже слева — регистрация первого участника, справа — следующая регистрация другого участника. Только после нажатия на кнопку браузера «Обновить» первый участник узнает что зарегистрирован еще один участник.
Мы исправим этот недостаток путем добавления интерактивного обновления таблицы участников на этой странице. Сделаем мы это с помощью гипермедийной системы. Добавим в приложение библиотеку поддержки работы гипермедийных систем Htmx.js. Нажмите правой кнопкой мыши на проекте — «Добавить» — «Клиентская библиотека». В открывшемся диалоговом окне наберите в строке поиска htmx и выберите последнюю версию библиотеки. После нажатия кнопки «Установить» необходимые файлы будут добавлены в невидимую папку lib/htmx.
Скопируйте файл htmx.js из этой скрытой папки в папку со скриптами приложения Scripts, как на следующем скриншоте.
Добавьте регистрацию бандла в файл конфигурации бандлов App_Config/BundleConfig.cs.
Листинг: Регистрация бандла HTMX в App_Config/BundleConfig.cs
…
bundles.Add(new Bundle("~/bundles/bootstrap").Include(
"~/Scripts/bootstrap.js"));
bundles.Add(new Bundle("~/bundles/htmx").Include(
"~/Scripts/htmx.js")); // Добавить
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/bootstrap.css",
"~/Content/site.css"));
…
В шаблон визуальных представлений Views/Shared/_Layout.cshtml нужно добавить использование этого нового бандла, как в следующем листинге.
Листинг: Шаблон _Layout.cshtml со ссылкой на страницу с правилами игры
…
<head>
…
@Scripts.Render("~/bundles/modernizr")
@Scripts.Render("~/bundles/htmx") // Добавить
</head>
…
А на визуальное представление ожидания участников нужно добавить первый гипермедийный визуальный элемент периодического обновления содержимого страницы. Отредактируйте файл Wait/Index.cshtml как в следующем листинге.
Листинг: Cтраница ожидания Wait/Index.cshtml с гипермедийным элементом
@model WaitWebModel
@{
ViewBag.Title = "Ожидание";
}
<h4>@ViewBag.Title</h4>
<p>Ожидание окончания регистрации всех участников игры.</p>
<div class="my-1">
<label>Ваше имя:</label>
<p><strong>@Model.Current.Name</strong></p>
</div>
<div class="my-1"> // Добавить весь блок
<div id="wait"
hx-get="@Url.Action("Index", "Wait", new { Model.Id })"
hx-trigger="every 1s">
@Html.Partial("Partial/WaitPartial", Model)
</div>
</div>
Мы на эту страницу вместо отображения таблицы мы поставили гипермедийный элемент. Новый атрибут hx‑trigger со значением «every 1s» определяет что этот элемент будет каждую 1 секунду отправлять запрос по указанному адресу и обновлять свое содержимое. Создайте новую папку Views/Wait/Partial и создайте в ней новое частичное представление WaitPartial.cshtml. В это частичное представление Wait/Partial/WaitPartial.cshtml нужно внести представленный на следующем листинге код. В это частичное представление мы вынесли элементы отображения таблицы пользователей из визуального представления Wait/Index.cshtml.
Листинг: Частичное представление отображения игроков Wait/Partial/WaitPartial.cshtml
@model WaitWebModel
<p>Все зарегистрированные игроки:</p>
<table class="table table-striped table-bordered">
<thead>
<tr><th>Id</th><th>Имя</th></tr>
</thead>
<tbody>
@foreach (var each in Model.All)
{
<tr>
<th scope="row" class="align-middle">@each.Id</th>
<td class="align-middle">@each.Name</td>
</tr>
}
</tbody>
</table>
Теперь при загрузке страницы Wait/Index.cshtml будет будет отрисована страница с частичным представлением, в котором будут показан изначальные данные по зарегистрированным пользователям. А уже потом, если кто-то еще зарегистрируется в игре - список отображения должен быть интерактивно обновлен. Осталось изменить метод действия в контроллере, чтобы он поддерживал работу гипермедийной системы.
Листинг: Контроллер Wait с методом поддержки гипермедийной системы
using System.Linq;
using System.Web.Mvc;
using SpyOnlineGame.Data;
using SpyOnlineGame.Web.Models;
namespace SpyOnlineGame.Controllers
{
public class WaitController : Controller
{
public ActionResult Index(int id)
{
var all = PlayersRepository.All.ToArray();
var player = PlayersRepository.GetById(id);
if (player is null) return new HttpNotFoundResult();
var model = new WaitWebModel
{
Id = id,
Current = player,
All = all,
};
if (Request.Headers.AllKeys.Contains("hx-request")) // Добавить
{
return PartialView("Partial/WaitPartial", model); // Добавить
}
return View(model);
}
}
}
Метод PartialView возвращает указанное в параметрах частичное представление. Теперь, чтобы проверить работу интерактивного элемента, запустите на выполнение наше приложение, зарегистрируйтесь в игре, а затем откройте еще одно окно браузера рядом и после второй регистрации в первом окне вы увидите изменения. В первом окне в результате очередного ежесекундного обновления обновилось отображение списка зарегистрированных пользователей.
Итак, мы добавили первый интерактивный элемент в нашу настольную игру. У вас теперь появилось понимание того, насколько просто добавлять интерактивность на страницы веб приложения. Далее в следующей части мы добавим ещё больше интерактивных элементов.
Завершение первой части
На этом заканчивается первая часть моей статьи. В этой части вы познакомились с гипермедийными системами, начали создавать новое приложение — настольную онлайн игру «Шпион» и добавили первый гипермедийный элемент на страницу ожидания игроков. В следующей части мы проведем интересный рефакторинг и продолжим наполнять эту страницу другими интерактивными элементами.
Комментарии (16)
dopusteam
20.09.2024 14:48Сам класс PlayersRepository и его члены снабжены ключевым словом static для облегчения сохранения и получения данных в разных частях приложения.
Жесть какая то
kanadeiar Автор
20.09.2024 14:48Ради упрощения примера. Это существенно сокращает объем кода в моделях и сервисе.
MuF_AsA
20.09.2024 14:48А в чём упрощение? Добавить в сервис провайдер хотя-бы singleton дело 10 секунд и одной строчки, а чтобы вытащить его даже строчки не нужно, достаточно дополнить уже существующую. Зато будет хороший пример, конечно зачастую проще всё статикой сделать, но нужно ли оно?
kanadeiar Автор
20.09.2024 14:48Я не согласен с вами, тут сложности с устаревшей платформой ASP.NET MVC 5. В ней отсутствует как сервис провайдер, так и внедрение зависимостей. Контейнер нужно отдельно добавлять пакетами и настраивать веб-приложение. А это, на мой взгляд, совсем не нужно в таком простом примере.
lair
Но зачем? Все то же самое (да, включая HTMX) можно делать на ASP.NET Core, и будет ничем не хуже (а местами сильно лучше).
kanadeiar Автор
Материал статьи будет очень полезен разработчикам, работающим на устаревших версиях ASP.NET.
lair
Достаточно ли их много (по сравнению с работающими на новых версиях), чтобы оправдать это смещение?
kanadeiar Автор
Я имею скромный опыт работы на разных версиях, и на новых версиях ASP.NET гораздо легче работать с HTMX.
lair
Это лишний аргумент не связываться со старыми версиями.
kanadeiar Автор
Я полностью с Вами согласен! Спасибо за вопросы.
AlexZyl
Что-то мне подсказывает что разработчики, которые всё еще работают с проектами на устаревших версиях ASP.NET уже и так знают эти версии фреймворка вдоль и поперек.
kanadeiar Автор
HTMX - дополнительный полезный инструмент им в копилочку.
ColdPhoenix
зачем на легаси проект пихать новую технологию?
kanadeiar Автор
Чтобы дать легаси проекту вторую жизнь!