Esercitazione: Aggiornare i dati correlati - ASP.NET MVC con EF Core

Nell'esercitazione precedente sono stati visualizzati dati correlati. In questa esercitazione i dati correlati verranno aggiornati tramite l'aggiornamento di campi di chiave esterna e proprietà di navigazione.

Le figure seguenti illustrano alcune delle pagine che verranno usate.

Course Edit page

Edit Instructor page

In questa esercitazione:

  • Personalizzare le pagine dei corsi
  • Aggiungere la pagina Edit per gli insegnanti
  • Aggiungere corsi alla pagina Edit
  • Aggiornare la pagina Delete
  • Aggiungere posizione dell'ufficio e corsi alla pagina Create

Prerequisiti

Personalizzare le pagine dei corsi

Quando viene creata una nuova Course entità, deve avere una relazione con un reparto esistente. Per semplificare il raggiungimento di questo obiettivo, il codice con scaffolding include i metodi del controller e le visualizzazioni di creazione e modifica includono un elenco a discesa per la selezione del dipartimento. L'elenco a discesa imposta la Course.DepartmentID proprietà di chiave esterna ed è tutto ciò che è necessario per caricare la Department proprietà di navigazione con l'entità appropriata Department . Verrà usato il codice con scaffolding, che però verrà modificato leggermente per aggiungere la gestione degli errori e l'ordinamento dell'elenco a discesa.

In CoursesController.cseliminare i quattro metodi Create e Edit e sostituirli con il codice seguente:

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

Dopo il metodo Edit HttpPost, creare un nuovo metodo che carichi le informazioni di dipartimento per l'elenco a discesa.

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

Il metodo PopulateDepartmentsDropDownList ottiene un elenco di tutti i dipartimenti ordinato per nome, crea una raccolta SelectList per un elenco a discesa e passa tale raccolta alla visualizzazione in ViewBag. Il metodo accetta il parametro facoltativo selectedDepartment, che consente al codice chiamante di specificare l'elemento che deve essere selezionato quando viene eseguito il rendering dell'elenco a discesa. La visualizzazione passerà il nome "DepartmentID" all'helper tag <select>, che quindi saprà di dover cercare nell'oggetto ViewBag una raccolta SelectList denominata "DepartmentID".

Il metodo Create HttpGet chiama il metodo PopulateDepartmentsDropDownList senza impostare l'elemento selezionato, perché per un nuovo corso il dipartimento non è ancora stabilito:

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

Il metodo Edit HttpGet imposta l'elemento selezionato, in base all'ID del dipartimento già assegnato al corso in fase di modifica:

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

I metodi HttpPost sia per Create che per Edit includono anche codice che imposta l'elemento selezionato quando la pagina viene visualizzata di nuovo dopo un errore. Ciò garantisce che, quando la pagina viene visualizzata di nuovo per mostrare il messaggio di errore, il dipartimento selezionato rimane selezionato.

Aggiungere .AsNoTracking ai metodi Details e Delete

Per ottimizzare le prestazioni delle pagine dei dettagli e di eliminazione del corso, aggiungere chiamate a AsNoTracking nei metodi Details e Delete HttpGet.

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

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

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

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

    return View(course);
}

Modificare le visualizzazioni dei corsi

In Views/Courses/Create.cshtmlaggiungere un'opzione "Seleziona reparto" all'elenco a discesa Reparto, modificare il didascalia da DepartmentID a Department e aggiungere un messaggio di convalida.

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

In Views/Courses/Edit.cshtmlapportare la stessa modifica per il campo Reparto appena fatto in Create.cshtml.

Views/Courses/Edit.cshtmlIn aggiungere anche un campo numero corso prima del campo Titolo. Poiché il numero di corso è la chiave primaria, viene visualizzato, ma non può essere modificato.

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

Per il numero di corso è già presente un campo nascosto (<input type="hidden">) nella visualizzazione di modifica. L'aggiunta di un helper tag <label> non elimina la necessità del campo nascosto, poiché senza di questo il numero di corso non viene incluso nei dati inviati quando l'utente fa clic su Save (Salva) nella pagina Edit (Modifica).

In Views/Courses/Delete.cshtmlaggiungere un campo numero corso nella parte superiore e modificare l'ID reparto in nome del reparto.

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

In Views/Courses/Details.cshtmlapportare la stessa modifica appena apportata per Delete.cshtml.

Testare le pagine del corso

Eseguire l'app, selezionare la scheda Courses (Corsi), fare clic su Create New (Crea nuovo) e immettere i dati per un nuovo corso:

Course Create page

Fai clic su Crea. La pagina di indice dei corsi viene visualizzata con il nuovo corso aggiunto all'elenco. Il nome del dipartimento nell'elenco della pagina di indice deriva dalla proprietà di navigazione, che mostra che la relazione è stata stabilita correttamente.

Fare clic su Edit (Modifica) per un corso nella pagina di indice dei corsi.

Course Edit page

Modificare i dati nella pagina e fare clic su Save (Salva). La pagina di indice dei corsi verrà visualizzata con i dati del corso aggiornati.

Aggiungere la pagina Edit per gli insegnanti

Quando si modifica il record di un insegnante, è necessario essere in grado di aggiornare l'assegnazione dell'ufficio. L'entità Instructor ha una relazione uno-a-zero-o-uno con l'entità OfficeAssignment , il che significa che il codice deve gestire le situazioni seguenti:

  • Se l'utente cancella l'assegnazione dell'ufficio e originariamente ha un valore, eliminare l'entità OfficeAssignment .

  • Se l'utente immette un valore di assegnazione dell'ufficio e originariamente era vuoto, creare una nuova OfficeAssignment entità.

  • Se l'utente modifica il valore di un'assegnazione di ufficio, modificare il valore in un'entità esistente OfficeAssignment .

Aggiornare il controller Instructors

In InstructorsController.csmodificare il codice nel metodo HttpGet Edit in modo che carichi la proprietà di navigazione dell'entità OfficeAssignment Instructor e chiami 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);
}

Sostituire il metodo Edit HttpPost con il codice seguente per gestire gli aggiornamenti delle assegnazioni di ufficio:

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

Il codice esegue le seguenti attività:

  • Modifica il nome del metodo in EditPost perché ora la firma è la stessa del metodo Edit HttpGet (l'attributo ActionName specifica che l'URL /Edit/ viene ancora usato).

  • Ottiene l'entità Instructor corrente dal database tramite il caricamento eager per la proprietà di navigazione OfficeAssignment. Ciò corrisponde a quanto effettuato nel metodo Edit HttpGet.

  • Aggiorna l'entità Instructor recuperata con valori dallo strumento di associazione di modelli. L'overload TryUpdateModel consente di dichiarare le proprietà da includere. In questo modo è possibile evitare l'overposting, come illustrato nella seconda esercitazione.

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    
  • Se la posizione dell'ufficio è vuota, imposta la Instructor.OfficeAssignment proprietà su Null in modo che la riga correlata nella OfficeAssignment tabella venga eliminata.

    if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
    {
        instructorToUpdate.OfficeAssignment = null;
    }
    
  • Salva le modifiche nel database.

Aggiornare la visualizzazione di modifica dell'insegnante

In Views/Instructors/Edit.cshtmlaggiungere un nuovo campo per modificare la posizione dell'ufficio, alla fine prima del pulsante Salva :

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

Eseguire l'app, selezionare la scheda Instructors (Insegnanti) e quindi fare clic su Edit (Modifica) per un insegnante. Modificare Office Location (Posizione ufficio) e fare clic su Save (Salva).

Instructor Edit page

Aggiungere corsi alla pagina Edit

Gli insegnanti possono tenere un numero qualsiasi di corsi. A questo punto si migliorerà la pagina Modifica insegnante aggiungendo la possibilità di modificare le assegnazioni dei corsi usando un gruppo di caselle di controllo, come illustrato nella schermata seguente:

Instructor Edit page with courses

La relazione tra le Course entità e Instructor è molti-a-molti. Per aggiungere e rimuovere relazioni, aggiungere e rimuovere entità da e verso il CourseAssignments set di entità di join.

L'interfaccia utente che consente di modificare i corsi a cui viene assegnato un insegnante è un gruppo di caselle di controllo. Viene visualizzata una casella di controllo per ogni corso del database e quelle a cui è attualmente assegnato l'insegnante sono selezionate. L'utente può selezionare o deselezionare le caselle di controllo per modificare le assegnazioni dei corsi. Se il numero di corsi fosse molto superiore, sarebbe probabilmente consigliabile usare un altro metodo di presentazione dei dati nella visualizzazione, ma si userebbe lo stesso metodo di modifica di un'entità di join per creare o eliminare relazioni.

Aggiornare il controller Instructors

Per fornire dati alla visualizzazione per l'elenco di caselle di controllo, si userà una classe del modello di visualizzazione.

Creare AssignedCourseData.cs nella cartella SchoolViewModels e sostituire il codice esistente con il codice seguente:

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

In InstructorsController.cssostituire il metodo HttpGet Edit con il codice seguente. Le modifiche sono evidenziate.

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

Il codice aggiunge il caricamento eager per la Courses proprietà di navigazione e chiama il nuovo PopulateAssignedCourseData metodo per fornire informazioni per la matrice di caselle di controllo usando la classe del AssignedCourseData modello di visualizzazione.

Il codice nel PopulateAssignedCourseData metodo legge tutte le Course entità per caricare un elenco di corsi usando la classe del modello di visualizzazione. Per ogni corso, il codice verifica se è presente nella proprietà di navigazione Courses dell'insegnante. Per creare un ricerca efficiente per la verifica dell'assegnazione di un corso all'insegnante, i corsi assegnati all'insegnante vengono inseriti in una raccolta HashSet. La proprietà Assigned è impostata su true per i corsi assegnati all'insegnante. La vista userà questa proprietà per determinare quali caselle di controllo devono essere visualizzate come selezionate. L'elenco, infine, viene passato alla visualizzazione in ViewData.

Aggiungere quindi il codice che viene eseguito quando l'utente fa clic su Save (Salva). Sostituire il metodo EditPost con il codice seguente e aggiungere un nuovo metodo che aggiorni la proprietà di navigazione Courses dell'entità Instructor.

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

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

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

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

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

La firma del metodo è ora diversa dal metodo Edit HttpGet, quindi il nome del metodo cambia di nuovo da EditPost a Edit.

Poiché la visualizzazione non ha una raccolta di entità Course, lo strumento di associazione di modelli non può aggiornare automaticamente la proprietà di navigazione CourseAssignments. Anziché usare lo strumento di associazione di modelli per aggiornare la proprietà di navigazione CourseAssignments, questa operazione viene eseguita nel nuovo metodo UpdateInstructorCourses. Pertanto, è necessario escludere la proprietà dall'associazione CourseAssignments di modelli. Ciò non richiede alcuna modifica al codice che chiama TryUpdateModel perché si usa l'overload che richiede l'approvazione esplicita e CourseAssignments non è incluso nell'elenco di inclusioni.

Se non sono state selezionate caselle di controllo, il codice in UpdateInstructorCourses inizializza la CourseAssignments proprietà di navigazione con una raccolta vuota e restituisce:

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

Il codice quindi esegue il ciclo di tutti i corsi nel database e controlla ogni corso a fronte di quelli assegnati all'insegnante rispetto a quelli selezionati nella visualizzazione. Per facilitare l'esecuzione di ricerche efficienti, le ultime due raccolte sono archiviate all'interno di oggetti HashSet.

Se la casella di controllo per un corso è stata selezionata, ma il corso non si trova nella Instructor.CourseAssignments proprietà di navigazione, il corso viene aggiunto alla raccolta nella proprietà di navigazione.

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 la casella di controllo per un corso non è stata selezionata, ma il corso si trova nella Instructor.CourseAssignments proprietà di navigazione, il corso viene rimosso dalla proprietà di navigazione.

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

Aggiornare le visualizzazioni dell'insegnante

In Views/Instructors/Edit.cshtmlaggiungere un campo Courses con una matrice di caselle di controllo aggiungendo il codice seguente immediatamente dopo gli div elementi per il campo di Office e prima dell'elemento div per il pulsante Salva .

Nota

Quando si incolla il codice in Visual Studio, è possibile che le interruzioni di riga vengano modificate in un modo che danneggia il codice. Se il codice ha un aspetto diverso dopo aver incollato, premere CTRL+Z una volta per annullare la formattazione automatica. Ciò corregge le interruzioni di riga, che vengono visualizzate come illustrato qui. Il rientro non deve necessariamente essere perfetto, ma le righe @:</tr><tr>, @:<td>, @:</td> e @:</tr> devono trovarsi in una sola riga, come illustrato. In caso contrario, viene visualizzato un errore di runtime. Dopo aver selezionato il blocco di nuovo codice, premere Tab tre volte per allineare il nuovo codice con il codice esistente. Questo problema è stato risolto in 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>

Questo codice crea una tabella HTML con tre colonne. In ogni colonna è presente una casella di controllo seguita da un didascalia costituito dal numero e dal titolo del corso. Le caselle di controllo hanno tutti lo stesso nome ("selectedCourses"), che informa il gestore di associazione di modelli che devono essere considerati come un gruppo. L'attributo value di ogni casella di controllo è impostato sul valore di CourseID. Quando viene pubblicata la pagina, il gestore di associazione di modelli passa una matrice al controller costituito dai valori solo per le caselle di CourseID controllo selezionate.

Quando viene inizialmente eseguito il rendering delle caselle di controllo, quelle destinate ai corsi assegnati all'insegnante hanno attributi selezionati, che li seleziona (visualizzali selezionati).

Eseguire l'app, selezionare la scheda Instructors (Insegnanti) e fare clic su Edit (Modifica) per un insegnante per visualizzare la pagina Edit (Modifica).

Instructor Edit page with courses

Modificare alcune assegnazioni di corsi e fare clic su Save (Salva). Le modifiche effettuate si riflettono nella pagina di indice.

Nota

L'approccio qui adottato per la modifica dei dati dei corsi degli insegnanti funziona bene quando è presente un numero limitato di corsi. Per raccolte molto più grandi, sarebbero necessari un'interfaccia utente diversa e un altro metodo di aggiornamento.

Aggiornare la pagina Delete

In InstructorsController.cseliminare il DeleteConfirmed metodo e inserire il codice seguente al suo posto.

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

Questo codice apporta le modifiche seguenti:

  • Esegue il caricamento eager per la proprietà di navigazione CourseAssignments. È necessario includere questa proprietà o EF non sarà a conoscenza delle entità CourseAssignment correlate e non le eliminerà. Per evitare la necessità di leggerle qui, è possibile configurare l'eliminazione a catena nel database.

  • Se l'insegnante da eliminare è assegnato come responsabile di un dipartimento, tale assegnazione viene rimossa dal dipartimento.

Aggiungere posizione dell'ufficio e corsi alla pagina Create

In InstructorsController.cseliminare i metodi HttpGet e HttpPost Create e quindi aggiungere il codice seguente al loro posto:

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

Questo codice è simile a quello dei metodi Edit, ad eccezione del fatto che inizialmente non è selezionato alcun corso. Il metodo Create HttpGet chiama il metodo PopulateAssignedCourseData non perché possono essere presenti corsi selezionati, ma per fornire una raccolta vuota per il ciclo foreach nella visualizzazioni (in caso contrario il codice di visualizzazione genera un'eccezione di riferimento Null).

Il metodo Create HttpPost aggiunge i corsi selezionati alla proprietà di navigazione CourseAssignments prima di controllare la presenza di errori di convalida e aggiungere il nuovo insegnante al database. I corsi vengono aggiunti anche in caso di errori di modello. Quindi se si verificano errori di questo tipo (ad esempio se l'utente digita una data non valida) e la pagina viene visualizzata di nuovo con un messaggio di errore, tutte le selezioni relative ai corsi effettuate vengono ripristinate automaticamente.

Si noti che, perché sia possibile aggiungere corsi alla proprietà di navigazione CourseAssignments, è necessario inizializzare la proprietà come raccolta vuota:

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

In alternativa a questa operazione nel codice del controller, è possibile farlo nel Instructor modello modificando il getter della proprietà per creare automaticamente la raccolta se non esiste, come illustrato nell'esempio seguente:

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

Se si modifica la proprietà CourseAssignments in questo modo, è possibile rimuovere il codice di inizializzazione esplicita della proprietà nel controller.

In Views/Instructor/Create.cshtmlaggiungere una casella di testo office location e caselle di controllo per i corsi prima del pulsante Invia. Come nel caso della pagina Edit (Modifica), correggere la formattazione se Visual Studio riformatta il codice quando lo si incolla.

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

Eseguire il test eseguendo l'app e creando un insegnante.

Gestione delle transazioni

Come spiegato nell' esercitazione su CRUD, per impostazione predefinita Entity Framework implementa in modo implicito le transazioni. Per gli scenari in cui è necessario un maggior controllo, ad esempio per includere le operazioni eseguite all'esterno di Entity Framework in una transazione, vedere Transazioni.

Ottenere il codice

Scaricare o visualizzare l'applicazione completata.

Passaggi successivi

In questa esercitazione:

  • Personalizzare le pagine dei corsi
  • Aggiungere la pagina Edit per gli insegnanti
  • Aggiungere corsi alla pagina Edit
  • Aggiornare la pagina Delete
  • Aggiungere posizione dell'ufficio e corsi alla pagina Create

Passare all'esercitazione successiva per informazioni su come gestire i conflitti di concorrenza.