Udostępnij za pośrednictwem


Samouczek: aktualizowanie powiązanych danych — ASP.NET MVC za pomocą polecenia EF Core

W poprzednim samouczku przedstawiono powiązane dane; W tym samouczku zaktualizujesz powiązane dane, aktualizując pola kluczy obcych i właściwości nawigacji.

Na poniższych ilustracjach przedstawiono niektóre strony, z którymi będziesz pracować.

Course Edit page

Edit Instructor page

W tym samouczku zostały wykonane następujące czynności:

  • Dostosowywanie stron kursów
  • Dodawanie strony edycji instruktorów
  • Dodawanie kursów do strony Edytowanie
  • Aktualizowanie strony Usuń
  • Dodawanie lokalizacji biura i kursów do strony Tworzenie

Wymagania wstępne

Dostosowywanie stron kursów

Po utworzeniu nowej Course jednostki musi ona mieć relację z istniejącym działem. Aby to ułatwić, kod szkieletowy zawiera metody kontrolera oraz widoki tworzenia i edytowania, które zawierają listę rozwijaną do wybierania działu. Lista rozwijana ustawia właściwość klucza obcego Course.DepartmentID , a to wszystko, co wymaga programu Entity Framework, aby załadować Department właściwość nawigacji z odpowiednią Department jednostką. Użyjesz kodu szkieletowego, ale zmienisz go nieco, aby dodać obsługę błędów i posortować listę rozwijaną.

W CoursesController.cspliku usuń cztery metody Create and Edit i zastąp je następującym kodem:

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

Po metodzie Edit HttpPost utwórz nową metodę, która ładuje informacje o dziale dla listy rozwijanej.

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

Metoda PopulateDepartmentsDropDownList pobiera listę wszystkich działów posortowanych według nazwy, tworzy SelectList kolekcję listy rozwijanej i przekazuje kolekcję do widoku w ViewBagprogramie . Metoda akceptuje opcjonalny selectedDepartment parametr, który umożliwia kod wywołujący określenie elementu, który zostanie wybrany podczas renderowania listy rozwijanej. Widok przekaże nazwę "DepartmentID" do pomocnika tagu <select> , a pomocnik następnie wie, aby wyszukać w ViewBag obiekcie SelectList o nazwie "DepartmentID".

Metoda HttpGet Create wywołuje metodę PopulateDepartmentsDropDownList bez ustawiania wybranego elementu, ponieważ dla nowego kursu dział nie został jeszcze ustanowiony:

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

Metoda HttpGet Edit ustawia wybrany element na podstawie identyfikatora działu, który jest już przypisany do edytowanego kursu:

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

Metody HttpPost dla obu Create tych metod, a Edit także zawierają kod, który ustawia wybrany element podczas ponownego redysponuj stronę po błędzie. Gwarantuje to, że po ponownym uruchomieniu strony w celu wyświetlenia komunikatu o błędzie wybrany dział pozostanie wybrany.

Dodaj. AsNoTracking do metod Details i Delete

Aby zoptymalizować wydajność stron Szczegóły kursu i Usuń, dodaj AsNoTracking wywołania metod Details i HttpGet 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);
}

Modyfikowanie widoków kursu

W Views/Courses/Create.cshtmlprogramie dodaj opcję "Wybierz dział" do listy rozwijanej Dział, zmień podpis z Identyfikator działu na Dział i dodaj komunikat weryfikacji.

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

W Views/Courses/Edit.cshtmlpliku wprowadź tę samą zmianę w polu Dział, które zostało właśnie w Create.cshtmlpliku .

Ponadto w pliku Views/Courses/Edit.cshtmldodaj pole numer kursu przed polem Tytuł . Ponieważ numer kursu jest kluczem podstawowym, jest wyświetlany, ale nie można go zmienić.

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

W widoku Edycja istnieje już ukryte pole (<input type="hidden">) dla numeru kursu. <label> Dodanie pomocnika tagów nie eliminuje potrzeby ukrytego pola, ponieważ nie powoduje, że numer kursu zostanie uwzględniony w opublikowanych danych, gdy użytkownik kliknie przycisk Zapisz na stronie Edytuj.

W Views/Courses/Delete.cshtmlpliku dodaj pole numer kursu u góry i zmień identyfikator działu na nazwę działu.

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

W Views/Courses/Details.cshtmlpliku wprowadź tę samą zmianę, którą właśnie wykonaliśmy dla elementu Delete.cshtml.

Testowanie stron kursu

Uruchom aplikację, wybierz kartę Kursy , kliknij pozycję Utwórz nowy i wprowadź dane dla nowego kursu:

Course Create page

Kliknij pozycję Utwórz. Zostanie wyświetlona strona Indeks kursów z nowym kursem dodanym do listy. Nazwa działu na liście stron indeksu pochodzi z właściwości nawigacji pokazującej, że relacja została prawidłowo ustanowiona.

Kliknij pozycję Edytuj na kursie na stronie Indeks kursów.

Course Edit page

Zmień dane na stronie i kliknij przycisk Zapisz. Strona Indeks kursów jest wyświetlana ze zaktualizowanymi danymi kursu.

Dodawanie strony edycji instruktorów

Podczas edytowania rekordu instruktora chcesz mieć możliwość zaktualizowania przydziału biura instruktora. Jednostka Instructor ma relację jeden do zera lub jednego z jednostką OfficeAssignment , co oznacza, że kod musi obsługiwać następujące sytuacje:

  • Jeśli użytkownik wyczyści przypisanie pakietu Office i pierwotnie miał wartość, usuń OfficeAssignment jednostkę.

  • Jeśli użytkownik wprowadzi wartość przypisania pakietu Office i pierwotnie był pusty, utwórz nową OfficeAssignment jednostkę.

  • Jeśli użytkownik zmieni wartość przypisania pakietu Office, zmień wartość w istniejącej OfficeAssignment jednostce.

Aktualizowanie kontrolera instruktorów

W InstructorsController.cspliku zmień kod w metodzie HttpGet Edit , aby załadować właściwość nawigacji jednostki OfficeAssignment Instruktor i wywołać metodę 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);
}

Zastąp metodę HttpPost Edit następującym kodem, aby obsługiwać aktualizacje przypisań pakietu Office:

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

Kod wykonuje następujące czynności:

  • Zmienia nazwę metody na EditPost , ponieważ podpis jest teraz taki sam jak metoda HttpGet Edit ( ActionName atrybut określa, że /Edit/ adres URL jest nadal używany).

  • Pobiera bieżącą Instructor jednostkę z bazy danych przy użyciu chętnego OfficeAssignment ładowania dla właściwości nawigacji. Jest to takie samo, jak w metodzie HttpGet Edit .

  • Aktualizacje pobraną Instructor jednostkę z wartościami z powiązania modelu. Przeciążenie TryUpdateModel umożliwia zadeklarowanie właściwości, które chcesz uwzględnić. Zapobiega to nadmiernemu delegowaniu, jak wyjaśniono w drugim samouczku.

    if (await TryUpdateModelAsync<Instructor>(
        instructorToUpdate,
        "",
        i => i.FirstMidName, i => i.LastName, i => i.HireDate, i => i.OfficeAssignment))
    
  • Jeśli lokalizacja biura jest pusta, ustaw Instructor.OfficeAssignment właściwość na null, aby powiązany wiersz w OfficeAssignment tabeli został usunięty.

    if (String.IsNullOrWhiteSpace(instructorToUpdate.OfficeAssignment?.Location))
    {
        instructorToUpdate.OfficeAssignment = null;
    }
    
  • Zapisuje zmiany w bazie danych.

Aktualizowanie widoku Edycji instruktora

W Views/Instructors/Edit.cshtmlpliku dodaj nowe pole do edytowania lokalizacji biura na końcu przed przyciskiem Zapisz :

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

Uruchom aplikację, wybierz kartę Instruktorzy , a następnie kliknij pozycję Edytuj na instruktorze. Zmień lokalizację pakietu Office i kliknij przycisk Zapisz.

Instructor Edit page

Dodawanie kursów do strony Edytowanie

Instruktorzy mogą uczyć dowolną liczbę kursów. Teraz ulepszysz stronę Edycja instruktora, dodając możliwość zmiany przypisań kursu przy użyciu grupy pól wyboru, jak pokazano na poniższym zrzucie ekranu:

Instructor Edit page with courses

Relacja między jednostkami i Course Instructor jest wiele do wielu. Aby dodawać i usuwać relacje, należy dodawać i usuwać jednostki do i z CourseAssignments zestawu jednostek sprzężenia.

Interfejs użytkownika, który umożliwia zmianę kursów przypisanych przez instruktora, jest grupą pól wyboru. Zostanie wyświetlone pole wyboru dla każdego kursu w bazie danych, a te, do których jest obecnie przypisany instruktor. Użytkownik może zaznaczyć lub wyczyścić pola wyboru, aby zmienić przydziały kursu. Jeśli liczba kursów była znacznie większa, prawdopodobnie chcesz użyć innej metody prezentowania danych w widoku, ale użyjesz tej samej metody manipulowania jednostką sprzężenia w celu utworzenia lub usunięcia relacji.

Aktualizowanie kontrolera instruktorów

Aby podać dane do widoku listy pól wyboru, użyjesz klasy modelu widoku.

Utwórz AssignedCourseData.cs w folderze SchoolViewModels i zastąp istniejący kod następującym kodem:

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

W InstructorsController.cspliku zastąp metodę HttpGet Edit następującym kodem. Zmiany są wyróżnione.

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

Kod dodaje chętne ładowanie dla Courses właściwości nawigacji i wywołuje nową PopulateAssignedCourseData metodę w celu udostępnienia informacji dla tablicy pól wyboru przy użyciu AssignedCourseData klasy modelu widoku.

Kod w metodzie PopulateAssignedCourseData odczytuje wszystkie Course jednostki, aby załadować listę kursów przy użyciu klasy modelu widoku. Dla każdego kursu kod sprawdza, czy kurs istnieje we właściwości nawigacji instruktora Courses . Aby utworzyć efektywne wyszukiwanie podczas sprawdzania, czy kurs jest przypisany do instruktora, kursy przypisane do instruktora HashSet są umieszczane w kolekcji. Właściwość jest ustawiona Assigned na wartość true dla kursów, do których jest przypisany instruktor. Widok użyje tej właściwości, aby określić, które pola wyboru muszą być wyświetlane jako zaznaczone. Na koniec lista jest przekazywana do widoku w ViewDatapliku .

Następnie dodaj kod, który jest wykonywany, gdy użytkownik kliknie przycisk Zapisz. Zastąp metodę EditPost poniższym kodem i dodaj nową metodę, która aktualizuje Courses właściwość nawigacji jednostki Instruktor.

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

Sygnatura metody różni się teraz od metody HttpGet Edit , więc nazwa metody zmienia się z EditPost powrotem na Edit.

Ponieważ widok nie ma kolekcji jednostek Course, powiązanie modelu nie może automatycznie zaktualizować CourseAssignments właściwości nawigacji. Zamiast używać powiązania modelu do aktualizowania CourseAssignments właściwości nawigacji, należy to zrobić w nowej UpdateInstructorCourses metodzie. W związku z tym należy wykluczyć CourseAssignments właściwość z powiązania modelu. Nie wymaga to żadnych zmian w kodzie, który wywołuje TryUpdateModel , ponieważ używasz przeciążenia, które wymaga jawnego zatwierdzenia i CourseAssignments nie znajduje się na liście dołączania.

Jeśli nie zaznaczono żadnych pól wyboru, kod inicjuje UpdateInstructorCourses CourseAssignments właściwość nawigacji z pustą kolekcją i zwraca następujące elementy:

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

Następnie kod przechodzi przez wszystkie kursy w bazie danych i sprawdza każdy kurs względem aktualnie przypisanych do instruktora w porównaniu z tymi, które zostały wybrane w widoku. Aby ułatwić wydajne wyszukiwanie, dwie ostatnie kolekcje są przechowywane w HashSet obiektach.

Jeśli pole wyboru kursu zostało wybrane, ale kurs nie znajduje się we Instructor.CourseAssignments właściwości nawigacji, kurs zostanie dodany do kolekcji we właściwości nawigacji.

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

Jeśli pole wyboru kursu nie zostało zaznaczone, ale kurs znajduje się we Instructor.CourseAssignments właściwości nawigacji, kurs zostanie usunięty z właściwości nawigacji.

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

Aktualizowanie widoków instruktora

W Views/Instructors/Edit.cshtmlpliku dodaj pole Courses z tablicą pól wyboru, dodając następujący kod bezpośrednio po div elementach pola Office i przed div elementem przycisku Zapisz .

Uwaga

Podczas wklejania kodu w programie Visual Studio podziały wierszy mogą zostać zmienione w sposób, który przerywa kod. Jeśli kod wygląda inaczej po wklejeniu, naciśnij klawisze Ctrl+Z raz, aby cofnąć automatyczne formatowanie. Spowoduje to naprawienie podziałów wierszy tak, aby wyglądały jak to, co widzisz tutaj. Wcięcie nie musi być idealne, ale @:</tr><tr>wiersze , @:<td>, @:</td>i @:</tr> muszą znajdować się w jednym wierszu, jak pokazano lub wystąpi błąd środowiska uruchomieniowego. Po wybraniu bloku nowego kodu naciśnij klawisz Tab trzy razy, aby ustawić nowy kod przy użyciu istniejącego kodu. Ten problem został rozwiązany w programie 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>

Ten kod tworzy tabelę HTML zawierającą trzy kolumny. W każdej kolumnie znajduje się pole wyboru, po którym następuje podpis składający się z numeru kursu i tytułu. Wszystkie pola wyboru mają taką samą nazwę ("selectedCourses"), która informuje powiązanie modelu, że mają być traktowane jako grupa. Atrybut wartości każdego pola wyboru jest ustawiony na wartość CourseID. Po opublikowaniu strony powiązanie modelu przekazuje tablicę do kontrolera składającego się z CourseID wartości tylko zaznaczonych pól wyboru.

Gdy pola wyboru są początkowo renderowane, te, które są przeznaczone dla kursów przypisanych do instruktora, sprawdziły atrybuty, które je wybierają (wyświetla je zaznaczone).

Uruchom aplikację, wybierz kartę Instruktorzy, a następnie kliknij pozycję Edytuj na instruktorze, aby wyświetlić stronę Edytuj.

Instructor Edit page with courses

Zmień niektóre przydziały kursu i kliknij przycisk Zapisz. Wprowadzone zmiany zostaną odzwierciedlone na stronie Indeks.

Uwaga

Podejście podjęte tutaj do edytowania danych kursu instruktora działa dobrze, gdy istnieje ograniczona liczba kursów. W przypadku kolekcji, które są znacznie większe, wymagany jest inny interfejs użytkownika i inna metoda aktualizacji.

Aktualizowanie strony Usuń

W InstructorsController.cspliku usuń metodę DeleteConfirmed i wstaw następujący kod w swoim miejscu.

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

Ten kod wprowadza następujące zmiany:

  • Wykonuje chętne CourseAssignments ładowanie dla właściwości nawigacji. Musisz uwzględnić tę opcję lub program EF nie będzie wiedział o powiązanych CourseAssignment jednostkach i nie będzie ich usuwać. Aby uniknąć konieczności odczytywania ich tutaj, możesz skonfigurować usuwanie kaskadowe w bazie danych.

  • Jeśli instruktor do usunięcia zostanie przypisany jako administrator jakichkolwiek działów, usuwa przydział instruktora z tych działów.

Dodawanie lokalizacji biura i kursów do strony Tworzenie

W InstructorsController.cspliku usuń metody HttpGet i HttpPost Create , a następnie dodaj następujący kod w ich miejscu:

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

Ten kod jest podobny do tego, co pokazano dla Edit metod, z tą różnicą, że początkowo nie wybrano żadnych kursów. Metoda HttpGet Create wywołuje metodę PopulateAssignedCourseData nie dlatego, że można wybrać kursy, ale w celu udostępnienia pustej kolekcji dla foreach pętli w widoku (w przeciwnym razie kod widoku zgłosi wyjątek odwołania o wartości null).

Metoda HttpPost Create dodaje każdy wybrany kurs do CourseAssignments właściwości nawigacji przed sprawdzeniem błędów walidacji i dodaje nowego instruktora do bazy danych. Kursy są dodawane nawet wtedy, gdy występują błędy modelu, tak aby w przypadku wystąpienia błędów modelu (na przykład użytkownik kluczował nieprawidłową datę), a strona jest odtwarzana z komunikatem o błędzie, wszystkie dokonane wybory kursu zostaną automatycznie przywrócone.

Zwróć uwagę, że aby można było dodać kursy do CourseAssignments właściwości nawigacji, musisz zainicjować właściwość jako pustą kolekcję:

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

Zamiast tego w kodzie kontrolera można to zrobić w Instructor modelu, zmieniając metodę pobierania właściwości, aby automatycznie utworzyć kolekcję, jeśli nie istnieje, jak pokazano w poniższym przykładzie:

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

Jeśli zmodyfikujesz CourseAssignments właściwość w ten sposób, możesz usunąć jawny kod inicjowania właściwości w kontrolerze.

W Views/Instructor/Create.cshtmlpliku dodaj pole tekstowe lokalizacji biura i pola wyboru dla kursów przed przyciskiem Prześlij. Tak jak w przypadku strony Edycja, popraw formatowanie, jeśli program Visual Studio ponownie sformatowa kod podczas wklejania.

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

Przetestuj, uruchamiając aplikację i tworząc instruktora.

Obsługa transakcji

Jak wyjaśniono w samouczku CRUD, program Entity Framework niejawnie implementuje transakcje. W przypadku scenariuszy, w których potrzebujesz większej kontroli — na przykład jeśli chcesz uwzględnić operacje wykonywane poza platformą Entity Framework w transakcji — zobacz Transakcje.

Uzyskiwanie kodu

Pobierz lub wyświetl ukończoną aplikację.

Następne kroki

W tym samouczku zostały wykonane następujące czynności:

  • Strony dostosowanych kursów
  • Dodano stronę edycji instruktorów
  • Dodano kursy do strony Edytowanie
  • Zaktualizowano stronę Usuwania
  • Dodano lokalizację biura i kursy do strony Tworzenie

Przejdź do następnego samouczka, aby dowiedzieć się, jak obsługiwać konflikty współbieżności.