Sistemas hipermedia en ASP.NET MVC 5. Segunda parte – continuación / Sudo Null IT News

Continuación del artículo sobre el desarrollo rápido de una aplicación web orientada a hipermedia con HTMX 2.0.

Continuación de la primera parte.

En la parte anterior, se familiarizó con los sistemas hipermedia, comenzó a crear una nueva aplicación: el juego de mesa en línea “Spy” y agregó el primer elemento hipermedia a la página de espera de los jugadores. En esta parte seguiremos trabajando en esta aplicación.

Refactorización

Antes de agregar los siguientes elementos hipermedia, necesitamos hacer una pequeña refactorización. Después de la refactorización, resulta más fácil agregar nuevas funciones.

Según la convención MVC, los controladores solo necesitan procesar solicitudes entrantes, manipular modelos y seleccionar una vista para mostrarla al usuario. Y nuestro ejemplo viola este acuerdo. El código de la parte del servidor de los sistemas hipermedia, por regla general, debe transferirse a modelos hipermedia ordinarios, y transferiremos a representaciones visuales no tipos de datos primitivos, sino modelos web llenos de datos ya preparados. Y los propios controladores no deben interactuar directamente con los modelos del repositorio o los modelos de dominio.

Modelo hipermedia de expectativas de los participantes del juego.

Cree una nueva carpeta Hypermedia en la carpeta Web. Esta carpeta contendrá modelos hipermedia que respaldan la operación de elementos hipermedia en representaciones visuales. En otras palabras, el código del servidor de los sistemas hipermedia. Cree una nueva clase WaitHypermedia en esta carpeta y rellénela con el contenido del listado siguiente.

Listado: Modelo hipermedia WaitHypermedia

using System.Linq;
using System.Web;
using SpyOnlineGame.Data;
using SpyOnlineGame.Models;
using SpyOnlineGame.Web.Models;

namespace SpyOnlineGame.Web.Hypermedia
{
    public class WaitHypermedia
    {
        private readonly HttpRequestBase _request;
        private readonly int _id;
        private readonly Player _current;
        
        public bool IsHtmx => _request.Headers.AllKeys.Contains("hx-request");

        public bool IsNotFound => _current is null;

        public WaitHypermedia(HttpRequestBase request, int id)
        {
            _request = request;
            _id = id;

            _current = PlayersRepository.GetById(_id);
        }

        public WaitWebModel Model()
        {
            return new WaitWebModel
            {
                Id = _id,
                Current = _current ?? new Player(),
                All = PlayersRepository.All,
            };
        }
    }
}

Simplificando el método de acción del controlador

Ahora puede simplificar el método Index del controlador Wait. Toda la lógica operativa se traslada del método de acción al modelo hipermedia, y solo la lógica específica del controlador permanece en el método de acción.

Listado: Controlador de espera con método simplificado

using System.Web.Mvc;
using SpyOnlineGame.Web.Hypermedia; // Добавить

namespace SpyOnlineGame.Controllers
{
    public class WaitController : Controller
    {
        public ActionResult Index(int id)
        {
            var hypermedia = new WaitHypermedia(Request, id); // Добавить
            if (hypermedia.IsNotFound) 
              return new HttpNotFoundResult(); // Добавить

            if (hypermedia.IsHtmx) // Добавить
            {
                return PartialView("Partial/WaitPartial", 
                  hypermedia.Model()); // Добавить
            }
            return View(hypermedia.Model()); // Добавить
        }
    }
}

Al comienzo de este método, creamos un modelo hipermedia. Luego, el operador de borde verifica si el modelo hipermedia pudo encontrar al usuario actual. Y al final, dependiendo de la naturaleza de la solicitud, devuelve una vista parcial o normal.

No estamos violando las convenciones de MVC de ninguna manera con esta refactorización. Al contrario, los enfatizamos. El método de acción del controlador trabaja con el modelo y selecciona qué representación visual devolver en respuesta a la solicitud. El modelo hipermedia concentrará la lógica hipermedia. En un modelo web, la lógica asociada a la presentación visual.

Es muy importante comprender que el modelo hipermedia de este ejemplo, el método de acción del controlador, el elemento hipermedia de la presentación visual y la comunicación entre ellos es un sistema hipermedia.

Una vez que se haya completado esta refactorización, puede comenzar a agregar los sistemas hipermedia restantes a la página de espera de registro del jugador. No trasladaremos la lógica de negocios del modelo hipermedia al modelo de dominio para simplificar el ejemplo, ya que en este ejemplo nuestro objetivo es principalmente familiarizarnos con los sistemas hipermedia.

Interactividad de la página de espera de registro de jugadores.

Ahora pasaremos a agregar el resto de los elementos hipermedia interactivos de la página de espera. Creo que es muy importante, antes de agregar cualquier funcionalidad nueva a una aplicación, refactorizar aquellas partes de la aplicación donde se espera que se realicen cambios. Esta regla evita que el código se pudra y nos da un aumento de rendimiento durante algún tiempo.

Actualizar elementos en la página según sea necesario

Ahora la lista de usuarios registrados se actualiza cada segundo en la página del navegador de cada participante registrado. Tiene más sentido actualizar sólo cuando alguien más se haya registrado como jugador. Agregaremos una actualización si es necesario.

En primer lugar, agreguemos una nueva propiedad al modelo de usuario por la necesidad de actualizar el reproductor. Agregue una nueva propiedad a la clase, resaltada en negrita en el listado a continuación.

Listado: Modelo de reproductor aumentado

namespace SpyOnlineGame.Models
{
    public class Player
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsNeedUpdate { get; set; } // Добавить
    }
}

Ajuste el código del repositorio de miembros de PlayersRepository para que cuando se agregue un nuevo miembro o se elimine cualquier miembro, se levante el indicador de requisito de actualización. Realice los cambios indicados en el siguiente listado.

Listado: Repositorio de jugadores corregido PlayersRepository

…
        public static int Add(Player player)
        {
            player.Id = _lastId++;
            _players.Add(player);
            IsNeedAllUpdate(); // Добавить
            return player.Id;
        }

        public static void Remove(int id)
        {
            var deleted = GetById(id);
            if (deleted is null) return;
            _players.Remove(deleted);
            IsNeedAllUpdate() // Добавить
        }

        public static void IsNeedAllUpdate() // Добавить метод
        {
            foreach (var each in All) each.IsNeedUpdate = true; // Добавить
        }
    }
}

Después de esto, agregue un nuevo método al modelo hipermedia de espera de participantes, que devolverá como resultado una indicación de que el modelo ya contiene datos antiguos modificados. También necesitamos agregar una nueva propiedad que encapsulará la lógica de la condición del operador de límite del método de acción del controlador. Agregue el método y la propiedad que se muestran en el siguiente listado.

Listado: Nuevo método en el modelo hipermedia WaitHypermedia

…
        public bool IsNotFound => _current is null;

        public bool IsNoContent => IsHtmx && HasOldData(); // Добавить

        public WaitHypermedia(HttpRequestBase request, int id)
        {
…
        }

        public bool HasOldData() // Добавить метод
        {
            if (_current?.IsNeedUpdate != true) return true; // Добавить
            _current.IsNeedUpdate = false; // Добавить
            return false; // Добавить
        }

        public WaitWebModel Model()
        {
…
        }
…

Este método solo debería permitir actualizar la lista de usuarios en la página para cada participante una vez y luego desactivarla nuevamente. Pero permítalo sólo después, por ejemplo, de registrar a otro participante en el juego.

Todo lo que queda por hacer es agregar un nuevo operador de límite al método Index del controlador Wait, como se muestra en la siguiente lista.

Listado: Nuevo operador de límite en el método Index de la clase WaitController

…
        public ActionResult Index(int id)
        {
            var hypermedia = new WaitHypermedia(Request, id);
            if (hypermedia.IsNotFound) return new HttpNotFoundResult();
            if (hypermedia.IsNoContent) // Добавить блок
              return new HttpStatusCodeResult(HttpStatusCode.NoContent); 

            if (hypermedia.IsHtmx)
            {
                return PartialView("Partial/WaitPartial", hypermedia.Model());
            }
            return View(hypermedia.Model());
        }
…

Usando un objeto HttpStatusCodeResult con el parámetro NoContent, devolvemos el código 204 como resultado. Al recibir dicho código, el elemento hipermedia no realiza ninguna acción y la tabla de participantes del juego no se actualiza.

Agregar el estado de preparación de los jugadores para el juego.

Agregue una señal de que el reproductor está listo para jugar en el modelo Player. Esta bandera indica que el jugador está listo para que comience el juego. Es necesario agregar una visualización de la preparación de todos los jugadores y un botón para cambiar el estado de preparación.

Listado: Modelo de reproductor con nueva señal de preparación.

namespace SpyOnlineGame.Models
{
    public class Player
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsReady { get; set; } // Добавить
        public bool IsNeedUpdate { get; set; }
    }
}

Cambia la apariencia visual parcial de la pantalla de los jugadores registrados. Agreguemos una visualización de la preparación del jugador para jugar en una nueva columna de la tabla. Además, diseñaremos la celda de preparación según el valor del estado de preparación.

Listado: Representación visual parcial de Wait/WaitPartial.cshtml con tabla de preparación

@model WaitWebModel

<p>Все зарегистрированные игроки:</p>
<table class="table table-striped table-bordered">
    <thead>
        <tr><th>Id</th><th>Имя</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>
            <td class="align-middle text-white 
              @(each.IsReady ? "bg-success" : "bg-danger")"> // Добавить блок
                @(each.IsReady ? "Готов" : "Не готов") 
            </td>
        </tr>
    }
    </tbody>
</table>

Ahora necesitamos agregar un método para cambiar el estado del reproductor al modelo hipermedia. Agreguemos un nuevo método al modelo hipermedia de acuerdo con el siguiente listado.

Listado: Modelo hipermedia WaitHypermedia con nuevo método

…
        public bool HasOldData()
        {
            if (_current?.IsNeedUpdate != true) return true;
            _current.IsNeedUpdate = false;
            return false;
        }

        public void SwitchReady() // Добавить новый метод
        {
            if (_current is null) return;
            _current.IsReady = !_current.IsReady;
            PlayersRepository.IsNeedAllUpdate();
        }
…

Este método invierte el signo de preparación del jugador y actualiza las páginas para todos los participantes. Antes de agregar un botón al objeto visual, debe agregar un nuevo método de acción al controlador Esperar.

Listado: Nuevo método para cambiar la preparación en el controlador de espera

…
        public ActionResult Index(int id)
        {
…
        }

        public ActionResult SwitchReady(int id) // Добавить новый метод
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.SwitchReady();
            return Index(id);
        }
…

Este método de acción, como puede ver, provoca el método de cambiar la preparación del modelo hipermedia de espera de los participantes. Y como resultado, devuelve una representación visual del método Index. Hicimos esto para que después de cambiar el estado de preparación de un participante del juego, viera inmediatamente el cambio en su estado. Todo lo que queda es agregar un botón estilizado para cambiar la preparación del jugador a la representación visual parcial.

Listado: Representación visual parcial de Wait/WaitPartial.cshtml con botón listo

@model WaitWebModel

<div class="my-1"> // Добавить весь новый блок
    <p class="form-label">Ваша готовность:</p>
    <button class="btn @(Model.Current.IsReady ? "btn-success" : "btn-danger")"
            hx-get="@Url.Action("SwitchReady", new { Model.Id })"
            hx-target="#wait">
        @(Model.Current.IsReady ? "Готов" : "Не готов")
    </button>
</div>

<p>Все зарегистрированные игроки:</p>
…

Ahora, después de iniciar la aplicación en varios navegadores, puedes ver que después de cambiar el estado de preparación de cualquier participante en el juego, todos los jugadores verán inmediatamente el cambio.

Cambiar el nombre de un participante

Agregaremos una oportunidad interactiva para cambiar su nombre para un participante ya registrado en el juego. Agregaremos esta oportunidad en forma de campo de texto directamente a la página de espera para los participantes del juego. El usuario cambia el texto en el campo: su nombre cambia para todos los participantes del juego. Con la ayuda de sistemas hipermedia, éstas y muchas otras posibilidades se pueden realizar muy fácilmente.

En primer lugar, agreguemos un nuevo método de cambio de nombre al modelo hipermedia de espera de los participantes del juego.

Listado: Modelo hipermedia WaitHypermedia con nuevo método de cambio de nombre

…
        public void SwitchReady()
        {
…
        }

        public void SetName(string name) // Добавить новый метод
        {
            if (_current is null || _current.Name == name) return;
            _current.Name = name;
            PlayersRepository.IsNeedAllUpdate();
        }
…

A continuación, agreguemos un nuevo método para cambiar el nombre de un participante registrado en el juego al controlador Wait.

Listado: Nuevo método para cambiar el nombre de un participante registrado en el juego en el controlador Wait

…
        public ActionResult SwitchReady(int id)
        {
…
        }

        public void SetName(int id, string name) // Добавить новый метод
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.SetName(name);
        }
…

Todo lo que queda es agregar un nuevo elemento hipermedia a la representación visual del Índice.

Listado: Representación visual de Wait/Index.cshtml con campo de texto de cambio de nombre

…
<h4>@ViewBag.Title</h4>
<p>Ожидание окончания регистрации всех участников игры.</p>

<div class="my-1"> // Добавить весь новый блок
    <label class="form-label">Ваше имя:</label>
    <input type="text" name="name" class="form-control"
           hx-get="@Url.Action("SetName", new { Model.Id })"
           hx-trigger="change, keyup delay:500ms changed"
           value="@Model.Current.Name" />
</div>
…

Para este nuevo elemento, configuramos el activador de ejecución para cambiar la tecla: desde soltar una tecla después de ingresar un nuevo carácter con un retraso de retraso: 500 ms en 500 milisegundos. Cuando se activa este activador, se realizará una solicitud a la dirección especificada. La peculiaridad es que especificamos el atributo de identificación de la solicitud, y el nombre del segundo atributo, nombre, se sustituirá automáticamente según el nombre del método y el valor se tomará del campo de texto.

Ejecute la aplicación y compruebe si es posible cambiar el nombre.

Botón salir del juego

Necesitamos agregar un método para salir del juego al modelo hipermedia del participante del juego. Al llamar a este método, un participante se elimina de la lista de jugadores registrados.

Listado: Modelo hipermedia WaitHypermedia para esperar a los jugadores con un nuevo método

…
        public void SetName(string name)
        {
…
        }

        public void Logout() // Добавить новый метод
        {
            PlayersRepository.Remove(_id);
        }
…

Después de eso, debe agregar un botón al objeto visual y agregar un método de acción al controlador de espera. Agregue un nuevo método de acción al controlador Wait, como se muestra en la siguiente lista.

Listado: Nuevo método para salir de la acción del juego en el controlador Esperar

…
        public void SetName(int id, string name)
        {
…
        }
 
        public ActionResult Logout(int id) // Добавить новый метод
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.Logout();

            return RedirectToAction("Index", "Home");
        }
…

Y al final, agregaremos un botón de cierre de sesión en la parte superior de la página de espera de registro del jugador.

Listado: Representación visual con botón de salida Wait/Index.cshtml

@model WaitWebModel
@{
    ViewBag.Title = "Ожидание";
}

@Html.ActionLink("Выход", "Logout", new { Model.Id }, 
    new { @class="btn btn-warning my-1" }) // Добавить

<h4>@ViewBag.Title</h4>
<p>Ожидание окончания регистрации всех участников игры.</p>
…

Ahora puedes iniciar la aplicación y comprobar la funcionalidad del botón. Cuando haces clic en este botón, se te redirige a la página principal del juego y todos los participantes ven los cambios en la lista de participantes.

Botones para eliminar jugadores de la lista.

Agreguemos la posibilidad de excluir jugadores adicionales ya registrados. En el repositorio de usuarios ya tenemos un método para eliminar jugadores. El modelo hipermedia utilizará este método. Empecemos a modificarlo. Agregue un nuevo método a este modelo, que se muestra en la lista a continuación. Coloque este método junto al método Cerrar sesión; estos son dos métodos que realizan funciones similares.

Listado: Modelo hipermedia WaitHypermedia para esperar a los jugadores con un nuevo método

…
        public void Kick(int playerId) // Добавить новый метод
        {
            PlayersRepository.Remove(playerId);
        }

        public void Logout()
        {
            PlayersRepository.Remove(_id);
        }
…

Ahora agreguemos un nuevo método al controlador Wait. Este método realizará una llamada al método del modelo hipermedia. También necesitamos modificar el método Index para que ahora redirija a los jugadores excluidos de la lista de participantes a la página de inicio de la aplicación.

Listado: Nuevo método de acción de excepción de jugador en el controlador Wait

…
        public ActionResult Index(int id)
        {
            var hypermedia = new WaitHypermedia(Request, id);
            if (hypermedia.IsNotFound) // Добавить весь блок
            {
                if (!hypermedia.IsHtmx) return RedirectToAction("Index", "Home");
                Response.Headers.Add("hx-redirect", Url.Action("Index", "Home"));
            }
            if (hypermedia.IsNoContent) 
                return new HttpStatusCodeResult(HttpStatusCode.NoContent);

            if (hypermedia.IsHtmx)
            {
                return PartialView("Partial/WaitPartial", hypermedia.Model());
            }
            return View(hypermedia.Model());
        }
…
        public ActionResult Kick(int id, int playerId) // Добавить новый метод
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.Kick(playerId);
            return Index(id);
        }

        public ActionResult Logout(int id)
        {
…
        }
…

Como resultado de la ejecución, el método de acción Kick llamará al método Index para devolver la vista parcial actual con la lista modificada de usuarios. Y en el método Index, si no hay ningún participante con el identificador especificado, ahora redireccionaremos a la página de inicio. En este ejemplo, la redirección a otra página ahora funciona utilizando el sistema hipermedia. Dado que la página del participante excluido se actualiza cada segundo, al llamar a este método se redireccionará inmediatamente después de que haya sido excluido.

Ya sólo queda modificar la tabla en la vista visual parcial. A esta tabla, agregue una nueva columna y un botón para poder excluir jugadores de la lista.

Listado: Vista parcial de Wait/WaitPartial.cshtml con botones de exclusión de jugadores

@model WaitWebModel

<button class="btn @(Model.Current.IsReady ? "btn-success" : "btn-danger")"
        hx-get="@Url.Action("SwitchReady", new { Model.Id })"
        hx-target="#wait">
    @(Model.Current.IsReady ? "Готов" : "Не готов")
</button>

<p>Все зарегистрированные игроки:</p>
<table class="table table-striped table-bordered">
    <thead>
        <tr><th>Id</th><th>Имя</th><th>Готовность</th>
          <th class="col-1"></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>
            <td class="align-middle text-white 
              @(each.IsReady ? "bg-success" : "bg-danger")">
                @(each.IsReady ? "Готов" : "Не готов")
            </td>
            <td> // Добавить весь новый блок
                @if (each.Id != Model.Id)
                {
                    <button class="btn btn-warning"
                            hx-get="@Url.Action("Kick", 
                              new { Model.Id, playerId = each.Id })"
                            hx-target="#wait">
                        Выгнать
                    </button>
                }
            </td>
        </tr>
    }
    </tbody>
</table>

Si ejecutas la aplicación, podrás comprobar la posibilidad de excluir jugadores de la lista de jugadores registrados.

La siguiente captura de pantalla grande muestra cómo funciona la funcionalidad de excluir a un jugador de la lista de jugadores. En las dos ventanas superiores se encuentran inicialmente las ventanas de dos jugadores. Y debajo, bajo la franja roja, se muestran las ventanas de los participantes tras pulsar el botón “Patear” en la ventana del primer jugador. Como resultado, la página principal de la aplicación con un formulario de registro comenzó a mostrarse en la ventana del navegador del segundo jugador. Y en la ventana izquierda del primer jugador, ahora solo se muestra un jugador en la lista de usuarios registrados en lugar de dos.

Los sistemas hipermedia hacen que sea muy fácil agregar comportamientos interactivos tan complejos a las páginas de aplicaciones web.

inicio del juego

Queda por agregar la última funcionalidad interactiva a nuestra página de espera de registro de jugadores. Este es el botón de inicio del juego. Debería estar disponible sólo cuando el número de participantes registrados en el juego sea superior a tres y todos estén listos para jugar. Pero todo se complica por el hecho de que después de presionar el botón de iniciar el juego, todos los jugadores registrados y listos para jugar deben ser redirigidos inmediatamente a la página del juego. Y a todos los demás jugadores se les debe prohibir registrarse. En esta aplicación, específicamente para simplificar el ejemplo, no implementamos sesiones de juego. Sólo habrá una sesión, y además será la última. Es decir, para iniciar un nuevo juego necesitas reiniciar la aplicación, y una vez finalizado el juego la aplicación será inútil. Esta es una convención especial sólo para simplificar al máximo.

Agregue una nueva propiedad al modelo de datos del participante, una señal de su participación en el juego.

Listado: Modelo de un Jugador participante con cartel de participación en el juego.

namespace SpyOnlineGame.Models
{
    public class Player
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsReady { get; set; }
        public bool IsPlay { get; set; } // Добавить
        public bool IsNeedUpdate { get; set; }
    }
}

Modifique el modelo web para esperar el registro del jugador para que ahora pueda transmitir a la representación visual no una lista de todos los usuarios, sino una lista de los que están esperando y una lista de los jugadores que ya están jugando. También agrega una señal de que puedes iniciar el juego. Y el juego sólo podrá iniciarse si el número de participantes es mayor o igual a tres y están todos preparados para jugar.

Listado: Modelo web modificado 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>();
        public bool IsMayBeStart { get; set; } // Добавить
    }
}

A continuación, tendremos que cambiar el modelo de espera del reproductor hipermedia WaitHypermedia. A este modelo necesitas agregar un nuevo método para iniciar el juego, un signo para iniciar el juego y modificar el método Model(). Realice cambios de acuerdo con el siguiente listado.

Listado: Modelo hipermedia modificado WaitHypermedia

using System.Linq;
using System.Web;
using SpyOnlineGame.Data;
using SpyOnlineGame.Models;
using SpyOnlineGame.Web.Models;

namespace SpyOnlineGame.Web.Hypermedia
{
    public class WaitHypermedia
    {
        private readonly HttpRequestBase _request;
        private readonly int _id;
        private readonly Player _current;
        
        public bool IsHtmx => _request.Headers.AllKeys.Contains("hx-request");

        public bool IsNotFound => _current is null;

        public bool IsNoContent => IsHtmx && HasOldData();

        public bool IsMayBeStart => PlayersRepository.All.Count() >= 3 && 
            PlayersRepository.All.All(x => x.IsReady) 
            && !IsGameStarted; // Добавить

        public bool IsGameStarted =>
            PlayersRepository.All.Any(p => p.IsPlay); // Добавить

        public WaitHypermedia(HttpRequestBase request, int id)
        {
            _request = request;
            _id = id;

            _current = PlayersRepository.GetById(_id);
        }

        public bool HasOldData()
        {
            if (_current?.IsNeedUpdate != true) return true;
            _current.IsNeedUpdate = false;
            return false;
        }

        public void SwitchReady()
        {
            if (_current is null) return;
            _current.IsReady = !_current.IsReady;
            PlayersRepository.IsNeedAllUpdate();
        }

        public void SetName(string name)
        {
            if (_current is null || _current.Name == name) return;
            _current.Name = name;
            PlayersRepository.IsNeedAllUpdate();
        }

        public void Kick(int playerId)
        {
            PlayersRepository.Remove(playerId);
        }

        public void Logout()
        {
            PlayersRepository.Remove(_id);
        }

        public void Start() // Добавить новый метод
        {
            if (!IsMayBeStart) return;
            foreach (var each in PlayersRepository.All)
            {
                each.IsPlay = true;
            }
            PlayersRepository.IsNeedAllUpdate();
        }

        public WaitWebModel Model()
        {
            return new WaitWebModel
            {
                Id = _id,
                Current = _current ?? new Player(),
                All = PlayersRepository.All,
                IsMayBeStart = IsMayBeStart, // Добавить
            };
        }
    }
}

Ahora modifique el objeto visual parcial WaitPartial.cshtml para mostrar un botón de inicio del juego.

Listado: Vista parcial modificada Wait/WaitPartial.cshtml

@model WaitWebModel

<div class="my-1">
    <p class="form-label">Ваша готовность:</p>
    <button class="btn @(Model.Current.IsReady ? "btn-success" : "btn-danger")"
            hx-get="@Url.Action("SwitchReady", new { Model.Id })"
            hx-target="#wait">
        @(Model.Current.IsReady ? "Готов" : "Не готов")
    </button>
</div>

<p>Все зарегистрированные игроки:</p>
<table class="table table-striped table-bordered">
    <thead>
        <tr><th>Id</th><th>Имя</th><th>Готовность</th>
          <th class="col-1"></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>
                <td class="align-middle text-white @(each.IsReady 
                  ? "bg-success" : "bg-danger")">
                    @(each.IsReady ? "Готов" : "Не готов")
                </td>
                <td>
                    @if (each.Id != Model.Id)
                    {
                        <button class="btn btn-warning"
                                hx-get="@Url.Action("Kick", 
                                new { Model.Id, playerId = each.Id })"
                                hx-target="#wait">
                            Выгнать
                        </button>
                    }
                </td>
            </tr>
        }
    </tbody>
</table>

@if (Model.IsMayBeStart) // Добавить весь новый блок
{
    <button class="btn btn-success"
            hx-get="@Url.Action("Start", new { Model.Id })"
            hx-swap="none">Начать игру</button>
}

Y lo último que debe hacer es realizar cambios en el controlador de espera. Debes agregarle un método de inicio del juego y en el método Índice agregar una redirección a otro controlador cuando se inicie el juego.

Listado: Controlador de espera modificado

using System.Net;
using System.Web.Mvc;
using SpyOnlineGame.Web.Hypermedia;

namespace SpyOnlineGame.Controllers
{
    public class WaitController : Controller
    {
        public ActionResult Index(int id)
        {
            var hypermedia = new WaitHypermedia(Request, id);
            if (hypermedia.IsNotFound)
            {
                if (!hypermedia.IsHtmx) return RedirectToAction("Index", "Home");
                Response.Headers.Add("hx-redirect", Url.Action("Index", "Home"));
            }
            if (hypermedia.IsGameStarted) // Добавить весь блок
            {
                Response.Headers.Add("hx-redirect", 
                  Url.Action("Index", "Game", new { id }));
            }
            if (hypermedia.IsNoContent) 
                return new HttpStatusCodeResult(HttpStatusCode.NoContent);

            if (hypermedia.IsHtmx)
            {
                return PartialView("Partial/WaitPartial", hypermedia.Model());
            }
            return View(hypermedia.Model());
        }
…
        public ActionResult Logout(int id)
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.Logout();

            return RedirectToAction("Index", "Home");
        }

        public void Start(int id) // Добавить новый метод
        {
            var hypermedia = new WaitHypermedia(Request, id);
            hypermedia.Start();
        }
    }
}

Ahora puedes iniciar la aplicación y comprobar los inicios del juego. Debes abrir dos pestañas adicionales del navegador, registrarte en el juego y activar el estado listo. Después de hacer clic en el botón Iniciar juego, las tres pestañas serán redirigidas al controlador de juego inexistente.

El método Index() es un operador de límite para verificar el estado de ejecución del juego y redirige al método de acción Index() del controlador del juego. Tal controlador y método aún no existen. Agréguelo y rellénelo con el contenido del siguiente listado.

Listado: Controlador de juego

using System.Web.Mvc;

namespace SpyOnlineGame.Controllers
{
    public class GameController : Controller
    {
        public ActionResult Index(int id)
        {
            return View();
        }
    }
}

Agregue una nueva imagen con contenido vacío para mostrar una página de juego en blanco.

Listado: Representación visual sin contenido Views/Game/Index.cshtml

@{
    ViewBag.Title = "Игра";
    Layout = "~/Views/Shared/_GameLayout.cshtml";
}

<h4>@ViewBag.Title</h4>

A continuación, agregaremos la prohibición de registrar nuevos participantes cuando el juego ya se esté ejecutando. Para hacer esto, agregaremos un nuevo operador de límite al método Registro() del controlador de inicio. Si hay al menos un usuario en el repositorio con la bandera de participación en el juego, lo redireccionaremos a la página principal del juego. Esto prohibirá el registro adicional de jugadores.

Listado: Método de acción de Registro() modificado del controlador de inicio

…
       (HttpPost)
       public ActionResult Registration(RegistrationWebModel model)
       {
           if (PlayersRepository.All.Any(p => p.IsPlay)) // Добавить
             return RedirectToAction("Index"); // Добавить
           var player = model.Map();
           var id = PlayersRepository.Add(player);
           return RedirectToAction("Index", "Wait", new { id });
       }
…

Inicie la aplicación en modo de depuración, abra dos ventanas adicionales y registre a tres participantes en el juego. Una vez que los tres participantes estén listos, aparecerá un botón para iniciar el juego. Después de hacer clic en este botón, los tres participantes del juego serán redirigidos a la página del juego.

La siguiente captura de pantalla en la parte superior muestra tres ventanas de aplicaciones en ejecución antes de que comience el juego. Y en la parte inferior de la ventana están las mismas ventanas después de hacer clic en el botón “Iniciar juego”.

Hemos completado con éxito la implementación de la página de espera de registro de participantes. Puedes comenzar a implementar en la página siguiente, con la funcionalidad del juego en sí.

pagina del juego

Pasemos a la parte principal de la aplicación. Comencemos a implementar la página del juego con lo más simple: mostrar el nombre del participante. Luego agregaremos la inicialización inicial del juego. Luego, poco a poco iremos llenando esta página con elementos de juego interactivos.

Cree un nuevo modelo de juego web llamado GameWebModel. Este modelo pasará datos para representar visualmente la página del juego. Rellénelo con el contenido del siguiente listado.

Listado: Modelo web inicial GameWebModel

using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class GameWebModel
    {
        public int Id { get; set; }
        public Player Current { get; set; }
    }
}

A continuación, cree un nuevo modelo hipermedia para el juego GameHypermedia. Toda la lógica del método de acción del controlador se transferirá a este modelo hipermedia. Será muy similar a otro modelo hipermedia. No refactorizaremos modelos hipermedia ni formaremos un modelo hipermedia básico abstracto debido a la impracticabilidad de dicha refactorización. Complete la clase de modelo hipermedia con el contenido del siguiente listado.

Listado: Modelo hipermedia GameHypermedia

using System.Linq;
using System.Web;
using SpyOnlineGame.Data;
using SpyOnlineGame.Models;
using SpyOnlineGame.Web.Models;

namespace SpyOnlineGame.Web.Hypermedia
{
    public class GameHypermedia
    {
        private readonly HttpRequestBase _request;
        private readonly int _id;
        private readonly Player _current;

        public bool IsHtmx => _request.Headers.AllKeys.Contains("hx-request");

        public bool IsNotFound => _current is null;

        public GameHypermedia(HttpRequestBase request, int id)
        {
            _request = request;
            _id = id;

            _current = PlayersRepository.GetById(_id);
        }

        public GameWebModel Model()
        {
            return new GameWebModel
            {
                Id = _id,
                Current = _current ?? new Player(),
            };
        }
    }
}

Ahora cambiemos el contenido del método de acción del controlador de juego para que pase la carga útil al objeto visual y también devuelva un objeto visual parcial con una tabla de votación en el caso de una consulta hipermedia. Agregue nuevo contenido de acuerdo con el siguiente listado.

Listado: Controlador de juego modificado

using System.Web.Mvc;

namespace SpyOnlineGame.Controllers
{
    public class GameController : Controller
    {
        public ActionResult Index(int id) // полностью заменить содержимое
        {
            var hypermedia = new GameHypermedia(Request, id);
            if (hypermedia.IsNotFound)
            {
                if (!hypermedia.IsHtmx) return RedirectToAction("Index", "Home");
                Response.Headers.Add("hx-redirect", Url.Action("Index", "Home"));
            }

            if (hypermedia.IsHtmx)
            {
                return PartialView("Partial/VotingPartial", hypermedia.Model());
            }
            return View(hypermedia.Model());
        }
    }
}

Cree una nueva vista de votación parcial Views/Game/Partial/VotingPartial.cshtml y rellénela con contenido vacío. Lo llenaremos de contenido más tarde.

Listado: Representación visual parcial Vistas/Juego/Partial/VotingPartial.cshtml

@model GameWebModel

<h6>Открытое голосование за определение шпиона</h6>

Cambie la apariencia visual de la página del juego como se muestra en la siguiente lista.

Listado: Representación visual Vistas/Juego/Index.cshtml

@model GameWebModel // Добавить
@{
    ViewBag.Title = "Игра";
    Layout = "~/Views/Shared/_GameLayout.cshtml";
}

<h4>@ViewBag.Title</h4>

<p>Ваше имя: <strong>@Model.Current.Name</strong></p> // Добавить

@Html.Partial("Partial/VotingPartial", Model) // Добавить

Por ahora, esta imagen no muestra nada útil más que el nombre del participante actual. Sin embargo, esta es la parte principal más voluminosa de la aplicación. Lo siguiente que necesitamos es agregar la funcionalidad de inicialización del juego.

Inicialización del juego

Esta funcionalidad solo se activa una vez cuando se inicia desde la página de registro de miembros en espera. Para que este ejemplo sea sencillo, no implementaremos un reinicio del juego. El objetivo principal del artículo es familiarizarse con los sistemas hipermedia. Por tanto, nos centraremos principalmente en los elementos visuales interactivos de las páginas. Sin embargo, antes de agregar los siguientes sistemas hipermedia, debemos asegurarnos de crear la lógica de inicialización para el juego. Sin un juego correctamente inicializado, no tiene sentido seguir adelante.

Primero, creemos una fuente de lugar. El propósito de esta fuente es seleccionar aleatoriamente una ubicación de la lista de disponibles. Cree una nueva clase LocationsSource en la carpeta Modelos y rellénela con el contenido del siguiente listado.

Listado: Fuente para la lista de ubicaciones disponibles LocationsSource

using System;

namespace SpyOnlineGame.Models
{
    public static class LocationsSource
    {
        private static Random _rand = new Random();

        private static string() _locations =
        {
            "Банк",
            "Казино",
            "Больница",
            "Офис",
            "Казино",
        };

        public static string GetRandomLocation()
        {
            var locationNum = _rand.Next(_locations.Length);

            return _locations(locationNum);
        }
    }
}

Puede agregar cualquier número de sus lugares únicos. El número de lugares en esta clase, como puede ver, no está limitado, lo principal es que el espía puede adivinarlos.

Agregue un campo para almacenar el rol asignado al jugador en la clase Jugador como se muestra en la siguiente lista. Durante la inicialización del juego, debes asignar a uno de los participantes el papel de espía. A este participante no se le debe mostrar una ubicación aleatoria elegida en secreto. Sólo los jugadores pacíficos deberían conocer este lugar.

Cree un nuevo tipo de enumeración RoleCode en la carpeta Modelos. Rellénelo con el contenido del siguiente listado.

Listado: Roles de jugador Código de rol

namespace SpyOnlineGame.Models
{
    public enum RoleCode
    {
        Honest,
        Spy,
    }
}

Agregue el uso de este nuevo tipo en la clase de participante del juego Jugador.

Listado: Modelo de participante del juego de jugador

namespace SpyOnlineGame.Models
{
    public class Player
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public bool IsReady { get; set; }
        public bool IsPlay { get; set; }
        public RoleCode Role { get; set; } // Добавить
        public bool IsNeedUpdate { get; set; }
    }
}

En el modelo web del juego, agregue nuevas propiedades para transmitir información sobre la ubicación seleccionada y quién hizo la pregunta primero a la representación visual.

Listado: Modelo web del juego GameWebModel con nuevas propiedades

using SpyOnlineGame.Models;

namespace SpyOnlineGame.Web.Models
{
    public class GameWebModel
    {
        public int Id { get; set; }
        public Player Current { get; set; }
        public string Location { get; set; } // Добавить
        public string FirstName { get; set; } // Добавить
    }
}

En el modelo hipermedia GameHypermedia, primero debemos agregar un nuevo método para inicializar un nuevo juego. Además, debes agregar una propiedad más útil: una señal de que es necesario iniciar el juego.

Listado: Modelo hipermedia actualizado GameHypermedia

using System;
using System.Linq;
using System.Web;
using SpyOnlineGame.Data;
using SpyOnlineGame.Models;
using SpyOnlineGame.Web.Models;

namespace SpyOnlineGame.Web.Hypermedia
{
    public class GameHypermedia
    {
        private static string _location; // Добавить
        private static string _firstName; // Добавить
        private readonly Random _rand = new Random(); // Добавить
        private readonly HttpRequestBase _request;
        private readonly int _id;
        private readonly Player _current;

        public bool IsHtmx => _request.Headers.AllKeys.Contains("hx-request");
        public bool IsNotFound => _current is null;

        public bool IsNeedInit => string.IsNullOrEmpty(_location) &&
          PlayersRepository.All.Any(p => p.IsPlay); // Добавить

        public GameHypermedia(HttpRequestBase request, int id)
        {
            _request = request;
            _id = id;
            _current = PlayersRepository.GetById(_id);
        }

        public void Init() // Добавить метод
        {
            if (!IsNeedInit) return;
            var all = PlayersRepository.All.ToArray();
            var firstNum = _rand.Next(all.Length);
            _firstName = all(firstNum).Name;
            _location = LocationsSource.GetRandomLocation();
            var spyNum = _rand.Next(all.Length);
            all(spyNum).Role = RoleCode.Spy;
        }

        public GameWebModel Model()
        {
            return new GameWebModel
            {
                Id = _id,
                Current = _current ?? new Player(),
                Location = _location, // Добавить
                FirstName = _firstName, // Добавить
            };
        }
    }
}

En el método de acción Index() del controlador de juego, debes agregar un operador de límite para verificar si es necesario iniciar el juego, como se muestra en la siguiente captura de pantalla. Si necesitas iniciarlo, debes llamar al método de inicialización del juego.

Listado: Método Index() actualizado del controlador de juego

…
        public ActionResult Index(int id)
        {
            var hypermedia = new GameHypermedia(Request, id);
            if (hypermedia.IsNotFound)
            {
                if (!hypermedia.IsHtmx) return RedirectToAction("Index", "Home");
                Response.Headers.Add("hx-redirect", Url.Action("Index", "Home"));
            }
            if (hypermedia.IsNeedInit) hypermedia.Init(); // Добавить

            if (hypermedia.IsHtmx)
            {
                return PartialView("Partial/VotingPartial", hypermedia.Model());
            }
            return View(hypermedia.Model());
        }
…

En este método, el juego se inicializa sólo una vez. Se realiza tan pronto como el primer participante accede a la página del juego desde la página de espera del jugador.

Terminemos de agregar esta nueva funcionalidad para mostrar información del juego en la página del juego. Después de esto, puedes iniciar la aplicación y comprobar que el juego se inicializó correctamente. Edite la representación visual del juego como se muestra en la siguiente lista.

Listado: Representación visual Vistas/Juego/Index.cshtml

@model GameWebModel
@{
    ViewBag.Title = "Игра";
    Layout = "~/Views/Shared/_GameLayout.cshtml";
}

<h4>@ViewBag.Title</h4>

<p>Ваше имя: <strong>@Model.Current.Name</strong></p>

<div class="my-1"> // Добавить новый блок
    <span>Загадано место: <strong>@Model.Location</strong>.</span>
</div>

<div class="my-1"> // Добавить новый блок
    @if (Model.Current.Name == Model.FirstName)
    {
        <p>Вы <strong>первым</strong> задаете вопрос любому другому игроку.</p>
    }
    else
    {
          Первым вопрос задаёт <p><strong>@Model.FirstName</strong>.</p>
    }
</div>

@Html.Partial("Partial/VotingPartial", Model)

Ahora ejecute la aplicación en modo de depuración. Inicie dos ventanas más del navegador, registre tres usuarios a la vez, establezca el estado listo y ejecute el juego. Debería ver el resultado que se muestra en la siguiente captura de pantalla. Debería mostrar una ubicación aleatoria e información sobre quién hace la pregunta primero. A cada participante en el juego se le debe asignar un rol; solo puede haber un espía.

Si todo está en orden con la inicialización del juego, puedes proceder a llenar esta página con elementos interactivos del juego.

Finalización de la segunda parte.

Con esto finaliza la segunda parte de mi artículo. En esta parte, refactorizó, llenó la página de espera de registro de los participantes del juego con elementos interactivos y creó la página para el juego en sí. En la siguiente parte llenaremos esta página con elementos interactivos y completaremos el desarrollo de esta pequeña aplicación de juegos.

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *