Samouczek: obsługa współbieżności za pomocą platformy EF w aplikacji MVC 5 ASP.NET

W poprzednich samouczkach przedstawiono sposób aktualizowania danych. W tym samouczku pokazano, jak używać optymistycznej współbieżności do obsługi konfliktów, gdy wielu użytkowników jednocześnie aktualizuje tę samą jednostkę. Strony internetowe współpracujące z jednostką Department są zmieniane tak, aby obsługiwały błędy współbieżności. Na poniższych ilustracjach przedstawiono strony Edytuj i Usuń, w tym niektóre komunikaty wyświetlane w przypadku konfliktu współbieżności.

Zrzut ekranu przedstawiający stronę Edytuj z wartościami Nazwa działu, Budżet, Data rozpoczęcia i Administrator z wyróżnionymi bieżącymi wartościami.

Zrzut ekranu przedstawiający stronę Usuń dla rekordu z komunikatem o operacji usuwania i przyciskiem Usuń.

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

  • Dowiedz się więcej o konfliktach współbieżności
  • Dodawanie optymistycznej współbieżności
  • Modyfikowanie kontrolera działu
  • Testowanie obsługi współbieżności
  • Aktualizowanie strony Usuwanie

Wymagania wstępne

Konflikty współbieżności

Konflikt współbieżności występuje, gdy jeden użytkownik wyświetla dane jednostki w celu ich edycji, a następnie inny użytkownik aktualizuje dane tej samej jednostki przed zapisaniem zmiany pierwszego użytkownika w bazie danych. Jeśli nie włączysz wykrywania takich konfliktów, kto po raz ostatni zaktualizuje bazę danych, zastąpi zmiany innego użytkownika. W wielu aplikacjach to ryzyko jest dopuszczalne: jeśli istnieje niewielu użytkowników lub kilka aktualizacji lub jeśli nie ma naprawdę krytycznego, jeśli niektóre zmiany zostaną zastąpione, koszt programowania dla współbieżności może przeważyć nad korzyścią. W takim przypadku nie trzeba konfigurować aplikacji do obsługi konfliktów współbieżności.

Pesymistyczne współbieżność (blokowanie)

Jeśli aplikacja musi zapobiec przypadkowej utracie danych w scenariuszach współbieżności, jednym ze sposobów jest użycie blokad bazy danych. Jest to nazywane pesymistyczną współbieżnością. Na przykład przed odczytaniem wiersza z bazy danych należy zażądać blokady tylko do odczytu lub dostępu do aktualizacji. Jeśli zablokujesz wiersz dostępu do aktualizacji, żaden inny użytkownik nie będzie mógł zablokować wiersza na potrzeby dostępu tylko do odczytu lub aktualizacji, ponieważ otrzymają kopię danych w procesie zmiany. Jeśli zablokujesz wiersz dostępu tylko do odczytu, inne osoby mogą również zablokować go na potrzeby dostępu tylko do odczytu, ale nie do aktualizacji.

Zarządzanie blokadami ma wady. Może to być skomplikowane do programowania. Wymaga to znaczących zasobów zarządzania bazami danych i może powodować problemy z wydajnością w miarę wzrostu liczby użytkowników aplikacji. Z tych powodów nie wszystkie systemy zarządzania bazami danych obsługują pesymistyczną współbieżność. Program Entity Framework nie zapewnia wbudowanej obsługi, a w tym samouczku nie pokazano, jak go zaimplementować.

Optymistyczna współbieżność

Alternatywą dla pesymistycznej współbieżności jest optymistyczna współbieżność. Optymistyczna współbieżność oznacza umożliwienie wystąpienia konfliktów współbieżności, a następnie odpowiednie reagowanie, jeśli tak. Na przykład Jan uruchamia stronę Edycja działów, zmienia kwotę budżetu dla działu angielskiego z 350 000,000 USD na 0,00 USD.

Przed kliknięciem przycisku Zapisz Jane uruchomi tę samą stronę i zmieni pole Data rozpoczęcia z 2007-09-09-2013.

Jan klika pozycję Zapisz jako pierwszy i widzi jego zmianę po powrocie przeglądarki na stronę Indeks, a następnie jane klika pozycję Zapisz. To, co się stanie dalej, zależy od sposobu obsługi konfliktów współbieżności. Niektóre opcje obejmują następujące elementy:

  • Możesz śledzić, która właściwość użytkownika zmodyfikowała i zaktualizować tylko odpowiednie kolumny w bazie danych. W przykładowym scenariuszu żadne dane nie zostaną utracone, ponieważ obaj użytkownicy zaktualizowali różne właściwości. Następnym razem, gdy ktoś przegląda dział angielski, zobaczy zmiany Zarówno Johna, jak i Jane'a — datę rozpoczęcia 8/8/2013 i budżet zero dolarów.

    Ta metoda aktualizowania może zmniejszyć liczbę konfliktów, które mogą spowodować utratę danych, ale nie może uniknąć utraty danych, jeśli konkurencyjne zmiany zostaną wprowadzone do tej samej właściwości jednostki. To, czy platforma Entity Framework działa w ten sposób, zależy od sposobu implementacji kodu aktualizacji. Często nie jest to praktyczne w aplikacji internetowej, ponieważ może wymagać utrzymania dużych ilości stanu w celu śledzenia wszystkich oryginalnych wartości właściwości dla jednostki, a także nowych wartości. Utrzymywanie dużych ilości stanu może mieć wpływ na wydajność aplikacji, ponieważ wymaga zasobów serwera lub musi być dołączone do samej strony internetowej (na przykład w ukrytych polach) lub w pliku cookie.

  • Możesz pozwolić zmiany Jane zastąpić zmianę Johna. Następnym razem, gdy ktoś przegląda angielski dział, zobaczy 8/8/2013 i przywróconą wartość $350,000.000. Jest to nazywane klientem wins lub Last in Wins scenariusz. (Wszystkie wartości od klienta mają pierwszeństwo przed tym, co znajduje się w magazynie danych). Jak wspomniano we wprowadzeniu do tej sekcji, jeśli nie wykonasz kodowania na potrzeby obsługi współbieżności, nastąpi to automatycznie.

  • Możesz zapobiec aktualizowaniu zmiany Jane w bazie danych. Zazwyczaj jest wyświetlany komunikat o błędzie, pokazywanie bieżącego stanu danych i ponowne ich stosować, jeśli nadal chce je wprowadzić. Jest to nazywane scenariuszem Store Wins . (Wartości magazynu danych mają pierwszeństwo przed wartościami przesłanimi przez klienta). W tym samouczku zaimplementujesz scenariusz Store Wins. Ta metoda gwarantuje, że żadne zmiany nie zostaną zastąpione bez powiadamiania użytkownika o tym, co się dzieje.

Wykrywanie konfliktów współbieżności

Konflikty można rozwiązać, obsługując wyjątki OptimisticConcurrencyException zgłaszane przez program Entity Framework. Aby wiedzieć, kiedy zgłaszać te wyjątki, program Entity Framework musi mieć możliwość wykrywania konfliktów. Dlatego należy odpowiednio skonfigurować bazę danych i model danych. Niektóre opcje włączania wykrywania konfliktów obejmują następujące opcje:

  • W tabeli bazy danych dołącz kolumnę śledzenia, która może służyć do określenia, kiedy wiersz został zmieniony. Następnie można skonfigurować platformę Entity Framework tak, aby zawierała kolumnę Where w klauzuli SQL Update lub Delete poleceniach.

    Typ danych kolumny śledzenia to zazwyczaj rowversion. Wartość rowversion jest sekwencyjną liczbą, która jest zwiększana za każdym razem, gdy wiersz jest aktualizowany. W poleceniu Update lub Delete klauzula Where zawiera oryginalną wartość kolumny śledzenia (oryginalna wersja wiersza). Jeśli aktualizowany wiersz został zmieniony przez innego użytkownika, wartość w rowversion kolumnie jest inna niż oryginalna wartość, więc Update instrukcja or Delete nie może znaleźć wiersza do zaktualizowania ze względu na klauzulę Where . Gdy program Entity Framework wykryje, że żadne wiersze nie zostały zaktualizowane przez Update polecenie lub Delete (czyli gdy liczba wierszy, których dotyczy problem, wynosi zero), interpretuje to jako konflikt współbieżności.

  • Skonfiguruj platformę Entity Framework, aby uwzględnić oryginalne wartości każdej kolumny w tabeli w Where klauzuli Update i Delete poleceń.

    Podobnie jak w przypadku pierwszej opcji, jeśli coś w wierszu uległo zmianie od czasu pierwszego odczytania wiersza, Where klauzula nie zwróci wiersza do zaktualizowania, który program Entity Framework interpretuje jako konflikt współbieżności. W przypadku tabel bazy danych, które mają wiele kolumn, takie podejście może spowodować bardzo duże Where klauzule i może wymagać utrzymania dużych ilości stanu. Jak wspomniano wcześniej, utrzymywanie dużych ilości stanu może mieć wpływ na wydajność aplikacji. W związku z tym to podejście zwykle nie jest zalecane i nie jest to metoda używana w tym samouczku.

    Jeśli chcesz zaimplementować to podejście do współbieżności, musisz oznaczyć wszystkie właściwości inne niż podstawowe w jednostce, dla której chcesz śledzić współbieżność, dodając do nich atrybut ConcurrencyCheck . Ta zmiana umożliwia platformie Entity Framework uwzględnienie wszystkich kolumn w klauzuli SQL WHERE instrukcji UPDATE .

W pozostałej części tego samouczka dodasz do jednostki właściwość Department śledzenia rowversion, utworzysz kontroler i widoki, a następnie sprawdzisz, czy wszystko działa prawidłowo.

Dodawanie optymistycznej współbieżności

W pliku Models\Department.cs dodaj właściwość śledzenia o nazwie RowVersion:

public class Department
{
    public int DepartmentID { get; set; }

    [StringLength(50, MinimumLength = 3)]
    public string Name { get; set; }

    [DataType(DataType.Currency)]
    [Column(TypeName = "money")]
    public decimal Budget { get; set; }

    [DataType(DataType.Date)]
    [DisplayFormat(DataFormatString = "{0:yyyy-MM-dd}", ApplyFormatInEditMode = true)]
    [Display(Name = "Start Date")]
    public DateTime StartDate { get; set; }

    [Display(Name = "Administrator")]
    public int? InstructorID { get; set; }

    [Timestamp]
    public byte[] RowVersion { get; set; }

    public virtual Instructor Administrator { get; set; }
    public virtual ICollection<Course> Courses { get; set; }
}

Atrybut Znacznik czasu określa, że ta kolumna zostanie uwzględniona w Where klauzuli Update i Delete poleceń wysyłanych do bazy danych. Atrybut jest nazywany sygnaturą czasową, ponieważ poprzednie wersje SQL Server używały typu danych sygnatury czasowej SQL przed zastąpieniem go przez wiersz SQL. Typ platformy .Net dla elementu rowversion jest tablicą bajtów.

Jeśli wolisz używać płynnego interfejsu API, możesz użyć metody IsConcurrencyToken , aby określić właściwość śledzenia, jak pokazano w poniższym przykładzie:

modelBuilder.Entity<Department>()
    .Property(p => p.RowVersion).IsConcurrencyToken();

Dodając właściwość zmienioną model bazy danych, więc należy przeprowadzić inną migrację. W konsoli menedżera pakietów (PMC) wprowadź następujące polecenia:

Add-Migration RowVersion
Update-Database

Modyfikowanie kontrolera działu

W pliku Controllers\DepartmentController.cs dodaj instrukcję using :

using System.Data.Entity.Infrastructure;

W pliku DepartmentController.cs zmień wszystkie cztery wystąpienia ciągu "LastName" na "FullName", aby listy rozwijane administratora działu zawierały pełną nazwę instruktora, a nie tylko nazwisko.

ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName");

Zastąp istniejący kod metody HttpPostEdit następującym kodem:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(int? id, byte[] rowVersion)
{
    string[] fieldsToBind = new string[] { "Name", "Budget", "StartDate", "InstructorID", "RowVersion" };

    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }

    var departmentToUpdate = await db.Departments.FindAsync(id);
    if (departmentToUpdate == null)
    {
        Department deletedDepartment = new Department();
        TryUpdateModel(deletedDepartment, fieldsToBind);
        ModelState.AddModelError(string.Empty,
            "Unable to save changes. The department was deleted by another user.");
        ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", deletedDepartment.InstructorID);
        return View(deletedDepartment);
    }

    if (TryUpdateModel(departmentToUpdate, fieldsToBind))
    {
        try
        {
            db.Entry(departmentToUpdate).OriginalValues["RowVersion"] = rowVersion;
            await db.SaveChangesAsync();

            return RedirectToAction("Index");
        }
        catch (DbUpdateConcurrencyException ex)
        {
            var entry = ex.Entries.Single();
            var clientValues = (Department)entry.Entity;
            var databaseEntry = entry.GetDatabaseValues();
            if (databaseEntry == null)
            {
                ModelState.AddModelError(string.Empty,
                    "Unable to save changes. The department was deleted by another user.");
            }
            else
            {
                var databaseValues = (Department)databaseEntry.ToObject();

                if (databaseValues.Name != clientValues.Name)
                    ModelState.AddModelError("Name", "Current value: "
                        + databaseValues.Name);
                if (databaseValues.Budget != clientValues.Budget)
                    ModelState.AddModelError("Budget", "Current value: "
                        + String.Format("{0:c}", databaseValues.Budget));
                if (databaseValues.StartDate != clientValues.StartDate)
                    ModelState.AddModelError("StartDate", "Current value: "
                        + String.Format("{0:d}", databaseValues.StartDate));
                if (databaseValues.InstructorID != clientValues.InstructorID)
                    ModelState.AddModelError("InstructorID", "Current value: "
                        + db.Instructors.Find(databaseValues.InstructorID).FullName);
                ModelState.AddModelError(string.Empty, "The record you attempted to edit "
                    + "was modified by another user after you got the original value. The "
                    + "edit operation was canceled and the current values in the database "
                    + "have been displayed. If you still want to edit this record, click "
                    + "the Save button again. Otherwise click the Back to List hyperlink.");
                departmentToUpdate.RowVersion = databaseValues.RowVersion;
            }
        }
        catch (RetryLimitExceededException /* dex */)
        {
            //Log the error (uncomment dex variable name 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.");
        }
    }
    ViewBag.InstructorID = new SelectList(db.Instructors, "ID", "FullName", departmentToUpdate.InstructorID);
    return View(departmentToUpdate);
}

FindAsync Jeśli metoda zwraca wartość null, dział został usunięty przez innego użytkownika. Pokazany kod używa opublikowanych wartości formularza do utworzenia jednostki działu, aby można było ponownie edytować stronę Edycji z komunikatem o błędzie. Alternatywnie nie trzeba będzie ponownie tworzyć jednostki działu, jeśli zostanie wyświetlony tylko komunikat o błędzie bez ponownego tworzenia pól działu.

Widok przechowuje oryginalną RowVersion wartość w polu ukrytym, a metoda odbiera ją w parametrze rowVersion . Przed wywołaniem SaveChangesmetody należy umieścić w kolekcji wartość oryginalnej RowVersionOriginalValues właściwości dla jednostki. Następnie, gdy program Entity Framework utworzy polecenie SQL UPDATE , to polecenie będzie zawierać klauzulę WHERE , która wyszukuje wiersz zawierający oryginalną RowVersion wartość.

Jeśli polecenie nie ma wpływu na UPDATE żadne wiersze (żadne wiersze nie mają oryginalnej RowVersion wartości), program Entity Framework zgłasza DbUpdateConcurrencyException wyjątek, a kod w catch bloku pobiera jednostkę, której dotyczy problem Department , z obiektu wyjątku.

var entry = ex.Entries.Single();

Ten obiekt ma nowe wartości wprowadzone przez użytkownika we właściwości Entity i można pobrać wartości odczytane z bazy danych, wywołując metodę GetDatabaseValues .

var clientValues = (Department)entry.Entity;
var databaseEntry = entry.GetDatabaseValues();

Metoda GetDatabaseValues zwraca wartość null, jeśli ktoś usunął wiersz z bazy danych. W przeciwnym razie należy rzutować zwrócony obiekt do klasy, aby uzyskać dostęp Department do Department właściwości. (Ponieważ już sprawdzono usunięcie, byłoby zerowe tylko wtedy, databaseEntry gdy dział został usunięty po FindAsync wykonaniu i przed SaveChanges wykonaniem).

if (databaseEntry == null)
{
    ModelState.AddModelError(string.Empty,
        "Unable to save changes. The department was deleted by another user.");
}
else
{
    var databaseValues = (Department)databaseEntry.ToObject();

Następnie kod dodaje niestandardowy komunikat o błędzie dla każdej kolumny zawierającej wartości bazy danych różniące się od tego, co użytkownik wprowadził na stronie Edycja:

if (databaseValues.Name != currentValues.Name)
    ModelState.AddModelError("Name", "Current value: " + databaseValues.Name);
    // ...

Dłuższy komunikat o błędzie wyjaśnia, co się stało i co z tym zrobić:

ModelState.AddModelError(string.Empty, "The record you attempted to edit "
    + "was modified by another user after you got the original value. The"
    + "edit operation was canceled and the current values in the database "
    + "have been displayed. If you still want to edit this record, click "
    + "the Save button again. Otherwise click the Back to List hyperlink.");

Na koniec kod ustawia RowVersion wartość Department obiektu na nową wartość pobraną z bazy danych. Ta nowa RowVersion wartość będzie przechowywana w ukrytym polu, gdy strona Edytuj jest odtwarzana ponownie, a następnym razem, gdy użytkownik kliknie przycisk Zapisz, zostaną przechwycone tylko błędy współbieżności, które występują od czasu przechwytu ponownego edytowania strony.

W obszarze Views\Department\Edit.cshtml dodaj ukryte pole, aby zapisać RowVersion wartość właściwości, bezpośrednio po ukrytym polu właściwości DepartmentID :

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Edit";
}

<h2>Edit</h2>

@using (Html.BeginForm())
{
    @Html.AntiForgeryToken()
    
    <div class="form-horizontal">
        <h4>Department</h4>
        <hr />
        @Html.ValidationSummary(true)
        @Html.HiddenFor(model => model.DepartmentID)
        @Html.HiddenFor(model => model.RowVersion)

Testowanie obsługi współbieżności

Uruchom witrynę i kliknij pozycję Działy.

Kliknij prawym przyciskiem myszy hiperlink Edytuj dla działu angielskiego i wybierz pozycję Otwórz na nowej karcie, a następnie kliknij hiperlink Edytuj dla działu angielskiego. Dwie karty zawierają te same informacje.

Zmień pole na pierwszej karcie przeglądarki i kliknij przycisk Zapisz.

W przeglądarce zostanie wyświetlona strona Indeks ze zmienioną wartością.

Zmień pole na drugiej karcie przeglądarki i kliknij przycisk Zapisz. Zostanie wyświetlony komunikat o błędzie:

Zrzut ekranu przedstawia stronę Edytuj z komunikatem, który wyjaśnia, że operacja została anulowana, ponieważ wartość została zmieniona przez innego użytkownika.

Kliknij ponownie pozycję Zapisz . Wartość wprowadzona na drugiej karcie przeglądarki jest zapisywana wraz z oryginalną wartością danych zmienionych w pierwszej przeglądarce. Zapisane wartości są widoczne po wyświetleniu strony Indeks.

Aktualizowanie strony Usuwanie

Na stronie Usuwanie platforma Entity Framework wykrywa konflikty współbieżności spowodowane przez inną osobę edytując dział w podobny sposób. HttpGetDelete Gdy metoda wyświetla widok potwierdzenia, widok zawiera oryginalną RowVersion wartość w polu ukrytym. Ta wartość jest następnie dostępna dla HttpPostDelete metody wywoływanej, gdy użytkownik potwierdzi usunięcie. Gdy program Entity Framework utworzy polecenie SQL DELETE , zawiera klauzulę WHERE z oryginalną RowVersion wartością. Jeśli polecenie powoduje, że wystąpił problem z zerowymi wierszami (co oznacza, że wiersz został zmieniony po wyświetleniu strony potwierdzenia Usuwania), zostanie zgłoszony wyjątek współbieżności, a HttpGet Delete metoda jest wywoływana z flagą błędu ustawioną na true w celu ponownego ponownego wyświetlenia strony potwierdzenia z komunikatem o błędzie. Istnieje również możliwość, że wystąpił problem z zerowymi wierszami, ponieważ wiersz został usunięty przez innego użytkownika, więc w takim przypadku jest wyświetlany inny komunikat o błędzie.

W pliku DepartmentController.cs zastąp metodę HttpGetDelete następującym kodem:

public async Task<ActionResult> Delete(int? id, bool? concurrencyError)
{
    if (id == null)
    {
        return new HttpStatusCodeResult(HttpStatusCode.BadRequest);
    }
    Department department = await db.Departments.FindAsync(id);
    if (department == null)
    {
        if (concurrencyError.GetValueOrDefault())
        {
            return RedirectToAction("Index");
        }
        return HttpNotFound();
    }

    if (concurrencyError.GetValueOrDefault())
    {
        ViewBag.ConcurrencyErrorMessage = "The record you attempted to delete "
            + "was modified by another user after you got the original values. "
            + "The delete operation was canceled and the current values in the "
            + "database have been displayed. If you still want to delete this "
            + "record, click the Delete button again. Otherwise "
            + "click the Back to List hyperlink.";
    }

    return View(department);
}

Metoda akceptuje opcjonalny parametr wskazujący, czy strona jest odtwarzana ponownie po błędzie współbieżności. Jeśli ta flaga to true, do widoku jest wysyłany komunikat o błędzie przy użyciu ViewBag właściwości.

Zastąp kod w metodzie HttpPostDelete (o nazwie DeleteConfirmed) następującym kodem:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Delete(Department department)
{
    try
    {
        db.Entry(department).State = EntityState.Deleted;
        await db.SaveChangesAsync();
        return RedirectToAction("Index");
    }
    catch (DbUpdateConcurrencyException)
    {
        return RedirectToAction("Delete", new { concurrencyError = true, id=department.DepartmentID });
    }
    catch (DataException /* dex */)
    {
        //Log the error (uncomment dex variable name after DataException and add a line here to write a log.
        ModelState.AddModelError(string.Empty, "Unable to delete. Try again, and if the problem persists contact your system administrator.");
        return View(department);
    }
}

W właśnie zastąpionym kodzie szkieletowym ta metoda zaakceptowała tylko identyfikator rekordu:

public async Task<ActionResult> DeleteConfirmed(int id)

Ten parametr został zmieniony na Department wystąpienie jednostki utworzone przez powiązanie modelu. Zapewnia to dostęp do RowVersion wartości właściwości oprócz klucza rekordu.

public async Task<ActionResult> Delete(Department department)

Zmieniono również nazwę metody akcji z DeleteConfirmed na Delete. Kod szkieletowy o nazwie HttpPostDelete metoda DeleteConfirmed w celu nadania HttpPost metodzie unikatowego podpisu. ( ClR wymaga przeciążonych metod, aby miały 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 zostanie przechwycony błąd współbieżności, kod redisplays strony potwierdzenia Usuń i zawiera flagę wskazującą, że powinna wyświetlić komunikat o błędzie współbieżności.

W pliku Views\Department\Delete.cshtml zastąp kod szkieletowy następującym kodem, który dodaje pole komunikatu o błędzie i ukryte pola właściwości DepartmentID i RowVersion. Zmiany są wyróżnione.

@model ContosoUniversity.Models.Department

@{
    ViewBag.Title = "Delete";
}

<h2>Delete</h2>

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

<h3>Are you sure you want to delete this?</h3>
<div>
    <h4>Department</h4>
    <hr />
    <dl class="dl-horizontal">
        <dt>
            Administrator
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Administrator.FullName)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.Name)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Name)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.Budget)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.Budget)
        </dd>

        <dt>
            @Html.DisplayNameFor(model => model.StartDate)
        </dt>

        <dd>
            @Html.DisplayFor(model => model.StartDate)
        </dd>

    </dl>

    @using (Html.BeginForm()) {
        @Html.AntiForgeryToken()
        @Html.HiddenFor(model => model.DepartmentID)
        @Html.HiddenFor(model => model.RowVersion)

        <div class="form-actions no-color">
            <input type="submit" value="Delete" class="btn btn-default" /> |
            @Html.ActionLink("Back to List", "Index")
        </div>
    }
</div>

Ten kod dodaje komunikat o błędzie między h2 nagłówkami i h3 :

<p class="error">@ViewBag.ConcurrencyErrorMessage</p>

Zastępuje LastName element w FullNameAdministrator polu:

<dt>
  Administrator
</dt>
<dd>
  @Html.DisplayFor(model => model.Administrator.FullName)
</dd>

Na koniec dodaje ukryte pola dla DepartmentID właściwości i RowVersion po instrukcji Html.BeginForm :

@Html.HiddenFor(model => model.DepartmentID)
@Html.HiddenFor(model => model.RowVersion)

Uruchom stronę Indeks działów. Kliknij prawym przyciskiem myszy hiperłącze Usuń dla działu angielskiego i wybierz pozycję Otwórz na nowej karcie, a następnie na pierwszej karcie kliknij hiperlink Edytuj dla działu angielskiego.

W pierwszym oknie zmień jedną z wartości, a następnie kliknij przycisk Zapisz.

Strona Indeks potwierdza zmianę.

Na drugiej karcie kliknij pozycję Usuń.

Zostanie wyświetlony komunikat o błędzie współbieżności, a wartości Dział są odświeżane z aktualnie w bazie danych.

Department_Delete_confirmation_page_with_concurrency_error

Po ponownym kliknięciu przycisku Usuń nastąpi przekierowanie do strony Indeks, która pokazuje, że dział został usunięty.

Uzyskiwanie kodu

Pobieranie ukończonego projektu

Dodatkowe zasoby

Linki do innych zasobów platformy Entity Framework można znaleźć w ASP.NET Dostęp do danych — zalecane zasoby.

Aby uzyskać informacje o innych sposobach obsługi różnych scenariuszy współbieżności, zobacz Optymistyczne wzorce współbieżności i Praca z wartościami właściwości w witrynie MSDN. W następnym samouczku pokazano, jak zaimplementować dziedziczenie tabeli na hierarchię Instructor dla jednostek i Student .

Następne kroki

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

  • Informacje o konfliktach współbieżności
  • Dodano optymistyczną współbieżność
  • Zmodyfikowany kontroler działu
  • Przetestowana obsługa współbieżności
  • Zaktualizowano stronę Usuwanie

Przejdź do następnego artykułu, aby dowiedzieć się, jak zaimplementować dziedziczenie w modelu danych.