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 optymistyczną kontrolę współbieżności, aby obsłużyć to ryzyko.

Wprowadzenie

W przypadku aplikacji internetowych, które zezwalają tylko użytkownikom na wyświetlanie danych, lub dla tych, którzy zawierają tylko jednego użytkownika, który może modyfikować dane, nie ma zagrożenia dla dwóch równoczesnych użytkowników przypadkowo zastępujących zmiany. W przypadku aplikacji internetowych, które umożliwiają wielu użytkownikom aktualizowanie lub usuwanie danych, istnieje jednak możliwość wystąpienia modyfikacji jednego użytkownika w celu starcia się z innymi współbieżnymi użytkownikami. Bez żadnych zasad współbieżności, gdy dwóch użytkowników jednocześnie edytuje pojedynczy rekord, użytkownik, który zatwierdza jej zmiany, ostatnio zastąpi zmiany wprowadzone przez pierwszy.

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. Oba przyciski kliknij przycisk Edytuj w elemecie GridView mniej więcej w tym samym czasie. Jisun zmienia nazwę produktu na "Chai Tea" i klika przycisk Aktualizuj. Wynikiem net jest UPDATE instrukcja, która jest wysyłana do bazy danych, która ustawia wszystkie pola z możliwością aktualizacji produktu (mimo że Jisun zaktualizował tylko jedno pole, ProductName). W tym momencie baza danych ma wartości "Chai Tea", kategorię Napoje, dostawca Egzotyczne płyny itd. dla tego konkretnego produktu. Jednak na ekranie Narzędzia GridView na ekranie Sam nadal wyświetlana jest nazwa produktu w edytowalnym wierszu GridView jako "Chai". Kilka sekund po zatwierdzeniu zmian jisun sam aktualizuje kategorię do condiments i klika pozycję Aktualizuj. UPDATE Spowoduje to wysłanie instrukcji do bazy danych, która ustawia nazwę produktu na "Chai"CategoryID, na odpowiadający identyfikator kategorii Napoje itd. Zmiany jisun w nazwie produktu zostały zastąpione. Rysunek 1 graficznie przedstawia tę serię zdarzeń.

Gdy dwóch użytkowników jednocześnie zaktualizuje rekord, istnieje możliwość zmiany jednego użytkownika w celu zastąpienia drugiego rekordu

Rysunek 1. Gdy dwóch użytkowników jednocześnie zaktualizuje rekord, istnieje możliwość zastąpienia zmian jednego użytkownika w celu zastąpienia pozostałych (kliknij, aby wyświetlić obraz pełnowymiarowy)

Podobnie, gdy dwóch użytkowników odwiedza stronę, jeden użytkownik może znajdować się w środku 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 — jeśli równoczesni użytkownicy modyfikują ten sam rekord, niech ostatnie zatwierdzenie wygra (zachowanie domyślne)
  • Optymistyczna współbieżność - załóżmy, że chociaż w tej chwili mogą występować konflikty współbieżności, 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ć ich zmian, ponieważ inny użytkownik zmodyfikował te same dane
  • Pesymistyczne współbieżność — załóżmy, że konflikty współbieżności są powszechne i że użytkownicy nie będą tolerować wprowadzania zmian, nie zostały zapisane z powodu współbieżnej aktywności innego użytkownika; w związku z tym, gdy jeden użytkownik rozpocznie aktualizowanie rekordu, zablokuj go, uniemożliwiając innym użytkownikom edytowanie lub usuwanie tego rekordu, dopóki użytkownik nie zatwierdzi ich modyfikacji

Wszystkie nasze samouczki do tej pory używały domyślnej strategii rozpoznawania współbieżności — a mianowicie pozwoliliśmy ostatniemu zapisowi wygrać. W tym samouczku sprawdzimy, jak zaimplementować optymistyczną kontrolę współbieżności.

Uwaga

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 dzień przed jego odblokowaniem, żaden inny użytkownik nie będzie mógł zaktualizować tego rekordu, dopóki oryginalny użytkownik nie zwróci i ukończy jego aktualizację. W związku z tym w sytuacjach, w których jest używana pesymistyczna współbieżność, zwykle występuje limit czasu, który w przypadku osiągnięcia tego problemu anuluje blokadę. Witryny internetowe sprzedaży biletów, które blokują konkretną lokalizację siedzenia 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 elemecie 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 element GridView. Później, gdy użytkownik wprowadza zmiany i klika przycisk 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 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 zakończyło się powodzeniem, 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 zapewnia jedną implementację, którą można skonfigurować tylko za pomocą znacznika pola wyboru. Włączenie optymistycznej współbieżności dla klasy TableAdapter w zestawie danych typed rozszerza instrukcje i TableAdapterUPDATE, aby uwzględnić porównanie wszystkich oryginalnych wartości w klauzuli WHERE .DELETE Poniższa UPDATE instrukcja aktualizuje na przykład 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 aktualizowania rekordu w elemencie GridView. 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

Ta UPDATE instrukcja została uproszczona w celu czytelności. W praktyce zaewidencjonowanie UnitPrice klauzuli WHERE byłoby bardziej zaangażowane, ponieważ UnitPrice może zawierać NULL wartości s i sprawdzać, czy NULL = NULL zawsze zwraca wartość False (zamiast tego należy użyć IS NULLmetody ).

Oprócz używania innej instrukcji bazowej UPDATE , skonfigurowanie klasy TableAdapter do korzystania z optymistycznej współbieżności modyfikuje również sygnaturę metod bezpośrednich bazy danych. Jak pamiętasz z naszego pierwszego samouczka, Tworzenie warstwy dostępu do danych, metody bezpośrednie bazy danych były tymi, które akceptują listę wartości skalarnych jako parametry wejściowe (a nie silnie typizowane wystąpienie DataRow lub DataTable). W przypadku korzystania z optymistycznej współbieżności metody bezpośrednie Update() bazy danych i Delete() zawierają również parametry wejściowe dla oryginalnych wartości. Ponadto kod w usłudze BLL do używania wzorca aktualizacji wsadowej ( Update() przeciążenia metody akceptujące wartości DataRows i DataTables zamiast wartości skalarnych) również muszą zostać zmienione.

Zamiast rozszerzać tabele TableAdapters istniejącego dal do korzystania z optymistycznej współbieżności (co wymagałoby zmiany BLL w celu dostosowania), zamiast tego utwórzmy nowy zestaw danych typu o nazwie NorthwindOptimisticConcurrency, do którego dodamy tabelę Products TableAdapter, która korzysta z optymistycznej współbieżności. Następnie utworzymy klasę ProductsOptimisticConcurrencyBLL Warstwy logiki biznesowej, która ma odpowiednie modyfikacje do obsługi optymistycznej współbieżności DAL. Po utworzeniu tej 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 nowego elementu TableAdapter do zestawu danych Typed, automatycznie uruchamiając Kreatora konfiguracji TableAdapter. Na pierwszym ekranie zostanie wyświetlony monit o określenie bazy danych do nawiązania połączenia — połącz się z tą samą bazą danych Northwind przy użyciu ustawienia z Web.config.NORTHWNDConnectionString

Nawiązywanie połączenia z tą samą bazą danych Northwind

Rysunek 3. Nawiązywanie połączenia z tą samą bazą danych Northwind (kliknij, aby wyświetlić obraz pełnowymiarowy)

Następnie zostanie wyświetlony monit o wykonywanie zapytań dotyczących danych: za pośrednictwem instrukcji ad hoc SQL, nowej procedury składowanej lub istniejącej procedury składowanej. Ponieważ w oryginalnym języku DAL użyliśmy zapytań AD hoc SQL, użyj tej opcji również tutaj.

Określanie danych do pobrania przy użyciu instrukcji AD-Hoc SQL

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 tabeli TableAdapter z oryginalnego dal, które zwraca wszystkie Product kolumny wraz z nazwami dostawców 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 tabeli ProductsAdapter w oryginalnym dal

Rysunek 5. Użyj tego samego zapytania SQL z tabeli Products TableAdapter w oryginalnym dal (kliknij, aby wyświetlić obraz pełnowymiarowy)

Przed przejściem na następny ekran kliknij przycisk Opcje zaawansowane. Aby ten element TableAdapter używał optymistycznej kontrolki współbieżności, zaznacz pole wyboru "Użyj optymistycznej współbieżności".

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

Rysunek 6. Włącz optymistyczną kontrolkę współbieżności, sprawdzając pole wyboru "Użyj optymistycznej współbieżności" (kliknij, aby wyświetlić obraz pełnowymiarowy)

Na koniec wskaż, że tableAdapter powinien używać wzorców dostępu do danych, które wypełniają tabelę DataTable i zwracają tabelę Danych; wskazuje również, że należy utworzyć metody bezpośrednie bazy danych. Zmień nazwę metody dla wzorca Return a DataTable z GetData na GetProducts, aby zdublować konwencje nazewnictwa używane w oryginalnym języku DAL.

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

Rysunek 7. Korzystanie z wszystkich wzorców dostępu do danych w programie TableAdapter (kliknij, aby wyświetlić obraz pełnowymiarowy)

Po ukończeniu pracy kreatora zestaw danych Projektant będzie zawierać silnie typizowane Products tabele danych i tableAdapter. Poświęć chwilę, aby zmienić nazwę 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.

Tabela danych i tabelaAdapter zostały dodane do typu zestawu danych

Rysunek 8. Tabela danych i TabelaAdapter zostały dodane do zestawu danych typowych (kliknij, aby wyświetlić obraz pełnowymiarowy)

Aby zobaczyć różnice między zapytaniami UPDATE i DELETE między tabelą ProductsOptimisticConcurrency TableAdapter (która używa optymistycznej współbieżności) i Produktami TableAdapter (co nie jest), kliknij tabelę TableAdapter i przejdź do okno Właściwości. DeleteCommand W podwłaściwości właściwości i UpdateCommandCommandText można zobaczyć rzeczywistą składnię SQL, która jest wysyłana do bazy danych, gdy są wywoływane metody aktualizacji lub usuwania dal. W przypadku obiektu TableAdapter użyto DELETE instrukcjiProductsOptimisticConcurrency:

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

DELETE Natomiast instrukcja Product TableAdapter w naszym oryginalnym dal jest znacznie prostsza:

DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))

Jak widać, klauzula WHERE w DELETE instrukcji tableAdapter, która używa optymistycznej współbieżności, zawiera porównanie między poszczególnymi Product istniejącymi wartościami kolumn tabeli 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 danych 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 Fill nazwami metod i GetProducts ), a następnie wybierz polecenie Dodaj zapytanie z menu kontekstowego. Spowoduje to uruchomienie Kreatora konfiguracji zapytań TableAdapter. Podobnie jak w przypadku konfiguracji początkowej tabeli TableAdapter, zdecyduj się utworzyć metodę GetProductByProductID(productID) przy użyciu instrukcji ad hoc SQL (zobacz Rysunek 4). GetProductByProductID(productID) Ponieważ metoda zwraca informacje o konkretnym produkcie, wskazuje, ż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 zostanie wyświetlony monit o użycie zapytania SQL ze 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 o pełnym rozmiarze)

Na koniec zmień wygenerowane nazwy metod na FillByProductID i GetProductByProductID.

Zmień nazwę metod na FillByProductID i GetProductByProductID

Rysunek 11. Zmiana nazwy metod na FillByProductID i GetProductByProductID (kliknij, aby wyświetlić obraz pełnowymiarowy)

Po zakończeniu pracy z tym kreatorem element 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 i UpdateProduct przeciążenia używają wzorca aktualizacji wsadowej, przekazując ProductRow wystąpienie do metody Update klasy TableAdapter. Z DeleteProduct drugiej strony metoda używa wzorca bezpośredniego bazy danych, wywołując metodę TableAdapter Delete(productID) .

W przypadku nowej ProductsOptimisticConcurrency metody TableAdapter metody bezpośrednie bazy danych wymagają teraz przekazania oryginalnych wartości. Na przykład Delete metoda oczekuje teraz dziesięciu parametrów wejściowych: oryginalnych ProductID, , SupplierIDUnitPriceCategoryIDProductNameUnitsInStockUnitsOnOrderQuantityPerUnitReorderLevel, i .Discontinued Używa tych dodatkowych wartości parametrów wejściowych w WHERE klauzuli DELETE instrukcji wysyłanej do bazy danych, usuwając tylko określony rekord, jeśli bieżące wartości bazy danych są mapowanie na oryginalne.

Chociaż sygnatura metody dla metody TableAdapter Update używana we wzorcu aktualizacji wsadowej nie uległa zmianie, 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ą ProductsBLL klasą DAL, utwórzmy nową klasę warstwy logiki biznesowej do pracy z naszym nowym dal.

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

Zanotuj instrukcję using NorthwindOptimisticConcurrencyTableAdapters powyżej początku deklaracji klasy. NorthwindOptimisticConcurrencyTableAdapters Przestrzeń nazw zawiera klasęProductsOptimisticConcurrencyTableAdapter, która udostępnia metody dal. Również przed deklaracją System.ComponentModel.DataObject klasy znajdziesz atrybut , który nakazuje programowi Visual Studio dołączenie tej klasy do listy rozwijanej kreatora ObjectDataSource.

Właściwość ProductsOptimisticConcurrencyBLL"s Adapter zapewnia szybki dostęp do wystąpienia ProductsOptimisticConcurrencyTableAdapter klasy i jest zgodna ze wzorcem używanym w naszych oryginalnych klasach BLL (ProductsBLL, CategoriesBLLitd.). GetProducts() Na koniec metoda po prostu wywołuje metodę w dół do metody 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 używającego optymistycznej współbieżności metody muszą zostać przekazane nowe i oryginalne wartości. Do usunięcia nie ma nowych wartości, więc należy przekazać tylko oryginalne wartości. W naszym BLL musimy zaakceptować wszystkie oryginalne parametry jako parametry wejściowe. Użyjmy DeleteProduct metody bezpośredniej ProductsOptimisticConcurrencyBLL bazy danych w klasie . Oznacza to, że ta metoda musi przyjmować wszystkie dziesięć pól danych produktu jako parametry wejściowe i przekazać je do dal, jak pokazano w poniższym kodzie:

<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 wzorca aktualizacji usługi Batch z optymistyczną współbieżnością

Jak wspomniano wcześniej, metoda TableAdapter Update dla wzorca aktualizacji wsadowej ma ten sam podpis metody niezależnie od tego, czy jest stosowana optymistyczna współbieżność. Update Metoda oczekuje wartości DataRow, tablicy DataRows, DataTable lub Typed DataSet. Brak dodatkowych parametrów wejściowych do określania oryginalnych wartości. Jest to możliwe, ponieważ tabela DataTable śledzi oryginalne i zmodyfikowane wartości elementów DataRow. Gdy dal wystawia instrukcję UPDATE , @original_ColumnName parametry są wypełniane oryginalnymi wartościami elementu DataRow, natomiast @ColumnName parametry są wypełniane zmodyfikowanymi wartościami elementu DataRow.

ProductsBLL W klasie (która używa oryginalnej, nie optymistycznej współbieżności DAL), w przypadku używania wzorca aktualizacji wsadowej do aktualizowania informacji o produkcie kod wykonuje następującą sekwencję zdarzeń:

  1. Odczytywanie bieżących informacji o produkcie bazy danych do ProductRow wystąpienia przy użyciu metody TableAdapter GetProductByProductID(productID)
  2. Przypisywanie nowych wartości do ProductRow wystąpienia z kroku 1
  3. Wywołaj metodę TableAdapter Update , przekazując ProductRow wystąpienie

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 w przypadku korzystania z optymistycznej funkcji DAL z włączoną współbieżnością musimy zmienić UpdateProduct przeciążenia metody, aby wykonać następujące kroki:

  1. Odczytywanie bieżących informacji o produkcie bazy danych do ProductsOptimisticConcurrencyRow wystąpienia przy użyciu metody TableAdapter GetProductByProductID(productID)
  2. Przypisywanie oryginalnychProductsOptimisticConcurrencyRow wartości do wystąpienia z kroku 1
  3. Wywołaj metodę ProductsOptimisticConcurrencyRow wystąpienia AcceptChanges() , która instruuje metodę DataRow, że jej bieżące wartości to "oryginalne"
  4. Przypisywanie nowych wartości do ProductsOptimisticConcurrencyRow wystąpienia
  5. Wywołaj metodę TableAdapter Update , przekazując ProductsOptimisticConcurrencyRow wystąpienie

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ędne dla tych przeciążeń, w których tylko podzbiór wartości kolumny są przekazywane jako parametry wejściowe. Po przypisaniu oryginalnych wartości do ProductsOptimisticConcurrencyRow wystąpienia wywoływana jest metoda , która oznacza bieżące wartości DataRow jako oryginalne wartości, AcceptChanges() które mają być używane w @original_ColumnName parametrach 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 UpdateProduct przeciążenie, które akceptuje wszystkie pola danych produktu jako parametry wejściowe. Chociaż nie pokazano tutaj, ProductsOptimisticConcurrencyBLL klasa zawarta w pobieraniu 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 ukończeniu dal i BLL wystarczy utworzyć stronę ASP.NET, która może korzystać z optymistycznej logiki współbieżności wbudowanej w system. W szczególności kontrolka internetowa 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 do bezproblemowego obsługi naruszeń współbieżności.

Zacznij od otwarcia OptimisticConcurrency.aspx strony w folderze EditInsertDelete i dodania kontrolki GridView do Projektant, ustawiając jej ID właściwość na ProductsGrid. Z poziomu tagu inteligentnego gridView wybierz opcję utworzenia nowego obiektu ObjectDataSource o nazwie ProductsOptimisticConcurrencyDataSource. Ponieważ chcemy, aby ta wartość ObjectDataSource korzystała z dal, która obsługuje optymistyczną współbieżność, skonfiguruj ją do używania ProductsOptimisticConcurrencyBLL obiektu .

Korzystanie z obiektu ObjectDataSource przy użyciu obiektu ProductsOptimisticConcurrencyBLL

Rysunek 13. Korzystanie z ProductsOptimisticConcurrencyBLL obiektu ObjectDataSource (kliknij, aby wyświetlić obraz o pełnym rozmiarze)

Wybierz metody GetProducts, UpdateProducti 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 zakoń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ć, DeleteParameters kolekcja zawiera Parameter wystąpienie dla każdego z dziesięciu parametrów wejściowych w metodzie ProductsOptimisticConcurrencyBLLDeleteProduct klasy. UpdateParameters Podobnie kolekcja zawiera Parameter wystąpienie dla każdego z parametrów wejściowych w pliku UpdateProduct.

W przypadku 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 usługi BLL, nie usuwaj tej właściwości.

Uwaga

Wartość OldValuesParameterFormatString właściwości musi być mapowania na nazwy parametrów wejściowych w BLL, które oczekują 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 mają 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 element ObjectDataSource. Właściwość 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ć ConflictDetection właściwość na CompareAllValues.

Konfigurowanie właściwości i pól kontrolki GridView

Po poprawnym skonfigurowaniu właściwości obiektu ObjectDataSource zwróćmy uwagę na konfigurowanie obiektu 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órego ShowEditButton wartości i ShowDeleteButton są ustawione na truewartość .

Po powiązaniu z obiektem ProductsOptimisticConcurrencyDataSource ObjectDataSource obiekt GridView zawiera pole dla każdego pola danych produktu. Chociaż taki element GridView można edytować, środowisko użytkownika jest niczym, ale akceptowalnym. Wartości CategoryID i SupplierID BoundFields będą renderowane jako TextBoxes, co wymaga od użytkownika wprowadzenia odpowiedniej kategorii i dostawcy jako numerów identyfikacyjnych. Nie będzie formatowania pól liczbowych i żadnych kontrolek walidacji, aby upewnić się, że nazwa produktu została podana i że cena jednostkowa, jednostki w magazynie, jednostki na zamówienie i wartości na poziomie zmiany kolejności są zarówno odpowiednimi wartościami liczbowymi, jak i są większe lub równe zero.

Jak omówiono w samouczkach Dodawanie kontrolek weryfikacji do edycji i wstawiania interfejsów oraz dostosowywanie interfejsu modyfikacji danych , interfejs użytkownika można dostosować, zastępując pola boundFields polami szablonów. Element GridView został zmodyfikowany i jego interfejs edycji w następujący sposób:

  • Usunięto pola ProductID, SupplierNamei CategoryName BoundFields
  • Przekonwertowano pole ProductName BoundField na pole szablonu i dodano kontrolkę RequiredFieldValidation.
  • Przekonwertowano CategoryID elementy i SupplierID BoundFields na TemplateFields i dostosowano interfejs edycji tak, aby używał list DropDownLists, a nie TextBoxes. W tych polach TemplateFields ItemTemplatesCategoryName zostaną wyświetlone pola danych i SupplierName .
  • Przekonwertowano kontrolki UnitPrice, , UnitsInStock, UnitsOnOrderi ReorderLevel BoundFields na TemplateFields 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 skradają się i spowodują problemy. Ponadto nadal potrzebujemy interfejsu, który ostrzega użytkownika po wystąpieniu naruszenia współbieżności.

Uwaga

Aby kontrolka sieci Web danych prawidłowo przekazała oryginalne wartości do obiektu ObjectDataSource (które są następnie przekazywane do biblioteki BLL), należy ustawić właściwość GridView EnableViewState na true wartość (wartość domyślna). Jeśli wyłączysz stan widoku, oryginalne wartości zostaną utracone podczas ogłaszania zwrotnego.

Przekazywanie poprawnych oryginalnych wartości do obiektu ObjectDataSource

Istnieje kilka problemów ze sposobem konfigurowania kontrolki GridView. Jeśli właściwość ObjectDataSource jest ustawiona na CompareAllValues (jak to jest nasza), gdy metody lub Delete() ObjectDataSource ConflictDetectionUpdate() są wywoływane przez obiekt GridView (lub DetailsView lub FormView), obiekt ObjectDataSource próbuje skopiować oryginalne wartości kontrolki GridView do odpowiednich Parameter wystąpień. Zapoznaj się z rysunkiem 2, aby uzyskać graficzną reprezentację tego procesu.

W szczególności oryginalne wartości kontrolki GridView są przypisywane do wartości w dwukierunkowych instrukcjach powiązania danych za każdym razem, gdy dane są powiązane z obiektem GridView. W związku z tym istotne jest, aby wymagane oryginalne wartości były przechwytywane za pośrednictwem dwukierunkowego powiązania danych i że są one dostarczane w formacie kabrioletu.

Aby zobaczyć, dlaczego jest to ważne, poświęć 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 lewej kolumnie.

Produkty są wyświetlane w siatce

Rysunek 14. Produkty są wyświetlane w siatce (kliknij, aby wyświetlić obraz w pełnym rozmiarze)

Kliknięcie przycisku Usuń dla dowolnego produktu FormatException spowoduje zgłoszenie.

Próba usunięcia dowolnego produktu powoduje wyjątek FormatException

Rysunek 15. Próba usunięcia jakichkolwiek wyników produktu w elemecie FormatException (kliknij, aby wyświetlić obraz pełnowymiarowy)

Obiekt FormatException jest zgłaszany, gdy obiekt ObjectDataSource próbuje odczytać wartość oryginalną UnitPrice . Ponieważ element ItemTemplate ma UnitPrice format waluty (<%# 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}") %>polecenia , po prostu użyj polecenia <%# Bind("UnitPrice") %>. Wadą tego jest to, że cena nie jest już sformatowana.
  • UnitPrice Wyświetl format jako walutę w elemecie ItemTemplate, ale użyj słowa kluczowego Eval , aby to osiągnąć. Pamiętaj, że Eval wykonuje jednokierunkowe powiązanie danych. Nadal musimy podać UnitPrice wartość oryginalnych wartości, więc nadal będziemy potrzebować dwukierunkowej instrukcji powiązania danych w elemecie ItemTemplate, ale można ją umieścić w kontrolce Sieci Web etykiety, której Visible właściwość jest ustawiona na falsewartość . 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 ItemTemplateelementu , używając polecenia <%# Bind("UnitPrice") %>. W procedurze obsługi zdarzeń obiektu GridView RowDataBound programowo uzyskaj dostęp do kontrolki Sieci Web etykiet, w której UnitPrice jest wyświetlana wartość, i ustaw jej Text właściwość na sformatowaną wersję.
  • UnitPrice Pozostaw formatowany jako waluta. W procedurze obsługi zdarzeń gridview RowDeleting zastąp istniejącą oryginalną UnitPrice wartość ($19.95) rzeczywistą wartością dziesiętną przy użyciu .Decimal.Parse Zobaczyliśmy, jak wykonać coś podobnego w procedurze obsługi zdarzeń w samouczku RowUpdatingObsługa wyjątków BLL i DAL-Level na stronie ASP.NET .

W moim przykładzie wybrano drugą metodę, dodając ukrytą kontrolkę Sieć Web Etykieta, której Text właściwość jest dwukierunkowa powiązana z niesformatowaną UnitPrice wartością.

Po rozwiązaniu tego problemu spróbuj ponownie kliknąć przycisk Usuń dla dowolnego produktu. Tym razem zobaczysz InvalidOperationException , kiedy obiekt ObjectDataSource próbuje wywołać metodę UpdateProduct 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 obiekt ObjectDataSource chce wywołać metodę BLL DeleteProduct , która zawiera original_CategoryName parametry wejściowe i original_SupplierName . Wynika to z tego, że ItemTemplate s dla pól CategoryID i SupplierID TemplateFields zawierają obecnie dwukierunkowe instrukcje Bind z polami CategoryName danych i SupplierName . Zamiast tego musimy uwzględnić Bind instrukcje z CategoryID polami danych i .SupplierID Aby to osiągnąć, 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 powią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 naruszenia współbieżności są wykrywane. Jednak na razie poświęć kilka minut, aby spróbować 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 sprawdzić, czy naruszenia współbieżności są wykrywane (zamiast w wyniku ślepego zastępowania danych), musimy otworzyć dwa okna przeglądarki na tej stronie. 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ę pomyślnie i przywrócić element GridView do stanu wstępnej edycji z wartością "Chai Tea" jako nową nazwą produktu.

Jednak w innym wystąpieniu okna przeglądarki nazwa produktu TextBox nadal jest wyświetlana jako "Chai". W drugim oknie przeglądarki zaktualizuj element UnitPrice na 25.00. Bez optymistycznej obsługi współbieżności kliknięcie aktualizacji w drugim wystąpieniu przeglądarki spowoduje zmianę nazwy produktu z powrotem na "Chai", co spowoduje zastąpienie zmian wprowadzonych przez pierwsze wystąpienie przeglądarki. Przy użyciu optymistycznej współbieżności kliknięcie przycisku Aktualizuj w drugim wystąpieniu 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 jest zgłaszany (kliknij, DBConcurrencyExceptionaby wyświetlić obraz w pełnym rozmiarze)

Parametr DBConcurrencyException jest zgłaszany tylko wtedy, gdy jest używany wzorzec aktualizacji wsadowej dal. Wzorzec bezpośredni bazy danych nie zgłasza wyjątku, ale jedynie wskazuje, że nie ma to wpływu na wiersze. Aby to zilustrować, zwróć obiekt GridView obu wystąpień 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 pozycji Chai.

Po kliknięciu przycisku Usuń obiekt GridView wywołuje metodę ObjectDataSource, a element ObjectDataSource Delete() wywołuje metodę w dół do ProductsOptimisticConcurrencyBLL metody klasy DeleteProduct , przekazując oryginalne wartości. Oryginalna ProductName wartość drugiego wystąpienia przeglądarki to "Chai Tea", która nie jest zgodna z bieżącą ProductName wartością w bazie danych. DELETE W związku z tym instrukcja wydana dla bazy danych ma wpływ na zero wierszy, ponieważ w bazie danych nie ma rekordu, który klauzula WHERE spełnia. Metoda DeleteProduct zwraca false wartość , a dane obiektu ObjectDataSource są odbicia do obiektu GridView.

Z perspektywy użytkownika końcowego, klikając przycisk Usuń dla Chai Tea w drugim oknie przeglądarki spowodował miganie ekranu i po powrocie produkt jest nadal tam, chociaż teraz jest on wymieniony jako "Chai" (zmiana nazwy produktu wprowadzonych przez pierwsze wystąpienie przeglądarki). Jeśli użytkownik kliknie ponownie przycisk Usuń, polecenie 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ółów nitty-gritty wyjątku DBConcurrencyException podczas korzystania ze wzorca aktualizacji wsadowej. A zachowanie podczas korzystania z wzorca bezpośredniego bazy danych jest nieco mylące, ponieważ polecenie użytkownika nie powiodło się, ale nie było dokładnego wskazania, dlaczego.

Aby rozwiązać te dwa problemy, możemy utworzyć kontrolki sieci Web etykiet na stronie, które zawierają 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 aktualizacji wsadowej dal, czy wzorca bezpośredniego bazy danych. W naszym samouczku używane są oba wzorce, z wzorcem aktualizacji wsadowej używanym do aktualizowania i wzorca bezpośredniego bazy danych używanego do usuwania. Aby rozpocząć, dodajmy do naszej strony dwie kontrolki sieci Web etykiet, które wyjaśniają, że wystąpiło naruszenie współbieżności podczas próby usunięcia lub zaktualizowania danych. Ustaw właściwości i kontrolki Visible Etykieta na falsewartość . Spowoduje to ukrycie ich na każdej wizycie strony z wyjątkiem tych wizyt na określonej stronie, gdzie ich Visible właściwość jest programowo ustawiona na true.EnableViewState

<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 ustawiania właściwości Visible, EnabledViewStatei Text ustawiono również CssClass właściwość na Warning, co powoduje, że etykieta ma być wyświetlana w dużej, czerwonej, kursywej, pogrubionej czcionki. 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 powinny wyglądać podobnie do rysunku 18.

Dwie kontrolki etykiet zostały dodane do strony

Rysunek 18. Dodano dwie kontrolki etykiety do strony (kliknij, aby wyświetlić obraz w pełnym rozmiarze)

Po utworzeniu tych kontrolek sieci Web etykiet możemy sprawdzić, jak określić, kiedy doszło do naruszenia współbieżności, w którym momencie można ustawić odpowiednią właściwość etykiety Visible na true, wyświetlając 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ą DBConcurrencyException zgłoszenie wyjątku, musimy dodać kod do naszej strony ASP.NET, aby określić, czy DBConcurrencyException wystąpił wyjątek podczas procesu aktualizacji. Jeśli tak, powinniśmy wyświetlić użytkownikowi komunikat wyjaśniający, że ich 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 samouczku dotyczącym strony ASP.NET , takie wyjątki można wykryć i pominąć w procedurach obsługi zdarzeń po poziomie kontrolki internetowej danych. W związku z tym musimy utworzyć procedurę obsługi zdarzeń dla zdarzenia gridView RowUpdated , które sprawdza, czy DBConcurrencyException został zgłoszony wyjątek. Ta procedura obsługi zdarzeń jest przekazywana jako odwołanie do każdego wyjątku, który został zgłoszony podczas procesu aktualizacji, jak pokazano w poniższym kodzie procedury obsługi 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 obliczu wyjątku ta procedura obsługi zdarzeń DBConcurrencyException wyświetla kontrolkę UpdateConflictMessage Etykieta i wskazuje, że wyjątek został obsłużony. Po wprowadzeniu tego kodu, gdy naruszenie współbieżności występuje podczas aktualizowania rekordu, zmiany użytkownika zostaną utracone, ponieważ zostałyby one zastąpione modyfikacjami 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 kontrolki GridView przy użyciu zmian innych użytkowników, które wcześniej nie były widoczne. Ponadto kontrolka UpdateConflictMessage Etykieta wyjaśni użytkownikowi, co właśnie się stało. Ta sekwencja zdarzeń jest szczegółowo opisana na rysunku 19.

Aktualizacje użytkownika są utracone w obliczu naruszenia współbieżności

Rysunek 19. Aktualizacje użytkownika są utracone w obliczu naruszenia współbieżności (kliknij, aby wyświetlić obraz pełnowymiarowy)

Uwaga

Alternatywnie, zamiast zwracać element GridView do stanu przed edycją, możemy pozostawić element GridView w stanie edycji, ustawiając KeepInEditMode właściwość przekazanego GridViewUpdatedEventArgs obiektu 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 tych wierszy kodu, aby element GridView pozostał w trybie edycji po naruszeniu współbieżności.

Reagowanie na naruszenia współbieżności podczas usuwania

W przypadku wzorca bezpośredniego bazy danych nie zgłoszono wyjątku w obliczu 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 ustalić, czy doszło do naruszenia 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 został zgłoszony wyjątek, a metoda została przerwana, zanim może zwrócić wartość. W związku z tym należy najpierw upewnić się, że ReturnValue właściwość nie null jest wartością logiczną i jest wartością logiczną. Przy założeniu, że to sprawdzenie przebiegnie, pokażemy kontrolkę DeleteConflictMessage Etykieta, jeśli element ReturnValue to 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. Widok 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 po kliknięciu przycisku Usuń. Gdy takie naruszenie okaże się, DeleteConflictMessage etykieta jest wyświetlana, wyjaśniając, co właśnie się stało (zobacz Rysunek 20).

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

Rysunek 20. Usunięcie użytkownika jest anulowane w obliczu naruszenia współbieżności (kliknij, aby wyświetlić obraz pełnowymiarowy)

Podsumowanie

Możliwości naruszeń współbieżności istnieją w każdej aplikacji, która umożliwia wielu współbieżnych użytkowników do aktualizowania lub usuwania danych. Jeśli takie naruszenia nie zostaną uwzględnione, gdy dwóch użytkowników jednocześnie zaktualizuje te same dane, które zostaną zapisane w ostatnim zapisie "wins", zastępując zmiany innych użytkowników. Alternatywnie deweloperzy mogą zaimplementować optymistyczną lub pesymistyczną kontrolę współbieżności. Optymistyczna kontrola współbieżności zakłada, że naruszenia współbieżności są rzadko występujące 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 jednego użytkownika polecenia nie jest dopuszczalne. W przypadku pesymistycznej kontrolki współbieżności aktualizacja rekordu obejmuje zablokowanie go, co uniemożliwia innym użytkownikom modyfikowanie lub usuwanie rekordu podczas jego blokowania.

Zestaw danych typed na platformie .NET udostępnia funkcje umożliwiające obsługę optymistycznej kontroli współbieżności. W szczególności instrukcje i DELETE wydane dla bazy danych zawierają wszystkie kolumny tabeli, zapewniając w ten sposób, że aktualizacja lub usunięcie wystąpi tylko wtedy, UPDATE gdy bieżące dane rekordu są zgodne z oryginalnymi danymi, które użytkownik miał podczas przeprowadzania aktualizacji lub usunięcia. Po skonfigurowaniu narzędzia DAL do obsługi optymistycznej współbieżności należy zaktualizować metody BLL. Ponadto strona ASP.NET wywołująca usługę BLL musi być skonfigurowana tak, aby obiekt ObjectDataSource pobierał oryginalne wartości z kontrolki sieci Web danych i przekazuje je do usługi BLL.

Jak pokazano w tym samouczku, implementowanie optymistycznej kontrolki współbieżności w aplikacji internetowej ASP.NET obejmuje aktualizowanie dal i BLL oraz dodawanie 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 dane, które aktualizują, różnią się od siebie, kontrola współbieżności nie jest kluczowym problemem. Jeśli jednak rutynowo masz wielu użytkowników w witrynie pracujących z tymi samymi danymi, kontrola współbieżności może pomóc zapobiec aktualizacjom lub usunięciom jednego użytkownika z nieświadomego zastępowania 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 do niego dotrzeć pod adresem mitchell@4GuysFromRolla.com. Lub za pośrednictwem swojego bloga, który można znaleźć na stronie http://ScottOnWriting.NET.