Delen via


Optimistische gelijktijdigheid (VB) implementeren

door Scott Mitchell

PDF downloaden

Voor een webtoepassing waarmee meerdere gebruikers gegevens kunnen bewerken, bestaat het risico dat twee gebruikers tegelijkertijd dezelfde gegevens kunnen bewerken. In deze handleiding implementeren we optimistisch gelijktijdigheidsbeheer om dit risico aan te pakken.

Introductie

Voor webtoepassingen die gebruikers alleen toestaan om gegevens weer te geven, of voor webtoepassingen die slechts één gebruiker bevatten die gegevens kan wijzigen, is er geen bedreiging voor twee gelijktijdige gebruikers die per ongeluk de wijzigingen van elkaar overschrijven. Voor webtoepassingen waarmee meerdere gebruikers gegevens kunnen bijwerken of verwijderen, is er echter de mogelijkheid dat de wijzigingen van één gebruiker conflicteren met een andere gelijktijdige gebruiker. Als twee gebruikers tegelijkertijd één record bewerken zonder gelijktijdigheidsbeleid, overschrijft de gebruiker die haar wijzigingen het laatst doorvoert de wijzigingen die door de eerste zijn aangebracht.

Stel dat twee gebruikers, Jisun en Sam, allebei een pagina bezochten in onze toepassing waarmee bezoekers de producten konden bijwerken en verwijderen via een GridView-besturingselement. Beide klikken op de knop Bewerken in rasterweergave rond dezelfde tijd. Jisun wijzigt de productnaam in 'Chai Tea' en klikt op de knop Bijwerken. Het nettoresultaat is een UPDATE instructie die naar de database wordt verzonden, waarmee alle bijwerkbare velden van het product worden ingesteld (hoewel Jisun slechts één veld heeft bijgewerkt, ProductName). Op dit moment heeft de database de waarden 'Chai Tea', de categorie Dranken, de leverancier Exotische Vloeistoffen, enzovoort voor dit specifieke product. In rasterweergave op het scherm van Sam wordt echter nog steeds de productnaam weergegeven in de bewerkbare GridView-rij als 'Chai'. Een paar seconden nadat de wijzigingen van Jisun zijn doorgevoerd, werkt Sam de categorie bij naar Condiments en klikt u op Bijwerken. Dit resulteert in een UPDATE instructie die wordt verzonden naar de database waarmee de productnaam wordt ingesteld op 'Chai', de CategoryID bijbehorende categorie-id voor dranken, enzovoort. De wijzigingen van Jisun in de productnaam zijn overschreven. Afbeelding 1 geeft deze reeks gebeurtenissen grafisch weer.

Wanneer twee gebruikers tegelijkertijd een record bijwerken, kunnen de wijzigingen van de ene gebruiker de andere overschrijven

Afbeelding 1: Wanneer twee gebruikers tegelijk een record bijwerken, is er mogelijk dat de wijzigingen van de ene gebruiker de andere overschrijven (klik om de volledige afbeelding weer te geven)

Als twee gebruikers een pagina bezoeken, bevindt één gebruiker zich mogelijk midden in het bijwerken van een record wanneer deze door een andere gebruiker wordt verwijderd. Of tussen wanneer een gebruiker een pagina laadt en op de knop Verwijderen klikt, heeft een andere gebruiker de inhoud van die record mogelijk gewijzigd.

Er zijn drie strategieën voor gelijktijdigheidsbeheer beschikbaar:

  • Niets doen -if gelijktijdige gebruikers zijn dezelfde gegevens aan het wijzigen, laat de laatste commit winnen (het standaardgedrag)
  • Optimistische gelijktijdigheid : stel dat er nu en dan gelijktijdigheidsconflicten kunnen optreden, de overgrote meerderheid van de tijd dat dergelijke conflicten niet ontstaan; als er een conflict optreedt, informeert u de gebruiker dat de wijzigingen niet kunnen worden opgeslagen omdat een andere gebruiker dezelfde gegevens heeft gewijzigd
  • Pessimistische gelijktijdigheid : stel dat gelijktijdigheidsconflicten gebruikelijk zijn en dat gebruikers niet tolereren dat hun wijzigingen niet zijn opgeslagen vanwege de gelijktijdige activiteit van een andere gebruiker; dus wanneer een gebruiker een record gaat bijwerken, vergrendelt u deze, waardoor andere gebruikers deze record niet kunnen bewerken of verwijderen totdat de gebruiker zijn wijzigingen doorvoert

Al onze tutorialsessies hebben tot nu toe de standaardstrategie voor gelijktijdigheid gebruikt, namelijk dat we de laatste schrijfbewerking hebben laten winnen. In deze tutorial onderzoeken we hoe we optimistisch gelijktijdigheidsbeheer implementeren.

Opmerking

We zullen geen aandacht besteden aan pessimistische gelijktijdigheidsvoorbeelden in deze zelfstudieserie. Pessimistische gelijktijdigheid wordt zelden gebruikt omdat dergelijke vergrendelingen, als deze niet correct worden afgeslagen, kunnen voorkomen dat andere gebruikers gegevens bijwerken. Als een gebruiker bijvoorbeeld een record vergrendelt voor bewerken en deze vervolgens voor de dag verlaat voordat deze wordt ontgrendeld, kan geen andere gebruiker die record bijwerken totdat de oorspronkelijke gebruiker de update retourneert en voltooit. Daarom is er in situaties waarin pessimistische gelijktijdigheid wordt gebruikt doorgaans een time-out die, wanneer bereikt, de vergrendeling annuleert. Websites voor ticketverkoop, die een bepaalde zitlocatie voor korte tijd vergrendelen terwijl de gebruiker het orderproces voltooit, is een voorbeeld van pessimistisch gelijktijdigheidsbeheer.

Stap 1: Kijken naar hoe optimistische gelijktijdigheid wordt geïmplementeerd

Optimistisch gelijktijdigheidsbeheer werkt door ervoor te zorgen dat de record die wordt bijgewerkt of verwijderd, dezelfde waarden heeft als bij het bijwerken of verwijderen van het proces. Wanneer u bijvoorbeeld op de knop Bewerken in een bewerkbare GridView klikt, worden de waarden van de record gelezen uit de database en weergegeven in tekstvakken en andere webbesturingselementen. Deze oorspronkelijke waarden worden opgeslagen door de GridView. Later, nadat de gebruiker haar wijzigingen heeft aangebracht en op de knop Bijwerken klikt, worden de oorspronkelijke waarden plus de nieuwe waarden verzonden naar de bedrijfslogicalaag en vervolgens omlaag naar de Data Access-laag. De Data Access-laag moet een SQL-instructie uitgeven waarmee alleen de record wordt bijgewerkt als de oorspronkelijke waarden die de gebruiker heeft bewerkt, identiek zijn aan de waarden die zich nog in de database bevinden. In afbeelding 2 ziet u deze reeks gebeurtenissen.

De oorspronkelijke waarden moeten gelijk zijn aan de huidige databasewaarden om te kunnen worden bijgewerkt of verwijderd

Afbeelding 2: Als u wilt bijwerken of verwijderen, moeten de oorspronkelijke waarden gelijk zijn aan de huidige databasewaarden (klik om de volledige afbeelding weer te geven)

Er zijn verschillende benaderingen voor het implementeren van optimistische gelijktijdigheid (zie Peter A. Bromberg'sOptimistische logica voor gelijktijdigheid bijwerken voor een kort overzicht van een aantal opties). De ADO.NET Getypte gegevensset biedt één implementatie die kan worden geconfigureerd door simpelweg een selectievakje aan te vinken. Het inschakelen van optimistische gelijktijdigheid voor een TableAdapter in de Getypte DataSet breidt de UPDATE en DELETE instructies van de TableAdapter uit om een vergelijking van alle oorspronkelijke waarden in de WHERE clausule op te nemen. Met de volgende UPDATE instructie wordt bijvoorbeeld alleen de naam en prijs van een product bijgewerkt als de huidige databasewaarden gelijk zijn aan de waarden die oorspronkelijk zijn opgehaald bij het bijwerken van de record in GridView. De @ProductName en @UnitPrice parameters bevatten de nieuwe waarden die door de gebruiker zijn ingevoerd, terwijl @original_ProductName ze @original_UnitPrice de waarden bevatten die oorspronkelijk in GridView zijn geladen toen op de knop Bewerken werd geklikt:

UPDATE Products SET
    ProductName = @ProductName,
    UnitPrice = @UnitPrice
WHERE
    ProductID = @original_ProductID AND
    ProductName = @original_ProductName AND
    UnitPrice = @original_UnitPrice

Opmerking

Deze UPDATE instructie is vereenvoudigd voor leesbaarheid. In de praktijk zou het controleren in de UnitPrice clausule meer betrokken zijn, omdat WHEREUnitPrice kan bevatten en het controleren of NULL altijd Onwaar retourneert (in plaats daarvan moet u NULL = NULL gebruiken).

Naast het gebruik van een andere onderliggende UPDATE instructie wijzigt het configureren van een TableAdapter voor het gebruik van optimistische gelijktijdigheid ook de handtekening van de directe DB-methoden. Herinner u onze eerste tutorial, Een Data Access-laag maken. Daarbij zijn het de DB-directe methoden die een lijst met scalaire waarden als invoerparameters accepteren, in plaats van een sterk getypeerde DataRow- of DataTable-instantie. Wanneer u optimistische gelijktijdigheid gebruikt, bevatten de directe Update() database en Delete() methoden ook invoerparameters voor de oorspronkelijke waarden. Bovendien moet de code in de BLL voor het gebruik van het batch-updatepatroon (de Update() methode overbelastingen die DataRows en DataTables accepteren in plaats van scalaire waarden) ook worden gewijzigd.

In plaats van de TableAdapters van de bestaande DAL's uit te breiden voor het gebruik van optimistische gelijktijdigheid (wat nodig zou zijn om de BLL te wijzigen om ruimte te bieden), maken we in plaats daarvan een nieuwe getypte dataset met de naam NorthwindOptimisticConcurrency, waaraan we een Products TableAdapter toevoegen die gebruikmaakt van optimistische gelijktijdigheid. Hierna maken we een ProductsOptimisticConcurrencyBLL business logic layer-klasse met de juiste wijzigingen ter ondersteuning van de optimistische gelijktijdigheids-DAL. Zodra dit grondwerk is gelegd, zijn we klaar om de ASP.NET pagina te maken.

Stap 2: een Gegevenstoegangslaag maken die optimistische gelijktijdigheid ondersteunt

Als u een nieuwe getypte gegevensset wilt maken, klikt u met de rechtermuisknop op de DAL map in de App_Code map en voegt u een nieuwe DataSet toe met de naam NorthwindOptimisticConcurrency. Zoals we in de eerste zelfstudie hebben gezien, voegt u hiermee een nieuwe TableAdapter toe aan de getypte gegevensset, waardoor de wizard TableAdapter-configuratie automatisch wordt gestart. In het eerste scherm wordt u gevraagd om de database op te geven waarmee u verbinding wilt maken. Maak verbinding met dezelfde Northwind-database met behulp van de NORTHWNDConnectionString instelling van Web.config.

Verbinding maken met dezelfde Northwind-database

Afbeelding 3: Verbinding maken met dezelfde Northwind-database (klik om de volledige afbeelding weer te geven)

Vervolgens wordt u gevraagd om een query uit te voeren op de gegevens: via een ad-hoc SQL-instructie, een nieuwe opgeslagen procedure of een bestaande opgeslagen procedure. Omdat we ad-hoc SQL-query's in onze oorspronkelijke DAL hebben gebruikt, gebruikt u deze optie hier ook.

Geef de gegevens op die moeten worden opgehaald met behulp van een ad-hoc SQL-instructie

Afbeelding 4: Geef de gegevens op die moeten worden opgehaald met behulp van een ad-hoc SQL-instructie (klik om de afbeelding op volledige grootte weer te geven)

Voer in het volgende scherm de SQL-query in die moet worden gebruikt om de productgegevens op te halen. Laten we exact dezelfde SQL-query gebruiken die wordt gebruikt voor de TableAdapter uit onze oorspronkelijke DAL, die alle kolommen retourneert samen met de leveranciers- en categorienamen van het product.

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

Dezelfde SQL-query gebruiken uit de Products TableAdapter in de oorspronkelijke DAL

Afbeelding 5: Gebruik dezelfde SQL-query uit de Products TableAdapter in de oorspronkelijke DAL (klik om de afbeelding op volledige grootte weer te geven)

Klik op de knop Geavanceerde opties voordat u naar het volgende scherm gaat. Als u deze TableAdapter optimistisch gelijktijdigheidsbeheer wilt gebruiken, schakelt u het selectievakje Optimistische gelijktijdigheid gebruiken in.

Optimistisch gelijktijdigheidsbeheer inschakelen door het selectievakje Optimistische gelijktijdigheid gebruiken in te schakelen

Afbeelding 6: Optimistisch gelijktijdigheidsbeheer inschakelen door het selectievakje Optimistische gelijktijdigheid gebruiken in te schakelen (klik om de afbeelding op volledige grootte weer te geven)

Geef ten slotte aan dat de TableAdapter de gegevenstoegangspatronen moet gebruiken die zowel een gegevenstabel vullen als een gegevenstabel retourneren; geeft ook aan dat de directe db-methoden moeten worden gemaakt. Wijzig de methodenaam voor het patroon Return a DataTable van GetData in GetProducts, zodat de naamconventies die we in de oorspronkelijke DAL hebben gebruikt, worden gespiegeld.

De TableAdapter alle gegevenstoegangspatronen laten gebruiken

Afbeelding 7: Laat tableAdapter alle gegevenstoegangspatronen gebruiken (klik om de afbeelding op volledige grootte weer te geven)

Nadat de wizard is voltooid, bevat de DataSet Designer een streng getypte Products DataTable en TableAdapter. Neem even de tijd om de gegevenstabel van Products naar ProductsOptimisticConcurrency te hernoemen. Dit kunt u doen door met de rechtermuisknop op de titelbalk van de gegevenstabel te klikken en in het contextmenu voor "Naam wijzigen" te kiezen.

Een DataTable en TableAdapter zijn toegevoegd aan de getypte dataset

Afbeelding 8: Er is een gegevenstabel en TableAdapter toegevoegd aan de getypte gegevensset (klik om de afbeelding op volledige grootte weer te geven)

Om de verschillen tussen de UPDATE en DELETE query's bij de ProductsOptimisticConcurrency TableAdapter (die gebruikmaakt van optimistische gelijktijdigheid) en de Products TableAdapter (die dat niet doet) te zien, klikt u op de TableAdapter en gaat naar het venster Eigenschappen. In de subproperties van de DeleteCommand eigenschappen UpdateCommandCommandText ziet u de werkelijke SQL-syntaxis die naar de database wordt verzonden wanneer de update- of verwijdermethoden van de DAL worden aangeroepen. Voor de ProductsOptimisticConcurrency TableAdapter is de DELETE gebruikte instructie:

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

De DELETE instructie voor de Product TableAdapter in onze oorspronkelijke DAL is veel eenvoudiger:

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

Zoals u ziet, bevat de WHERE component in de DELETE instructie voor de TableAdapter die gebruikmaakt van optimistische gelijktijdigheid een vergelijking tussen de bestaande kolomwaarden van de Product tabel en de oorspronkelijke waarden op het moment dat de GridView (of DetailsView of FormView) voor het laatst is ingevuld. Omdat alle andere velden dan ProductID, ProductNameen Discontinued waarden kunnen hebbenNULL, worden aanvullende parameters en controles opgenomen om waarden in de NULL component correct te vergelijkenWHERE.

Voor deze zelfstudie voegen we geen extra DataTables toe aan de optimistische gegevensset met gelijktijdigheid, omdat onze ASP.NET-pagina alleen productgegevens bijwerkt en verwijdert. We moeten de GetProductByProductID(productID) methode echter nog steeds toevoegen aan de ProductsOptimisticConcurrency TableAdapter.

Hiervoor klikt u met de rechtermuisknop op de titelbalk van TableAdapter (het gebied rechts boven de Fill en GetProducts methodenamen) en kiest u Query toevoegen in het contextmenu. Hiermee wordt de TableAdapter Query Configuratiewizard gestart. Net als bij de eerste configuratie van TableAdapter kunt u ervoor kiezen om de GetProductByProductID(productID) methode te maken met behulp van een ad-hoc SQL-instructie (zie afbeelding 4). Omdat de GetProductByProductID(productID) methode informatie over een bepaald product retourneert, geeft u aan dat deze query een SELECT querytype is dat rijen retourneert.

Markeer het querytype als een 'SELECT die rijen retourneert'

Afbeelding 9: Het querytype markeren als een 'SELECT die rijen retourneert' (klik om de afbeelding op volledige grootte weer te geven)

In het volgende scherm wordt u gevraagd om de SQL-query te gebruiken, waarbij de standaardquery van TableAdapter vooraf is geladen. Vergroot de bestaande query om de component WHERE ProductID = @ProductIDop te nemen, zoals wordt weergegeven in afbeelding 10.

Een WHERE-component toevoegen aan de vooraf geladen query om een specifieke productrecord te retourneren

Afbeelding 10: Voeg een WHERE component toe aan de vooraf geladen query om een specifieke productrecord te retourneren (klik om de volledige afbeelding weer te geven)

Wijzig ten slotte de gegenereerde methodenamen in FillByProductID en GetProductByProductID.

Wijzig de naam van de methoden in FillByProductID en GetProductByProductID

Afbeelding 11: Wijzig de naam van de methoden in FillByProductID en GetProductByProductID (klik om de afbeelding op volledige grootte weer te geven)

Als deze wizard is voltooid, bevat de TableAdapter nu twee methoden voor het ophalen van gegevens: GetProducts(), waarmee alle producten worden geretourneerd; en GetProductByProductID(productID), waarmee het opgegeven product wordt geretourneerd.

Stap 3: Een bedrijfslogicalaag maken voor de optimistische Concurrency-Enabled DAL

Onze bestaande ProductsBLL klasse bevat voorbeelden van het gebruik van zowel de batch-update als directe DB-patronen. De AddProduct-methode en UpdateProduct-overloads maken beide gebruik van het batch-updatepatroon, waarbij een ProductRow-object wordt doorgegeven aan de updatemethode van de TableAdapter. De DeleteProduct methode gebruikt daarentegen het directe DB-patroon, waarbij de methode van TableAdapter Delete(productID) wordt aangeroepen.

Met de nieuwe ProductsOptimisticConcurrency TableAdapter moeten de directe db-methoden nu ook de oorspronkelijke waarden doorgeven. De methode verwacht nu bijvoorbeeld tien invoerparameters: de oorspronkelijke Delete, ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder en ReorderLevel. Deze aanvullende invoerparameters worden gebruikt in de WHERE component van de DELETE instructie die naar de database wordt verzonden, waarbij alleen de opgegeven record wordt verwijderd als de huidige waarden van de database zijn toegewezen aan de oorspronkelijke waarden.

Hoewel de methodehandtekening voor de methode van de TableAdapter's Update, die wordt gebruikt in het batch-update-patroon, niet is gewijzigd, heeft de code die nodig is om de oorspronkelijke en nieuwe waarden vast te leggen wel veranderd. Daarom gaan we, in plaats van de optimistische dal met gelijktijdigheid te gebruiken met onze bestaande ProductsBLL klasse, een nieuwe business logic layer-klasse maken voor het werken met onze nieuwe DAL.

Voeg een klasse toe met de naam ProductsOptimisticConcurrencyBLL aan de BLL map in de App_Code map.

De klasse ProductsOptimisticConcurrencyBLL toevoegen aan de BLL-map

Afbeelding 12: De ProductsOptimisticConcurrencyBLL klasse toevoegen aan de BLL-map

Voeg vervolgens de volgende code toe aan de ProductsOptimisticConcurrencyBLL klasse:

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

Noteer de using-instructie NorthwindOptimisticConcurrencyTableAdapters boven het begin van de klassedeclaratie. De NorthwindOptimisticConcurrencyTableAdapters naamruimte bevat de ProductsOptimisticConcurrencyTableAdapter klasse, die de methoden van de DAL biedt. Ook vóór de klassedeclaratie vindt u het System.ComponentModel.DataObject kenmerk, dat Visual Studio instrueert om deze klasse op te nemen in de vervolgkeuzelijst van de wizard ObjectDataSource.

De eigenschap ProductsOptimisticConcurrencyBLL van Adapter biedt snelle toegang tot een exemplaar van de ProductsOptimisticConcurrencyTableAdapter-klasse en volgt het patroon dat wordt gebruikt in onze oorspronkelijke BLL-klassen (ProductsBLL, CategoriesBLL enzovoort). Ten slotte roept de GetProducts() methode eenvoudig de GetProducts() methode van DAL aan en retourneert een ProductsOptimisticConcurrencyDataTable object dat is gevuld met een ProductsOptimisticConcurrencyRow instantie voor elke productvermelding in de database.

Een product verwijderen met behulp van het directe DB-patroon met optimistische gelijktijdigheid

Wanneer u het directe DB-patroon gebruikt voor een DAL dat optimistische gelijktijdigheid gebruikt, moeten de nieuwe en oorspronkelijke waarden aan de methoden worden doorgegeven. Voor het verwijderen zijn er geen nieuwe waarden, dus alleen de oorspronkelijke waarden moeten worden doorgegeven. In onze BLL moeten we alle oorspronkelijke parameters accepteren als invoerparameters. Laten we de DeleteProduct methode in de ProductsOptimisticConcurrencyBLL klasse gebruiken met de directe db-methode. Dit betekent dat deze methode alle tien productgegevensvelden als invoerparameters moet opnemen en deze moet doorgeven aan de DAL, zoals wordt weergegeven in de volgende code:

<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

Als de oorspronkelijke waarden ( die voor het laatst in de GridView (of DetailsView of FormView) zijn geladen - verschillen van de waarden in de database wanneer de gebruiker op de knop Verwijderen klikt, komt de WHERE component niet overeen met een databaserecord en worden er geen records beïnvloed. De methode Delete van de TableAdapter retourneert daarom 0 en de methode DeleteProduct van de BLL retourneert false.

Een product bijwerken met behulp van het batch-updatepatroon met optimistische gelijktijdigheid

Zoals eerder is aangegeven, heeft de methode van TableAdapter Update voor het batch-updatepatroon dezelfde methodehandtekening, ongeacht of er optimistische gelijktijdigheid wordt gebruikt. Update De methode verwacht namelijk een DataRow, een matrix van DataRows, een gegevenstabel of een getypte gegevensset. Er zijn geen aanvullende invoerparameters voor het opgeven van de oorspronkelijke waarden. Dit is mogelijk omdat de DataTable de oorspronkelijke en gewijzigde waarden voor de Bijbehorende DataRow(s) bijhoudt. Wanneer de DAL de instructie uitgeeft UPDATE , worden de @original_ColumnName parameters gevuld met de oorspronkelijke waarden van DataRow, terwijl de @ColumnName parameters worden gevuld met de gewijzigde waarden van DataRow.

In de ProductsBLL klasse (die gebruikmaakt van de oorspronkelijke, niet-optimistische gelijktijdigheids-DAL), voert de code de volgende reeks gebeurtenissen uit wanneer u het batch-updatepatroon gebruikt om productgegevens bij te werken:

  1. Lees de huidige databaseproductinformatie in een ProductRow instantie met behulp van de methode van de TableAdapter GetProductByProductID(productID)
  2. De nieuwe waarden toewijzen aan het ProductRow exemplaar vanuit stap 1
  3. Roep de methode van de TableAdapter Update aan, waarbij de instantie ProductRow wordt doorgegeven.

Deze reeks stappen biedt echter geen juiste ondersteuning voor optimistische gelijktijdigheid, omdat de ProductRow ingevulde in stap 1 rechtstreeks vanuit de database wordt ingevuld, wat betekent dat de oorspronkelijke waarden die door de DataRow worden gebruikt, de waarden zijn die momenteel aanwezig zijn in de database, en niet de waarden die aan de GridView aan het begin van het bewerkingsproces zijn gebonden. In plaats daarvan moeten we, bij het gebruik van een optimistische gelijktijdigheidsfunctie in de DAL, de UpdateProduct-methode overschrijven om de volgende stappen te volgen:

  1. Lees de huidige databaseproductinformatie in een ProductsOptimisticConcurrencyRow instantie met behulp van de methode van de TableAdapter GetProductByProductID(productID)
  2. De oorspronkelijke waarden toewijzen aan het ProductsOptimisticConcurrencyRow exemplaar uit stap 1
  3. Roep de ProductsOptimisticConcurrencyRow methode aan van het AcceptChanges() exemplaar, dat de DataRow aangeeft dat de huidige waarden de oorspronkelijke waarden zijn.
  4. Wijs de nieuwe waarden toe aan het ProductsOptimisticConcurrencyRow exemplaar
  5. Roep de methode van de TableAdapter Update aan, waarbij de instantie ProductsOptimisticConcurrencyRow wordt doorgegeven.

Stap 1 leest alle huidige databasewaarden voor de opgegeven productrecord. Deze stap is overbodig in de UpdateProduct overload die alle productkolommen bijwerkt (omdat deze waarden worden overschreven in stap 2), maar is essentieel voor die overloads waarbij slechts een subset van de kolomwaarden als invoerparameters wordt doorgegeven. Zodra de oorspronkelijke waarden zijn toegewezen aan het ProductsOptimisticConcurrencyRow exemplaar, wordt de AcceptChanges() methode aangeroepen, die de huidige DataRow-waarden markeert als de oorspronkelijke waarden die moeten worden gebruikt in de @original_ColumnName parameters in de UPDATE instructie. Vervolgens worden de nieuwe parameterwaarden toegewezen aan de ProductsOptimisticConcurrencyRow, en tot slot wordt de Update methode aangeroepen waarbij de DataRow wordt doorgegeven.

De volgende code toont de UpdateProduct overbelasting die alle productgegevensvelden accepteert als invoerparameters. Hoewel deze niet hier wordt weergegeven, bevat de ProductsOptimisticConcurrencyBLL klasse die is opgenomen in de download voor deze zelfstudie ook een UpdateProduct overbelasting die alleen de naam en prijs van het product accepteert als invoerparameters.

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

Stap 4: de oorspronkelijke en nieuwe waarden van de ASP.NET-pagina doorgeven aan de BLL-methoden

Als de DAL en BLL zijn voltooid, hoeft u alleen maar een ASP.NET pagina te maken die de optimistische gelijktijdigheidslogica kan gebruiken die is ingebouwd in het systeem. Het gegevenswebbesturingselement (gridview, DetailsView of FormView) moet met name de oorspronkelijke waarden onthouden en de ObjectDataSource moet beide sets waarden doorgeven aan de bedrijfslogicalaag. Bovendien moet de ASP.NET-pagina worden geconfigureerd om gelijktijdigheidsschendingen correct af te handelen.

Begin met het openen van de OptimisticConcurrency.aspx pagina in de EditInsertDelete map en het toevoegen van een GridView aan de ontwerper, waarbij de eigenschap wordt ingesteld ID op ProductsGrid. Kies er vanuit de infotag van GridView voor om een nieuwe ObjectDataSource te maken met de naam ProductsOptimisticConcurrencyDataSource. Omdat we willen dat deze ObjectDataSource gebruikmaakt van de DAL die optimistische gelijktijdigheid ondersteunt, configureert u deze voor het gebruik van het ProductsOptimisticConcurrencyBLL object.

Laat objectdatasource het object ProductsOptimisticConcurrencyBLL gebruiken

Afbeelding 13: Laat de ObjectDataSource het ProductsOptimisticConcurrencyBLL object gebruiken (klik om de afbeelding op volledige grootte weer te geven)

Kies de GetProductsen UpdateProductDeleteProduct methoden in de vervolgkeuzelijsten in de wizard. Gebruik voor de UpdateProduct-methode de overload die alle gegevensvelden van het product aanneemt.

Eigenschappen van het ObjectDataSource-besturingselement configureren

Nadat de wizard is voltooid, moet de declaratieve markering van ObjectDataSource er als volgt uitzien:

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

Zoals u ziet, bevat de DeleteParameters verzameling een Parameter exemplaar voor elk van de tien invoerparameters in de methode van ProductsOptimisticConcurrencyBLL de DeleteProduct klasse. Op dezelfde manier bevat de UpdateParameters verzameling een Parameter exemplaar voor elk van de invoerparameters in UpdateProduct.

Voor de vorige zelfstudies die betrekking hebben op het wijzigen van gegevens, verwijderen we de eigenschap ObjectDataSource OldValuesParameterFormatString op dit moment, omdat deze eigenschap aangeeft dat de BLL-methode verwacht dat de oude (of oorspronkelijke) waarden worden doorgegeven, evenals de nieuwe waarden. Bovendien geeft deze eigenschapswaarde de invoerparameternamen voor de oorspronkelijke waarden aan. Omdat we de oorspronkelijke waarden doorgeven aan de BLL, moet u deze eigenschap niet verwijderen.

Opmerking

De waarde van de OldValuesParameterFormatString eigenschap moet worden toegewezen aan de invoerparameternamen in de BLL die de oorspronkelijke waarden verwachten. Omdat we deze parameters original_productNamehebben genoemd, original_supplierIDenzovoort, kunt u de OldValuesParameterFormatString eigenschapswaarde laten staan als original_{0}. Als de invoerparameters van de BLL-methoden echter namen hebben zoals old_productName, old_supplierIDenzovoort, moet u de OldValuesParameterFormatString eigenschap bijwerken naar old_{0}.

Er is één laatste instelling voor de eigenschap die moet worden ingesteld om de ObjectDataSource de oorspronkelijke waarden correct door te geven aan de BLL-methoden. ObjectDataSource heeft een eigenschap ConflictDetection die kan worden toegewezen aan een van de twee waarden:

  • OverwriteChanges - de standaardwaarde; verzendt de oorspronkelijke waarden niet naar de oorspronkelijke invoerparameters van de BLL-methoden
  • CompareAllValues - verzendt de oorspronkelijke waarden naar de BLL-methoden; kies deze optie bij het gebruik van optimistische gelijktijdigheid

Neem even de tijd om de ConflictDetection eigenschap in te stellen op CompareAllValues.

Eigenschappen en velden van GridView configureren

Nu de eigenschappen van ObjectDataSource correct zijn geconfigureerd, richten we onze aandacht op het instellen van de GridView. Eerst, omdat we willen dat GridView ondersteuning biedt voor bewerken en verwijderen, klikt u op de selectievakjes Bewerken inschakelen en Verwijderen inschakelen vanuit de infotag van GridView. Hiermee wordt een CommandField toegevoegd waarvan ShowEditButton en ShowDeleteButton beide zijn ingesteld op true.

Wanneer deze is gebonden aan de ProductsOptimisticConcurrencyDataSource ObjectDataSource, bevat de GridView een veld voor elk van de gegevensvelden van het product. Hoewel een dergelijke GridView kan worden bewerkt, is de gebruikerservaring alles behalve acceptabel. De CategoryID en SupplierID BoundFields worden weergegeven als tekstvakken, waardoor de gebruiker de juiste categorie en leverancier als id-nummers moet invoeren. Er is geen opmaak voor de numerieke velden en geen validatiebesturingselementen om ervoor te zorgen dat de naam van het product is opgegeven en dat de eenheidsprijs, eenheden op voorraad, eenheden op volgorde en herordeniveauwaarden zowel de juiste numerieke waarden zijn als groter dan of gelijk aan nul.

Zoals we hebben besproken in de zelfstudie over het toevoegen van validatiebesturingselementen aan de bewerkings- en invoeginterfaces en de zelfstudie over het aanpassen van de interface voor gegevenswijziging, kan de gebruikersinterface worden aangepast door de BoundFields te vervangen door TemplateFields. Ik heb deze GridView en de bewerkingsinterface op de volgende manieren gewijzigd:

  • ProductID, SupplierName, en CategoryName BoundFields zijn verwijderd
  • ProductName Het BoundField geconverteerd naar een TemplateField en een RequiredFieldValidation-besturingselement toegevoegd.
  • De BoundFields met CategoryID en SupplierID geconverteerd naar TemplateFields en de bewerkingsinterface aangepast om keuzelijsten te gebruiken in plaats van tekstvakken. In deze TemplateFields ItemTemplatesworden de CategoryName velden en SupplierName gegevensvelden weergegeven.
  • Converteer de UnitPrice, UnitsInStock, UnitsOnOrderen ReorderLevel BoundFields naar TemplateFields en voeg CompareValidator-besturingselementen toe.

Omdat we al hebben onderzocht hoe we deze taken in eerdere zelfstudies kunnen uitvoeren, vermeld ik hier alleen de uiteindelijke declaratieve syntaxis en laat ik de implementatie als praktijk achter.

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

We zijn erg dicht bij een volledig werkend voorbeeld. Er zijn echter enkele subtiliteiten die zullen opduiken en ons problemen zullen geven. Daarnaast hebben we nog steeds een interface nodig waarmee de gebruiker wordt gewaarschuwd wanneer er een gelijktijdigheidsfout is opgetreden.

Opmerking

Om ervoor te zorgen dat een gegevenswebbesturingselement de oorspronkelijke waarden correct doorgeeft aan de ObjectDataSource (die vervolgens worden doorgegeven aan de BLL), is het essentieel dat de eigenschap van GridView EnableViewState is ingesteld true op (de standaardinstelling). Als u de weergavestatus uitschakelt, gaan de oorspronkelijke waarden verloren bij terugschrijven.

De juiste oorspronkelijke waarden doorgeven aan de ObjectDataSource

Er zijn een aantal problemen met de manier waarop de GridView is geconfigureerd. Als de eigenschap van ConflictDetection ObjectDataSource is ingesteld op CompareAllValues (zoals die van ons), wordt de oorspronkelijke waarden van De GridView (of DetailsView of FormView) van De ObjectDataSource door de ObjectDataSource Update()Delete() naar de juiste Parameter instanties gekopieerd. Raadpleeg afbeelding 2 voor een grafische weergave van dit proces.

De oorspronkelijke waarden van GridView worden met name toegewezen aan de waarden in de gegevensbindingsinstructies in twee richtingen telkens wanneer de gegevens zijn gebonden aan de GridView. Daarom is het essentieel dat de vereiste oorspronkelijke waarden allemaal worden vastgelegd via gegevensbinding in twee richtingen en dat ze worden geleverd in een converteerbare indeling.

Neem even de tijd om te zien waarom dit belangrijk is om onze pagina in een browser te bezoeken. Zoals verwacht, wordt in GridView elk product weergegeven met de knop Bewerken en Verwijderen in de meest linkse kolom.

De producten worden vermeld in een GridView

Afbeelding 14: De producten worden weergegeven in een Rasterweergave (klik om de afbeelding op volledige grootte weer te geven)

Als u op de knop Verwijderen voor een product klikt, wordt er een FormatException gegenereerd.

Proberen een product te verwijderen resulteert in een FormatException

Afbeelding 15: Proberen om alle productresultaten in een FormatException te verwijderen (klik om de volledige afbeelding weer te geven)

De FormatException waarde wordt gegenereerd wanneer de ObjectDataSource probeert de oorspronkelijke UnitPrice waarde te lezen. Omdat de ItemTemplateUnitPrice notatie is opgemaakt als een valuta (<%# Bind("UnitPrice", "{0:C}") %>), bevat het een valutasymbool, zoals $ 19,95. De FormatException fout treedt op wanneer de ObjectDataSource deze tekenreeks probeert te converteren naar een decimal. We hebben een aantal opties om dit probleem te omzeilen:

  • Verwijder de valutaopmaak uit de ItemTemplate. Dat wil zeggen, in plaats van <%# Bind("UnitPrice", "{0:C}") %>, gebruik simpelweg <%# Bind("UnitPrice") %>. Het nadeel hiervan is dat de prijs niet meer is geformatteerd.
  • Geef de UnitPrice als valuta weer in de ItemTemplate, maar gebruik het Eval trefwoord om dit te bereiken. Houd er rekening mee dat Eval eenrichtingsgegevensbinding uitvoert. We moeten nog steeds de UnitPrice waarde voor de oorspronkelijke waarden opgeven, dus we hebben nog steeds een instructie voor gegevensbinding in twee richtingen in de ItemTemplateinstructie nodig, maar dit kan worden geplaatst in een labelwebbesturingselement waarvan Visible de eigenschap is ingesteld op false. We kunnen de volgende markeringen gebruiken in de 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>
  • Verwijder de valutaopmaak uit de ItemTemplate, met behulp van <%# Bind("UnitPrice") %>. Benader in de gebeurtenis-handler van de GridView programmatisch het Label-webbesturingselement waarin de RowDataBound-waarde wordt weergegeven, en stel de eigenschap UnitPrice ervan in op de opgemaakte versie.
  • Laat de UnitPrice geformatteerd staan als een valuta. Vervang in de gebeurtenis-handler van RowDeleting GridView de bestaande oorspronkelijke UnitPrice waarde ($19,95) door een werkelijke decimale waarde met behulp van Decimal.Parse. We hebben gezien hoe u iets soortgelijks kunt bereiken in de RowUpdating eventhandler in de BLL- en DAL-Level-uitzonderingen in een ASP.NET Pagina zelfstudie.

Voor mijn voorbeeld heb ik ervoor gekozen om bij de tweede aanpak een webcontrole voor een verborgen label toe te voegen waarvan Text de eigenschap tweeweg gegevenskoppeling heeft met de niet-opgemaakte UnitPrice waarde.

Nadat u dit probleem hebt opgelost, klikt u nogmaals op de knop Verwijderen voor een product. Deze keer krijgt u een InvalidOperationException wanneer de ObjectDataSource probeert de BLL's UpdateProduct-methode aan te roepen.

De ObjectDataSource kan geen methode vinden met de invoerparameters die het wil verzenden

Afbeelding 16: De ObjectDataSource kan geen methode vinden met de invoerparameters die het wil verzenden (klik om de volledige afbeelding weer te geven)

Als u het bericht van de uitzondering bekijkt, is het duidelijk dat de ObjectDataSource een BLL-methode DeleteProduct wil aanroepen die de invoerparameters original_CategoryName en original_SupplierName bevat. Dit komt doordat de ItemTemplate s voor de CategoryID en SupplierID TemplateFields momenteel tweerichtingsbindingsinstructies bevatten met de CategoryName en SupplierName gegevensvelden. In plaats daarvan moeten we de instructies Bind toevoegen aan de CategoryID en SupplierID gegevensvelden. Hiervoor vervangt u de bestaande Bind-instructies door Eval instructies en voegt u verborgen labelbesturingselementen toe waarvan Text de eigenschappen zijn gebonden aan de CategoryID velden en SupplierID gegevensvelden met behulp van gegevensbinding in twee richtingen, zoals hieronder wordt weergegeven:

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

Met deze wijzigingen kunnen we nu productinformatie verwijderen en bewerken. In stap 5 bekijken we hoe u kunt controleren of er gelijktijdigheidsschendingen worden gedetecteerd. Neem een paar minuten de tijd om een paar gegevens bij te werken en te verwijderen, zodat je kunt verifiëren dat het bijwerken en verwijderen voor een enkele gebruiker naar verwachting werkt.

Stap 5: Het testen van de ondersteuning voor optimistische gelijktijdigheid

Om te controleren of er gelijktijdigheidsschendingen worden gedetecteerd (in plaats van dat gegevens blind worden overschreven), moeten we twee browservensters naar deze pagina openen. Klik in beide browserexemplaren op de knop Bewerken voor Chai. Wijzig vervolgens in slechts één van de browsers de naam in 'Chai Tea' en klik op Bijwerken. De update moet slagen en de GridView terugbrengen naar de staat van vóór de bewerking, met "Chai Tea" als de nieuwe productnaam.

In het andere browservensterexemplaren wordt in het tekstvak productnaam echter nog steeds 'Chai' weergegeven. Werk in dit tweede browservenster de UnitPrice optie bij naar 25.00. Zonder ondersteuning voor optimistische gelijktijdigheid zou het klikken op Update in de tweede browserinstantie de productnaam weer wijzigen in 'Chai', waardoor de wijzigingen van de eerste browserinstantie worden overschreven. Als optimistische gelijktijdigheid wordt gebruikt, klikt u echter op de knop Bijwerken in het tweede browserexemplaar en leidt dit tot een DBConcurrencyException.

Wanneer een schending van gelijktijdigheid wordt gedetecteerd, wordt een DBConcurrencyException gegenereerd

Afbeelding 17: Wanneer een schending van gelijktijdigheid wordt gedetecteerd, wordt er een DBConcurrencyException gegenereerd (klik om de afbeelding op volledige grootte weer te geven)

De DBConcurrencyException wordt alleen gegenereerd wanneer het batch-updatepatroon van de DAL wordt gebruikt. Het directe DB-patroon genereert geen uitzondering, maar geeft alleen aan dat er geen rijen zijn beïnvloed. Om dit te illustreren, zet u de GridView van beide browserexemplaren terug naar hun oorspronkelijke staat. Klik vervolgens in het eerste browserexemplaren op de knop Bewerken en wijzig de productnaam van 'Chai Tea' terug in 'Chai' en klik op Bijwerken. Klik in het tweede browservenster op de knop Verwijderen voor Chai.

Wanneer u op Verwijderen klikt, wordt de pagina vernieuwd, roept GridView de Delete() methode van ObjectDataSource aan en roept ObjectDataSource de ProductsOptimisticConcurrencyBLL methode van de DeleteProduct klasse aan, waarbij de oorspronkelijke waarden worden doorgegeven. De oorspronkelijke ProductName waarde voor de tweede browserinstantie is 'Chai Tea', dat niet overeenkomt met de huidige ProductName waarde in de database. DELETE De instructie die aan de database wordt uitgegeven, heeft daarom geen invloed op rijen, omdat er geen record is in de database waaraan de WHERE clausule voldoet. De DeleteProduct methode retourneert false en de gegevens van ObjectDataSource worden hersteld naar de GridView.

Vanuit het perspectief van de eindgebruiker, veroorzaakt het klikken op de Verwijderknop voor Chai Tea in het tweede browservenster dat het scherm knippert, en bij terugkeer is het product nog steeds aanwezig, hoewel het nu wordt vermeld als 'Chai' (de productnaamwijziging gemaakt door het eerste browservenster). Als de gebruiker nogmaals op de knop Verwijderen klikt, slaagt de delete,omdat de oorspronkelijke ProductName waarde van GridView ('Chai') nu overeenkomt met de waarde in de database.

In beide gevallen is de gebruikerservaring verre van ideaal. We willen de gebruiker duidelijk niet de details van de DBConcurrencyException uitzondering laten zien wanneer het batch-updatepatroon wordt gebruikt. En het gedrag bij het gebruik van het directe DB-patroon is enigszins verwarrend omdat de opdracht gebruikers is mislukt, maar er was geen nauwkeurige indicatie van waarom.

Om deze twee problemen op te lossen, kunnen we labelwebbesturingselementen maken op de pagina die een uitleg geven waarom een update of verwijdering is mislukt. Voor het batch-updatepatroon kunnen we bepalen of er al dan niet een DBConcurrencyException uitzondering is opgetreden in de gebeurtenis-handler op het niveau van GridView, waarbij het waarschuwingslabel indien nodig wordt weergegeven. Voor de directe DB-methode kunnen we de retourwaarde van de BLL-methode onderzoeken (die true is als één rij is gewijzigd, false anders) en zo nodig een informatief bericht weergeven.

Stap 6: Informatieve berichten toevoegen en weergeven in het gezicht van een gelijktijdigheidsschending

Wanneer een gelijktijdigheidsfout optreedt, hangt het gedrag af van of de batch update van de DAL of het directe patroon van de DB is gebruikt. In onze zelfstudie worden beide patronen gebruikt, waarbij het batch-updatepatroon wordt gebruikt voor het bijwerken en het directe DB-patroon dat wordt gebruikt voor het verwijderen. Om aan de slag te gaan, voegen we twee labelwebbesturingselementen toe aan onze pagina waarin wordt uitgelegd dat er een gelijktijdigheidsfout is opgetreden bij het verwijderen of bijwerken van gegevens. Stel de eigenschappen Visible en EnableViewState van het labelbesturingselement in op false; dit zorgt ervoor dat ze tijdens elk paginabezoek worden verborgen, met uitzondering van die specifieke paginabezoeken waarop hun Visible-eigenschap programmatisch is ingesteld op 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." />

Naast het instellen van hun Visible, EnabledViewStateen Text eigenschappen, heb ik ook de CssClass eigenschap Warningingesteld op , waardoor het label wordt weergegeven in een groot, rood, cursief, vet lettertype. Deze CSS-klasse Warning is gedefinieerd en toegevoegd aan Styles.css in de zelfstudie De gebeurtenissen onderzoeken die zijn gekoppeld aan het invoegen, bijwerken en verwijderen.

Nadat u deze labels hebt toegevoegd, moet de ontwerpfunctie in Visual Studio er ongeveer uitzien als afbeelding 18.

Er zijn twee labelbesturingselementen toegevoegd aan de pagina

Afbeelding 18: Er zijn twee labelbesturingselementen toegevoegd aan de pagina (klik om de afbeelding op volledige grootte weer te geven)

Nu deze Label webonderdelen zijn ingesteld, zijn we klaar om te onderzoeken hoe u kunt bepalen wanneer er een gelijktijdigheidsfout is opgetreden. Op dat moment kan de eigenschap van het juiste Label Visible worden ingesteld op true, zodat het informatieve bericht wordt weergegeven.

Gelijktijdigheidsschendingen verwerken bij het bijwerken

Laten we eerst eens kijken hoe u gelijktijdigheidsschendingen kunt afhandelen bij het gebruik van het batch-updatepatroon. Omdat dergelijke schendingen met het batch-updatepatroon ertoe leiden dat er een DBConcurrencyException uitzondering wordt gegenereerd, moeten we code toevoegen aan onze ASP.NET-pagina om te bepalen of er een DBConcurrencyException uitzondering is opgetreden tijdens het updateproces. Als dat het geval is, moeten we een bericht aan de gebruiker weergeven waarin wordt uitgelegd dat hun wijzigingen niet zijn opgeslagen omdat een andere gebruiker dezelfde gegevens had gewijzigd tussen het moment waarop de record werd bewerkt en op de knop Bijwerken klikte.

Zoals we in de zelfstudie Over het verwerken van BLL- en DAL-Level-uitzonderingen in een ASP.NET Pagina hebben gezien, kunnen dergelijke uitzonderingen worden gedetecteerd en onderdrukt in de postniveau gebeurtenis-afhandelaars van de gegevens-webcontrol. Daarom moeten we een gebeurtenis-handler maken voor de gebeurtenis van GridView RowUpdated die controleert of er een DBConcurrencyException uitzondering is opgetreden. Deze eventhandler wordt een verwijzing naar een uitzondering gegeven die is gegenereerd tijdens het updateproces, zoals te zien is in de onderstaande eventhandlercode.

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

In het geval van een DBConcurrencyException uitzondering wordt met deze eventhandler het UpdateConflictMessage besturingselement Label weergegeven en aangegeven dat de uitzondering is verwerkt. Wanneer deze code is ingesteld, gaan de wijzigingen van de gebruiker verloren wanneer er een gelijktijdigheidsfout optreedt bij het bijwerken van een record, omdat de wijzigingen van een andere gebruiker tegelijkertijd zijn overschreven. Specifiek wordt de GridView teruggezet naar de oorspronkelijke staat en verbonden met de huidige databasegegevens. Hiermee wordt de GridView-rij bijgewerkt met de wijzigingen van de andere gebruiker, die eerder niet zichtbaar waren. Daarnaast legt het UpdateConflictMessage besturingselement Label de gebruiker uit wat er zojuist is gebeurd. Deze reeks gebeurtenissen wordt beschreven in afbeelding 19.

De updates van een gebruiker gaan verloren bij een concurrentieschending

Afbeelding 19: Updates van gebruikers gaan verloren in het gezicht van een gelijktijdigheidsschending (klik om de afbeelding op volledige grootte weer te geven)

Opmerking

In plaats van de GridView terug te zetten naar de status van voor het bewerken, kunnen we de GridView in de bewerkingsstatus laten staan door de KeepInEditMode eigenschap van het doorgegeven GridViewUpdatedEventArgs object op true te zetten. Als u deze methode echter volgt, moet u ervoor zorgen dat de gegevens opnieuw worden gekoppeld aan GridView (door de methode ervan aan te DataBind() roepen), zodat de waarden van de andere gebruiker in de bewerkingsinterface worden geladen. De code die met deze zelfstudie kan worden gedownload, bevat deze twee regels code in de RowUpdated gebeurtenishandler die als commentaar is toegevoegd. Verwijder gewoon de opmerkingen bij deze regels code om ervoor te zorgen dat GridView in de bewerkingsmodus blijft na een schending van gelijktijdigheid.

Reageren op gelijktijdigheidsschendingen bij het verwijderen

Met het directe DB-patroon wordt er geen uitzondering opgeworpen bij een gelijktijdigheidsschending. In plaats daarvan beïnvloedt de database-instructie geen records, omdat de WHERE-clausule niet overeenkomt met enig record. Alle methoden voor gegevenswijziging die in de BLL zijn gemaakt, zijn zodanig ontworpen dat ze een Booleaanse waarde retourneren die aangeeft of ze precies één record hebben beïnvloed. Om te bepalen of er een gelijktijdigheidsfout is opgetreden bij het verwijderen van een record, kunnen we daarom de retourwaarde van de methode BLL DeleteProduct onderzoeken.

De retourwaarde voor een BLL-methode kan worden onderzocht in de gebeurtenis-handlers op het niveau van ObjectDataSource via de ReturnValue eigenschap van het ObjectDataSourceStatusEventArgs object dat is doorgegeven aan de gebeurtenis-handler. Omdat we geïnteresseerd zijn in het bepalen van de retourwaarde van de DeleteProduct methode, moeten we een gebeurtenis-handler maken voor de gebeurtenis van ObjectDataSource Deleted . De ReturnValue eigenschap is van het type object en kan zijn null als er een uitzondering is gegenereerd en de methode is onderbroken voordat een waarde kon worden geretourneerd. Daarom moeten we eerst ervoor zorgen dat de ReturnValue eigenschap niet null is en een Booleaanse waarde is. Ervan uitgaande dat deze controle is geslaagd, wordt het Label-besturingselement weergegeven als DeleteConflictMessageReturnValue. Dit kan worden bereikt met behulp van de volgende code:

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

In het gezicht van een schending van gelijktijdigheid wordt de verwijderingsaanvraag van de gebruiker geannuleerd. De GridView wordt vernieuwd, met de wijzigingen die zijn opgetreden voor die record tussen het moment dat de gebruiker de pagina heeft geladen en toen hij op de knop Verwijderen klikte. Wanneer een dergelijke schending overgaat, wordt het DeleteConflictMessage label weergegeven, waarin wordt uitgelegd wat er zojuist is gebeurd (zie afbeelding 20).

Het verwijderen van een gebruiker wordt geannuleerd bij een schending van concurrentie

Afbeelding 20: Het verwijderen van een gebruiker is geannuleerd wegens een concurrentieschending (Klik om de afbeelding op volledige grootte weer te geven)

Samenvatting

Er bestaan mogelijkheden voor gelijktijdigheidsschendingen in elke toepassing waarmee meerdere, gelijktijdige gebruikers gegevens kunnen bijwerken of verwijderen. Als dergelijke schendingen niet worden verwerkt, worden de wijzigingen van de andere gebruiker overschreven wanneer twee gebruikers tegelijkertijd dezelfde gegevens bijwerken, en degene die de laatste schrijft, 'wint' en de wijzigingen van de ander overschrijft. Ontwikkelaars kunnen optimistische of pessimistische gelijktijdigheidscontrole implementeren. Optimistisch gelijktijdigheidsbeheer gaat ervan uit dat gelijktijdigheidsschendingen zeldzaam zijn en staat eenvoudigweg een opdracht voor bijwerken of verwijderen niet toe die een gelijktijdigheidsschending zou vormen. Pessimistisch gelijktijdigheidsbeheer gaat ervan uit dat gelijktijdigheidsschendingen frequent zijn en dat het negeren van de opdracht voor het bijwerken of verwijderen van één gebruiker niet acceptabel is. Met pessimistisch gelijktijdigheidsbeheer omvat het bijwerken van een record het vergrendelen, waardoor andere gebruikers de record niet kunnen wijzigen of verwijderen terwijl deze is vergrendeld.

De Getypte DataSet in .NET biedt functionaliteit voor het ondersteunen van optimistisch gelijktijdigheidsbeheer. Met name de UPDATE en DELETE instructies die aan de database worden uitgegeven, bevatten alle kolommen van de tabel, waardoor de update of verwijdering alleen plaatsvindt als de huidige gegevens van de record overeenkomen met de oorspronkelijke gegevens die de gebruiker had bij het bijwerken of verwijderen. Zodra de DAL is geconfigureerd ter ondersteuning van optimistische gelijktijdigheid, moeten de BLL-methoden worden bijgewerkt. Bovendien moet de ASP.NET-pagina die in de BLL wordt aangeroepen zo worden geconfigureerd dat de ObjectDataSource de oorspronkelijke waarden ophaalt uit het gegevenswebbeheer en deze doorgeeft aan de BLL.

Zoals we in deze zelfstudie hebben gezien, omvat het implementeren van optimistisch gelijktijdigheidsbeheer in een ASP.NET-webtoepassing het bijwerken van de DAL en BLL en het toevoegen van ondersteuning op de pagina ASP.NET. Of dit toegevoegde werk al dan niet een verstandige investering van uw tijd en inspanning is, is afhankelijk van uw toepassing. Als u niet vaak gelijktijdige gebruikers hebt die gegevens bijwerken of als de gegevens die ze bijwerken verschillen van elkaar, is gelijktijdigheidsbeheer geen belangrijk probleem. Als u echter regelmatig meerdere gebruikers op uw site hebt die met dezelfde gegevens werken, kan gelijktijdigheidsbeheer helpen voorkomen dat de updates of verwijderingen van één gebruiker onbedoeld worden overschreven.

Veel plezier met programmeren!

Over de auteur

Scott Mitchell, auteur van zeven ASP/ASP.NET-boeken en oprichter van 4GuysFromRolla.com, werkt sinds 1998 met Microsoft-webtechnologieën. Scott werkt als onafhankelijk consultant, trainer en schrijver. Zijn laatste boek is Sams Teach Yourself ASP.NET 2.0 in 24 uur. Hij kan worden bereikt op mitchell@4GuysFromRolla.com.