Tutorial: Actualización de datos relacionados: ASP.NET MVC con EF Core

En el tutorial anterior, mostró los datos relacionados; en este tutorial, actualizará los datos relacionados mediante la actualización de campos de clave externa y las propiedades de navegación.

En las ilustraciones siguientes se muestran algunas de las páginas con las que va a trabajar.

Course Edit page

Edit Instructor page

En este tutorial ha:

  • Personaliza las páginas de cursos
  • Agrega la página de edición de instructores
  • Agrega cursos a la página de edición
  • Actualiza la página Delete
  • Agrega la ubicación de la oficina y cursos a la página Create

Requisitos previos

Personaliza las páginas de cursos

Cuando se crea una entidad de Course, debe tener una relación con un departamento existente. Para facilitar esto, el código con scaffolding incluye métodos de controlador y vistas de Create y Edit que incluyen una lista desplegable para seleccionar el departamento. La lista desplegable establece la propiedad de clave externa Course.DepartmentID, y eso es todo lo que necesita Entity Framework para cargar la propiedad de navegación Department con la entidad Department adecuada. Podrá usar el código con scaffolding, pero cámbielo ligeramente para agregar el control de errores y ordenar la lista desplegable.

En CoursesController.cs, elimine los cuatro métodos de creación y edición, y reemplácelos con el código siguiente:

public IActionResult Create()
{
    PopulateDepartmentsDropDownList();
    return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("CourseID,Credits,DepartmentID,Title")] Course course)
{
    if (ModelState.IsValid)
    {
        _context.Add(course);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}
public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}
[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var courseToUpdate = await _context.Courses
        .FirstOrDefaultAsync(c => c.CourseID == id);

    if (await TryUpdateModelAsync<Course>(courseToUpdate,
        "",
        c => c.Credits, c => c.DepartmentID, c => c.Title))
    {
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
        return RedirectToAction(nameof(Index));
    }
    PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
    return View(courseToUpdate);
}

Después del método HttpPost de Edit, cree un método que cargue la información de departamento para la lista desplegable.

private void PopulateDepartmentsDropDownList(object selectedDepartment = null)
{
    var departmentsQuery = from d in _context.Departments
                           orderby d.Name
                           select d;
    ViewBag.DepartmentID = new SelectList(departmentsQuery.AsNoTracking(), "DepartmentID", "Name", selectedDepartment);
}

El método PopulateDepartmentsDropDownList obtiene una lista de todos los departamentos ordenados por nombre, crea una colección SelectList para obtener una lista desplegable y pasa la colección a la vista en ViewBag. El método acepta el parámetro opcional selectedDepartment, que permite al código que realiza la llamada especificar el elemento que se seleccionará cuando se procese la lista desplegable. La vista pasará el nombre "DepartmentID" al asistente de etiquetas <select>, y luego el asistente sabe que puede buscar en el objeto ViewBag una SelectList denominada "DepartmentID".

El método Create de HttpGet llama al método PopulateDepartmentsDropDownList sin configurar el elemento seleccionado, ya que el departamento todavía no está establecido para un nuevo curso:

public IActionResult Create()
{
    PopulateDepartmentsDropDownList();
    return View();
}

El método Edit de HttpGet establece el elemento seleccionado, basándose en el identificador del departamento que ya está asignado a la línea que se está editando:

public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

Los métodos HttpPost para Create y Edit también incluyen código que configura el elemento seleccionado cuando vuelven a mostrar la página después de un error. Esto garantiza que, cuando vuelve a aparecer la página para mostrar el mensaje de error, el departamento que se haya seleccionado permanece seleccionado.

Agregar AsNoTracking a los métodos Details y Delete

Para optimizar el rendimiento de las páginas Course Details y Delete, agregue llamadas AsNoTracking en los métodos Details y Delete de HttpGet.

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

    var course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }

    return View(course);
}
public async Task<IActionResult> Delete(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var course = await _context.Courses
        .Include(c => c.Department)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.CourseID == id);
    if (course == null)
    {
        return NotFound();
    }

    return View(course);
}

Modificar las vistas de Course

En Views/Courses/Create.cshtml, agregue una opción "Select Department" a la lista desplegable Department, cambie el título de DepartmentID a Department y agregue un mensaje de validación.

<div class="form-group">
    <label asp-for="Department" class="control-label"></label>
    <select asp-for="DepartmentID" class="form-control" asp-items="ViewBag.DepartmentID">
        <option value="">-- Select Department --</option>
    </select>
    <span asp-validation-for="DepartmentID" class="text-danger" />
</div>

En Views/Courses/Edit.cshtml, realice el mismo cambio que acaba de hacer en Create.cshtml en el campo Department.

También en Views/Courses/Edit.cshtml, agregue un campo de número de curso antes del campo Title. Dado que el número de curso es la clave principal, esta se muestra, pero no se puede cambiar.

<div class="form-group">
    <label asp-for="CourseID" class="control-label"></label>
    <div>@Html.DisplayFor(model => model.CourseID)</div>
</div>

Ya hay un campo oculto (<input type="hidden">) para el número de curso en la vista Edit. Agregar un asistente de etiquetas <label> no elimina la necesidad de un campo oculto, porque no hace que el número de curso se incluya en los datos enviados cuando el usuario hace clic en Save en la página Edit.

En Views/Courses/Delete.cshtml, agregue un campo de número de curso en la parte superior y cambie el identificador del departamento por el nombre del departamento.

@model ContosoUniversity.Models.Course

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

<h2>Delete</h2>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Course</h4>
    <hr />
    <dl class="row">
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.CourseID)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.CourseID)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Title)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Title)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Credits)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Credits)
        </dd>
        <dt class="col-sm-2">
            @Html.DisplayNameFor(model => model.Department)
        </dt>
        <dd class="col-sm-10">
            @Html.DisplayFor(model => model.Department.Name)
        </dd>
    </dl>
    
    <form asp-action="Delete">
        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            <a asp-action="Index">Back to List</a>
        </div>
    </form>
</div>

En Views/Courses/Details.cshtml, realice el mismo cambio que acaba de hacer en Delete.cshtml.

Probar las páginas Course

Ejecute la aplicación, seleccione la pestaña Courses, haga clic en Create New y escriba los datos del curso nuevo:

Course Create page

Haga clic en Crear. Se muestra la página de índice de cursos con el nuevo curso agregado a la lista. El nombre de departamento de la lista de páginas de índice proviene de la propiedad de navegación, que muestra que la relación se estableció correctamente.

Haga clic en Edit en un curso en la página de índice de cursos.

Course Edit page

Cambie los datos en la página y haga clic en Save. Se muestra la página de índice de cursos con los datos del curso actualizados.

Agrega la página de edición de instructores

Al editar un registro de instructor, necesita poder actualizar la asignación de la oficina del instructor. La entidad Instructor tiene una relación de uno a cero o uno con la entidad OfficeAssignment, lo que significa que el código tiene que controlar las situaciones siguientes:

  • Si el usuario borra la asignación de oficina y esta tenía originalmente un valor, elimine la entidad OfficeAssignment.

  • Si el usuario escribe un valor de asignación de oficina y originalmente estaba vacío, cree una entidad OfficeAssignment.

  • Si el usuario cambia el valor de una asignación de oficina, cambie el valor en una entidad OfficeAssignment existente.

Actualizar el controlador de Instructors

En InstructorsController.cs, cambie el código en el método Edit de HttpGet para que cargue la propiedad de navegación OfficeAssignment de la entidad Instructor y llame a AsNoTracking:

public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructor = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);
    if (instructor == null)
    {
        return NotFound();
    }
    return View(instructor);
}

Reemplace el método Edit de HttpPost con el siguiente código para controlar las actualizaciones de asignaciones de oficina:

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EditPost(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructorToUpdate = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .FirstOrDefaultAsync(s => s.ID == id);

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    {
        if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
        {
            instructorToUpdate.OfficeAssignment = null;
        }
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
        return RedirectToAction(nameof(Index));
    }
    return View(instructorToUpdate);
}

El código realiza lo siguiente:

  • Cambia el nombre del método a EditPost porque la firma ahora es la misma que el método Edit de HttpGet (el atributo ActionName especifica que la dirección URL de /Edit/ aún está en uso).

  • Obtiene la entidad Instructor actual de la base de datos mediante la carga diligente de la propiedad de navegación OfficeAssignment. Esto es lo mismo que hizo en el método Edit de HttpGet.

  • Actualiza la entidad Instructor recuperada con valores del enlazador de modelos. La sobrecarga de TryUpdateModel le permite declarar las propiedades que quiera incluir. Esto evita el registro excesivo, como se explica en el segundo tutorial.

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    
  • Si la ubicación de la oficina está en blanco, establece la propiedad Instructor.OfficeAssignment en NULL para que se elimine la fila relacionada en la tabla OfficeAssignment.

    if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
    {
        instructorToUpdate.OfficeAssignment = null;
    }
    
  • Guarda los cambios en la base de datos.

Actualizar la vista de Edit de Instructor

En Views/Instructors/Edit.cshtml, agregue un nuevo campo para editar la ubicación de la oficina, al final antes del botón Save:

<div class="form-group">
    <label asp-for="OfficeAssignment.Location" class="control-label"></label>
    <input asp-for="OfficeAssignment.Location" class="form-control" />
    <span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>

Ejecute la aplicación, seleccione la pestaña Instructors y, después, haga clic en Edit en un instructor. Cambie el valor de Office Location y haga clic en Save.

Instructor Edit page

Agrega cursos a la página de edición

Los instructores pueden impartir cualquier número de cursos. Ahora mejorará la página de edición de instructores al agregar la capacidad de cambiar las asignaciones de cursos mediante un grupo de casillas, tal y como se muestra en la siguiente captura de pantalla:

Instructor Edit page with courses

La relación entre las entidades Course y Instructor es de varios a varios. Para agregar y eliminar relaciones, agregue y quite entidades del conjunto de entidades combinadas CourseAssignments.

La interfaz de usuario que le permite cambiar los cursos a los que está asignado un instructor es un grupo de casillas. Se muestra una casilla para cada curso en la base de datos y se seleccionan aquellos a los que está asignado actualmente el instructor. El usuario puede activar o desactivar las casillas para cambiar las asignaciones de los cursos. Si el número de cursos fuera mucho mayor, probablemente tendría que usar un método diferente de presentar los datos en la vista, pero usaría el mismo método de manipulación de una entidad de combinación para crear o eliminar relaciones.

Actualizar el controlador de Instructors

Para proporcionar datos a la vista de la lista de casillas, deberá usar una clase de modelo de vista.

Cree AssignedCourseData.cs en la carpeta SchoolViewModels y reemplace el código existente con el código siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace ContosoUniversity.Models.SchoolViewModels
{
    public class AssignedCourseData
    {
        public int CourseID { get; set; }
        public string Title { get; set; }
        public bool Assigned { get; set; }
    }
}

En InstructorsController.cs, reemplace el método Edit de HttpGet por el código siguiente. Los cambios aparecen resaltados.

public async Task<IActionResult> Edit(int? id)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructor = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.CourseAssignments).ThenInclude(i => i.Course)
        .AsNoTracking()
        .FirstOrDefaultAsync(m => m.ID == id);
    if (instructor == null)
    {
        return NotFound();
    }
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}

private void PopulateAssignedCourseData(Instructor instructor)
{
    var allCourses = _context.Courses;
    var instructorCourses = new HashSet<int>(instructor.CourseAssignments.Select(c => c.CourseID));
    var viewModel = new List<AssignedCourseData>();
    foreach (var course in allCourses)
    {
        viewModel.Add(new AssignedCourseData
        {
            CourseID = course.CourseID,
            Title = course.Title,
            Assigned = instructorCourses.Contains(course.CourseID)
        });
    }
    ViewData["Courses"] = viewModel;
}

El código agrega carga diligente para la propiedad de navegación Courses y llama al método PopulateAssignedCourseData nuevo para proporcionar información de la matriz de casilla mediante la clase de modelo de vista AssignedCourseData.

El código en el método PopulateAssignedCourseData lee todas las entidades Course para cargar una lista de cursos mediante la clase de modelo de vista. Para cada curso, el código comprueba si existe el curso en la propiedad de navegación Courses del instructor. Para crear una búsqueda eficaz al comprobar si un curso está asignado al instructor, los cursos asignados a él se colocan en una colección HashSet. La propiedad Assigned está establecida en true para los cursos a los que está asignado el instructor. La vista usará esta propiedad para determinar qué casilla debe mostrarse como seleccionada. Por último, la lista se pasa a la vista en ViewData.

A continuación, agregue el código que se ejecuta cuando el usuario hace clic en Save. Reemplace el método EditPost con el siguiente código y agregue un nuevo método que actualiza la propiedad de navegación Courses de la entidad Instructor.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int? id, string[] selectedCourses)
{
    if (id == null)
    {
        return NotFound();
    }

    var instructorToUpdate = await _context.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.CourseAssignments)
            .ThenInclude(i => i.Course)
        .FirstOrDefaultAsync(m => m.ID == id);

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    {
        if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
        {
            instructorToUpdate.OfficeAssignment = null;
        }
        UpdateInstructorCourses(selectedCourses, instructorToUpdate);
        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateException /* ex */)
        {
            //Log the error (uncomment ex variable name and write a log.)
            ModelState.AddModelError("", "Unable to save changes. " +
                "Try again, and if the problem persists, " +
                "see your system administrator.");
        }
        return RedirectToAction(nameof(Index));
    }
    UpdateInstructorCourses(selectedCourses, instructorToUpdate);
    PopulateAssignedCourseData(instructorToUpdate);
    return View(instructorToUpdate);
}
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

La firma del método ahora es diferente del método Edit de HttpGet, por lo que el nombre del método cambia de EditPost a Edit.

Puesto que la vista no tiene una colección de entidades Course, el enlazador de modelos no puede actualizar automáticamente la propiedad de navegación CourseAssignments. En lugar de usar el enlazador de modelos para actualizar la propiedad de navegación CourseAssignments, lo hace en el nuevo método UpdateInstructorCourses. Por lo tanto, tendrá que excluir la propiedad CourseAssignments del enlace de modelos. Esto no requiere ningún cambio en el código que llama a TryUpdateModel porque está usando la sobrecarga que requiere aprobación explícita y CourseAssignments no está en la lista de inclusión.

Si no se ha seleccionado ninguna casilla, el código en UpdateInstructorCourses inicializa la propiedad de navegación CourseAssignments con una colección vacía y devuelve:

private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

A continuación, el código recorre en bucle todos los cursos de la base de datos y coteja los que están asignados actualmente al instructor frente a los que se han seleccionado en la vista. Para facilitar las búsquedas eficaces, estas dos últimas colecciones se almacenan en objetos HashSet.

Si se ha activado la casilla para un curso, pero este no se encuentra en la propiedad de navegación Instructor.CourseAssignments, el curso se agrega a la colección en la propiedad de navegación.

private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

Si no se ha activado la casilla para un curso, pero este se encuentra en la propiedad de navegación Instructor.CourseAssignments, el curso se quita de la colección en la propiedad de navegación.

private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
    if (selectedCourses == null)
    {
        instructorToUpdate.CourseAssignments = new List<CourseAssignment>();
        return;
    }

    var selectedCoursesHS = new HashSet<string>(selectedCourses);
    var instructorCourses = new HashSet<int>
        (instructorToUpdate.CourseAssignments.Select(c => c.Course.CourseID));
    foreach (var course in _context.Courses)
    {
        if (selectedCoursesHS.Contains(course.CourseID.ToString()))
        {
            if (!instructorCourses.Contains(course.CourseID))
            {
                instructorToUpdate.CourseAssignments.Add(new CourseAssignment { InstructorID = instructorToUpdate.ID, CourseID = course.CourseID });
            }
        }
        else
        {

            if (instructorCourses.Contains(course.CourseID))
            {
                CourseAssignment courseToRemove = instructorToUpdate.CourseAssignments.FirstOrDefault(i => i.CourseID == course.CourseID);
                _context.Remove(courseToRemove);
            }
        }
    }
}

Actualizar las vistas de Instructor

En Views/Instructors/Edit.cshtml, agregue un campo Courses con una matriz de casillas al agregar el siguiente código inmediatamente después de los elementos div del campo Office y antes del elemento div del botón Guardar.

Nota:

Al pegar el código en Visual Studio, los saltos de línea podrían cambiarse de tal forma que el código se interrumpiese. Si el código tiene un aspecto diferente después de pegarlo, presione CTRL+Z una vez para deshacer el formato automático. Esto corregirá los saltos de línea para que se muestren como se ven aquí. No es necesario que la sangría sea perfecta, pero las líneas @:</tr><tr>, @:<td>, @:</td> y @:</tr> deben estar en una única línea tal y como se muestra, de lo contrario, obtendrá un error en tiempo de ejecución. Con el bloque de código nuevo seleccionado, presione tres veces la tecla TAB para alinearlo con el código existente. Este problema se ha corregido en Visual Studio 2019.

<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <table>
            <tr>
                @{
                    int cnt = 0;
                    List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData> courses = ViewBag.Courses;

                    foreach (var course in courses)
                    {
                        if (cnt++ % 3 == 0)
                        {
                            @:</tr><tr>
                        }
                        @:<td>
                            <input type="checkbox"
                                   name="selectedCourses"
                                   value="@course.CourseID"
                                   @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
                                   @course.CourseID @:  @course.Title
                        @:</td>
                    }
                    @:</tr>
                }
        </table>
    </div>
</div>

Este código crea una tabla HTML que tiene tres columnas. En cada columna hay una casilla seguida de una leyenda que está formada por el número y el título del curso. Todas las casillas tienen el mismo nombre ("selectedCourses"), que informa al enlazador de modelos que se deben tratar como un grupo. El atributo de valor de cada casilla se establece en el valor de CourseID. Cuando se envía la página, el enlazador de modelos pasa una matriz al controlador formada solo por los valores CourseID de las casillas activadas.

Cuando las casillas se representan inicialmente, aquellas que son para cursos asignados al instructor tienen atributos seleccionados, lo que las selecciona (las muestra activadas).

Ejecute la aplicación, seleccione la pestaña Instructors y haga clic en Edit en un instructor para ver la página Edit.

Instructor Edit page with courses

Cambie algunas asignaciones de cursos y haga clic en Save. Los cambios que haga se reflejan en la página de índice.

Nota

El enfoque que se aplica aquí para modificar datos de los cursos del instructor funciona bien cuando hay un número limitado de cursos. Para las colecciones que son mucho más grandes, se necesitaría una interfaz de usuario y un método de actualización diferentes.

Actualiza la página Delete

En InstructorsController.cs, elimine el método DeleteConfirmed e inserte el siguiente código en su lugar.

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
    Instructor instructor = await _context.Instructors
        .Include(i => i.CourseAssignments)
        .SingleAsync(i => i.ID == id);

    var departments = await _context.Departments
        .Where(d => d.InstructorID == id)
        .ToListAsync();
    departments.ForEach(d => d.InstructorID = null);

    _context.Instructors.Remove(instructor);

    await _context.SaveChangesAsync();
    return RedirectToAction(nameof(Index));
}

Este código realiza los cambios siguientes:

  • Hace la carga diligente para la propiedad de navegación CourseAssignments. Tiene que incluir esto o EF no conocerá las entidades CourseAssignment relacionadas y no las eliminará. Para evitar la necesidad de leerlos aquí, puede configurar la eliminación en cascada en la base de datos.

  • Si el instructor que se va a eliminar está asignado como administrador de cualquiera de los departamentos, quita la asignación de instructor de esos departamentos.

Agrega la ubicación de la oficina y cursos a la página Create

En InstructorsController.cs, elimine los métodos Create de HttpGet y HttpPost y, después, agregue el código siguiente en su lugar:

public IActionResult Create()
{
    var instructor = new Instructor();
    instructor.CourseAssignments = new List<CourseAssignment>();
    PopulateAssignedCourseData(instructor);
    return View();
}

// POST: Instructors/Create
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("FirstMidName,HireDate,LastName,OfficeAssignment")] Instructor instructor, string[] selectedCourses)
{
    if (selectedCourses != null)
    {
        instructor.CourseAssignments = new List<CourseAssignment>();
        foreach (var course in selectedCourses)
        {
            var courseToAdd = new CourseAssignment { InstructorID = instructor.ID, CourseID = int.Parse(course) };
            instructor.CourseAssignments.Add(courseToAdd);
        }
    }
    if (ModelState.IsValid)
    {
        _context.Add(instructor);
        await _context.SaveChangesAsync();
        return RedirectToAction(nameof(Index));
    }
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}

Este código es similar a lo que ha visto para los métodos Edit, excepto que no hay cursos seleccionados inicialmente. El método Create de HttpGet no llama al método PopulateAssignedCourseData porque pueda haber cursos seleccionados sino para proporcionar una colección vacía para el bucle foreach en la vista (en caso contrario, el código de vista podría producir una excepción de referencia nula).

El método Create de HttpPost agrega cada curso seleccionado a la propiedad de navegación CourseAssignments antes de comprobar si hay errores de validación y agrega el instructor nuevo a la base de datos. Los cursos se agregan incluso si hay errores de modelo, por lo que cuando hay errores del modelo (por ejemplo, el usuario escribió una fecha no válida) y se vuelve a abrir la página con un mensaje de error, las selecciones de cursos que se habían realizado se restauran todas automáticamente.

Tenga en cuenta que, para poder agregar cursos a la propiedad de navegación CourseAssignments, debe inicializar la propiedad como una colección vacía:

instructor.CourseAssignments = new List<CourseAssignment>();

Como alternativa a hacerlo en el código de control, podría hacerlo en el modelo de Instructor si cambia el captador de propiedad para que cree automáticamente la colección en caso de que no exista, como se muestra en el ejemplo siguiente:

private ICollection<CourseAssignment> _courseAssignments;
public ICollection<CourseAssignment> CourseAssignments
{
    get
    {
        return _courseAssignments ?? (_courseAssignments = new List<CourseAssignment>());
    }
    set
    {
        _courseAssignments = value;
    }
}

Si modifica la propiedad CourseAssignments de esta manera, puede quitar el código de inicialización de propiedad explícito del controlador.

En Views/Instructor/Create.cshtml, agregue un cuadro de texto de la ubicación de la oficina y casillas para cursos antes del botón Enviar. Al igual que en el caso de la página Edit, corrija el formato si Visual Studio vuelve a aplicar formato al código al pegarlo.

<div class="form-group">
    <label asp-for="OfficeAssignment.Location" class="control-label"></label>
    <input asp-for="OfficeAssignment.Location" class="form-control" />
    <span asp-validation-for="OfficeAssignment.Location" class="text-danger" />
</div>

<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <table>
            <tr>
                @{
                    int cnt = 0;
                    List<ContosoUniversity.Models.SchoolViewModels.AssignedCourseData> courses = ViewBag.Courses;

                    foreach (var course in courses)
                    {
                        if (cnt++ % 3 == 0)
                        {
                            @:</tr><tr>
                        }
                        @:<td>
                            <input type="checkbox"
                                   name="selectedCourses"
                                   value="@course.CourseID"
                                   @(Html.Raw(course.Assigned ? "checked=\"checked\"" : "")) />
                                   @course.CourseID @:  @course.Title
                            @:</td>
                    }
                    @:</tr>
                }
        </table>
    </div>
</div>

Pruebe a ejecutar la aplicación y crear un instructor.

Control de transacciones

Como se explicó en el tutorial de CRUD, Entity Framework implementa las transacciones de manera implícita. Para escenarios donde se necesita más control, por ejemplo, si se quieren incluir operaciones realizadas fuera de Entity Framework en una transacción, vea Transacciones.

Obtención del código

Descargue o vea la aplicación completa.

Pasos siguientes

En este tutorial ha:

  • Personalizado las páginas de cursos
  • Agregado la página de edición de instructores
  • Agregado cursos a la página de edición
  • Actualizado la página Delete
  • Agregado la ubicación de la oficina y cursos a la página Create

Pase al tutorial siguiente para obtener información sobre cómo controlar conflictos de simultaneidad.