Agregando un modelo a nuestra aplicación ASP.NET Core MVC

En esta parte del tutorial, vamos a agregar algunas clases para administrar películas en una base de datos. Estas clases formaran parte del “Modelo” de la aplicación MVC.

Vamos a usar estas clases con Entity Framework Core (EF Core) para trabajar con la base de datos. Entity Framework Core es un framework de mapeo de objetos a relacional (ORM) que simplifica el acceso a datos haciendo que tengamos que escribir mucho menos código. EF Core soporta muchos motores distintos de base de datos

Las clases que vamos a crear se suelen llamar clases POCO (del acrónimo Plain Old CLR Object, los viejos y simples objetos CLR ) porque no tienen ninguna dependencia en EF Core. Esto es solo una forma de nombrarlos que van a encontrar por ahí, solo significa eso, que no dependen de EF Core. Solo definen las propiedades en los datos que se van a guardar en la base de datos.

En este tutorial vamos a escribir las clases de modelo primero, y después van a crear la base de datos. Una alternativa a esto es utilizar una base de datos existente aunque utilizando una base de datos existente, se dificulta trabajar con migraciones de una versión de la base de datos a otra.

Agregar una clase de datos de modelo

Hagamos click derecho en la carpeta modelo y elijamos Agregar > Clase. Le pongamos de nombre a la clase Película y agreguemos las siguientes propiedades:

using System;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}

El campo Id es requerido para que la base de datos lo use como clave primaria.

Compilemos el proyecto para verificar que no hemos cometido ningun error. Ahora tenemos un Modelo en nuestra aplicación MVC.

Haciendo Scaffolding de un controlador

En el Explorador de Soluciones, hagamos click derecho en la carpeta Controllers y elijamos Agregar>Controlador. (Si estamos usando la 2.1 va a decir Nuevo item de Scaffolding pero es lo mismo)

Agregar nuevo item scaffolded

Si nos sale algun cuadro de dialogo de Agregar dependencias MVC:

En el cuadro de dialogo Agregar Scaffold, toquemos Controlador de MVC con Vistas, usando Entity Framework y después en Agregar.

Dialogo nuevo item scaffolded

Completemos el cuadro de dialogo Agregar controlador:

  • Clase de Modelo: Movie (MvcMovie.Models)
  • Clase de DataContext: Selecciona el ícono m y agreguemos el valor por defecto MvcMovie.Models.MvcMovieContext

Dialogo agregar controlador con nuevo datacontext

  • Vistas: Elijamos las opciones por defecto chequeadas.
  • Nombre de controlador: Elijamos el por defecto MoviesController.
  • Elijamos Agregar

Dialogo agregar controlador

Visual Studio crea:

  • Una clase de contexto de base de datos o DbContext (Data/MvcMovieContext.cs)
  • Un controlador para peliculas (Controller/MoviesController)
  • Archivos de vista para las páginas Crear (Create), Borrar (Delete), Edit(Editar) e Index (Views/Movies/*.cshtml)

La creación automática del DbContext y los métodos y vistas del ABMC (alta, baja, modificación y consulta) se conoce como scaffolding. Pronto vamos a tener una aplicación completamente funcional que nos deja administrar una base de datos de películas.

Si ejecutamos la aplicación y hacemos click en el enlace Mvc Movie veremos el siguiente error:

An unhandled exception occurred while processing the request.

SqlException: Cannot open database "MvcMovieContext-<GUID removed>" requested by the login. The login failed.
Login failed for user 'Rick'.

System.Data.SqlClient.SqlInternalConnectionTds..ctor(DbConnectionPoolIdentity identity, SqlConnectionString

Necesitamos crear la base de datos y utilizar las migraciones de EF Core.

Las migraciones nos dejan crear una base de datos que va con nuestro modelo de datos y actualizar el esquema de base de datos cuando nuestro modelo de datos vaya cambiando.

Agregar las herramientas de EF y la migración inicial

En esta seccion vamos a utilizar la Consola del Administrador de Paquetes para:

  • Agregar el paquete Entity Framework Core Tools. Este paquete se requiere para agregar las migraciones y actualizar la base de datos.
  • Agregar la migración inicial.
  • Actualizar la base de datos con la migración inicial.

Desde el menú Herramientas, seleccionemos Administrador de paquetes Nuget > Consola del Administrador de Paquetes

Menu herramientas con Package Manager Console seleccionado

En la consola que se va a abrir, ingresemos los siguientes comandos:

Add-Migration Initial
Update-Database

Ignoremos el siguiente mensaje de error, lo vamos a arreglar en la siguiente parte del tutorial:

Microsoft.EntityFrameworkCore.Model.Validation[30000]
No type was specified for the decimal column 'Price' on entity type 'Movie'. This will cause values to be silently truncated if they do not fit in the default precision and scale. Explicitly specify the SQL server column type that can accommodate all the values using 'ForHasColumnType()'.

El comando Add-Migration crea código que se va a usar para crear el esquema inicial de la base de datos. El esquema está basado en el modelo especificado en el DbContext (En el archivo Data/MvcMovieContext.cs). El argumento Initial es el nombre que le damos a la primera migración. Puedes utilizar cualquier nombre pero la convención es usar uno que describa la migración. Ver Introduction to Migrations para más información.

El comando Update-Database ejecuta el método Up en el archivo de la migración Migrations/_Initial.cs lo cual crea la base de datos.

Se pueden hacer los mismos pasos utilizando la interfaz de línea de comandos (cli) en lugar de usar la consola del administrador de paquetes (pmc).

  • Agrega las herramientas de EF Core al archivo csproj.
  • Ejecuta los siguientes comandos desde la consola
dotnet ef migrations add Initial
dotnet ef database update

Si ejecutamos la app y nos da el error:

SqlException: Cannot open database "Movie" requested by the login.
The login failed.
Login failed for user 'user name'.

Es porque es probable que no hayamos ejecutado el comando dotnet ef database update.

Probemos la aplicación

  • Ejecutemos la aplicación y toquemos el enlace MVC Movie.
  • Toquemos en el enlace Create New y creemos una película.

[Imagen del formulario pelicula lleno]

Pantalla crear pelicula

  • Puede que no podamos entrar entrar puntos decimales o coma en el campo Price. Para soportar la validación de jquery en sistemas que no utilicen el idioma inglés, que usan la coma para el punto decimal, y todos los formatos de fecha distintos al de estados unidos debemos hacer los pasos para globalizar nuestra aplicación. Ver https://github.com/aspnet/Docs/issues/4076 y los recursos adicionales para más información. Por ahora solo pongamos el número 10.

  • En algunas configuraciones regionales necesitamos especificar el formato de fecha. Mira el siguiente código:

using System;
using System.ComponentModel.DataAnnotations;

namespace MvcMovie.Models
{
    public class Movie
    {
        public int ID { get; set; }
        public string Title { get; set; }
        [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
        public DateTime ReleaseDate { get; set; }
        public string Genre { get; set; }
        public decimal Price { get; set; }
    }
}

Vamos a hablar sobre las DataAnnotations después en el tutorial.

Tocar en Create causa que el formulario se postee al servidor, donde la información de las películas se guarda en la base de datos. La aplicación redirecciona a la URL /Movies, donde la información se nos muestra.

Pantalla lista de películas

Creemos un par de películas más. Probemos los enlaces Edit, Details y Delete que todos funcionan.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        // This lambda determines whether user consent for non-essential cookies is needed for a given request.
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });


    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddDbContext<MvcMovieContext>(options =>
            options.UseSqlServer(Configuration.GetConnectionString("MvcMovieContext")));
}

El código de arriba muestra al contexto de base de datos (dbContext) agregado al contenedor de Inyección de dependencias en el archivo (Startup.cs).

services.AddDbContext<MvcMovieContext>(options => especifica que base de datos usar. => es un operador lambda

Abramos el archivo Controllers/MoviesController.cs y examinemos el constructor:

public class MoviesController : Controller
{
    private readonly MvcMovieContext _context;

    public MoviesController(MvcMovieContext context)
    {
        _context = context;
    }

El constructor utiliza la Inyección de Dependencias para inyectar el dbContext (MvcMovieContext) en el controlador. El dbContext se usa para cada uno de los métodos ABMC del constructor.

Modelos fuertemente tipeados con la palabra clave @model

Antes en este tutorial, vimos como un controlador puede pasar objetos a la vista usando el diccionario ViewData. El diccionario ViewData es un objeto dinámico que provee una forma conveniente de pasar información sin controlar los tipos a la vista.

MVC también nos permite pasar modelos fuertemente tipados a la vista. Esto quiere decir que la vista va a conocer que propiedades tiene nuestro modelo. Este enfoque fuertemente tipado nos permite tener mejores chequeos en tiempo de compilación. El mecanismo de scaffolding utiliza este enfoque (es decir, pasa un modelo fuertemente tipado) con el controlador de Movies cuando crea los métodos y las vistas.

Examinemos el método generado Details en el archivo Controllers/MoviesController.cs:

// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.ID == id);
    if (movie == null)
    {
        return NotFound();
    }

    return View(movie);
}

El parámetro id generalmente se pasa como datos de ruta. Por ejemplo: http://localhost:5000/movies/details/1 setea:

  • Como controlador al parámetro movies (primer segmento de la URL)
  • Como acción a details (el segundo segmento de la URL)
  • El id como 1 (el último segmento de la url)

También podemos pasar el id con una query string de la siguiente manera:

http://localhost:1234/movies/details?id=1

El parámetro id se define como tipo nullable (?int) en caso de que el valor de ID no sea provisto.

Una expresión lambda se pasa al método FirstOrDefaultAsync para seleccionar las entidades de películas que unan a los datos de ruta con el valor de la cadena de conexión.

var movie = await _context.Movie
.FirstOrDefaultAsync(m => m.ID == id);

Si encontramos una película, la instancia del modelo Movie es pasada a la vista Details:

return View(movie);

Examinemos el contenido del archivo Views/Movies/Details.cshtml:

@model MvcMovie.Models.Movie

@{
    ViewData["Title"] = "Details";
}

<h2>Details</h2>

<div>
    <h4>Movie</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.ReleaseDate)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.ReleaseDate)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Genre)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Genre)
        </dd>
        <dt>
            @Html.DisplayNameFor(model => model.Price)
        </dt>
        <dd>
            @Html.DisplayFor(model => model.Price)
        </dd>
    </dl>
</div>
<div>
    <a asp-action="Edit" asp-route-id="@Model.ID">Edit</a> |
    <a asp-action="Index">Back to List</a>
</div>

Incluyendo una sentencia @model al principio del archivo de vista, estamos especificando el tipo de objeto que la vista espera. Cuando creamos el controlador de películas, Visual Studio incluyó automáticamente la siguiente sentencia @model al principio del archivo Details.cshtml:

@model MvcMovie.Models.Movie

Esta directiva @model nos permite acceder a la película que el controlador le pasó a la vista usando un objeto Modelo que es fuertemente tipado. Por ejemplo, en la vista Details.cshtml, el código para cada campo de la película a los métodos HTML Helpers DisplayNameFor y DisplayFor con el objeto Modelo fuertemente tipado. Los métodos Create y Edit también pasan un objeto de modelo.

Examinemos la vista Index.cshtml y el método Index del controlador Movies. Notemos como el código obtiene una lista del DbContext y luego le pasa este objeto de lista a la vista.

// GET: Movies
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}

Cuando creamos el controlador de películas, de manera automática usando el scaffolding, se incluyó la siguiente sentencia en la parte de arriba del archivo Index.cshtml.

@model IEnumerable<MvcMovie.Models.Movie>

La directiva @model nos permite acceder a la lista de películas que el controlador le paso a la vista usando un objeto de Model que es fuertemente tipado. Por ejemplo, en la vista Index.cshtml, el código va recorriendo a través de las películas con una sentencia foreach a través del objeto Modelo fuertemente tipado.

@model IEnumerable<MvcMovie.Models.Movie>

@{
    ViewData["Title"] = "Index";
}

<h2>Index</h2>

<p>
    <a asp-action="Create">Create New</a>
</p>
<table class="table">
    <thead>
        <tr>
                <th>
                    @Html.DisplayNameFor(model => model.Title)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.ReleaseDate)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Genre)
                </th>
                <th>
                    @Html.DisplayNameFor(model => model.Price)
                </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
@foreach (var item in Model) {
        <tr>
            <td>
                @Html.DisplayFor(modelItem => item.Title)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.ReleaseDate)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Genre)
            </td>
            <td>
                @Html.DisplayFor(modelItem => item.Price)
            </td>
            <td>
                <a asp-action="Edit" asp-route-id="@item.ID">Edit</a> |
                <a asp-action="Details" asp-route-id="@item.ID">Details</a> |
                <a asp-action="Delete" asp-route-id="@item.ID">Delete</a>
            </td>
        </tr>
}
    </tbody>
</table>

Porque el objeto Modelo es fuertemente tipado (como un objeto IEnumerable<Movie>), cada item en el ciclo es de tipo Movie. Entre otros beneficios esto significa que obtenemos chequeo en tiempo de compilación del código (nos autocompleta los nombres de las propiedades):

Autocompletado del código CSHTML

Vamos a ver como trabajar con la base datos y como poner datos de inicialización en la siguiente parte del tutorial.