Uwaga
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
Autor : Scott Mitchell
W przypadku aplikacji internetowej, która umożliwia wielu użytkownikom edytowanie danych, istnieje ryzyko, że dwóch użytkowników może jednocześnie edytować te same dane. W tym samouczku zaimplementujemy optymistyczne sterowanie współbieżnością, aby obsłużyć to ryzyko.
Wprowadzenie
W przypadku aplikacji internetowych, które pozwalają użytkownikom tylko na wyświetlanie danych, lub tych, które mają tylko jednego użytkownika, który może modyfikować dane, nie ma ryzyka, że dwóch jednoczesnych użytkowników przypadkowo nadpisze zmiany wprowadzane przez siebie nawzajem. W przypadku aplikacji internetowych, które umożliwiają wielu użytkownikom aktualizowanie lub usuwanie danych, istnieje jednak możliwość, że modyfikacje jednego użytkownika będą zderzać się z innymi współbieżnymi użytkownikami. Przy braku polityki współbieżności, gdy dwóch użytkowników jednocześnie edytuje jeden rekord, użytkownik, który zatwierdzi swoje zmiany jako ostatni, zastąpi zmiany wprowadzone przez pierwszego użytkownika.
Załóżmy na przykład, że dwóch użytkowników, Jisun i Sam, odwiedzało stronę w naszej aplikacji, która zezwalała odwiedzającym na aktualizowanie i usuwanie produktów za pomocą kontrolki GridView. Obaj użytkownicy klikają przycisk Edytuj w GridView w tym samym czasie. Jisun zmienia nazwę produktu na "Chai Tea" i klika przycisk Aktualizuj. Ostateczny wynik to instrukcja UPDATE
wysyłana do bazy danych, która ustawia wszystkie pola z możliwością aktualizacji produktu (mimo że Jisun zaktualizował tylko jedno pole, ProductName
). Obecnie baza danych zawiera wartości "Chai Tea", kategorię Napoje, dostawcę Egzotyczne płyny itd. dla tego konkretnego produktu. Jednak widok GridView na ekranie Sam nadal wyświetla nazwę produktu w edytowalnym wierszu GridView jako "Chai". Kilka sekund po zatwierdzeniu zmian przez Jisun, Sam aktualizuje kategorię na Przyprawy i klika "Aktualizuj". To skutkuje wysłaniem instrukcji do bazy danych, która ustawia nazwę produktu na "Chai", a UPDATE
na odpowiadający identyfikator kategorii Napoje i tak dalej. Zmiany Jisun w nazwie produktu zostały nadpisane. Rysunek 1 graficznie przedstawia tę serię zdarzeń.
Rysunek 1: Gdy dwóch użytkowników jednocześnie zaktualizuje rekord, istnieje ryzyko, że zmiany jednego użytkownika zostaną nadpisane przez zmiany drugiego użytkownika (kliknij, aby wyświetlić obraz w pełnym rozmiarze)
Podobnie, gdy dwóch użytkowników odwiedza stronę, jeden użytkownik może znajdować się w trakcie aktualizowania rekordu po usunięciu go przez innego użytkownika. Lub między, gdy użytkownik ładuje stronę i po kliknięciu przycisku Usuń, inny użytkownik mógł zmodyfikować zawartość tego rekordu.
Dostępne są trzy strategie kontroli współbieżności :
- Nie rób nic -if jednoczesnych użytkowników modyfikuje ten sam rekord, niech wygra ostatnie zatwierdzenie (zachowanie domyślne)
- Optymistyczna współbieżność — załóżmy, że chociaż mogą występować konflikty współbieżności co jakiś czas, zdecydowana większość czasu takich konfliktów nie wystąpi; w związku z tym, jeśli wystąpi konflikt, po prostu poinformuj użytkownika, że nie można zapisać zmian, ponieważ inny użytkownik zmodyfikował te same dane
- Pesymistyczna współbieżność — zakładamy, że konflikty współbieżności są częste, a użytkownicy nie będą tolerować sytuacji, gdy dowiedzą się, że ich zmiany nie zostały zapisane z powodu współbieżnej aktywności innego użytkownika. Dlatego gdy jeden użytkownik zaczyna aktualizować rekord, należy go zablokować, aby uniemożliwić innym użytkownikom edytowanie lub usuwanie tego rekordu, dopóki użytkownik nie zatwierdzi swoich modyfikacji.
Wszystkie nasze samouczki do tej pory używały domyślnej strategii rozwiązania współbieżności — czyli zastosowaliśmy zasadę, że ostatni zapis jest nadrzędny. W tym samouczku sprawdzimy, jak zaimplementować optymistyczną kontrolę współbieżności.
Uwaga / Notatka
W tej serii samouczków nie przyjrzymy się pesymistycznym przykładom współbieżności. Pesymistyczna współbieżność jest rzadko używana, ponieważ takie blokady, jeśli nie zostały prawidłowo wycofane, mogą uniemożliwić innym użytkownikom aktualizowanie danych. Jeśli na przykład użytkownik zablokuje rekord do edycji, a następnie opuści go dzień przed jego odblokowaniem, żaden inny użytkownik nie będzie mógł zaktualizować tego rekordu do momentu, gdy oryginalny użytkownik zwróci i ukończy jego aktualizację. W związku z tym w sytuacjach, w których jest używana pesymistyczna współbieżność, zazwyczaj występuje limit czasu, który, jeśli zostanie osiągnięty, anuluje blokadę. Witryny internetowe sprzedaży biletów, które blokują określoną lokalizację miejsc siedzących przez krótki okres, gdy użytkownik ukończy proces zamówienia, jest przykładem pesymistycznej kontroli współbieżności.
Krok 1. Sprawdzanie, jak zaimplementowano optymistyczną współbieżność
Optymistyczna kontrola współbieżności działa, upewniając się, że rekord aktualizowany lub usuwany ma te same wartości co podczas uruchamiania procesu aktualizowania lub usuwania. Na przykład po kliknięciu przycisku Edytuj w edytowalnym kontrolce GridView wartości rekordu są odczytywane z bazy danych i wyświetlane w polach TextBoxes i innych kontrolkach sieci Web. Te oryginalne wartości są zapisywane przez kontrolkę GridView. Później po wprowadzeniu zmian przez użytkownika i kliknięciu przycisku Aktualizuj oryginalne wartości oraz nowe wartości są wysyłane do warstwy logiki biznesowej, a następnie w dół do warstwy dostępu do danych. Warstwa dostępu do danych musi wydać instrukcję SQL, która zaktualizuje rekord tylko wtedy, gdy oryginalne wartości, które użytkownik zaczął edytować, są identyczne z wartościami nadal w bazie danych. Rysunek 2 przedstawia tę sekwencję zdarzeń.
Rysunek 2. Aby aktualizacja lub usunięcie powiodło się, oryginalne wartości muszą być równe bieżącym wartościom bazy danych (kliknij, aby wyświetlić obraz o pełnym rozmiarze)
Istnieją różne podejścia do implementowania optymistycznej współbieżności (zobacz Optymistyczna logika aktualizacji współbieżnościPetera A. Bromberga, aby zapoznać się z kilkoma opcjami). Zestaw danych typu ADO.NET zawiera jedną implementację, którą można skonfigurować tylko za pomocą zaznaczenia pola wyboru. Włączenie optymistycznej współbieżności dla TableAdaptera w Typed DataSet rozszerza instrukcje UPDATE
i DELETE
TableAdaptera, aby uwzględniać porównanie wszystkich oryginalnych wartości w klauzuli WHERE
. Poniższa UPDATE
instrukcja, na przykład, aktualizuje nazwę i cenę produktu tylko wtedy, gdy bieżące wartości bazy danych są równe wartościom, które zostały pierwotnie pobrane podczas aktualizacji rekordu w widoku siatki. Parametry @ProductName
i @UnitPrice
zawierają nowe wartości wprowadzone przez użytkownika, natomiast @original_ProductName
i @original_UnitPrice
zawierają wartości, które zostały pierwotnie załadowane do kontrolki GridView po kliknięciu przycisku Edytuj:
UPDATE Products SET
ProductName = @ProductName,
UnitPrice = @UnitPrice
WHERE
ProductID = @original_ProductID AND
ProductName = @original_ProductName AND
UnitPrice = @original_UnitPrice
Uwaga / Notatka
Ta UPDATE
instrukcja została uproszczona w celu zapewnienia czytelności. W praktyce sprawdzanie w klauzuli UnitPrice
byłoby bardziej złożone, ponieważ WHERE
może zawierać UnitPrice
i sprawdzanie, czy NULL
zawsze zwraca False (zamiast tego należy użyć NULL = NULL
).
Oprócz używania innej instrukcji bazowej UPDATE
, skonfigurowanie klasy TableAdapter do korzystania z optymistycznej współbieżności modyfikuje również podpis metod bezpośrednich bazy danych. Przypomnij sobie z naszego pierwszego samouczka Tworzenie warstwy dostępu do danych, że metody bazy danych bezpośrednio akceptowały listę wartości skalarnych jako parametry wejściowe, a nie silnie typizowany obiekt, taki jak DataRow lub DataTable. Z wykorzystaniem optymistycznej współbieżności, metody bazy danych Update()
i Delete()
obejmują również parametry wejściowe dla oryginalnych wartości. Ponadto kod w BLL do używania wzorca aktualizacji wsadowej (przeciążenie metody Update()
akceptujące wiersze danych DataRows i tabele DataTables, a nie wartości skalarne) powinno być zmienione.
Zamiast rozszerzać istniejące TableAdaptery w warstwie DAL do użycia optymistycznej współbieżności (co wymagałoby zmian w warstwie BLL, aby ją uwzględnić), utwórzmy nowy Typed DataSet o nazwie NorthwindOptimisticConcurrency
, do którego dodamy TableAdapter używający optymistycznej współbieżności. Następnie utworzymy klasę ProductsOptimisticConcurrencyBLL
Warstwy logiki biznesowej, która ma odpowiednie modyfikacje w celu obsługi optymistycznej współbieżności DAL. Gdy tylko położymy te podstawy, będziemy gotowi utworzyć stronę ASP.NET.
Krok 2. Tworzenie warstwy dostępu do danych, która obsługuje optymistyczną współbieżność
Aby utworzyć nowy typowy zestaw danych, kliknij prawym przyciskiem myszy DAL
folder w App_Code
folderze i dodaj nowy zestaw danych o nazwie NorthwindOptimisticConcurrency
. Jak pokazano w pierwszym samouczku, spowoduje to dodanie nowej klasy TableAdapter do zestawu danych Typed, automatycznie uruchamiając Kreatora konfiguracji tableAdapter. Na pierwszym ekranie pojawi się prośba o określenie bazy danych, z którą chcemy się połączyć — połącz się z tą samą bazą danych Northwind, używając ustawień NORTHWNDConnectionString
z Web.config
.
Rysunek 3: Połącz z tą samą bazą danych Northwind (Kliknij, aby wyświetlić obraz w pełnym rozmiarze)
Następnie zostanie wyświetlony monit o wykonywanie zapytań dotyczących danych: za pomocą instrukcji ad hoc SQL, nowej procedury składowanej lub istniejącej procedury składowanej. Ponieważ w naszym oryginalnym DAL użyliśmy zapytań ad hoc SQL, użyj tej opcji także tutaj.
Rysunek 4. Określanie danych do pobrania przy użyciu instrukcji AD Hoc SQL (kliknij, aby wyświetlić obraz pełnowymiarowy)
Na poniższym ekranie wprowadź zapytanie SQL, które ma być używane do pobierania informacji o produkcie. Użyjmy dokładnie tego samego zapytania SQL używanego dla Products
TableAdapter z oryginalnego DAL, które zwraca wszystkie Product
kolumny wraz z nazwami dostawcy i kategorii produktu.
SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
(SELECT CategoryName FROM Categories
WHERE Categories.CategoryID = Products.CategoryID)
as CategoryName,
(SELECT CompanyName FROM Suppliers
WHERE Suppliers.SupplierID = Products.SupplierID)
as SupplierName
FROM Products
Rysunek 5: Użyj tego samego zapytania SQL z Products
TableAdapter w oryginalnym DAL (kliknij, aby wyświetlić obraz pełnowymiarowy)
Przed przejściem na następny ekran kliknij przycisk Opcje zaawansowane. Aby to narzędzie TableAdapter używało optymistycznej kontroli współbieżności, po prostu zaznacz pole wyboru "Użyj optymistycznej współbieżności".
Rysunek 6. Włącz optymistyczną kontrolę współbieżności, sprawdzając pole wyboru "Użyj optymistycznej współbieżności" (kliknij, aby wyświetlić obraz o pełnym rozmiarze)
Na koniec należy wskazać, że TableAdapter powinien używać wzorców dostępu do danych, które zarówno wypełniają, jak i zwracają obiekt DataTable; należy również wskazać, że metody bezpośrednie bazy danych powinny zostać utworzone. Zmień nazwę metody dla wzorca zwracania tabeli danych z GetData na GetProducts, aby odzwierciedlić konwencje nazewnictwa używane w naszym oryginalnym DAL.
Rysunek 7: Zastosowanie przez TableAdapter wszystkich wzorców dostępu do danych (kliknij, aby wyświetlić obraz w pełnym rozmiarze)
Po ukończeniu pracy kreatora projektant zestawu danych będzie zawierać silnie typizowane Products
tabele DataTable i TableAdapter. Pośmiń chwilę na zmianę nazwy tabeli DataTable z Products
na ProductsOptimisticConcurrency
, co można zrobić, klikając prawym przyciskiem myszy pasek tytułu tabeli DataTable i wybierając polecenie Zmień nazwę z menu kontekstowego.
Rysunek 8: Dodano DataTable i TableAdapter do typizowanego zestawu danych (kliknij, aby wyświetlić obraz o pełnym rozmiarze)
Aby zobaczyć różnice między zapytaniami UPDATE
i DELETE
między TableAdapterem ProductsOptimisticConcurrency
(który używa optymistycznej współbieżności) a TableAdapterem Products (który nie używa), kliknij TableAdapter i przejdź do okna Właściwości. W podwłaściwościach właściwości DeleteCommand
i UpdateCommand
możesz zobaczyć rzeczywistą składnię SQL, która jest wysyłana do bazy danych podczas wywoływania metod aktualizacji lub usuwania w DAL. W przypadku obiektu ProductsOptimisticConcurrency
TableAdapter użyta DELETE
instrukcja to:
DELETE FROM [Products]
WHERE (([ProductID] = @Original_ProductID)
AND ([ProductName] = @Original_ProductName)
AND ((@IsNull_SupplierID = 1 AND [SupplierID] IS NULL)
OR ([SupplierID] = @Original_SupplierID))
AND ((@IsNull_CategoryID = 1 AND [CategoryID] IS NULL)
OR ([CategoryID] = @Original_CategoryID))
AND ((@IsNull_QuantityPerUnit = 1 AND [QuantityPerUnit] IS NULL)
OR ([QuantityPerUnit] = @Original_QuantityPerUnit))
AND ((@IsNull_UnitPrice = 1 AND [UnitPrice] IS NULL)
OR ([UnitPrice] = @Original_UnitPrice))
AND ((@IsNull_UnitsInStock = 1 AND [UnitsInStock] IS NULL)
OR ([UnitsInStock] = @Original_UnitsInStock))
AND ((@IsNull_UnitsOnOrder = 1 AND [UnitsOnOrder] IS NULL)
OR ([UnitsOnOrder] = @Original_UnitsOnOrder))
AND ((@IsNull_ReorderLevel = 1 AND [ReorderLevel] IS NULL)
OR ([ReorderLevel] = @Original_ReorderLevel))
AND ([Discontinued] = @Original_Discontinued))
Natomiast instrukcja dla DELETE
Product TableAdapter w naszym oryginalnym DAL jest znacznie prostsza:
DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))
Jak widać, klauzula WHERE
w instrukcji DELETE
dla TableAdapter, który używa optymistycznej współbieżności, zawiera porównanie między poszczególnymi wartościami kolumn tabeli Product
a oryginalnymi wartościami w momencie ostatniego wypełnienia kontrolki GridView (lub DetailsView lub FormView). Ponieważ wszystkie pola inne niż ProductID
, ProductName
i Discontinued
mogą mieć NULL
wartości, dodatkowe parametry i kontrole są uwzględniane w celu poprawnego porównania NULL
wartości w klauzuli WHERE
.
Nie będziemy dodawać żadnych dodatkowych tabel DataTable do optymistycznego zestawu danych z obsługą współbieżności na potrzeby tego samouczka, ponieważ nasza strona ASP.NET będzie dostarczać tylko informacje o aktualizowaniu i usuwaniu produktów. Jednak nadal musimy dodać metodę GetProductByProductID(productID)
do klasy ProductsOptimisticConcurrency
TableAdapter.
Aby to osiągnąć, kliknij prawym przyciskiem myszy pasek tytułu TableAdapter (obszar tuż nad nazwami metod Fill
i GetProducts
) i wybierz polecenie Dodaj zapytanie z menu kontekstowego. Spowoduje to uruchomienie Kreatora konfiguracji zapytań TableAdapter. Podobnie jak w przypadku początkowej konfiguracji naszego TableAdaptera, wybierz utworzenie metody GetProductByProductID(productID)
przy użyciu instrukcji SQL ad hoc (zobacz na rysunku 4).
GetProductByProductID(productID)
Ponieważ metoda zwraca informacje o konkretnym produkcie, wskaż, że to zapytanie jest typem SELECT
zapytania, który zwraca wiersze.
Rysunek 9. Oznaczanie typu zapytania jako "SELECT
zwracającego wiersze" (kliknij, aby wyświetlić obraz o pełnym rozmiarze)
Na następnym ekranie będziemy poproszeni o wprowadzenie zapytania SQL, z domyślnie wstępnie załadowanym zapytaniem TableAdapter. Rozszerz istniejące zapytanie, aby uwzględnić klauzulę WHERE ProductID = @ProductID
, jak pokazano na rysunku 10.
Rysunek 10. Dodawanie klauzuli WHERE
do wstępnie załadowanego zapytania w celu zwrócenia określonego rekordu produktu (kliknij, aby wyświetlić obraz pełnowymiarowy)
Na koniec zmień wygenerowane nazwy metod na FillByProductID
i GetProductByProductID
.
Rysunek 11. Zmiana nazwy metod na FillByProductID
i GetProductByProductID
(kliknij, aby wyświetlić obraz o pełnym rozmiarze)
Po zakończeniu pracy z tym kreatorem narzędzie TableAdapter zawiera teraz dwie metody pobierania danych: GetProducts()
, która zwraca wszystkie produkty i GetProductByProductID(productID)
, która zwraca określony produkt.
Krok 3. Tworzenie warstwy logiki biznesowej dla optymistycznej Concurrency-Enabled DAL
Nasza istniejąca ProductsBLL
klasa zawiera przykłady użycia zarówno aktualizacji wsadowej, jak i wzorców bezpośrednich bazy danych. Metoda AddProduct
oraz przeciążenia UpdateProduct
używają zarówno wzorca aktualizacji wsadowej, przekazując instancję ProductRow
do metody Update w klasie TableAdapter. Metoda DeleteProduct
, z drugiej strony, używa bezpośredniego wzorca DB, wywołując metodę TableAdaptera Delete(productID)
.
W przypadku nowego ProductsOptimisticConcurrency
TableAdapter, bezpośrednie metody DB teraz wymagają przekazania również oryginalnych wartości. Na przykład metoda Delete
oczekuje teraz dziesięciu parametrów wejściowych: oryginalnych ProductID
, ProductName
, SupplierID
, CategoryID
, QuantityPerUnit
, UnitPrice
, UnitsInStock
, UnitsOnOrder
, ReorderLevel
i Discontinued
. Używa tych dodatkowych wartości parametrów wejściowych w klauzuli WHERE
instrukcji DELETE
wysyłanej do bazy danych, usuwając tylko wtedy określony rekord, jeśli bieżące wartości bazy danych odpowiadają oryginalnym.
Chociaż sygnatura metody Update
w TableAdapter używana we wzorcu aktualizacji wsadowej nie uległa zmianie, zmienił się kod potrzebny do zarejestrowania oryginalnych i nowych wartości. W związku z tym, zamiast próbować korzystać z optymistycznej współbieżności z istniejącą klasą ProductsBLL
DAL, utwórzmy nową klasę warstwy logiki biznesowej do pracy z nowym DAL-em.
Dodaj klasę o nazwie ProductsOptimisticConcurrencyBLL
do BLL
folderu w folderze App_Code
.
Rysunek 12. Dodawanie ProductsOptimisticConcurrencyBLL
klasy do folderu BLL
Następnie dodaj następujący kod do ProductsOptimisticConcurrencyBLL
klasy:
Imports NorthwindOptimisticConcurrencyTableAdapters
<System.ComponentModel.DataObject()> _
Public Class ProductsOptimisticConcurrencyBLL
Private _productsAdapter As ProductsOptimisticConcurrencyTableAdapter = Nothing
Protected ReadOnly Property Adapter() As ProductsOptimisticConcurrencyTableAdapter
Get
If _productsAdapter Is Nothing Then
_productsAdapter = New ProductsOptimisticConcurrencyTableAdapter()
End If
Return _productsAdapter
End Get
End Property
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Select, True)> _
Public Function GetProducts() As _
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable
Return Adapter.GetProducts()
End Function
End Class
Zwróć uwagę na instrukcję using NorthwindOptimisticConcurrencyTableAdapters
powyżej początku deklaracji klasy.
NorthwindOptimisticConcurrencyTableAdapters
Przestrzeń nazw obejmuje ProductsOptimisticConcurrencyTableAdapter
klasę, która udostępnia metody warstwy dostępu do danych (DAL). Przed deklaracją klasy System.ComponentModel.DataObject
znajdziesz również atrybut, który instruuje program Visual Studio, aby dołączył tę klasę do listy rozwijanej kreatora ObjectDataSource.
Właściwość ProductsOptimisticConcurrencyBLL
Adapter
zapewnia szybki dostęp do wystąpienia klasy ProductsOptimisticConcurrencyTableAdapter
i jest zgodna ze wzorcem używanym w naszych oryginalnych klasach BLL (ProductsBLL
, CategoriesBLL
, itd.).
GetProducts()
Na koniec metoda po prostu wywołuje metodę dal GetProducts()
i zwraca ProductsOptimisticConcurrencyDataTable
obiekt wypełniony wystąpieniem ProductsOptimisticConcurrencyRow
dla każdego rekordu produktu w bazie danych.
Usuwanie produktu przy użyciu wzorca bezpośredniego bazy danych z optymistyczną współbieżnością
W przypadku używania wzorca bezpośredniego bazy danych względem dal korzystającego z optymistycznej współbieżności metody muszą zostać przekazane nowe i oryginalne wartości. W przypadku usuwania nie ma nowych wartości, więc należy przekazać tylko oryginalne wartości. W naszej usłudze BLL musimy zaakceptować wszystkie oryginalne parametry jako parametry wejściowe. Użyjmy metody DeleteProduct
, która w klasie ProductsOptimisticConcurrencyBLL
korzysta z bezpośredniej metody DB. Oznacza to, że ta metoda musi przyjąć wszystkie dziesięć pól danych produktu jako parametry wejściowe i przekazać je do DAL, zgodnie z poniższym kodem.
<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteProduct( _
ByVal original_productID As Integer, ByVal original_productName As String, _
ByVal original_supplierID As Nullable(Of Integer), _
ByVal original_categoryID As Nullable(Of Integer), _
ByVal original_quantityPerUnit As String, _
ByVal original_unitPrice As Nullable(Of Decimal), _
ByVal original_unitsInStock As Nullable(Of Short), _
ByVal original_unitsOnOrder As Nullable(Of Short), _
ByVal original_reorderLevel As Nullable(Of Short), _
ByVal original_discontinued As Boolean) _
As Boolean
Dim rowsAffected As Integer = Adapter.Delete(
original_productID, _
original_productName, _
original_supplierID, _
original_categoryID, _
original_quantityPerUnit, _
original_unitPrice, _
original_unitsInStock, _
original_unitsOnOrder, _
original_reorderLevel, _
original_discontinued)
' Return true if precisely one row was deleted, otherwise false
Return rowsAffected = 1
End Function
Jeśli oryginalne wartości — te wartości, które zostały ostatnio załadowane do kontrolki GridView (lub DetailsView lub FormView) — różnią się od wartości w bazie danych, gdy użytkownik kliknie przycisk WHERE
Usuń, klauzula nie będzie zgodna z żadnym rekordem bazy danych i nie wpłynie to na żadne rekordy. W związku z tym metoda TableAdapter Delete
zwróci wartość 0
, a metoda BLL DeleteProduct
zwróci wartość false
.
Aktualizowanie produktu przy użyciu schematu aktualizacji wsadowej z optymistyczną współbieżnością
Jak wspomniano wcześniej, metoda Update
TableAdaptera dla wzorca aktualizacji wsadowej ma ten sam podpis metody, niezależnie od tego, czy stosowana jest optymistyczna współbieżność, czy nie. Metoda Update
wymaga elementu DataRow, tablicy DataRows, tabeli DataTable lub zestawu danych Typed DataSet. Brak dodatkowych parametrów wejściowych do określania oryginalnych wartości. Jest to możliwe, ponieważ w tabeli DataTable śledzone są oryginalne i zmodyfikowane wartości jej wierszy DataRow. Gdy DAL wystawia instrukcję UPDATE
, parametry @original_ColumnName
są wypełniane oryginalnymi wartościami DataRow, podczas gdy parametry @ColumnName
są wypełniane zmodyfikowanymi wartościami DataRow.
W klasie ProductsBLL
(która używa oryginalnego, nieoptymistycznego modelu współbieżności DAL), podczas używania wzorca aktualizacji zbiorczej, aby zaktualizować informacje o produkcie, nasz kod wykonuje następującą sekwencję zdarzeń:
- Odczytaj bieżące informacje o produkcie bazy danych do wystąpienia
ProductRow
przy użyciu metody TableAdapterGetProductByProductID(productID)
. - Przypisywanie nowych wartości do
ProductRow
wystąpienia z kroku 1 - Wywołaj metodę
Update
TableAdapter, przekazując instancjęProductRow
Jednak ta sekwencja kroków nie będzie poprawnie obsługiwać optymistycznej współbieżności, ponieważ ProductRow
wypełnione w kroku 1 jest wypełniane bezpośrednio z bazy danych, co oznacza, że oryginalne wartości używane przez element DataRow to te, które obecnie istnieją w bazie danych, a nie te, które zostały powiązane z obiektem GridView na początku procesu edycji. Zamiast tego, przy korzystaniu z warstwy dostępu do danych (DAL) z włączoną obsługą optymistycznej współbieżności, musimy zmodyfikować przeciążenia metody UpdateProduct
, aby wykonać następujące kroki:
- Odczytaj bieżące informacje o produkcie bazy danych do wystąpienia
ProductsOptimisticConcurrencyRow
przy użyciu metody TableAdapterGetProductByProductID(productID)
. - Przypisz oryginalne wartości do instancji z kroku 1
- Wywołaj metodę
ProductsOptimisticConcurrencyRow
wystąpieniaAcceptChanges()
, która instruuje element DataRow, że jego bieżące wartości to "oryginalne" wartości. - Przypisywanie nowych wartości do
ProductsOptimisticConcurrencyRow
wystąpienia - Wywołaj metodę
Update
TableAdapter, przekazując instancjęProductsOptimisticConcurrencyRow
Krok 1 odczytuje wszystkie bieżące wartości bazy danych dla określonego rekordu produktu. Ten krok jest zbędny w UpdateProduct
przeciążeniu, które aktualizuje wszystkie kolumny produktu (ponieważ te wartości są zastępowane w kroku 2), ale jest niezbędny dla przeciążeń, w których jako parametry wejściowe przekazywany jest tylko podzbiór wartości kolumn. Po przypisaniu oryginalnych wartości do wystąpienia ProductsOptimisticConcurrencyRow
, wywoływana jest metoda AcceptChanges()
, która oznacza bieżące wartości DataRow jako oryginalne wartości, które mają być używane w parametrach @original_ColumnName
w instrukcji UPDATE
. Następnie nowe wartości parametrów są przypisywane do ProductsOptimisticConcurrencyRow
metody i, na koniec, Update
metoda jest wywoływana, przekazując element DataRow.
Poniższy kod przedstawia przeciążenie UpdateProduct
, które przyjmuje wszystkie pola danych produktu jako parametry wejściowe. Chociaż nie pokazano tutaj, ProductsOptimisticConcurrencyBLL
klasa dołączona do pobierania dla tego samouczka zawiera UpdateProduct
również przeciążenie, które akceptuje tylko nazwę i cenę produktu jako parametry wejściowe.
Protected Sub AssignAllProductValues( _
ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _
ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
ByVal discontinued As Boolean)
product.ProductName = productName
If Not supplierID.HasValue Then
product.SetSupplierIDNull()
Else
product.SupplierID = supplierID.Value
End If
If Not categoryID.HasValue Then
product.SetCategoryIDNull()
Else
product.CategoryID = categoryID.Value
End If
If quantityPerUnit Is Nothing Then
product.SetQuantityPerUnitNull()
Else
product.QuantityPerUnit = quantityPerUnit
End If
If Not unitPrice.HasValue Then
product.SetUnitPriceNull()
Else
product.UnitPrice = unitPrice.Value
End If
If Not unitsInStock.HasValue Then
product.SetUnitsInStockNull()
Else
product.UnitsInStock = unitsInStock.Value
End If
If Not unitsOnOrder.HasValue Then
product.SetUnitsOnOrderNull()
Else
product.UnitsOnOrder = unitsOnOrder.Value
End If
If Not reorderLevel.HasValue Then
product.SetReorderLevelNull()
Else
product.ReorderLevel = reorderLevel.Value
End If
product.Discontinued = discontinued
End Sub
<System.ComponentModel.DataObjectMethodAttribute( _
System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct(
ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
ByVal discontinued As Boolean, ByVal productID As Integer, _
_
ByVal original_productName As String, _
ByVal original_supplierID As Nullable(Of Integer), _
ByVal original_categoryID As Nullable(Of Integer), _
ByVal original_quantityPerUnit As String, _
ByVal original_unitPrice As Nullable(Of Decimal), _
ByVal original_unitsInStock As Nullable(Of Short), _
ByVal original_unitsOnOrder As Nullable(Of Short), _
ByVal original_reorderLevel As Nullable(Of Short), _
ByVal original_discontinued As Boolean, _
ByVal original_productID As Integer) _
As Boolean
'STEP 1: Read in the current database product information
Dim products As _
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _
Adapter.GetProductByProductID(original_productID)
If products.Count = 0 Then
' no matching record found, return false
Return False
End If
Dim product As _
NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = products(0)
'STEP 2: Assign the original values to the product instance
AssignAllProductValues( _
product, original_productName, original_supplierID, _
original_categoryID, original_quantityPerUnit, original_unitPrice, _
original_unitsInStock, original_unitsOnOrder, original_reorderLevel, _
original_discontinued)
'STEP 3: Accept the changes
product.AcceptChanges()
'STEP 4: Assign the new values to the product instance
AssignAllProductValues( _
product, productName, supplierID, categoryID, quantityPerUnit, unitPrice, _
unitsInStock, unitsOnOrder, reorderLevel, discontinued)
'STEP 5: Update the product record
Dim rowsAffected As Integer = Adapter.Update(product)
' Return true if precisely one row was updated, otherwise false
Return rowsAffected = 1
End Function
Krok 4. Przekazywanie oryginalnych i nowych wartości ze strony ASP.NET do metod BLL
Po zakończeniu implementacji DAL i BLL, pozostaje tylko utworzenie strony ASP.NET, która może korzystać z logiki optymistycznej współbieżności wbudowanej w system. W szczególności kontrolka Sieci Web danych (GridView, DetailsView lub FormView) musi pamiętać swoje oryginalne wartości, a obiekt ObjectDataSource musi przekazać oba zestawy wartości do warstwy logiki biznesowej. Ponadto strona ASP.NET musi być skonfigurowana tak, aby bezpiecznie obsługiwać naruszenia współbieżności.
Zacznij od otwarcia strony OptimisticConcurrency.aspx
w folderze EditInsertDelete
i dodania kontrolki GridView do projektanta, ustawiając jej właściwość ID
na ProductsGrid
. Na podstawie tagu inteligentnego gridView wybierz opcję utworzenia nowego obiektu ObjectDataSource o nazwie ProductsOptimisticConcurrencyDataSource
. Ponieważ chcemy, aby ta usługa ObjectDataSource korzystała z DAL obsługującego optymistyczną współbieżność, skonfiguruj ją tak, aby korzystała z obiektu ProductsOptimisticConcurrencyBLL
.
Rysunek 13. Korzystanie z ProductsOptimisticConcurrencyBLL
obiektu ObjectDataSource (kliknij, aby wyświetlić obraz o pełnym rozmiarze)
Wybierz metody GetProducts
, UpdateProduct
i DeleteProduct
z list rozwijanych w kreatorze. W przypadku metody UpdateProduct użyj przeciążenia, które akceptuje wszystkie pola danych produktu.
Konfigurowanie właściwości kontrolki ObjectDataSource
Po ukończeniu pracy kreatora znacznik deklaratywny ObjectDataSource powinien wyglądać następująco:
<asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
UpdateMethod="UpdateProduct">
<DeleteParameters>
<asp:Parameter Name="original_productID" Type="Int32" />
<asp:Parameter Name="original_productName" Type="String" />
<asp:Parameter Name="original_supplierID" Type="Int32" />
<asp:Parameter Name="original_categoryID" Type="Int32" />
<asp:Parameter Name="original_quantityPerUnit" Type="String" />
<asp:Parameter Name="original_unitPrice" Type="Decimal" />
<asp:Parameter Name="original_unitsInStock" Type="Int16" />
<asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
<asp:Parameter Name="original_reorderLevel" Type="Int16" />
<asp:Parameter Name="original_discontinued" Type="Boolean" />
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Name="productName" Type="String" />
<asp:Parameter Name="supplierID" Type="Int32" />
<asp:Parameter Name="categoryID" Type="Int32" />
<asp:Parameter Name="quantityPerUnit" Type="String" />
<asp:Parameter Name="unitPrice" Type="Decimal" />
<asp:Parameter Name="unitsInStock" Type="Int16" />
<asp:Parameter Name="unitsOnOrder" Type="Int16" />
<asp:Parameter Name="reorderLevel" Type="Int16" />
<asp:Parameter Name="discontinued" Type="Boolean" />
<asp:Parameter Name="productID" Type="Int32" />
<asp:Parameter Name="original_productName" Type="String" />
<asp:Parameter Name="original_supplierID" Type="Int32" />
<asp:Parameter Name="original_categoryID" Type="Int32" />
<asp:Parameter Name="original_quantityPerUnit" Type="String" />
<asp:Parameter Name="original_unitPrice" Type="Decimal" />
<asp:Parameter Name="original_unitsInStock" Type="Int16" />
<asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
<asp:Parameter Name="original_reorderLevel" Type="Int16" />
<asp:Parameter Name="original_discontinued" Type="Boolean" />
<asp:Parameter Name="original_productID" Type="Int32" />
</UpdateParameters>
</asp:ObjectDataSource>
Jak widać, kolekcja DeleteParameters
zawiera wystąpienie Parameter
dla każdego z dziesięciu parametrów wejściowych w metodzie ProductsOptimisticConcurrencyBLL
klasy DeleteProduct
.
UpdateParameters
Podobnie kolekcja zawiera Parameter
wystąpienie dla każdego z parametrów wejściowych w pliku UpdateProduct
.
W przypadku tych poprzednich samouczków, które obejmowały modyfikację danych, w tym momencie usuniemy właściwość ObjectDataSource OldValuesParameterFormatString
, ponieważ ta właściwość wskazuje, że metoda BLL oczekuje przekazania starych (lub oryginalnych) wartości, a także nowych wartości. Ponadto ta wartość właściwości wskazuje nazwy parametrów wejściowych dla oryginalnych wartości. Ponieważ przekazujemy oryginalne wartości do BLL, nie usuwaj tej właściwości.
Uwaga / Notatka
Wartość właściwości OldValuesParameterFormatString
musi być zmapowana na nazwy parametrów wejściowych w BLL, które oczekują użycia oryginalnych wartości. Ponieważ nazwaliśmy te parametry original_productName
, original_supplierID
i tak dalej, możesz pozostawić OldValuesParameterFormatString
wartość właściwości jako original_{0}
. Jeśli jednak parametry wejściowe metod BLL miały nazwy takie jak old_productName
, old_supplierID
i tak dalej, należy zaktualizować OldValuesParameterFormatString
właściwość do old_{0}
.
Istnieje jedno ostateczne ustawienie właściwości, które należy wykonać w celu poprawnego przekazania oryginalnych wartości do metod BLL przez obiekt ObjectDataSource. Obiekt ObjectDataSource ma właściwość ConflictDetection , którą można przypisać do jednej z dwóch wartości:
-
OverwriteChanges
- wartość domyślna; nie wysyła oryginalnych wartości do oryginalnych parametrów wejściowych metod BLL -
CompareAllValues
- wysyła oryginalne wartości do metod BLL; wybierz tę opcję podczas korzystania z optymistycznej współbieżności
Poświęć chwilę, aby ustawić właściwość ConflictDetection
na CompareAllValues
.
Konfigurowanie właściwości i pól kontrolki GridView
Po prawidłowym skonfigurowaniu właściwości obiektu ObjectDataSource zwróćmy uwagę na skonfigurowanie kontrolki GridView. Najpierw, ponieważ chcemy, aby kontrolka GridView obsługiwała edytowanie i usuwanie, kliknij pola wyboru Włącz edytowanie i Włącz usuwanie z tagu inteligentnego GridView. Spowoduje to dodanie pola CommandField, których ShowEditButton
i ShowDeleteButton
są ustawione na true
.
Po powiązaniu z obiektem ProductsOptimisticConcurrencyDataSource
ObjectDataSource obiekt GridView zawiera pole dla każdego pola danych produktu. Chociaż taki element GridView można edytować, doświadczenie użytkownika jest dalekie od akceptowalnego. Pola CategoryID
i SupplierID
będą renderowane jako pola tekstowe, wymagając od użytkownika wprowadzenia odpowiedniej kategorii i dostawcy jako numerów identyfikatorów. Nie będzie formatowania pól wartości liczbowych ani żadnych kontrolek sprawdzania poprawności, aby upewnić się, że nazwa produktu została podana. Upewnij się również, że cena jednostkowa, jednostki w magazynie, jednostki w zamówieniu i wartości poziomu ponownego zamówienia są zarówno odpowiednimi wartościami liczbowymi, jak i większe lub równe zero.
Jak omówiono w samouczkach Dodawanie kontrolek walidacji do interfejsów edycji i wstawiania oraz dostosowywanie interfejsu modyfikacji danych , interfejs użytkownika można dostosować, zastępując pola BoundFields wartością TemplateFields. Zmodyfikowałem ten element GridView i jego interfejs edycji w następujący sposób:
- Usunięto pola
ProductID
,SupplierName
iCategoryName
BoundFields - Przekonwertowano pole
ProductName
BoundField na pole szablonu i dodano kontrolkę RequiredFieldValidation. - Przekonwertowano pola
CategoryID
iSupplierID
z BoundFields na TemplateFields i dostosowano interfejs edycji, aby używał list rozwijanych zamiast pól tekstowych. W tych polach TemplateFieldsItemTemplates
pola danychCategoryName
iSupplierName
są wyświetlane. - Przekonwertowano kontrolki
UnitPrice
,UnitsInStock
,UnitsOnOrder
iReorderLevel
BoundFields na Pola szablonów i dodano kontrolki CompareValidator.
Ponieważ już sprawdziliśmy, jak wykonać te zadania w poprzednich samouczkach, po prostu wymienię tutaj ostateczną składnię deklaratywną i pozostawię implementację jako praktykę.
<asp:GridView ID="ProductsGrid" runat="server" AutoGenerateColumns="False"
DataKeyNames="ProductID" DataSourceID="ProductsOptimisticConcurrencyDataSource"
OnRowUpdated="ProductsGrid_RowUpdated">
<Columns>
<asp:CommandField ShowDeleteButton="True" ShowEditButton="True" />
<asp:TemplateField HeaderText="Product" SortExpression="ProductName">
<EditItemTemplate>
<asp:TextBox ID="EditProductName" runat="server"
Text='<%# Bind("ProductName") %>'></asp:TextBox>
<asp:RequiredFieldValidator ID="RequiredFieldValidator1"
ControlToValidate="EditProductName"
ErrorMessage="You must enter a product name."
runat="server">*</asp:RequiredFieldValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label1" runat="server"
Text='<%# Bind("ProductName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
<EditItemTemplate>
<asp:DropDownList ID="EditCategoryID" runat="server"
DataSourceID="CategoriesDataSource" AppendDataBoundItems="true"
DataTextField="CategoryName" DataValueField="CategoryID"
SelectedValue='<%# Bind("CategoryID") %>'>
<asp:ListItem Value=">(None)</asp:ListItem>
</asp:DropDownList><asp:ObjectDataSource ID="CategoriesDataSource"
runat="server" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetCategories" TypeName="CategoriesBLL">
</asp:ObjectDataSource>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label2" runat="server"
Text='<%# Bind("CategoryName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
<EditItemTemplate>
<asp:DropDownList ID="EditSuppliersID" runat="server"
DataSourceID="SuppliersDataSource" AppendDataBoundItems="true"
DataTextField="CompanyName" DataValueField="SupplierID"
SelectedValue='<%# Bind("SupplierID") %>'>
<asp:ListItem Value=">(None)</asp:ListItem>
</asp:DropDownList><asp:ObjectDataSource ID="SuppliersDataSource"
runat="server" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetSuppliers" TypeName="SuppliersBLL">
</asp:ObjectDataSource>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label3" runat="server"
Text='<%# Bind("SupplierName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
SortExpression="QuantityPerUnit" />
<asp:TemplateField HeaderText="Price" SortExpression="UnitPrice">
<EditItemTemplate>
<asp:TextBox ID="EditUnitPrice" runat="server"
Text='<%# Bind("UnitPrice", "{0:N2}") %>' Columns="8" />
<asp:CompareValidator ID="CompareValidator1" runat="server"
ControlToValidate="EditUnitPrice"
ErrorMessage="Unit price must be a valid currency value without the
currency symbol and must have a value greater than or equal to zero."
Operator="GreaterThanEqual" Type="Currency"
ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label4" runat="server"
Text='<%# Bind("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Units In Stock" SortExpression="UnitsInStock">
<EditItemTemplate>
<asp:TextBox ID="EditUnitsInStock" runat="server"
Text='<%# Bind("UnitsInStock") %>' Columns="6"></asp:TextBox>
<asp:CompareValidator ID="CompareValidator2" runat="server"
ControlToValidate="EditUnitsInStock"
ErrorMessage="Units in stock must be a valid number
greater than or equal to zero."
Operator="GreaterThanEqual" Type="Integer"
ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label5" runat="server"
Text='<%# Bind("UnitsInStock", "{0:N0}") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Units On Order" SortExpression="UnitsOnOrder">
<EditItemTemplate>
<asp:TextBox ID="EditUnitsOnOrder" runat="server"
Text='<%# Bind("UnitsOnOrder") %>' Columns="6"></asp:TextBox>
<asp:CompareValidator ID="CompareValidator3" runat="server"
ControlToValidate="EditUnitsOnOrder"
ErrorMessage="Units on order must be a valid numeric value
greater than or equal to zero."
Operator="GreaterThanEqual" Type="Integer"
ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label6" runat="server"
Text='<%# Bind("UnitsOnOrder", "{0:N0}") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Reorder Level" SortExpression="ReorderLevel">
<EditItemTemplate>
<asp:TextBox ID="EditReorderLevel" runat="server"
Text='<%# Bind("ReorderLevel") %>' Columns="6"></asp:TextBox>
<asp:CompareValidator ID="CompareValidator4" runat="server"
ControlToValidate="EditReorderLevel"
ErrorMessage="Reorder level must be a valid numeric value
greater than or equal to zero."
Operator="GreaterThanEqual" Type="Integer"
ValueToCompare="0">*</asp:CompareValidator>
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="Label7" runat="server"
Text='<%# Bind("ReorderLevel", "{0:N0}") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:CheckBoxField DataField="Discontinued" HeaderText="Discontinued"
SortExpression="Discontinued" />
</Columns>
</asp:GridView>
Jesteśmy bardzo blisko posiadania w pełni działającego przykładu. Istnieje jednak kilka subtelności, które pojawiają się niespodziewanie i mogą sprawić problemy. Ponadto nadal potrzebujemy interfejsu, który ostrzega użytkownika po wystąpieniu naruszenia współbieżności.
Uwaga / Notatka
Aby kontrolka sieci Web danych poprawnie przekazać oryginalne wartości do obiektu ObjectDataSource (które są następnie przekazywane do biblioteki BLL), ważne jest, aby właściwość GridView EnableViewState
została ustawiona na true
(wartość domyślna). Jeśli wyłączysz stan widoku, oryginalne wartości zostaną utracone podczas odświeżenia.
Przekazywanie poprawnych oryginalnych wartości do obiektu ObjectDataSource
Istnieje kilka problemów ze sposobem skonfigurowania kontrolki GridView. Jeśli właściwość ConflictDetection
elementu ObjectDataSource jest ustawiona na CompareAllValues
(tak jak nasza), to gdy jego metody Update()
lub Delete()
są wywoływane przez kontrolkę GridView (lub DetailsView czy FormView), ObjectDataSource próbuje skopiować oryginalne wartości z GridView do odpowiednich wystąpień Parameter
. Wróć do rysunku 2, aby zapoznać się z graficzną reprezentacją tego procesu.
W szczególności oryginalnym wartościom kontrolki GridView przypisywane są wartości w dwukierunkowych deklaracjach łączenia danych za każdym razem, gdy dane są wiązane z kontrolką GridView. W związku z tym ważne jest, aby wymagane oryginalne wartości były przechwytywane przy użyciu dwukierunkowego wiązania danych i udostępniane w formacie konwertowalnym.
Aby zobaczyć, dlaczego jest to ważne, pośmiń chwilę, aby odwiedzić naszą stronę w przeglądarce. Zgodnie z oczekiwaniami kontrolka GridView wyświetla listę każdego produktu z przyciskiem Edytuj i Usuń w kolumnie po lewej stronie.
Rysunek 14. Produkty są wyświetlane w siatce (kliknij, aby wyświetlić obraz pełnowymiarowy)
Jeśli klikniesz przycisk Usuń dla dowolnego produktu, zostanie zgłoszony wyjątek FormatException
.
Rysunek 15: Próba usunięcia dowolnego produktu powoduje FormatException
(kliknij, aby wyświetlić obraz w pełnym rozmiarze)
Element FormatException
jest generowany, gdy ObjectDataSource próbuje odczytać oryginalną wartość UnitPrice
. Ponieważ element ItemTemplate
ma UnitPrice
formatowany jako waluta (<%# Bind("UnitPrice", "{0:C}") %>
), zawiera symbol waluty, taki jak $19.95. Występuje FormatException
, gdy obiekt ObjectDataSource próbuje przekonwertować ten ciąg na decimal
. Aby obejść ten problem, mamy kilka opcji:
- Usuń formatowanie waluty z pliku
ItemTemplate
. Oznacza to, że zamiast używać<%# Bind("UnitPrice", "{0:C}") %>
, po prostu użyj<%# Bind("UnitPrice") %>
. Wadą tego jest to, że cena nie jest już sformatowana. - Wyświetl
UnitPrice
sformatowane jako walutę wItemTemplate
, ale użyj słowa kluczowegoEval
, aby to osiągnąć. Pamiętaj, żeEval
wykonuje jednokierunkowe powiązanie danych. Nadal musimy podać wartośćUnitPrice
dla oryginalnych wartości, więc nadal będziemy potrzebować instrukcji dwukierunkowego powiązania danych w elemencieItemTemplate
, ale można to umieścić w kontrolce sieciowej etykiety, której właściwośćVisible
jest ustawiona na wartośćfalse
. Możemy użyć następującego znacznika w elemencie ItemTemplate:
<ItemTemplate>
<asp:Label ID="DummyUnitPrice" runat="server"
Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label>
<asp:Label ID="Label4" runat="server"
Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
- Usuń formatowanie waluty z elementu
ItemTemplate
, używając<%# Bind("UnitPrice") %>
. W procedurze obsługi zdarzeń GridViewRowDataBound
, programowo uzyskaj dostęp do kontrolki sieciowej Label, w której wartośćUnitPrice
jest wyświetlana, i ustaw jej właściwośćText
na sformatowaną wersję. - Pozostaw
UnitPrice
w formacie waluty. W procedurze obsługi zdarzenia GridViewRowDeleting
zastąp istniejącą oryginalnąUnitPrice
wartość ($19.95) rzeczywistą wartością dziesiętną, stosującDecimal.Parse
. Zobaczyliśmy, jak wykonać coś podobnego, obsługując zdarzenia w samouczkuRowUpdating
Obsługa wyjątków BLL i DAL-Level na stronie ASP.NET.
Dla mojego przykładu zdecydowałem się na drugie podejście, dodając ukrytą kontrolkę sieci Web typu Etykieta, której właściwość Text
jest dwukierunkowo powiązana z niesformatowaną wartością UnitPrice
.
Po rozwiązaniu tego problemu spróbuj ponownie kliknąć przycisk Usuń dla dowolnego produktu. Tym razem otrzymasz InvalidOperationException
, kiedy ObjectDataSource spróbuje wywołać metodę UpdateProduct
z BLL.
Rysunek 16. Obiekt ObjectDataSource nie może odnaleźć metody z parametrami wejściowymi, które chce wysłać (kliknij, aby wyświetlić obraz o pełnym rozmiarze)
Patrząc na komunikat wyjątku, jasne jest, że ObjectDataSource chce wywołać metodę BLL DeleteProduct
z parametrami wejściowymi original_CategoryName
i original_SupplierName
. Jest to spowodowane tym, że pola ItemTemplate
i CategoryID
TemplateFields obecnie zawierają dwukierunkowe instrukcje Bind z polami danych SupplierID
i CategoryName
. Zamiast tego musimy uwzględnić Bind
instrukcje z CategoryID
i SupplierID
polami danych. W tym celu zastąp istniejące instrukcje "Bind" instrukcjami Eval
, a następnie dodaj ukryte kontrolki Etykieta, których Text
właściwości są powiązane z CategoryID
polami danych i SupplierID
przy użyciu dwukierunkowego wiązania danych, jak pokazano poniżej.
<asp:TemplateField HeaderText="Category" SortExpression="CategoryName">
<EditItemTemplate>
...
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="DummyCategoryID" runat="server"
Text='<%# Bind("CategoryID") %>' Visible="False"></asp:Label>
<asp:Label ID="Label2" runat="server"
Text='<%# Eval("CategoryName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
<asp:TemplateField HeaderText="Supplier" SortExpression="SupplierName">
<EditItemTemplate>
...
</EditItemTemplate>
<ItemTemplate>
<asp:Label ID="DummySupplierID" runat="server"
Text='<%# Bind("SupplierID") %>' Visible="False"></asp:Label>
<asp:Label ID="Label3" runat="server"
Text='<%# Eval("SupplierName") %>'></asp:Label>
</ItemTemplate>
</asp:TemplateField>
Dzięki tym zmianom możemy teraz pomyślnie usunąć i edytować informacje o produkcie! W kroku 5 przyjrzymy się, jak sprawdzić, czy wykrywane są naruszenia współbieżności. Jednak na razie spróbuj zaktualizować i usunąć kilka rekordów, aby upewnić się, że aktualizowanie i usuwanie pojedynczego użytkownika działa zgodnie z oczekiwaniami.
Krok 5. Testowanie optymistycznej obsługi współbieżności
Aby upewnić się, że naruszenia współbieżności są wykrywane (a nie prowadzą do bezwładnego nadpisania danych), musimy otworzyć dwa okna przeglądarki z tą stroną. W obu wystąpieniach przeglądarki kliknij przycisk Edytuj dla Chai. Następnie w jednej z przeglądarek zmień nazwę na "Chai Tea" i kliknij przycisk Aktualizuj. Aktualizacja powinna zakończyć się powodzeniem i zwrócić GridView do stanu wstępnej edycji z nazwą "Chai Tea" jako nową nazwą produktu.
Jednak w innym wystąpieniu okna przeglądarki nazwa produktu TextBox nadal jest wyświetlana jako "Chai". W tym drugim oknie przeglądarki zaktualizuj wartość UnitPrice
na 25.00
. Bez wsparcia dla optymistycznej współbieżności, kliknięcie aktualizacji w drugiej instancji przeglądarki zmieni nazwę produktu z powrotem na "Chai", nadpisując zmiany wprowadzone przez pierwszą instancję przeglądarki. Jeśli jednak zastosowano optymistyczną współbieżność, kliknięcie przycisku Aktualizuj w drugiej instancji przeglądarki powoduje wyjątek DBConcurrencyException.
Rysunek 17: Po wykryciu naruszenia współbieżności zgłaszany jest wyjątek (DBConcurrencyException
)
Parametr DBConcurrencyException
jest wyrzucany tylko wtedy, gdy wzorzec aktualizacji wsadowej DAL jest używany. Bezpośredni wzorzec DB nie generuje wyjątku, wskazuje jedynie, że żadna linia nie została zmodyfikowana. Aby to zilustrować, przywróć GridView w obu wystąpieniach przeglądarki do stanu przed edycją. Następnie w pierwszym wystąpieniu przeglądarki kliknij przycisk Edytuj i zmień nazwę produktu z "Chai Tea" z powrotem na "Chai" i kliknij przycisk Aktualizuj. W drugim oknie przeglądarki kliknij przycisk Usuń dla Chai.
Po kliknięciu przycisku Usuń, strona przesyła dane z powrotem, kontrolka GridView wywołuje metodę Delete()
elementu ObjectDataSource, a ObjectDataSource wywołuje metodę ProductsOptimisticConcurrencyBLL
w klasie DeleteProduct
, przekazując wartości oryginalne. Oryginalna ProductName
wartość dla drugiego okna przeglądarki to "Chai Tea", która nie pasuje do bieżącej ProductName
wartości w bazie danych. W związku z tym instrukcja DELETE
wydana dla bazy danych nie wpływa na żadne wiersze, ponieważ w bazie danych nie ma rekordu spełniającego klauzulę WHERE
. Metoda DeleteProduct
zwraca wartość false
, a dane obiektu ObjectDataSource są przywracane do kontrolki GridView.
Z perspektywy użytkownika końcowego, kliknięcie przycisku Usuń dla Chai Tea w drugim oknie przeglądarki spowodowało, że ekran mignął i po powrocie produkt nadal tam jest, choć teraz widnieje jako "Chai" (zmiana nazwy produktu wprowadzona przez pierwsze wystąpienie przeglądarki). Jeśli użytkownik kliknie ponownie przycisk Usuń, funkcja Usuń zakończy się pomyślnie, ponieważ oryginalna ProductName
wartość kontrolki GridView ("Chai") jest teraz zgodna z wartością w bazie danych.
W obu tych przypadkach środowisko użytkownika jest dalekie od idealnego. Wyraźnie nie chcemy pokazywać użytkownikowi szczegółowych informacji o wyjątku DBConcurrencyException
podczas korzystania ze wsadowego wzorca aktualizacji. A zachowanie przy korzystaniu ze wzorca bezpośredniego dostępu do bazy danych jest nieco mylące, ponieważ komenda użytkownika nie powiodła się, ale nie było precyzyjnego wyjaśnienia przyczyny.
Aby rozwiązać te dwa problemy, możemy utworzyć na stronie kontrolki etykiet Web, które podają wyjaśnienie, dlaczego aktualizacja lub usunięcie nie powiodło się. W przypadku wzorca aktualizacji wsadowej możemy określić, czy DBConcurrencyException
wystąpił wyjątek w procedurze obsługi zdarzeń po poziomie usługi GridView, wyświetlając etykietę ostrzeżenia zgodnie z potrzebami. W przypadku metody bezpośredniej bazy danych możemy zbadać wartość zwracaną metody BLL (czyli true
jeśli dotyczy to jednego wiersza, false
w przeciwnym razie) i wyświetlić komunikat informacyjny zgodnie z potrzebami.
Krok 6. Dodawanie komunikatów informacyjnych i wyświetlanie ich w obliczu naruszenia współbieżności
W przypadku naruszenia współbieżności zachowanie, które miało miejsce, zależy od tego, czy użyto wsadowej aktualizacji DAL, czy bezpośredniego wzorca bazy danych. W naszym samouczku użyto obu wzorców: wzorzec aktualizacji wsadowej jest stosowany do aktualizacji, a bezpośredni wzorzec bazy danych do usuwania. Aby rozpocząć, dodajmy do naszej strony dwie kontrolki etykiety sieci Web, które wyjaśniają, że podczas próby usunięcia lub zaktualizowania danych wystąpiło naruszenie współbieżności. Ustaw właściwości kontrolki Etykieta Visible
i EnableViewState
na false
; spowoduje to ukrycie ich podczas każdej wizyty na stronie, z wyjątkiem wizyt, gdzie ich właściwość Visible
jest programowo ustawiona na true
.
<asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
EnableViewState="False" CssClass="Warning"
Text="The record you attempted to delete has been modified by another user
since you last visited this page. Your delete was cancelled to allow
you to review the other user's changes and determine if you want to
continue deleting this record." />
<asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
EnableViewState="False" CssClass="Warning"
Text="The record you attempted to update has been modified by another user
since you started the update process. Your changes have been replaced
with the current values. Please review the existing values and make
any needed changes." />
Oprócz ustawienia właściwości Visible
, EnabledViewState
i Text
, ustawiłem również właściwość CssClass
na Warning
, co powoduje, że etykiety są wyświetlane dużą, czerwoną, kursywną, pogrubioną czcionką. Ta klasa CSS Warning
została zdefiniowana i dodana do Styles.css z powrotem w samouczku Badanie zdarzeń skojarzonych z wstawianiem, aktualizowaniem i usuwaniem .
Po dodaniu tych etykiet projektant w programie Visual Studio powinien wyglądać podobnie do rysunku 18.
Rysunek 18. Do strony zostały dodane dwie kontrolki etykiety (kliknij, aby wyświetlić obraz pełnowymiarowy)
Po umieszczeniu tych kontrolek etykiety w sieci Web jesteśmy gotowi rozważyć, jak ustalić, kiedy doszło do naruszenia współbieżności, w którym momencie można ustawić odpowiednią właściwość kontrolki etykiety Visible
na true
, aby wyświetlić komunikat informacyjny.
Obsługa naruszeń współbieżności podczas aktualizowania
Najpierw przyjrzyjmy się sposobom obsługi naruszeń współbieżności podczas korzystania ze wzorca aktualizacji wsadowej. Ponieważ takie naruszenia wzorca aktualizacji wsadowej powodują zgłoszenie wyjątku DBConcurrencyException
, musimy dodać kod do naszej strony ASP.NET, aby określić, czy podczas procesu aktualizacji wystąpił wyjątek DBConcurrencyException
. Jeśli tak, powinniśmy wyświetlić użytkownikowi komunikat wyjaśniający, że zmiany nie zostały zapisane, ponieważ inny użytkownik zmodyfikował te same dane między rozpoczęciem edytowania rekordu a kliknięciem przycisku Aktualizuj.
Jak pokazano w samouczku Obsługa wyjątków BLL i DAL-Level w stronie ASP.NET, takie wyjątki można wykrywać i tłumić w obsłudze zdarzeń na poziomie kontrolki danych w sieci Web. W związku z tym musimy utworzyć procedurę obsługi dla zdarzenia RowUpdated
w GridView, która sprawdza, czy zgłoszono wyjątek DBConcurrencyException
. Ten obsługiwacz zdarzeń otrzymuje referencję do każdego wyjątku, który został zgłoszony podczas procesu aktualizacji, jak pokazano w poniższym kodzie obsługiwacza zdarzeń:
Protected Sub ProductsGrid_RowUpdated _
(ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _
Handles ProductsGrid.RowUpdated
If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then
If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then
' Display the warning message and note that the exception has
' been handled...
UpdateConflictMessage.Visible = True
e.ExceptionHandled = True
End If
End If
End Sub
W przypadku wyjątku DBConcurrencyException
program obsługi zdarzeń wyświetla kontrolkę Etykieta UpdateConflictMessage
i wskazuje, że wyjątek został obsłużony. Po wprowadzeniu tego kodu, gdy podczas aktualizowania rekordu wystąpi naruszenie współbieżności, zmiany użytkownika zostaną utracone, ponieważ zastąpiłyby modyfikacje innego użytkownika w tym samym czasie. W szczególności element GridView jest zwracany do stanu wstępnej edycji i powiązany z bieżącymi danymi bazy danych. Spowoduje to zaktualizowanie wiersza GridView przy użyciu zmian innego użytkownika, które wcześniej nie były widoczne. Ponadto kontrolka Etykieta UpdateConflictMessage
wyjaśni użytkownikowi, co się stało. Ta sekwencja zdarzeń jest szczegółowa na rysunku 19.
Rysunek 19. Aktualizacje użytkownika są tracone w obliczu naruszenia współbieżności (kliknij, aby wyświetlić obraz o pełnym rozmiarze)
Uwaga / Notatka
Alternatywnie, zamiast przywracać GridView do stanu przed edycją, możemy pozostawić element GridView w stanie edytowania, ustawiając właściwość KeepInEditMode
przekazanego obiektu GridViewUpdatedEventArgs
na true. Jeśli jednak podejmiesz takie podejście, pamiętaj, aby ponownie połączyć dane z kontrolką GridView (wywołując jej DataBind()
metodę), aby wartości innych użytkowników zostały załadowane do interfejsu edycji. Kod dostępny do pobrania z tego samouczka zawiera te dwa wiersze kodu w RowUpdated
programie obsługi zdarzeń oznaczone jako komentarz. Po prostu usuń komentarz z tych wierszy kodu, aby kontrolka GridView pozostała w trybie edycji po naruszeniu współbieżności.
Reagowanie na naruszenia współbieżności podczas usuwania
W przypadku wzorca bezpośredniego dostępu do bazy danych nie zostaje zgłoszony wyjątek w sytuacji naruszenia współbieżności. Zamiast tego instrukcja bazy danych po prostu nie ma wpływu na żadne rekordy, ponieważ klauzula WHERE nie jest zgodna z żadnym rekordem. Wszystkie metody modyfikacji danych utworzone w usłudze BLL zostały zaprojektowane tak, aby zwracały wartość logiczną wskazującą, czy dotyczyły dokładnie jednego rekordu. W związku z tym, aby określić, czy wystąpiło naruszenie współbieżności podczas usuwania rekordu, możemy zbadać wartość zwracaną metody BLL DeleteProduct
.
Wartość zwracana dla metody BLL można zbadać w programach obsługi zdarzeń po poziomie obiektu ObjectDataSource za pośrednictwem ReturnValue
właściwości ObjectDataSourceStatusEventArgs
obiektu przekazanego do procedury obsługi zdarzeń. Ponieważ interesuje nas określenie wartości zwracanej z DeleteProduct
metody, musimy utworzyć procedurę obsługi zdarzeń dla zdarzenia ObjectDataSource Deleted
. Właściwość ReturnValue
jest typu object
i może być null
, jeśli wyjątek został zgłoszony, a metoda została przerwana, zanim będzie mogła zwrócić wartość. Dlatego najpierw należy upewnić się, że właściwość ReturnValue
nie jest ani null, ani undefined oraz że jest wartością logiczną. Zakładając, że ta weryfikacja przebiegnie pomyślnie, wyświetlamy kontrolkę DeleteConflictMessage
etykieta, jeśli ReturnValue
jest false
. Można to zrobić za pomocą następującego kodu:
Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _
(ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _
Handles ProductsOptimisticConcurrencyDataSource.Deleted
If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then
Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean)
If deleteReturnValue = False Then
' No row was deleted, display the warning message
DeleteConflictMessage.Visible = True
End If
End If
End Sub
W obliczu naruszenia współbieżności żądanie usunięcia użytkownika zostanie anulowane. Element GridView jest odświeżany, pokazując zmiany, które wystąpiły dla tego rekordu między czasem załadowania strony przez użytkownika a kliknięciem przycisku Usuń. Kiedy dochodzi do takiego naruszenia, pojawia się Etykieta DeleteConflictMessage
, wyjaśniająca, co się właśnie stało (patrz Rysunek 20).
Rysunek 20: Usunięcie użytkownika jest anulowane z powodu naruszenia współbieżności (kliknij, aby wyświetlić obraz w pełnym rozmiarze)
Podsumowanie
W każdej aplikacji istnieją możliwości naruszenia współbieżności, które umożliwiają wielu równoczesnych użytkowników aktualizowanie lub usuwanie danych. Jeśli takie naruszenia nie zostaną uwzględnione, gdy dwóch użytkowników jednocześnie zaktualizuje te same dane, ten, kto dokona ostatniego zapisu, "wygrywa", nadpisując zmiany drugiego użytkownika. Alternatywnie deweloperzy mogą implementować optymistyczną lub pesymistyczną kontrolę współbieżności. Optymistyczna kontrola współbieżności zakłada, że naruszenia współbieżności są rzadkie i po prostu nie zezwalają na aktualizację lub usuwanie polecenia, które stanowiłoby naruszenie współbieżności. Pesymistyczna kontrola współbieżności zakłada, że naruszenia współbieżności są częste i po prostu odrzucanie aktualizacji lub usuwania polecenia jednego użytkownika jest nie do przyjęcia. W przypadku pesymistycznej kontroli współbieżności aktualizowanie rekordu polega na zablokowaniu go, co uniemożliwia innym użytkownikom modyfikowanie lub usuwanie rekordu podczas jego blokowania.
Typowany Zestaw Danych na platformie .NET oferuje funkcjonalność wspierającą optymistyczną kontrolę współbieżności. W szczególności instrukcje UPDATE
i DELETE
wydane dla bazy danych zawierają wszystkie kolumny tabeli, co zapewnia, że aktualizacja lub usunięcie nastąpi tylko wtedy, gdy bieżące dane rekordu będą zgodne z oryginalnymi danymi, które użytkownik miał podczas przeprowadzania aktualizacji lub usunięcia. Po skonfigurowaniu funkcji DAL do obsługi optymistycznej współbieżności należy zaktualizować metody BLL. Ponadto należy skonfigurować stronę ASP.NET, która wywołuje metodę BLL, tak aby źródło ObjectDataSource pobierało oryginalne wartości z kontrolki sieci Web danych i przekazuje je do usługi BLL.
Jak pokazano w tym samouczku, implementowanie optymistycznej kontroli współbieżności w aplikacji internetowej ASP.NET obejmuje aktualizowanie DAL i BLL oraz dodanie obsługi na stronie ASP.NET. Niezależnie od tego, czy ta dodana praca jest mądrą inwestycją w czas i nakład pracy zależy od aplikacji. Jeśli często użytkownicy współbieżni aktualizują dane lub aktualizowane dane różnią się od siebie, kontrola współbieżności nie jest kluczowym problemem. Jeśli jednak rutynowo masz wielu użytkowników na swojej stronie pracujących z tymi samymi danymi, kontrola współbieżności może pomóc zapobiec aktualizowaniu lub usuwaniu danych przez jednego użytkownika, które nieświadomie zastępują dane innych.
Szczęśliwe programowanie!
Informacje o autorze
Scott Mitchell, autor siedmiu książek ASP/ASP.NET i założyciel 4GuysFromRolla.com, współpracuje z technologiami internetowymi firmy Microsoft od 1998 roku. Scott pracuje jako niezależny konsultant, trener i pisarz. Jego najnowsza książka to Sams Teach Yourself ASP.NET 2.0 w ciągu 24 godzin. Można go uzyskać pod adresem mitchell@4GuysFromRolla.com.