En este artículo abordaremos el mecanismo Model Binding, el encargado de asignar datos de solicitudes HTTP a parámetros de acción.

Cuando se trabaja en un proyecto MVC es tedioso tener que asociar los parámetros de manera manual en los métodos de acción, además de tedioso, no se ve limpio. Para resolver este problema se desarrolló una herramienta que asocia automáticamente los parámetros a un modelo. Esta herramienta es conocida como Model Binding.

Este post está enfocado a desarrolladores principiantes que buscan simplificar su código para sus proyecto MVC con ASP.NET Core en su versión 2.1.

Para ayudarnos, realizaremos una pequeña aplicación CRUD (Create, Update, Delete, Read) alrededor del modelo Libro.cs. Empezaremos por crear los modelos en la carpeta Models:

Modelos

Género

public enum Genero
    {
        Policiaca, Aventuras, Fantastica, Infantil, Historica, Drama, Comedia, Poesia, Ficcion
    }

Libro

public class Libro
    {
        [BindingBehavior(behavior: BindingBehavior.Optional)]
        public int Id { get; set; }

        [BindRequired]
        public String Titulo { get; set; }

        [BindRequired]
        public Genero Genero { get; set; }

        public Autor Autor { get; set; }
    }

Autor

    public class Autor
    {
        public int Id { get; set; }

        public String Nombre { get; set; }
    }

En el modelo, se puede especificar si sus propiedades serán incluidas al ser llamadas por los métodos de acción:

  • [BindingBehavior()] permite especificar si el modelo incluirá el campo, no lo incluirá, o si depende del método de acción.
  • [BindRequired] obliga a que la petición incluya la propiedad indicada.
  • [BindNever] impide que el método de acción reciba la propiedad

Interfaces

Para simular un servicio, optaremos por crear un repositorio que inicialice dos listas que más tarde serán utilizadas en las vistas.

IRepositoryLibreria

Utilizaremos la interfaz IRepositoryLibreria para definir los métodos que utilizará el repositorio de los libros:

public interface IRepositoryLibreria     
{         
    List<Autor> Autores { get; set; }         
    List<Libro> Libros { get; set; }
    List<Libro> GetLibros();         
    void CrearLibro(String titulo, int idAutor, Genero genero);         
    void EditarLibro(int id, String titulo, Genero genero);         
    Libro MostrarLibroPorId(int id);                  
    void EliminarLibro(int id);     
}

Repositorios

Los repositorios son los encargados de realizar las acciones sobre los datos.

RepositoryLibreria

Cuando el repositorio inicia, inicializa las listas de libros y autores, los demás métodos serán los encargados de realizar las acciones CRUD sobre las listas. Esto es tan solo una simulación, para aplicarlo en proyecto real solo sería necesario conectarlo con un DbContext asociado a una BBDD.

public class RepositoryLibreria : IRepositoryLibreria
    {

        public List<Autor> Autores { get; set; }

        public List<Libro> Libros { get; set; }

        public RepositoryLibreria()
        {
            Autores = new List<Autor>{
                new Autor{ Id = 1, Nombre = "Miguel de Cervantes" },
                new Autor{ Id = 2, Nombre = "Edgar Allan Poe" },
                new Autor{ Id = 3, Nombre = "Gabriel García Márquez" },
                new Autor{ Id = 4, Nombre = "George Orwell" },
                new Autor{ Id = 5, Nombre = "John Stuart Mill" },
                new Autor{ Id = 6, Nombre = "Ludwig von Mises" },
                new Autor{ Id = 7, Nombre = "Friedrich Hayek" },
                new Autor{ Id = 8, Nombre = "Milton Friedman" },
                new Autor{ Id = 9, Nombre = "Murray Rothbard" }
            };

            Libros = new List<Libro> {
                new Libro{ Id = 1, Titulo = "El ingenioso Hidalgo don Quijote de la Mancha",
                    Autor = this.Autores.Where(x => x.Nombre == "Miguel de Cervantes").FirstOrDefault(),
                    Genero = Genero.Aventuras},
                new Libro{ Id = 2, Titulo = "El cuervo",
                    Autor = this.Autores.Where(x => x.Nombre == "Edgar Allan Poe").FirstOrDefault(),
                    Genero = Genero.Poesia},
                new Libro{ Id = 3, Titulo = "Cien años de soledad",
                    Autor = this.Autores.Where(x => x.Nombre == "Gabriel García Márquez").FirstOrDefault(),
                    Genero = Genero.Ficcion},
                new Libro{ Id = 4, Titulo = "1984",
                    Autor = this.Autores.Where(x => x.Nombre == "George Orwell").FirstOrDefault(),
                    Genero = Genero.Ficcion}
            };
        }


        public Libro MostrarLibroPorId(int id)
        {
            return Libros.Where(x => x.Id == id).FirstOrDefault();
        }

        public List<Libro> GetLibros()
        {
            return Libros.ToList();
        }

        public void CrearLibro(string titulo, int idAutor, Genero genero)
        {
            int IdLibro = Libros.Max(x => x.Id) + 1;
            Libros.Add(
                new Libro
                {
                    Id = IdLibro,
                    Titulo = titulo,
                    Autor = Autores.Where(x => x.Id == idAutor).FirstOrDefault(),
                    Genero = genero
                });
        }

        public void EditarLibro(int id, string titulo, Genero genero)
        {
            Libro libro = Libros.Where(x => x.Id == id).FirstOrDefault();
            libro.Titulo = titulo;
            libro.Genero = genero;
        }
        
        public void EliminarLibro(int id){
            Libro libro = Libros.Where(x => x.Id == id).FirstOrDefault();
            Libros.Remove(libro);
        }
        
    }

Controladores

El controlador creado para administrar las vistas será el encargado de comunicar al repositorio que acciones realizar. Nuestro LibrosController, creado en la carpeta Controllers, se vería de la siguiente manera:

LibrosController

public class LibrosController : Controller
    {
        private readonly IRepositoryLibreria _repoLibreria;
        public LibrosController(IRepositoryLibreria repoLibreria)
        {
            this._repoLibreria = repoLibreria;
        }

        public IActionResult Index()
        {
            List<Libro> libros = _repoLibreria.GetLibros();
            return View(libros);
        }


        public IActionResult Detalles(int id)
        {
            return View(_repoLibreria.MostrarLibroPorId(id));
        }

        public IActionResult Crear()
        {
            ViewData["Autores"] = _repoLibreria.Autores.ToList();
            return View();
        }

        [HttpPost]
        public IActionResult Crear(Libro libro)
        {
            try
            {
                _repoLibreria.CrearLibro(libro.Titulo, libro.Autor.Id, libro.Genero);
            }
            catch (Exception ex)
            {
                throw ex;
            }

            TempData["Mensaje"] = "Libro creado con exito";
            return RedirectToAction("Index");
        }


        public IActionResult Modificar(int id)
        {
            return View(_repoLibreria.MostrarLibroPorId(id));
        }

        [HttpPost]
        public IActionResult Modificar([FromForm]Libro libro)
        {
            try
            {
                _repoLibreria.EditarLibro(libro.Id, libro.Titulo, libro.Genero);
            }
            catch (Exception ex)
            {
                throw ex;
            }

            TempData["Mensaje"] = "Libro modificado con exito";
            return RedirectToAction("Index");
        }
        
        public IActionResult Eliminar(int id)
        {
            try 
            {
                _repoLibreria.EliminarLibro(id);
            }
            catch (Exception ex)
            {
                throw ex;
            }

            TempData["Mensaje"] = "Libro eliminado con exito";
            return RedirectToAction("Index");
        }

    }

Como se puede observar, el método Crear() recibirá, gracias al Model Binding, el modelo Libro, y nos las propiedades de este, teniendo así un código más limpio, y a la vez, nos permite ahorrar lógica.

Otra característica de este mecanismo que se puede observar en el método Modificar(), es el uso del atributo [FromForm], además de este, existen otros como: [FromQuery][FromHeader][FromRoute], que permiten especificar de donde recibira la informacion el método de acción.

Vistas

Nos queda la V del patrón MVC, situadas dentro de la carpeta Views, crearemos otra carpeta con el nombre del Controller al que pertenecen, después crearemos las siguientes vistas:

Crear.cshtml

@{
    ViewData["Title"] = "Crear libro";
    List<Autor> Autores = ViewData["Autores"] as List<Autor>;
}

<div class="card">
    <form asp-controller="Libros" asp-action="Crear" method="post">
        <input hidden type="number" value="" name="id"/>
        <div class="form-group">
            <label>Título</label>
            <input type="text" value="" class="form-control" name="titulo"/>
        </div>
        <div class="form-group">
            <label>Género</label>
            @Html.DropDownList("Genero", 
                Html.GetEnumSelectList<Genero>(),
                "Selecciona un genero",new { @class = "form-control" })
        </div>
        <div class="form-group">
            <select name="Autor.Id" class="form-control">
                @foreach(Autor item in Autores){
                    <option value="@item.Id">@item.Nombre</option>
                }
            </select>
        </div>
        <div class="text-right">
            <input type="submit" name="submit" value="Modificar" class="btn btn-primary"/>
        </div>
    </form>
</div>

Detalles.cshtml

@model Libro

@{
    ViewData["Title"] = @Model.Titulo;
}

<div class="card">
    <h3>@Model.Titulo</h3>
    <ul>
        <li><b>Autor</b>: @Model.Autor.Nombre</li>
        <li><b>Género</b>: @Model.Genero</li>
    </ul>
    <div class="text-right">
        <a asp-controller="Libros" asp-action="Modificar" asp-route-id="@Model.Id">Editar</a>
    </div>
</div>

Index.cshtml

@model List<Libro>
    
@{
    ViewData["Title"] = "Libros";
}


@if(TempData["Mensaje"] != null){
    <div class="card" style="background: darkseagreen; color: white">@TempData["Mensaje"]
        <span onclick="closeAlert(this)" class="toggleAlertButton">×</span>
    </div>
}

<div class="card">
    <table class="table">
        <thead>
            <tr>
                <th>Título</th>
                <th>Autor</th>
                <th>Género</th>
            </tr>
        </thead>
        <tbody>
            @foreach (Libro item in Model)
            {
                <tr>
                    <td>
                        <a asp-controller="Libros" 
                        asp-action="Detalles" 
                        asp-route-id="@item.Id">@item.Titulo</a>
                    </td>
                    <td>@item.Autor.Nombre</td>
                    <td>@item.Genero</td>
                </tr>
            }
        </tbody>
    </table>
</div>

@section scripts{
    <script type="text/javascript">
        function closeAlert(element){
            element.parentNode.style.display = "none";
        }
    </script>
}

Modificar.cshtml

@model Libro

@{
    ViewData["Title"] = @Model.Titulo;
}

<div class="card">
    <form asp-controller="Libros" asp-action="Modificar" method="post">
        <input hidden type="number" value="@Model.Id" name="id"/>
        <div class="form-group">
            <label>Título</label>
            <input type="text" value="@Model.Titulo" class="form-control" name="titulo"/>
        </div>
        <div class="form-group">
            <label>Género</label>
            @Html.DropDownList("Genero", 
                Html.GetEnumSelectList<Genero>(),
                "Selecciona un genero",new { @class = "form-control" })
        </div>
        <div class="text-right">
            <input type="submit" name="submit" value="Modificar" class="btn btn-primary"/>
        </div>
    </form>
</div>

Layout.cshtml

Modificaremos el layout establecido por defecto para que se muestre de esta manera:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>@ViewData["Title"] - Mcsd</title>

    <environment include="Development">
        <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.css" />
        <link rel="stylesheet" href="~/css/site.css" />
    </environment>
    <environment exclude="Development">
        <link rel="stylesheet" href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/css/bootstrap.min.css"
              asp-fallback-href="~/lib/bootstrap/dist/css/bootstrap.min.css"
              asp-fallback-test-class="sr-only" asp-fallback-test-property="position" 
              asp-fallback-test-value="absolute" />
        <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" />
    </environment>
</head>
<body>
    <nav class="navbar navbar-inverse ">
        <div class="container">
            <div class="navbar-header">
                <button type="button" class="navbar-toggle" data-toggle="collapse" 
                    data-target=".navbar-collapse">
                    <span class="sr-only">Toggle navigation</span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                    <span class="icon-bar"></span>
                </button>
                <a asp-area="" asp-controller="Home" asp-action="Index" class="navbar-brand">Model binding</a>
            </div>
            <div class="navbar-collapse collapse">
                <ul class="nav navbar-nav">
                    <li><a asp-area="" asp-controller="Libros" asp-action="Index">Libros</a></li>
                    <li><a asp-area="" asp-controller="Libros" asp-action="Crear">Añadir libro</a></li>
                </ul>
            </div>
        </div>
    </nav>

    <partial name="_CookieConsentPartial" />

    <div class="container body-content">
        @RenderBody()
        <hr />
        <footer>
            <p>© 2019 - mcsd_modelbinding</p>
        </footer>
    </div>

    <environment include="Development">
        <script src="~/lib/jquery/dist/jquery.js"></script>
        <script src="~/lib/bootstrap/dist/js/bootstrap.js"></script>
        <script src="~/js/site.js" asp-append-version="true"></script>
    </environment>
    <environment exclude="Development">
        <script src="https://ajax.aspnetcdn.com/ajax/jquery/jquery-3.3.1.min.js"
                asp-fallback-src="~/lib/jquery/dist/jquery.min.js"
                asp-fallback-test="window.jQuery"
                crossorigin="anonymous"
                integrity="sha384-tsQFqpEReu7ZLhBV2VZlAu7zcOV+rXbYlF2cqB8txI/8aZajjp4Bqd+V6D5IgvKT">
        </script>
        <script src="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.7/bootstrap.min.js"
                asp-fallback-src="~/lib/bootstrap/dist/js/bootstrap.min.js"
                asp-fallback-test="window.jQuery && window.jQuery.fn && window.jQuery.fn.modal"
                crossorigin="anonymous"
                integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa">
        </script>
        <script src="~/js/site.min.js" asp-append-version="true"></script>
    </environment>

    @RenderSection("Scripts", required: false)
</body>
</html>

Último toque

Para que todo lo anterior funcione, hay que configurar la inyección de dependencias de la interfaz IRepositoryLibreria. Para ello, hay que añadir la siguiente linea de código en el método ConfigureServices del archivo startup.cs :

services.AddSingleton<IRepositoryLibreria, RepositoryLibreria>();

Resultado

Index
Vista Index de la aplicación
Detalle
Vista Detalle de la aplicación
Modificar
Vista modificar de la aplicación
Crear
Vista crear de la aplicación

A la hora de trabajar con Model Binding es importante colocar los atributos del modelo, pues así te aseguras de que no estableces a null una propiedad del mismo. Para ver el código del proyecto puedes acceder a su repositorio de Github.

Autor: Guillermo Priego Sierra

Curso: Microsoft MCSA Web Applications + Microsoft MCSD App Builder + Xamarin

Centro: Tajamar

Año académico: 2018-2019

Código / recursos utilizados / Otros datos de interés:
Repositorio

Leave a Comment

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

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.