Tutorial: Actualización de datos relacionados con EF en una aplicación ASP.NET MVC

En el tutorial anterior, ha mostrado datos relacionados. En este tutorial actualizará los datos relacionados. Para la mayoría de las relaciones, esto se puede hacer mediante la actualización de campos de clave externa o propiedades de navegación. En el caso de las relaciones de varios a varios, Entity Framework no expone la tabla de combinación directamente, por lo que se agregan y quitan entidades a las propiedades de navegación adecuadas, y desde ellas.

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

Course_create_page

Instructor_edit_page_with_courses

Instructor edit with courses

En este tutorial ha:

  • Personalización de las páginas de cursos
  • Adición de una oficina a la página de instructores
  • Adición de cursos a la página de instructores
  • Actualización de DeleteConfirmed
  • Agregar la ubicación de la oficina y cursos a la página Create

Requisitos previos

Personalización de las páginas de cursos

Cuando se crea una entidad de curso, 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 CourseController.cs, elimine los cuatro métodos de Create y Edit, y reemplácelos por el código siguiente:

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

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "CourseID,Title,Credits,DepartmentID")]Course course)
{
    try
    {
        if (ModelState.IsValid)
        {
            db.Courses.Add(course);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    }
    catch (RetryLimitExceededException /* dex */)
    {
        //Log the error (uncomment dex variable name and add a line here to write a log.)
        ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

public ActionResult Edit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Course course = db.Courses.Find(id);
    if (course == null)
    {
        return HttpNotFound();
    }
    PopulateDepartmentsDropDownList(course.DepartmentID);
    return View(course);
}

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    var courseToUpdate = db.Courses.Find(id);
    if (TryUpdateModel(courseToUpdate, "",
       new string[] { "Title", "Credits", "DepartmentID" }))
    {
        try
        {
            db.SaveChanges();

            return RedirectToAction("Index");
        }
        catch (RetryLimitExceededException /* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
        }
    }
    PopulateDepartmentsDropDownList(courseToUpdate.DepartmentID);
    return View(courseToUpdate);
}

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

Agregue la instrucción using siguiente al principio del archivo:

using System.Data.Entity.Infrastructure;

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 una propiedad 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 DropDownList y luego el asistente sabe que puede buscar en el objeto ViewBag para 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 ActionResult 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 ActionResult Edit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Course course = db.Courses.Find(id);
    if (course == null)
    {
        return HttpNotFound();
    }
    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:

catch (RetryLimitExceededException /* dex */)
{
    //Log the error (uncomment dex variable name and add a line here to write a log.)
    ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
}
PopulateDepartmentsDropDownList(course.DepartmentID);
return View(course);

Este código garantiza que, cuando vuelve a aparecer la página para mostrar el mensaje de error, el departamento que se haya seleccionado permanece seleccionado.

Las vistas Course ya están con scaffolding con listas desplegables para el campo de departamento, pero no desea el título DepartmentID para este campo, por lo que debe modificar el siguiente cambio resaltado en el archivo Views\Course\Create.cshtml para cambiar el título.

@model ContosoUniversity.Models.Course

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm()) 
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>Course</h4>
        <hr />
        @Html.ValidationSummary(true)

        <div class="form-group">
            @Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.CourseID)
                @Html.ValidationMessageFor(model => model.CourseID)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Title, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Title)
                @Html.ValidationMessageFor(model => model.Title)
            </div>
        </div>

        <div class="form-group">
            @Html.LabelFor(model => model.Credits, new { @class = "control-label col-md-2" })
            <div class="col-md-10">
                @Html.EditorFor(model => model.Credits)
                @Html.ValidationMessageFor(model => model.Credits)
            </div>
        </div>

        <div class="form-group">
            <label class="control-label col-md-2" for="DepartmentID">Department</label>
            <div class="col-md-10">
                @Html.DropDownList("DepartmentID", String.Empty)
                @Html.ValidationMessageFor(model => model.DepartmentID)
            </div>
        </div>

        <div class="form-group">
            <div class="col-md-offset-2 col-md-10">
                <input type="submit" value="Create" class="btn btn-default" />
            </div>
        </div>
    </div>
}

<div>
    @Html.ActionLink("Back to List", "Index")
</div>

@section Scripts {
    @Scripts.Render("~/bundles/jqueryval")
}

Haga el mismo cambio en Views\Course\Edit.cshtml.

Normalmente, el scaffolder no aplica scaffolding a una clave principal porque la base de datos genera el valor de clave y no se puede cambiar y no es un valor significativo que se mostrará a los usuarios. En el caso de las entidades Course, el scaffolder incluye un cuadro de texto para el campo CourseID porque entiende que el atributo DatabaseGeneratedOption.None significa que el usuario debe poder escribir el valor de clave principal. Pero no entiende que, dado que el número es significativo, quiere verlo en las otras vistas, por lo que debe agregarlo manualmente.

En Views\Course\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">
    @Html.LabelFor(model => model.CourseID, new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.DisplayFor(model => model.CourseID)
    </div>
</div>

Ya hay un campo oculto (asistente de Html.HiddenFor) para el número de curso en la vista Edit. Agregar un asistente Html.LabelFor 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\Course\Delete.cshtml y Views\Course\Details.cshtml, cambie el título de nombre del departamento de "Name" a "Department" y agregue un campo de número de curso antes del campo Title.

<dt>
    Department
</dt>

<dd>
    @Html.DisplayFor(model => model.Department.Name)
</dd>

<dt>
    @Html.DisplayNameFor(model => model.CourseID)
</dt>

<dd>
    @Html.DisplayFor(model => model.CourseID)
</dd>

Ejecute la página Create (muestre la página Course Index y haga clic en Create new) y escriba los datos de un nuevo curso:

Valor Configuración
Número Escriba 1000.
Título Escriba Algebra.
Créditos Escriba 4.
department Seleccione Mathematics.

Haga clic en Crear. Se muestra la página Course Index 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.

Ejecute la página Edit (muestre la página Course Index y haga clic en Edit en un curso).

Cambie los datos en la página y haga clic en Save. Se muestra la página Course Index con los datos del curso actualizados.

Adición de una oficina a la página 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 tiene que controlar las situaciones siguientes:

  • Si el usuario borra la asignación de oficina y originalmente tenía un valor, tiene que quitar y eliminar la entidad OfficeAssignment.
  • Si el usuario escribe un valor de asignación de oficina y originalmente estaba vacío, tiene que crear una entidad OfficeAssignment.
  • Si el usuario cambia el valor de una asignación de oficina, tiene que cambiar el valor en una entidad OfficeAssignment existente.

Abra InstructorController.cs y examine el método Edit de HttpGet:

{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Instructor instructor = db.Instructors.Find(id);
    if (instructor == null)
    {
        return HttpNotFound();
    }
    ViewBag.ID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.ID);
    return View(instructor);
}

El código con scaffolding aquí no es lo que quiere. Está configurando datos para una lista desplegable, pero lo que necesita es un cuadro de texto. Reemplace este método con el código siguiente:

public ActionResult Edit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Instructor instructor = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Where(i => i.ID == id)
        .Single();
    if (instructor == null)
    {
        return HttpNotFound();
    }
    return View(instructor);
}

Este código quita la instrucción ViewBag y agrega carga diligente para la entidad OfficeAssignment asociada. No puede realizar la carga diligente con el método Find, por lo que los métodos Where y Single se usan en su lugar para seleccionar el instructor.

Reemplace el método HttpPostEdit por el código siguiente. que controla las actualizaciones de asignación de oficina:

[HttpPost, ActionName("Edit")]
[ValidateAntiForgeryToken]
public ActionResult EditPost(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    var instructorToUpdate = db.Instructors
       .Include(i => i.OfficeAssignment)
       .Where(i => i.ID == id)
       .Single();

    if (TryUpdateModel(instructorToUpdate, "",
       new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
    {
       try
       {
          if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
          {
             instructorToUpdate.OfficeAssignment = null;
          }

          db.SaveChanges();

          return RedirectToAction("Index");
       }
       catch (RetryLimitExceededException /* dex */)
      {
         //Log the error (uncomment dex variable name and add a line here to write a log.
         ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
      }
   }
   return View(instructorToUpdate);
}

La referencia a RetryLimitExceededException requiere una instrucción using; para agregarla, mantenga el cursor sobre RetryLimitExceededException. Aparecerá el siguiente mensaje:  Retry exception message

Seleccione Show potential fixes y, después, use System.Data.Entity.Infrastructure.

Resolve Retry exception

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 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 HttpGet de Edit.

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

    if (TryUpdateModel(instructorToUpdate, "",
          new string[] { "LastName", "FirstMidName", "HireDate", "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.

En Views\Instructor\Edit.cshtml, después de los elementos div del campo Hire Date, agregue un nuevo campo para editar la ubicación de la oficina:

<div class="form-group">
    @Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.OfficeAssignment.Location)
        @Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
    </div>
</div>

Ejecute la página (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.

Adición de cursos a la página de instructores

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.

La relación entre las entidades Course y Instructor es de varios a varios, lo que significa que no tiene acceso directo a las propiedades de clave externa que se encuentran en la tabla de combinación. En su lugar, puede agregar y quitar entidades a la propiedad de navegación Instructor.Courses y desde ella.

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 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.

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 ViewModels y reemplace el código existente con el código siguiente:

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

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

public ActionResult Edit(int? id)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Instructor instructor = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses)
        .Where(i => i.ID == id)
        .Single();
    if (instructor == null)
    {
        return HttpNotFound();
    }
    PopulateAssignedCourseData(instructor);
    return View(instructor);
}

private void PopulateAssignedCourseData(Instructor instructor)
{
    var allCourses = db.Courses;
    var instructorCourses = new HashSet<int>(instructor.Courses.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)
        });
    }
    ViewBag.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 una propiedad ViewBag.

A continuación, agregue el código que se ejecuta cuando el usuario hace clic en Save. Reemplace el método EditPost por el siguiente código, que llama a un nuevo método que actualiza la propiedad de navegación Courses de la entidad Instructor. Los cambios aparecen resaltados.

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(int? id, string[] selectedCourses)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    var instructorToUpdate = db.Instructors
       .Include(i => i.OfficeAssignment)
       .Include(i => i.Courses)
       .Where(i => i.ID == id)
       .Single();

    if (TryUpdateModel(instructorToUpdate, "",
       new string[] { "LastName", "FirstMidName", "HireDate", "OfficeAssignment" }))
    {
        try
        {
            if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment.Location))
            {
                instructorToUpdate.OfficeAssignment = null;
            }

            UpdateInstructorCourses(selectedCourses, instructorToUpdate);

            db.SaveChanges();

            return RedirectToAction("Index");
        }
        catch (RetryLimitExceededException /* dex */)
        {
            //Log the error (uncomment dex variable name and add a line here to write a log.
            ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists, see your system administrator.");
        }
    }
    PopulateAssignedCourseData(instructorToUpdate);
    return View(instructorToUpdate);
}
private void UpdateInstructorCourses(string[] selectedCourses, Instructor instructorToUpdate)
{
   if (selectedCourses == null)
   {
      instructorToUpdate.Courses = new List<Course>();
      return;
   }
 
   var selectedCoursesHS = new HashSet<string>(selectedCourses);
   var instructorCourses = new HashSet<int>
       (instructorToUpdate.Courses.Select(c => c.CourseID));
   foreach (var course in db.Courses)
   {
      if (selectedCoursesHS.Contains(course.CourseID.ToString()))
      {
         if (!instructorCourses.Contains(course.CourseID))
         {
            instructorToUpdate.Courses.Add(course);
         }
      }
      else
      {
         if (instructorCourses.Contains(course.CourseID))
         {
            instructorToUpdate.Courses.Remove(course);
         }
      }
   }
}

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 Courses. En lugar de usar el enlazador de modelos para actualizar la propiedad de navegación Courses, lo hará en el nuevo método UpdateInstructorCourses. Por lo tanto, tendrá que excluir la propiedad Courses del enlace de modelos. Esto no requiere ningún cambio en el código que llama a TryUpdateModel porque está usando la sobrecarga de listas explícita y Courses no se encuentra en la lista de inclusión.

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

if (selectedCourses == null)
{
    instructorToUpdate.Courses = new List<Course>();
    return;
}

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.Courses, el curso se agrega a la colección en la propiedad de navegación.

if (selectedCoursesHS.Contains(course.CourseID.ToString()))
{
    if (!instructorCourses.Contains(course.CourseID))
    {
        instructorToUpdate.Courses.Add(course);
    }
}

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

else
{
    if (instructorCourses.Contains(course.CourseID))
    {
        instructorToUpdate.Courses.Remove(course);
    }
}

En Views\Instructor\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 OfficeAssignment y antes del elemento div del botón Save:

<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <table>
            <tr>
                @{
                    int cnt = 0;
                    List<ContosoUniversity.ViewModels.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>

Después de pegar el código, si los saltos de línea y la sangría no tienen el aspecto que hacen aquí, corrija manualmente todo para que parezca lo que ve 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.

Este código crea una tabla HTML que tiene tres columnas. En cada columna hay una casilla seguida de un título que está formado 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 value de cada casilla se establece en el valor de CourseID.. Cuando se publica la página, el enlazador de modelos pasa una matriz al controlador que consta de los valores CourseID solo para las casillas que están seleccionadas.

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

Después de cambiar las asignaciones de curso, querrá poder comprobar los cambios cuando el sitio vuelva a la página Index. Por lo tanto, debe agregar una columna a la tabla de esa página. En este caso, no es necesario usar el objeto ViewBag, ya que la información que quiere mostrar ya está en la propiedad de navegación Courses de la entidad Instructor que va a pasar a la página como modelo.

En Views\Instructor\Index.cshtml, agregue el título Courses inmediatamente después del encabezado Office, como se muestra en el ejemplo siguiente:

<tr> 
    <th>Last Name</th> 
    <th>First Name</th> 
    <th>Hire Date</th> 
    <th>Office</th>
    <th>Courses</th>
    <th></th> 
</tr>

A continuación, agregue una nueva celda de detalle inmediatamente después de la celda de detalles de la ubicación de la oficina:

<td>
    @if (item.OfficeAssignment != null)
    {
        @item.OfficeAssignment.Location
    }
</td>
<td>
    @{
        foreach (var course in item.Courses)
        {
            @course.CourseID @:  @course.Title <br />
        }
    }
</td>
<td>
    @Html.ActionLink("Select", "Index", new { id = item.ID }) |
    @Html.ActionLink("Edit", "Edit", new { id = item.ID }) |
    @Html.ActionLink("Details", "Details", new { id = item.ID }) |
    @Html.ActionLink("Delete", "Delete", new { id = item.ID })
</td>

Ejecute la página Instructor Index para ver los cursos asignados a cada instructor.

Haga clic en Edit en un instructor para ver la página Edit.

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.

Actualización de DeleteConfirmed

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

[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public ActionResult DeleteConfirmed(int id)
{
   Instructor instructor = db.Instructors
     .Include(i => i.OfficeAssignment)
     .Where(i => i.ID == id)
     .Single();

   db.Instructors.Remove(instructor);

    var department = db.Departments
        .Where(d => d.InstructorID == id)
        .SingleOrDefault();
    if (department != null)
    {
        department.InstructorID = null;
    }

   db.SaveChanges();
   return RedirectToAction("Index");
}

Este código realiza los cambios siguientes:

  • Si el instructor está asignado como administrador de cualquiera de los departamentos, quita la asignación de instructor de ese departamento. Sin este código, obtendrá un error de integridad referencial si intentó eliminar un instructor que se asignó como administrador para un departamento.

Este código no controla el escenario de un instructor asignado como administrador para varios departamentos. En el último tutorial, agregará código que evite que se produzca ese escenario.

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

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

public ActionResult Create()
{
    var instructor = new Instructor();
    instructor.Courses = new List<Course>();
    PopulateAssignedCourseData(instructor);
    return View();
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "LastName,FirstMidName,HireDate,OfficeAssignment" )]Instructor instructor, string[] selectedCourses)
{
    if (selectedCourses != null)
    {
        instructor.Courses = new List<Course>();
        foreach (var course in selectedCourses)
        {
            var courseToAdd = db.Courses.Find(int.Parse(course));
            instructor.Courses.Add(courseToAdd);
        }
    }
    if (ModelState.IsValid)
    {
        db.Instructors.Add(instructor);
        db.SaveChanges();
        return RedirectToAction("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 Courses 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 Courses, debe inicializar la propiedad como una colección vacía:

instructor.Courses = new List<Course>();

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

private ICollection<Course> _courses;
public virtual ICollection<Course> Courses 
{ 
    get
    {
        return _courses ?? (_courses = new List<Course>());
    }
    set
    {
        _courses = value;
    } 
}

Si modifica la propiedad Courses 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 después del campo de fecha de contratación y antes del botón Submit.

<div class="form-group">
    @Html.LabelFor(model => model.OfficeAssignment.Location, new { @class = "control-label col-md-2" })
    <div class="col-md-10">
        @Html.EditorFor(model => model.OfficeAssignment.Location)
        @Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
    </div>
</div>

<div class="form-group">
    <div class="col-md-offset-2 col-md-10">
        <table>
            <tr>
                @{
                    int cnt = 0;
                    List<ContosoUniversity.ViewModels.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>

Después de pegar el código, corrija los saltos de línea y la sangría como hizo anteriormente para la página Edit.

Ejecute la página Create y agregue un instructor.

Controlar transacciones

Como se explica en el tutorial sobre la funcionalidad CRUD básica, de forma predeterminada, Entity Framework implementa implícitamente las transacciones. Para escenarios donde se necesita más control, por ejemplo, si se quiere incluir operaciones realizadas fuera de Entity Framework en una transacción, consulte Trabajar con transacciones en MSDN.

Obtención del código

Descargar el proyecto completado

Recursos adicionales

Encontrará vínculos a otros recursos de Entity Framework en Acceso a datos de ASP.NET: Recursos recomendados.

Paso siguiente

En este tutorial ha:

  • Personalizado páginas de cursos
  • Agregado una oficina a la página de instructores
  • Agregado cursos a la página de instructores
  • Actualizado DeleteConfirmed
  • Agregado la ubicación de la oficina y cursos a la página de creación

Pase al siguiente artículo para aprender a implementar un modelo de programación asincrónica.