Udostępnij za pośrednictwem


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 tworzyć 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 programu SQL Server LocalDB. W tym samouczku zapoznasz się i dostosujesz kod CRUD (tworzenie, odczytywanie, aktualizowanie i usuwanie), który szkielet MVC tworzy automatycznie w 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ę Contoso University Student Details.

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

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

Zrzut ekranu przedstawiający stronę Usuwanie ucznia.

Tworzenie strony szczegółów

Kod szkieletu 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 obszarze Controllers\StudentController.cs metoda akcji widoku Details 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ętnego Courses ładowania dla właściwości nawigacji, więc przy pierwszej próbie uzyskania dostępu do tej właściwości zapytanie jest wysyłane do bazy danych w celu pobrania danych. Więcej informacji na temat leniwego ładowania i chętnego ładowania można przeczytać w samouczku Reading Related Data (Odczytywanie powiązanych danych) w dalszej części tej serii).

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

    Student_Details_page

Aktualizowanie strony tworzenia

  1. W 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 Student jednostkę utworzoną przez powiązanie modelu MVC ASP.NET 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 formularza na typy CLR i przekazuje je do metody akcji w parametrach. W tym 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łszerzmu żą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 1.09.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 EDYTUJ POST

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

Zastąp jednak metodę HttpPost Edit akcji następującym kodem, aby dodać try-catch 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 HttpPost Create . Jednak zamiast dodawać jednostkę utworzoną przez powiązanie 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 późniejszym samouczku w tej serii).

Stany jednostek oraz 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ę stanie 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. W tej jednostce SaveChanges nie trzeba nic robić przy użyciu metody . Podczas odczytywania jednostki z bazy danych jednostka zaczyna 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 wprowadzasz 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 odczytujący jednostkę jest usuwany po renderowaniu strony. Po wywołaniu HttpPost Edit metody akcji zostanie wykonane nowe żądanie i masz nowe wystąpienie obiektu DbContext, dlatego należy 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 możliwości poznania, które właściwości zostały zmienione.

Jeśli chcesz, aby instrukcja SQL Update aktualizował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 wywołaniu HttpPost Edit 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ć metodę SaveChanges. Aby uzyskać więcej informacji, zobacz Stany jednostki i SaveChanges i Dane lokalne w Centrum deweloperów danych MSDN.

Kod w pliku Views\Student\Edit.cshtml jest podobny do tego, co zostało wyświetlone 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 obszarze Controllers\StudentController.cs kod szablonu metody HttpGet Delete używa Find metody do pobrania wybranej Student jednostki, jak pokazano w metodach Details i Edit . Jednak aby zaimplementować niestandardowy komunikat o błędzie, gdy wywołanie zakończy się SaveChanges niepowodzeniem, do tej metody dodasz pewne funkcje i odpowiadający jej widok.

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 daje użytkownikowi szansę zatwierdzenia lub anulowania operacji usuwania. Jeśli użytkownik go zatwierdzi, zostanie utworzone żądanie POST. W takim przypadku metoda jest wywoływana, HttpPost Delete a następnie metoda faktycznie wykonuje operację usuwania.

Do metody dodasz try-catch blok, HttpPost Delete aby obsłużyć wszelkie błędy, które mogą wystąpić po zaktualizowaniu bazy danych. Jeśli wystąpi błąd, HttpPost Delete metoda wywołuje metodę HttpGet Delete , 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ę HttpGet Delete 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 HttpGet Delete metoda jest wywoływana bez wcześniejszego błędu. Gdy jest wywoływana przez metodę HttpPost Delete w odpowiedzi na błąd aktualizacji bazy danych, parametr jest true i komunikat o błędzie jest przekazywany do widoku.

  2. Zastąp metodę HttpPost Delete 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. Kod szkieletowy o nazwie HttpPost Delete metody DeleteConfirmed w celu nadania metodzie HttpPost unikatowego podpisu. ( 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 w celu usunięcia jednostki.

    Jak wspomniano, HttpGet Delete 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ą zabezpieczeń 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 Usuń. Strona Indeks jest wyświetlana bez usuniętego ucznia. (Zobaczysz przykład kodu obsługi błędów w działaniu w pliku Obsługa samouczka współbieżności w dalszej części tej serii).

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 i zasoby, które zostały zwolnione, należy zobaczyć, że wystąpienie kontekstu jest usuwane. Dlatego kod szkieletowy udostępnia metodę Dispose na końcu StudentController klasy w 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żyto 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 jest dla MVC 3, ale nadal jest 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.