Udostępnij za pośrednictwem


Implementowanie optymistycznej współbieżności (VB)

Autor : Scott Mitchell

Pobierz plik PDF

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

Gdy dwóch użytkowników jednocześnie zaktualizuje rekord, istnieje ryzyko, że zmiany jednego użytkownika zostaną nadpisane przez zmiany drugiego.

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

Aby aktualizacja lub usunięcie powiodło się, oryginalne wartości muszą być równe bieżącym wartościom bazy danych

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.

Połącz się z tą samą bazą danych Northwind

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.

Określanie danych do pobrania przy użyciu instrukcji SQL ad hoc

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

Użyj tego samego zapytania SQL z TableAdapter w oryginalnym DAL

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

Włącz optymistyczną kontrolę współbieżności, sprawdzając pole wyboru

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.

Korzystanie ze wszystkich wzorców dostępu do danych za pomocą narzędzia TableAdapter

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.

Tabele DataTable i TableAdapter zostały dodane do typowanego zestawu danych

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, ProductNamei 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.

Oznacz typ zapytania jako

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.

Dodawanie klauzuli WHERE do wstępnie załadowanego zapytania w celu zwrócenia określonego rekordu produktu

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.

Zmień 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 .

Dodawanie klasy ProductsOptimisticConcurrencyBLL do folderu BLL

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ść ProductsOptimisticConcurrencyBLLAdapter 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ń:

  1. Odczytaj bieżące informacje o produkcie bazy danych do wystąpienia ProductRow przy użyciu metody TableAdapter GetProductByProductID(productID).
  2. Przypisywanie nowych wartości do ProductRow wystąpienia z kroku 1
  3. 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:

  1. Odczytaj bieżące informacje o produkcie bazy danych do wystąpienia ProductsOptimisticConcurrencyRow przy użyciu metody TableAdapter GetProductByProductID(productID).
  2. Przypisz oryginalne wartości do instancji z kroku 1
  3. Wywołaj metodę ProductsOptimisticConcurrencyRow wystąpienia AcceptChanges(), która instruuje element DataRow, że jego bieżące wartości to "oryginalne" wartości.
  4. Przypisywanie nowych wartości do ProductsOptimisticConcurrencyRow wystąpienia
  5. 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.

Spraw, by ObjectDataSource używał 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_supplierIDi 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_supplierIDi 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 i CategoryName BoundFields
  • Przekonwertowano pole ProductName BoundField na pole szablonu i dodano kontrolkę RequiredFieldValidation.
  • Przekonwertowano pola CategoryID i SupplierID z BoundFields na TemplateFields i dostosowano interfejs edycji, aby używał list rozwijanych zamiast pól tekstowych. W tych polach TemplateFields ItemTemplates pola danych CategoryName i SupplierName są wyświetlane.
  • Przekonwertowano kontrolki UnitPrice, UnitsInStock, UnitsOnOrder i ReorderLevel 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.

Produkty są wyświetlane w widoku tabeli

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.

Próba usunięcia jakiegokolwiek produktu skutkuje błędem 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ę w ItemTemplate, ale użyj słowa kluczowego Eval, aby to osiągnąć. Pamiętaj, że Eval 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 elemencie ItemTemplate, 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ń GridView RowDataBound, 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 GridView RowDeleting zastąp istniejącą oryginalną UnitPrice wartość ($19.95) rzeczywistą wartością dziesiętną, stosując Decimal.Parse. Zobaczyliśmy, jak wykonać coś podobnego, obsługując zdarzenia w samouczku RowUpdatingObsł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.

Obiekt ObjectDataSource nie może odnaleźć metody z parametrami wejściowymi, które chce wysłać

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.

Po wykryciu naruszenia współbieżności zgłaszany jest 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.

Do strony dodano dwie kontrolki etykiety

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.

Aktualizacje użytkownika są tracone w przypadku naruszenia współbieżności

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

Usunięcie użytkownika zostaje anulowane w przypadku naruszenia współbieżności

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.