Actualización de datos relacionados con Entity Framework en aplicaciones ASP.NET MVC (6 de 10)

por Tom Dykstra

En la aplicación web de ejemplo Contoso University, se muestra cómo crear aplicaciones ASP.NET MVC 4 con Code First de Entity Framework 5 y Visual Studio 2012. Para obtener información sobre la serie de tutoriales, consulte el primer tutorial de la serie.

Nota:

Si se encontrase problemas que no pudiera resolver, descargue el capítulo completo e intente reproducir el problema. Por lo general, es posible encontrar la solución al problema comparando el propio código con el código completado. Para conocer algunos errores comunes y cómo resolverlos, consulte Errores y soluciones alternativas.

En el tutorial anterior, mostró los datos relacionados; en este tutorial, actualizará los datos relacionados. Para la mayoría de las relaciones, esto se puede hacer actualizando los campos de clave externa adecuados. En el caso de las relaciones de varios a varios, Entity Framework no expone la tabla de combinación directamente, por lo que se deberán agregar y quitar explícitamente entidades a las propiedades de navegación adecuadas, y desde ellas.

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

Screenshot that shows the Create Course page.

Screenshot that displays the Instructor Edit page.

Personalizar las páginas Create y Edit de Courses

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 Edit y Create, 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 (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException 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)
{
   Course course = db.Courses.Find(id);
   PopulateDepartmentsDropDownList(course.DepartmentID);
   return View(course);
}

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
    [Bind(Include = "CourseID,Title,Credits,DepartmentID")]
    Course course)
{
   try
   {
      if (ModelState.IsValid)
      {
         db.Entry(course).State = EntityState.Modified;
         db.SaveChanges();
         return RedirectToAction("Index");
      }
   }
   catch (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException 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);
}

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);
} 

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 DepartmentIDal asistente DropDownList y, a continuación, el asistente sabrá que puede buscar en el objeto ViewBag para una SelectList denominada DepartmentID.

El método HttpGetCreate 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 HttpGetEdit 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)
{
    Course course = db.Courses.Find(id);
    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 (DataException /* dex */)
{
    //Log the error (uncomment dex variable name after DataException 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 permanezca seleccionado.

En Views\Course\Create.cshtml, agregue el código resaltado para crear un nuevo campo de número de curso antes del campo Título. Como se explicó en un tutorial anterior, en los campos de clave principal no se aplica scaffolding de forma predeterminada, pero esta clave principal es significativa, por lo que requerirá que el usuario pueda escribir el valor de clave.

@model ContosoUniversity.Models.Course

@{
    ViewBag.Title = "Create";
}

<h2>Create</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Course</legend>

        <div class="editor-label">
            @Html.LabelFor(model => model.CourseID)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.CourseID)
            @Html.ValidationMessageFor(model => model.CourseID)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Title)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Title)
            @Html.ValidationMessageFor(model => model.Title)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Credits)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Credits)
            @Html.ValidationMessageFor(model => model.Credits)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.DepartmentID, "Department")
        </div>
        <div class="editor-field">
            @Html.DropDownList("DepartmentID", String.Empty)
            @Html.ValidationMessageFor(model => model.DepartmentID)
        </div>

        <p>
            <input type="submit" value="Create" />
        </p>
    </fieldset>
}

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

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

En Views\Course\Edit.cshtml, Views\Course\Delete.cshtml y Views\Course\Details.cshtml, agregue un campo de número de curso antes del campo Título. Dado que es la clave principal, se muestra, pero no se puede cambiar.

<div class="editor-label">
    @Html.LabelFor(model => model.CourseID)
</div>
<div class="editor-field">
    @Html.DisplayFor(model => model.CourseID)
</div>

Ejecute la página Crear (muestre la página Índice de cursos y haga clic en Crear nuevo) y escriba los datos de un nuevo curso:

Course_create_page

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

Course_Index_page_showing_new_course

Ejecute la página Editar (muestre la página Índice de cursos y haga clic en Editar en un curso).

Course_edit_page

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

Agregar una página Editar para 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 es necesario controlar las situaciones siguientes:

  • Si el usuario borrase la asignación de oficina y originalmente tenía un valor, tendrá que quitar y eliminar la entidad OfficeAssignment.
  • Si el usuario escribiese un valor de asignación de oficina y originalmente estaba vacío, tendrá que crear una entidad OfficeAssignment.
  • Si el usuario cambiase el valor de una asignación de oficina, tendrá que cambiar el valor en una entidad OfficeAssignment existente.

Abra InstructorController.cs y examine el método HttpGetEdit:

public ActionResult Edit(int id = 0)
{
    Instructor instructor = db.Instructors.Find(id);
    if (instructor == null)
    {
        return HttpNotFound();
    }
    ViewBag.InstructorID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", instructor.InstructorID);
    return View(instructor);
}

El código con scaffolding de 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)
{
    Instructor instructor = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Where(i => i.InstructorID == id)
        .Single();
    return View(instructor);
}

Este código quita la instrucción ViewBag y agrega carga diligente para la entidad OfficeAssignment asociada. No es posible 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]
[ValidateAntiForgeryToken]
public ActionResult Edit(int id, FormCollection formCollection)
{
   var instructorToUpdate = db.Instructors
       .Include(i => i.OfficeAssignment)
       .Where(i => i.InstructorID == id)
       .Single();

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

         db.Entry(instructorToUpdate).State = EntityState.Modified;
         db.SaveChanges();

         return RedirectToAction("Index");
      }
      catch (DataException /* dex */)
      {
         //Log the error (uncomment dex variable name after DataException 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.");
      }
   }
   ViewBag.InstructorID = new SelectList(db.OfficeAssignments, "InstructorID", "Location", id);
   return View(instructorToUpdate);
}

El código realiza lo siguiente:

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

  • Actualiza la entidad Instructor recuperada con valores del enlazador de modelos. La sobrecarga TryUpdateModel usada permite listar en una lista segura las propiedades que desee 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 Fecha de contratación, agregue un nuevo campo para editar la ubicación de la oficina:

<div class="editor-label">
    @Html.LabelFor(model => model.OfficeAssignment.Location)
</div>
<div class="editor-field">
    @Html.EditorFor(model => model.OfficeAssignment.Location)
    @Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
</div>

Ejecute la página (seleccione la pestaña Instructores y, después, haga clic en Editar en un instructor). Cambie el valor de Office Location y haga clic en Save.

Changing_the_office_location

Agregar asignaciones de cursos a la página Editar instructores

Los instructores pueden impartir cualquier número de cursos. Ahora mejorará la página Edit Instructor 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:

Screenshot that shows the Instructor Edit page with courses.

La relación entre las entidades Course y Instructor es de varios a varios, lo que significa que no tiene acceso directo a la tabla de combinación. En su lugar, agregará y quitará entidades hacia y desde la propiedad de navegación Instructor.Courses.

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 propiedades de navegació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 HttpGetEdit por el siguiente código. Los cambios aparecen resaltados.

public ActionResult Edit(int id)
{
    Instructor instructor = db.Instructors
        .Include(i => i.OfficeAssignment)
        .Include(i => i.Courses)
        .Where(i => i.InstructorID == id)
        .Single();
    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 este se colocan en una colección HashSet. La propiedad Assigned está establecida en true para aquellos 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 HttpPostEdit 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, FormCollection formCollection, string[] selectedCourses)
{
   var instructorToUpdate = db.Instructors
       .Include(i => i.OfficeAssignment)
       .Include(i => i.Courses)
       .Where(i => i.InstructorID == 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.Entry(instructorToUpdate).State = EntityState.Modified;
         db.SaveChanges();

         return RedirectToAction("Index");
      }
      catch (DataException /* dex */)
      {
         //Log the error (uncomment dex variable name after DataException 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);
         }
      }
   }
}

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 Navegación de cursos, 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, ya que está usando la sobrecarga de listas seguras y Courses no se encuentra en la lista de inclusión.

Si no se ha seleccionado ninguna casilla, el código de UpdateInstructorCourses inicializará 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 Cursos con una matriz de casillas al agregar el siguiente código resaltado inmediatamente después de los elementos div del campo OfficeAssignment:

@model ContosoUniversity.Models.Instructor

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    @Html.ValidationSummary(true)

    <fieldset>
        <legend>Instructor</legend>

        @Html.HiddenFor(model => model.InstructorID)

        <div class="editor-label">
            @Html.LabelFor(model => model.LastName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.LastName)
            @Html.ValidationMessageFor(model => model.LastName)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.FirstMidName)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.FirstMidName)
            @Html.ValidationMessageFor(model => model.FirstMidName)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.HireDate)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.HireDate)
            @Html.ValidationMessageFor(model => model.HireDate)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.OfficeAssignment.Location)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.OfficeAssignment.Location)
            @Html.ValidationMessageFor(model => model.OfficeAssignment.Location)
        </div>

        <div class="editor-field">
    <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>
        <p>
            <input type="submit" value="Save" />
        </p>
    </fieldset>
}

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

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

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

Cuando las casillas se representan inicialmente, aquellas que sean para cursos asignados al instructor tendrán 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, será necesario 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 Cursos inmediatamente después del encabezado Oficina, tal y como se muestra en el ejemplo siguiente:

<tr> 
    <th></th> 
    <th>Last Name</th> 
    <th>First Name</th> 
    <th>Hire Date</th> 
    <th>Office</th>
    <th>Courses</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:

@model ContosoUniversity.ViewModels.InstructorIndexData

@{
    ViewBag.Title = "Instructors";
}

<h2>Instructors</h2>

<p>
    @Html.ActionLink("Create New", "Create")
</p>
<table>
    <tr>
        <th></th>
        <th>Last Name</th>
        <th>First Name</th>
        <th>Hire Date</th>
        <th>Office</th>
        <th>Courses</th>
    </tr>
    @foreach (var item in Model.Instructors)
    {
        string selectedRow = "";
        if (item.InstructorID == ViewBag.InstructorID)
        {
            selectedRow = "selectedrow";
        } 
        <tr class="@selectedRow" valign="top">
            <td>
                @Html.ActionLink("Select", "Index", new { id = item.InstructorID }) | 
                @Html.ActionLink("Edit", "Edit", new { id = item.InstructorID }) | 
                @Html.ActionLink("Details", "Details", new { id = item.InstructorID }) | 
                @Html.ActionLink("Delete", "Delete", new { id = item.InstructorID })
            </td>
            <td>
                @item.LastName
            </td>
            <td>
                @item.FirstMidName
            </td>
            <td>
                @String.Format("{0:d}", item.HireDate)
            </td>
            <td>
                @if (item.OfficeAssignment != null)
                { 
                    @item.OfficeAssignment.Location  
                }
            </td>
            <td>
                @{
                foreach (var course in item.Courses)
                {
                    @course.CourseID @:  @course.Title <br />
                }
                }
            </td>
        </tr> 
    }
</table>

@if (Model.Courses != null)
{ 
    <h3>Courses Taught by Selected Instructor</h3> 
    <table>
        <tr>
            <th></th>
            <th>ID</th>
            <th>Title</th>
            <th>Department</th>
        </tr>

        @foreach (var item in Model.Courses)
        {
            string selectedRow = "";
            if (item.CourseID == ViewBag.CourseID)
            {
                selectedRow = "selectedrow";
            } 
        
            <tr class="@selectedRow">

                <td>
                    @Html.ActionLink("Select", "Index", new { courseID = item.CourseID })
                </td>
                <td>
                    @item.CourseID
                </td>
                <td>
                    @item.Title
                </td>
                <td>
                    @item.Department.Name
                </td>
            </tr> 
        }

    </table> 
}
@if (Model.Enrollments != null)
{ 
    <h3>Students Enrolled in Selected Course</h3> 
    <table>
        <tr>
            <th>Name</th>
            <th>Grade</th>
        </tr>
        @foreach (var item in Model.Enrollments)
        { 
            <tr>
                <td>
                    @item.Student.FullName
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.Grade)
                </td>
            </tr> 
        }
    </table> 
}

Ejecute la página Índice del instructor para ver los cursos asignados a cada instructor:

Instructor_index_page

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

Instructor_edit_page_with_courses

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

Nota: el enfoque que se aplica para editar los 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.

Actualizar el método Delete

Cambie el código del método HttpPost Delete para que el registro de asignación de la oficina (si existiera) se elimine cuando se elimine el instructor:

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

   instructor.OfficeAssignment = null;
   db.Instructors.Remove(instructor);
   db.SaveChanges();
   return RedirectToAction("Index");
}

Si intentase eliminar un instructor asignado a un departamento como administrador, obtendrá un error de integridad referencial. Consulte la versión actual de este tutorial para obtener código adicional que quitará automáticamente el instructor de cualquier departamento en el que el instructor esté asignado como administrador.

Resumen

Ya completó esta introducción para trabajar con datos relacionados. Hasta ahora, en estos tutoriales realizó una amplia gama de operaciones CRUD, pero no trató problemas de simultaneidad. En el siguiente tutorial, se presentará el tema de la simultaneidad, se explicarán las opciones para controlarla y se agregará el control de simultaneidad al código CRUD que ya escribió para un tipo de entidad.

Los vínculos a otros recursos de Entity Framework se pueden encontrar al final del último tutorial de esta serie.