Tutorial: Atualizar dados relacionados – ASP.NET MVC com EF Core

No tutorial anterior, você exibiu dados relacionados; neste tutorial, você atualizará dados relacionados pela atualização dos campos de chave estrangeira e das propriedades de navegação.

As ilustrações a seguir mostram algumas das páginas com as quais você trabalhará.

Course Edit page

Edit Instructor page

Neste tutorial, você:

  • Personalizar as páginas Cursos
  • Adicionar a página Editar Instrutores
  • Adicionar cursos à página Editar
  • Atualizar a página Excluir
  • Adicionar o local do escritório e cursos à página Criar

Pré-requisitos

Personalizar as páginas Cursos

Quando uma nova entidade Course é criada, ela precisa ter uma relação com um departamento existente. Para facilitar isso, o código gerado por scaffolding inclui métodos do controlador e exibições Criar e Editar que incluem uma lista suspensa para seleção do departamento. A lista suspensa define a propriedade de chave estrangeira Course.DepartmentID, e isso é tudo o que o Entity Framework precisa para carregar a propriedade de navegação Department com a entidade Department apropriada. Você usará o código gerado por scaffolding, mas o alterará ligeiramente para adicionar tratamento de erro e classificação à lista suspensa.

Em CoursesController.cs, exclua os quatro métodos Create e Edit e substitua-os pelo seguinte código:

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

Após o método HttpPost Edit, crie um novo método que carrega informações de departamento para a lista suspensa.

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

O método PopulateDepartmentsDropDownList obtém uma lista de todos os departamentos classificados por nome, cria uma coleção SelectList para uma lista suspensa e passa a coleção para a exibição em ViewBag. O método aceita o parâmetro selectedDepartment opcional que permite que o código de chamada especifique o item que será selecionado quando a lista suspensa for renderizada. A exibição passará o nome "DepartmentID" para o auxiliar de marcação <select> e o auxiliar então saberá que deve examinar o objeto ViewBag em busca de uma SelectList chamada "DepartmentID".

O método HttpGet Create chama o método PopulateDepartmentsDropDownList sem definir o item selecionado, porque um novo curso do departamento ainda não foi estabelecido:

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

O método HttpGet Edit define o item selecionado, com base na ID do departamento já atribuído ao curso que está sendo editado:

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

Os métodos HttpPost para Create e Edit também incluem o código que define o item selecionado quando eles exibem novamente a página após um erro. Isso garante que quando a página for exibida novamente para mostrar a mensagem de erro, qualquer que tenha sido o departamento selecionado permaneça selecionado.

Adicionar .AsNoTracking aos métodos Details e Delete

Para otimizar o desempenho das páginas Detalhes do Curso e Excluir, adicione chamadas AsNoTracking aos métodos HttpGet Details e Delete.

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 as exibições Curso

Em Views/Courses/Create.cshtml, adicione uma opção "Selecionar Departamento" à lista suspensa Departamento, altere a legenda de DepartmentID para Departamento e adicione uma mensagem de validação.

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

Em Views/Courses/Edit.cshtml, faça a mesma alteração no campo Departamento feita em Create.cshtml.

Além disso, em Views/Courses/Edit.cshtml, adicione um campo de número de curso antes do campo Título. Como o número de curso é a chave primária, ele é exibido, mas não pode ser alterado.

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

Já existe um campo oculto (<input type="hidden">) para o número de curso na exibição Editar. A adição de um auxiliar de marcação <label> não elimina a necessidade do campo oculto, porque ele não faz com que o número de curso seja incluído nos dados postados quando o usuário clica em Salvar na página Editar.

Em Views/Courses/Delete.cshtml, adicione um campo de número de curso na parte superior e altere a ID do departamento com o nome do 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>

Em Views/Courses/Details.cshtml, faça a mesma alteração que você acabou de fazer em Delete.cshtml.

Testar as páginas Curso

Execute o aplicativo, selecione a guia Cursos, clique em Criar Novo e insira dados para um novo curso:

Course Create page

Clique em Criar. A página Índice de Cursos é exibida com o novo curso adicionado à lista. O nome do departamento na lista de páginas de Índice é obtido da propriedade de navegação, mostrando que a relação foi estabelecida corretamente.

Clique em Editar em um curso na página Índice de Cursos.

Course Edit page

Altere dados na página e clique em Salvar. A página Índice de Cursos é exibida com os dados de cursos atualizados.

Adicionar a página Editar Instrutores

Quando você edita um registro de instrutor, deseja poder atualizar a atribuição de escritório do instrutor. A entidade Instructor tem uma relação um para zero ou um com a entidade OfficeAssignment, o que significa que o código tem que manipular as seguintes situações:

  • Se o usuário apagar a atribuição de escritório e ela originalmente tinha um valor, exclua a entidade OfficeAssignment.

  • Se o usuário inserir um valor de atribuição de escritório e ele originalmente estava vazio, crie uma entidade OfficeAssignment.

  • Se o usuário alterar o valor de uma atribuição de escritório, altere o valor em uma entidade OfficeAssignment existente.

Atualizar o controlador Instrutores

Em InstructorsController.cs, altere o código no método HttpGet Edit para que ele carregue a propriedade de navegação OfficeAssignment da entidade Instructor e chame 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);
}

Substitua o método HttpPost Edit pelo seguinte código para manipular atualizações de atribuição de escritório:

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

O código faz o seguinte:

  • Altera o nome do método para EditPost porque a assinatura agora é a mesma do método HttpGet Edit (o atributo ActionName especifica que a URL /Edit/ ainda é usada).

  • Obtém a entidade Instructor atual do banco de dados usando o carregamento adiantado para a propriedade de navegação OfficeAssignment. Isso é o mesmo que você fez no método HttpGet Edit.

  • Atualiza a entidade Instructor recuperada com valores do associador de modelos. A sobrecarga TryUpdateModel permite que você declare as propriedades que deseja incluir. Isso impede o excesso de postagem, conforme explicado no segundo tutorial.

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    
  • Se o local do escritório estiver em branco, define a propriedade Instructor.OfficeAssignment como nula para que a linha relacionada na tabela OfficeAssignment seja excluída.

    if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
    {
        instructorToUpdate.OfficeAssignment = null;
    }
    
  • Salva as alterações no banco de dados.

Atualizar a exibição Editar Instrutor

Em Views/Instructors/Edit.cshtml, adicione um novo campo para editar o local do escritório, no final antes do botão Salvar:

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

Execute o aplicativo, selecione a guia Instrutores e, em seguida, clique em Editar em um instrutor. Altere o Local do Escritório e clique em Salvar.

Instructor Edit page

Adicionar cursos à página Editar

Os instrutores podem ministrar a quantidade de cursos que desejarem. Agora, você aprimorará a página Editar Instrutor adicionando a capacidade de alterar as atribuições de curso usando um grupo de caixas de seleção, conforme mostrado na seguinte captura de tela:

Instructor Edit page with courses

A relação entre as entidades Instructor e Course é muitos para muitos. Para adicionar e remover relações, adicione e remova entidades do conjunto de entidades de junção CourseAssignments.

A interface do usuário que permite alterar a quais cursos um instrutor é atribuído é um grupo de caixas de seleção. Uma caixa de seleção é exibida para cada curso no banco de dados, e aqueles aos quais o instrutor está atribuído no momento são marcados. O usuário pode marcar ou desmarcar as caixas de seleção para alterar as atribuições de curso. Se a quantidade de cursos for muito maior, provavelmente, você desejará usar outro método de apresentação dos dados na exibição, mas usará o mesmo método de manipulação de uma entidade de junção para criar ou excluir relações.

Atualizar o controlador Instrutores

Para fornecer dados à exibição para a lista de caixas de seleção, você usará uma classe de modelo de exibição.

Crie AssignedCourseData.cs na pasta SchoolViewModels e substitua o código existente pelo seguinte código:

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

Em InstructorsController.cs, substitua o método HttpGet Edit pelo código a seguir. As alterações são realçadas.

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

O código adiciona o carregamento adiantado à propriedade de navegação Courses e chama o novo método PopulateAssignedCourseData para fornecer informações para a matriz de caixa de seleção usando a classe de modelo de exibição AssignedCourseData.

O código no método PopulateAssignedCourseData lê todas as entidades Course para carregar uma lista de cursos usando a classe de modelo de exibição. Para cada curso, o código verifica se o curso existe na propriedade de navegação Courses do instrutor. Para criar uma pesquisa eficiente ao verificar se um curso é atribuído ao instrutor, os cursos atribuídos ao instrutor são colocados em uma coleção HashSet. A propriedade Assigned está definida como verdadeiro para os cursos aos quais instrutor é atribuído. A exibição usará essa propriedade para determinar quais caixas de seleção precisam ser exibidas como selecionadas. Por fim, a lista é passada para a exibição em ViewData.

Em seguida, adicione o código que é executado quando o usuário clica em Salvar. Substitua o método EditPost pelo código a seguir e adicione um novo método que atualiza a propriedade de navegação Courses da entidade 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);
            }
        }
    }
}

A assinatura do método agora é diferente do método HttpGet Edit e, portanto, o nome do método é alterado de EditPost para Edit novamente.

Como a exibição não tem uma coleção de entidades Course, o associador de modelos não pode atualizar automaticamente a propriedade de navegação CourseAssignments. Em vez de usar o associador de modelos para atualizar a propriedade de navegação CourseAssignments, faça isso no novo método UpdateInstructorCourses. Portanto, você precisa excluir a propriedade CourseAssignments do model binding. Isso não requer nenhuma alteração no código que chama TryUpdateModel, pois você está usando a sobrecarga que requer aprovação explícita e CourseAssignments não está na lista de inclusão.

Se nenhuma caixa de seleção foi marcada, o código em UpdateInstructorCourses inicializa a propriedade de navegação CourseAssignments com uma coleção vazia e retorna:

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

Em seguida, o código executa um loop em todos os cursos no banco de dados e verifica cada curso em relação àqueles atribuídos no momento ao instrutor e em relação àqueles que foram selecionados na exibição. Para facilitar pesquisas eficientes, as últimas duas coleções são armazenadas em objetos HashSet.

Se a caixa de seleção para um curso foi marcada, mas o curso não está na propriedade de navegação Instructor.CourseAssignments, o curso é adicionado à coleção na propriedade de navegação.

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

Se a caixa de seleção para um curso não foi marcada, mas o curso está na propriedade de navegação Instructor.CourseAssignments, o curso é removido da propriedade de navegação.

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

Atualizar as exibições Instrutor

Em Views/Instructors/Edit.cshtml, adicione um campo Cursos com uma matriz de caixas de seleção, adicionando o código a seguir imediatamente após os elementos div para o campo Escritório e antes do elemento div para o botão Salvar.

Observação

Quando você colar o código no Visual Studio, as quebras de linha poderão ser alteradas de uma forma que divide o código. Se o código ficar com aparência diferente depois de colá-lo, pressione Ctrl + Z uma vez para desfazer a formatação automática. Isso corrigirá as quebras de linha para que elas se pareçam com o que você vê aqui. O recuo não precisa ser perfeito, mas cada uma das linhas @:</tr><tr>, @:<td>, @:</td> e @:</tr> precisa estar em uma única linha, conforme mostrado, ou você receberá um erro de runtime. Com o bloco de novo código selecionado, pressione Tab três vezes para alinhar o novo código com o código existente. Esse problema foi corrigido no 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>

Esse código cria uma tabela HTML que contém três colunas. Em cada coluna há uma caixa de seleção, seguida de uma legenda que consiste no número e título do curso. Todas as caixas de seleção têm o mesmo nome ("selectedCourses"), o que informa ao associador de modelos de que elas devem ser tratadas como um grupo. O atributo de valor de cada caixa de seleção é definido com o valor de CourseID. Quando a página é postada, o associador de modelos passa uma matriz para o controlador que consiste nos valores CourseID para apenas as caixas de seleção marcadas.

Quando as caixas de seleção são inicialmente renderizadas, aquelas que se destinam aos cursos atribuídos ao instrutor têm atributos marcados, que os seleciona (exibe-os como marcados).

Execute o aplicativo, selecione a guia Instrutores e clique em Editar em um instrutor para ver a página Editar.

Instructor Edit page with courses

Altere algumas atribuições de curso e clique em Salvar. As alterações feitas são refletidas na página Índice.

Observação

A abordagem usada aqui para editar os dados de curso do instrutor funciona bem quando há uma quantidade limitada de cursos. Para coleções muito maiores, uma interface do usuário e um método de atualização diferentes são necessários.

Atualizar a página Excluir

Em InstructorsController.cs, exclua o método DeleteConfirmed e insira o código a seguir em seu 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 faz as seguintes alterações:

  • Executa o carregamento adiantado para a propriedade de navegação CourseAssignments. Você precisa incluir isso ou o EF não reconhecerá as entidades CourseAssignment relacionadas e não as excluirá. Para evitar a necessidade de lê-las aqui, você pode configurar a exclusão em cascata no banco de dados.

  • Se o instrutor a ser excluído é atribuído como administrador de qualquer departamento, remove a atribuição de instrutor desse departamento.

Adicionar o local do escritório e cursos à página Criar

Em InstructorsController.cs, exclua os métodos HttpGet e HttpPost Create e adicione o seguinte código em seu 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);
}

Esse código é semelhante ao que você viu nos métodos Edit, exceto que inicialmente nenhum curso está selecionado. O método HttpGet Create chama o método PopulateAssignedCourseData, não porque pode haver cursos selecionados, mas para fornecer uma coleção vazia para o loop foreach na exibição (caso contrário, o código de exibição gera uma exceção de referência nula).

O método HttpPost Create adiciona cada curso selecionado à propriedade de navegação CourseAssignments antes que ele verifica se há erros de validação e adiciona o novo instrutor ao banco de dados. Os cursos são adicionados, mesmo se há erros de modelo, de modo que quando houver erros de modelo (por exemplo, o usuário inseriu uma data inválida) e a página for exibida novamente com uma mensagem de erro, as seleções de cursos que foram feitas sejam restauradas automaticamente.

Observe que para poder adicionar cursos à propriedade de navegação CourseAssignments, é necessário inicializar a propriedade como uma coleção vazia:

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

Como alternativa a fazer isso no código do controlador, faça isso no modelo Instructor alterando o getter de propriedade para criar automaticamente a coleção se ela não existir, conforme mostrado no seguinte exemplo:

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

Se você modificar a propriedade CourseAssignments dessa forma, poderá remover o código de inicialização de propriedade explícita no controlador.

Em Views/Instructor/Create.cshtml, adicione uma caixa de texto de localização do escritório e caixas de seleção para cursos antes do botão Enviar. Como no caso da página Editar, corrija a formatação se o Visual Studio reformatar o código quando você o colar.

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

Faça o teste executando o aplicativo e criando um instrutor.

Manipulando transações

Conforme explicado no tutorial do CRUD, o Entity Framework implementa transações de forma implícita. Para cenários em que você precisa de mais controle – por exemplo, se desejar incluir operações feitas fora do Entity Framework em uma transação, consulte Transações.

Obter o código

Baixe ou exiba o aplicativo concluído.

Próximas etapas

Neste tutorial, você:

  • Personalizou as páginas Cursos
  • Adicionou a página Editar Instrutores
  • Adicionou cursos à página Editar
  • Atualizou a página Excluir
  • Adicionou o local do escritório e cursos à página Criar

Vá para o próximo tutorial para saber como lidar com conflitos de simultaneidade.