Implementowanie podstawowych funkcji CRUD za pomocą platformy Entity Framework w aplikacji MVC ASP.NET (2 z 10)

Autor : Tom Dykstra

Przykładowa aplikacja internetowa Contoso University pokazuje, jak utworzyć aplikacje ASP.NET MVC 4 przy użyciu programu Entity Framework 5 Code First i Visual Studio 2012. Aby uzyskać informacje na temat serii samouczków, zobacz pierwszy samouczek z serii.

Uwaga

Jeśli napotkasz problem, którego nie możesz rozwiązać, pobierz ukończony rozdział i spróbuj odtworzyć problem. Zazwyczaj rozwiązanie problemu można znaleźć, porównując kod z ukończonym kodem. Aby uzyskać informacje o niektórych typowych błędach i sposobach ich rozwiązywania, zobacz Błędy i obejścia.

W poprzednim samouczku utworzono aplikację MVC, która przechowuje i wyświetla dane przy użyciu programu Entity Framework i SQL Server LocalDB. W tym samouczku przejrzysz i dostosujesz kod CRUD (tworzenie, odczytywanie, aktualizowanie, usuwanie), który szkielet MVC jest tworzony automatycznie na kontrolerach i widokach.

Uwaga

Typowym rozwiązaniem jest zaimplementowanie wzorca repozytorium w celu utworzenia warstwy abstrakcji między kontrolerem a warstwą dostępu do danych. Aby zachować prostotę tych samouczków, nie zaimplementujesz repozytorium do późniejszego samouczka z tej serii.

W tym samouczku utworzysz następujące strony internetowe:

Zrzut ekranu przedstawiający stronę Szczegóły ucznia uniwersytetu Contoso.

Zrzut ekranu przedstawiający stronę Edycji studenta uniwersytetu Contoso.

Zrzut ekranu przedstawiający stronę Tworzenie uczniów uniwersytetu Contoso.

Zrzut ekranu przedstawiający stronę Usuwanie uczniów.

Tworzenie strony szczegółów

Kod szkieletowy strony Uczniowie Index pominął Enrollments właściwość, ponieważ ta właściwość zawiera kolekcję. Details Na stronie zostanie wyświetlona zawartość kolekcji w tabeli HTML.

W pliku Controllers\StudentController.cs metoda akcji dla Details widoku używa Find metody do pobrania pojedynczej Student jednostki.

public ActionResult Details(int id = 0)
{
    Student student = db.Students.Find(id);
    if (student == null)
    {
        return HttpNotFound();
    }
    return View(student);
}

Wartość klucza jest przekazywana do metody jako parametru id i pochodzi z danych trasy w hiperlinku Szczegóły na stronie Indeks.

  1. Otwórz plik Views\Student\Details.cshtml. Każde pole jest wyświetlane przy użyciu DisplayFor pomocnika, jak pokazano w poniższym przykładzie:

    <div class="display-label">
             @Html.DisplayNameFor(model => model.LastName)
        </div>
        <div class="display-field">
            @Html.DisplayFor(model => model.LastName)
        </div>
    
  2. EnrollmentDate Po polu i bezpośrednio przed tagiem zamykającym fieldset dodaj kod, aby wyświetlić listę rejestracji, jak pokazano w poniższym przykładzie:

    <div class="display-label">
            @Html.LabelFor(model => model.Enrollments)
        </div>
        <div class="display-field">
            <table>
                <tr>
                    <th>Course Title</th>
                    <th>Grade</th>
                </tr>
                @foreach (var item in Model.Enrollments)
                {
                    <tr>
                        <td>
                            @Html.DisplayFor(modelItem => item.Course.Title)
                        </td>
                        <td>
                            @Html.DisplayFor(modelItem => item.Grade)
                        </td>
                    </tr>
                }
            </table>
        </div>
    </fieldset>
    <p>
        @Html.ActionLink("Edit", "Edit", new { id=Model.StudentID }) |
        @Html.ActionLink("Back to List", "Index")
    </p>
    

    Ten kod przechodzi przez jednostki we Enrollments właściwości nawigacji. Dla każdej Enrollment jednostki we właściwości wyświetla tytuł kursu i ocenę. Tytuł kursu jest pobierany z Course jednostki przechowywanej Course we właściwości Enrollments nawigacji jednostki. Wszystkie te dane są pobierane z bazy danych automatycznie, gdy są potrzebne. (Innymi słowy, używasz tutaj leniwego ładowania. Nie określono chętnegoCourses ładowania dla właściwości nawigacji, więc po raz pierwszy próbujesz uzyskać dostęp do tej właściwości, zapytanie jest wysyłane do bazy danych w celu pobrania danych. Więcej informacji na temat ładowania leniwego i chętnych do załadowania można przeczytać w samouczku Dotyczącym powiązanych danych w dalszej części tej serii).

  3. Uruchom stronę, wybierając kartę Uczniowie i klikając link Szczegóły dla Alexander Carson. Zostanie wyświetlona lista kursów i ocen dla wybranego ucznia:

    Student_Details_page

Aktualizowanie strony tworzenia

  1. W pliku Controllers\StudentController.cs zastąp HttpPost``Create metodę try-catch akcji następującym kodem, aby dodać blok i atrybut Bind do metody szkieletowej:

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(
       [Bind(Include = "LastName, FirstMidName, EnrollmentDate")]
       Student student)
    {
       try
       {
          if (ModelState.IsValid)
          {
             db.Students.Add(student);
             db.SaveChanges();
             return RedirectToAction("Index");
          }
       }
       catch (DataException /* dex */)
       {
          //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
          ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
       }
       return View(student);
    }
    

    Ten kod dodaje jednostkę utworzoną Student przez powiązanie modelu ASP.NET MVC z zestawem Students jednostek, a następnie zapisuje zmiany w bazie danych. (Binder modelu odnosi się do ASP.NET funkcji MVC, która ułatwia pracę z danymi przesłanymi przez formularz; binder modelu konwertuje opublikowane wartości formularzy na typy CLR i przekazuje je do metody akcji w parametrach. W takim przypadku binder modelu tworzy wystąpienie Student jednostki za pomocą wartości właściwości z Form kolekcji).

    Atrybut ValidateAntiForgeryToken pomaga zapobiegać atakom fałszerowania żądań między witrynami .

> [!WARNING]
    > Security - The `Bind` attribute is added to protect against *over-posting*. For example, suppose the `Student` entity includes a `Secret` property that you don't want this web page to update.
    > 
    > [!code-csharp[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample5.cs?highlight=7)]
    > 
    > Even if you don't have a `Secret` field on the web page, a hacker could use a tool such as [fiddler](http://fiddler2.com/home), or write some JavaScript, to post a `Secret` form value. Without the [Bind](https://msdn.microsoft.com/library/system.web.mvc.bindattribute(v=vs.108).aspx) attribute limiting the fields that the model binder uses when it creates a `Student` instance*,* the model binder would pick up that `Secret` form value and use it to update the `Student` entity instance. Then whatever value the hacker specified for the `Secret` form field would be updated in your database. The following image shows the fiddler tool adding the `Secret` field (with the value "OverPost") to the posted form values.
    > 
    > ![](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/_static/image6.png)  
    > 
    > The value "OverPost" would then be successfully added to the `Secret` property of the inserted row, although you never intended that the web page be able to update that property.
    > 
    > It's a security best practice to use the `Include` parameter with the `Bind` attribute to *allowed attributes* fields. It's also possible to use the `Exclude` parameter to *blocked attributes* fields you want to exclude. The reason `Include` is more secure is that when you add a new property to the entity, the new field is not automatically protected by an `Exclude` list.
    > 
    > Another alternative approach, and one preferred by many, is to use only view models with model binding. The view model contains only the properties you want to bind. Once the MVC model binder has finished, you copy the view model properties to the entity instance.

    Other than the `Bind` attribute, the `try-catch` block is the only change you've made to the scaffolded code. If an exception that derives from [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) is caught while the changes are being saved, a generic error message is displayed. [DataException](https://msdn.microsoft.com/library/system.data.dataexception.aspx) exceptions are sometimes caused by something external to the application rather than a programming error, so the user is advised to try again. Although not implemented in this sample, a production quality application would log the exception (and non-null inner exceptions ) with a logging mechanism such as [ELMAH](https://code.google.com/p/elmah/).

    The code in *Views\Student\Create.cshtml* is similar to what you saw in *Details.cshtml*, except that `EditorFor` and `ValidationMessageFor` helpers are used for each field instead of `DisplayFor`. The following example shows the relevant code:

    [!code-cshtml[Main](implementing-basic-crud-functionality-with-the-entity-framework-in-asp-net-mvc-application/samples/sample6.cshtml)]

    *Create.cshtml* also includes `@Html.AntiForgeryToken()`, which works with the `ValidateAntiForgeryToken` attribute in the controller to help prevent [cross-site request forgery](../../security/xsrfcsrf-prevention-in-aspnet-mvc-and-web-pages.md) attacks.

    No changes are required in *Create.cshtml*.
  1. Uruchom stronę, wybierając kartę Uczniowie i klikając pozycję Utwórz nowy.

    Student_Create_page

    Niektóre sprawdzanie poprawności danych działa domyślnie. Wprowadź nazwy i nieprawidłową datę, a następnie kliknij przycisk Utwórz , aby wyświetlić komunikat o błędzie.

    Students_Create_page_error_message

    Poniższy wyróżniony kod przedstawia sprawdzanie poprawności modelu.

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Create(Student student)
    {
        if (ModelState.IsValid)
        {
            db.Students.Add(student);
            db.SaveChanges();
            return RedirectToAction("Index");
        }
    
        return View(student);
    }
    

    Zmień datę na prawidłową wartość, taką jak 9/1/2005, a następnie kliknij przycisk Utwórz , aby zobaczyć, jak nowy student pojawi się na stronie Indeks .

    Students_Index_page_with_new_student

Aktualizowanie strony Edit POST (Edytowanie strony POST)

W pliku Controllers\StudentController.csHttpGetEdit metoda (bez atrybutuHttpPost) używa Find metody do pobrania wybranej Student jednostki, jak pokazano w metodzie Details . Nie musisz zmieniać tej metody.

Zastąp HttpPostEdit jednak metodę try-catch akcji następującym kodem, aby dodać blok i atrybut Bind:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
   [Bind(Include = "StudentID, LastName, FirstMidName, EnrollmentDate")]
   Student student)
{
   try
   {
      if (ModelState.IsValid)
      {
         db.Entry(student).State = EntityState.Modified;
         db.SaveChanges();
         return RedirectToAction("Index");
      }
   }
   catch (DataException /* dex */)
   {
      //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
      ModelState.AddModelError("", "Unable to save changes. Try again, and if the problem persists see your system administrator.");
   }
   return View(student);
}

Ten kod jest podobny do tego, co pokazano w metodzie HttpPostCreate . Jednak zamiast dodawać jednostkę utworzoną przez binder modelu do zestawu jednostek, ten kod ustawia flagę na jednostce wskazującej, że została zmieniona. Po wywołaniu metody SaveChanges flaga Zmodyfikowana powoduje, że program Entity Framework tworzy instrukcje SQL w celu zaktualizowania wiersza bazy danych. Wszystkie kolumny wiersza bazy danych zostaną zaktualizowane, w tym te, które użytkownik nie zmienił, a konflikty współbieżności są ignorowane. (Dowiesz się, jak obsługiwać współbieżność w dalszej części tego samouczka).

Stany jednostek i metody Attach i SaveChanges

Kontekst bazy danych śledzi, czy jednostki w pamięci są zsynchronizowane z odpowiednimi wierszami w bazie danych, a te informacje określają, co się dzieje po wywołaniu SaveChanges metody. Na przykład po przekazaniu nowej jednostki do metody Add stan tej jednostki jest ustawiony na Addedwartość . Następnie po wywołaniu metody SaveChanges kontekst bazy danych wystawia polecenie SQL INSERT .

Jednostka może znajdować się w jednym znastępujących stanów:

  • Added. Jednostka nie istnieje jeszcze w bazie danych. Metoda SaveChanges musi wydać instrukcję INSERT .
  • Unchanged. Nie trzeba nic robić z tą jednostką SaveChanges za pomocą metody . Podczas odczytywania jednostki z bazy danych jednostka rozpoczyna się od tego stanu.
  • Modified. Niektóre lub wszystkie wartości właściwości jednostki zostały zmodyfikowane. Metoda SaveChanges musi wydać instrukcję UPDATE .
  • Deleted. Jednostka została oznaczona do usunięcia. Metoda SaveChanges musi wydać instrukcję DELETE .
  • Detached. Jednostka nie jest śledzona przez kontekst bazy danych.

W aplikacji klasycznej zmiany stanu są zwykle ustawiane automatycznie. W typie aplikacji klasycznej odczytujesz jednostkę i wprowadzisz zmiany w niektórych jej wartościach właściwości. Powoduje to automatyczne zmianę stanu jednostki na Modified. Następnie po wywołaniu SaveChangesmetody program Entity Framework generuje instrukcję SQL UPDATE , która aktualizuje tylko rzeczywiste właściwości, które uległy zmianie.

Rozłączony charakter aplikacji internetowych nie zezwala na tę sekwencję ciągłą. Obiekt DbContext, który odczytuje jednostkę, jest usuwany po renderowaniu strony. Po wywołaniu HttpPostEdit metody akcji zostanie wykonane nowe żądanie i masz nowe wystąpienie obiektu DbContext, więc musisz ręcznie ustawić stan jednostki na Modified. Wartość Następnie po wywołaniu SaveChangesprogramu Entity Framework aktualizuje wszystkie kolumny wiersza bazy danych, ponieważ kontekst nie ma sposobu, aby wiedzieć, które właściwości zostały zmienione.

Jeśli chcesz, aby instrukcja SQL Update zaktualizowała tylko pola, które użytkownik rzeczywiście zmienił, możesz zapisać oryginalne wartości w jakiś sposób (na przykład ukryte pola), aby były dostępne po HttpPostEdit wywołaniu metody. Następnie możesz utworzyć jednostkę przy użyciu oryginalnych wartości, wywołać Attach metodę Student z tą oryginalną wersją jednostki, zaktualizować wartości jednostki do nowych wartości, a następnie wywołać wywołanie SaveChanges. Aby uzyskać więcej informacji, zobacz Stany jednostek i SaveChanges i Dane lokalne w Centrum deweloperów danych MSDN.

Kod w pliku Views\Student\Edit.cshtml jest podobny do tego, co pokazano w pliku Create.cshtml i nie są wymagane żadne zmiany.

Uruchom stronę, wybierając kartę Uczniowie , a następnie klikając hiperlink Edytuj .

Student_Edit_page

Zmień niektóre dane i kliknij przycisk Zapisz. Zmienione dane są widoczne na stronie Indeks.

Students_Index_page_after_edit

Aktualizowanie strony usuwania

W pliku Controllers\StudentController.cs kod HttpGetDelete szablonu metody używa Find metody do pobrania wybranej Student jednostki, jak pokazano w metodach Details i Edit . Jednak w celu zaimplementowania niestandardowego komunikatu o błędzie, gdy wywołanie SaveChanges zakończy się niepowodzeniem, dodasz pewne funkcje do tej metody i odpowiedniego widoku.

Jak pokazano na potrzeby operacji aktualizacji i tworzenia, operacje usuwania wymagają dwóch metod akcji. Metoda wywoływana w odpowiedzi na żądanie GET wyświetla widok, który umożliwia użytkownikowi zatwierdzenie lub anulowanie operacji usuwania. Jeśli użytkownik go zatwierdzi, zostanie utworzone żądanie POST. W takim przypadku metoda jest wywoływana, HttpPostDelete a następnie ta metoda rzeczywiście wykonuje operację usuwania.

Do metody dodasz try-catch blok, HttpPostDelete aby obsłużyć wszelkie błędy, które mogą wystąpić po zaktualizowaniu bazy danych. Jeśli wystąpi błąd, HttpPostDelete metoda wywołuje metodę HttpGetDelete , przekazując jej parametr wskazujący, że wystąpił błąd. Następnie HttpGet Delete metoda redisplays strony potwierdzenia wraz z komunikatem o błędzie, dając użytkownikowi możliwość anulowania lub próby ponownie.

  1. Zastąp metodę HttpGetDelete akcji następującym kodem, który zarządza raportowaniem błędów:

    public ActionResult Delete(bool? saveChangesError=false, int id = 0)
    {
        if (saveChangesError.GetValueOrDefault())
        {
            ViewBag.ErrorMessage = "Delete failed. Try again, and if the problem persists see your system administrator.";
        }
        Student student = db.Students.Find(id);
        if (student == null)
        {
            return HttpNotFound();
        }
        return View(student);
    }
    

    Ten kod akceptuje opcjonalny parametr logiczny wskazujący, czy został wywołany po niepowodzeniu zapisywania zmian. Ten parametr występuje false , gdy HttpGetDelete metoda jest wywoływana bez wcześniejszego błędu. Gdy jest wywoływana przez metodę HttpPostDelete w odpowiedzi na błąd aktualizacji bazy danych, parametr jest true i komunikat o błędzie jest przekazywany do widoku.

  2. Zastąp metodę HttpPostDelete akcji (o nazwie DeleteConfirmed) następującym kodem, który wykonuje rzeczywistą operację usuwania i przechwytuje wszelkie błędy aktualizacji bazy danych.

    [HttpPost]
    [ValidateAntiForgeryToken]
    public ActionResult Delete(int id)
    {
        try
        {
            Student student = db.Students.Find(id);
            db.Students.Remove(student);
            db.SaveChanges();
        }
        catch (DataException/* dex */)
        {
            // uncomment dex and log error. 
            return RedirectToAction("Delete", new { id = id, saveChangesError = true });
        }
        return RedirectToAction("Index");
    }
    

    Ten kod pobiera wybraną jednostkę, a następnie wywołuje metodę Remove , aby ustawić stan jednostki na Deleted. Po SaveChanges wywołaniu jest generowane polecenie SQL DELETE . Zmieniono również nazwę metody akcji z DeleteConfirmed na Delete. Szkieletowy kod o nazwie HttpPostDelete metoda DeleteConfirmed dająca metodzie HttpPost unikatowy podpis. ( ClR wymaga przeciążonych metod, aby mieć różne parametry metody. Teraz, gdy podpisy są unikatowe, możesz trzymać się konwencji MVC i używać tej samej nazwy dla HttpPost metod i HttpGet usuwania.

    Jeśli zwiększenie wydajności w aplikacji o dużej ilości jest priorytetem, można uniknąć niepotrzebnego zapytania SQL w celu pobrania wiersza, zastępując wiersze kodu, które wywołają Find metody i Remove następującym kodem, jak pokazano w żółtym wyróżnieniu:

    Student studentToDelete = new Student() { StudentID = id };
    db.Entry(studentToDelete).State = EntityState.Deleted;
    

    Ten kod tworzy wystąpienie Student jednostki przy użyciu tylko wartości klucza podstawowego, a następnie ustawia stan jednostki na Deleted. To wszystko, co wymaga program Entity Framework, aby usunąć jednostkę.

    Jak wspomniano, HttpGetDelete metoda nie usuwa danych. Wykonanie operacji usuwania w odpowiedzi na żądanie GET (lub w tym przypadku wykonanie dowolnej operacji edycji, operacji tworzenia lub dowolnej innej operacji, która zmienia dane) powoduje zagrożenie bezpieczeństwa. Aby uzyskać więcej informacji, zobacz ASP.NET MVC Tip #46 — Nie używaj linków usuwania, ponieważ tworzą one luki w zabezpieczeniach na blogu Stephena Walthera.

  3. W pliku Views\Student\Delete.cshtml dodaj komunikat o błędzie między nagłówkiem h2 a nagłówkiem h3 , jak pokazano w poniższym przykładzie:

    <h2>Delete</h2>
    <p class="error">@ViewBag.ErrorMessage</p>
    <h3>Are you sure you want to delete this?</h3>
    

    Uruchom stronę, wybierając kartę Uczniowie i klikając hiperlink Usuń :

    Student_Delete_page

  4. Kliknij polecenie Usuń. Strona Indeks jest wyświetlana bez usuniętego ucznia. (W dalszej części tej serii zobaczysz przykład kodu obsługi błędów w akcji w samouczku Obsługa współbieżności ).

Upewnienie się, że połączenia z bazą danych nie są otwarte

Aby upewnić się, że Połączenia z bazą danych są prawidłowo zamknięte, a zasoby, które przechowują, zostały zwolnione, powinno zostać wyświetlone, że wystąpienie kontekstu zostało usunięte. Dlatego kod szkieletowy udostępnia metodę Dispose na końcu StudentController klasy w pliku StudentController.cs, jak pokazano w poniższym przykładzie:

protected override void Dispose(bool disposing)
{
    db.Dispose();
    base.Dispose(disposing);
}

Klasa bazowa Controller już implementuje IDisposable interfejs, więc ten kod po prostu dodaje przesłonięcia do Dispose(bool) metody w celu jawnego usunięcia wystąpienia kontekstu.

Podsumowanie

Masz teraz kompletny zestaw stron, które wykonują proste operacje CRUD dla Student jednostek. Użyliśmy pomocników MVC do generowania elementów interfejsu użytkownika dla pól danych. Aby uzyskać więcej informacji na temat pomocników MVC, zobacz Renderowanie formularza przy użyciu pomocników HTML (strona dotyczy wzorca MVC 3, ale jest nadal odpowiednia dla MVC 4).

W następnym samouczku rozszerzysz funkcjonalność strony Indeks, dodając sortowanie i stronicowanie.

Linki do innych zasobów programu Entity Framework można znaleźć na mapie zawartości dostępu do danych ASP.NET.