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á.
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:
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.
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 HttpGetEdit
(o atributoActionName
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çãoOfficeAssignment
. Isso é o mesmo que você fez no método HttpGetEdit
.Atualiza a entidade
Instructor
recuperada com valores do associador de modelos. A sobrecargaTryUpdateModel
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 tabelaOfficeAssignment
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.
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:
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.
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 entidadesCourseAssignment
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.