Dela via


Implementera optimistisk konkurrenshantering (C#)

av Scott Mitchell

Ladda ned PDF

För ett webbprogram som gör att flera användare kan redigera data finns det en risk att två användare redigerar samma data samtidigt. I den här självstudien implementerar vi optimistisk samtidighetskontroll för att hantera risken.

Inledning

För webbprogram som bara tillåter användare att visa data, eller för dem som bara innehåller en enskild användare som kan ändra data, finns det inget hot om att två samtidiga användare oavsiktligt skriver över varandras ändringar. För webbprogram som gör det möjligt för flera användare att uppdatera eller ta bort data finns det dock potential att en användares ändringar krockar med en annan samtidig användares. Utan någon samtidighetsprincip på plats, när två användare samtidigt redigerar en enda post, åsidosätter den användare som genomför sina ändringar senast de ändringar som görs av den första.

Anta till exempel att två användare, Jisun och Sam, båda besökte en sida i vårt program som gjorde det möjligt för besökare att uppdatera och ta bort produkterna via en GridView-kontroll. Båda klickar på knappen Redigera i GridView ungefär samtidigt. Jisun ändrar produktnamnet till "Chai Tea" och klickar på knappen Uppdatera. Nettoresultatet är en UPDATE instruktion som skickas till databasen, som anger alla produkters uppdateringsbara fält (även om Jisun bara uppdaterade ett fält, ProductName). Vid den här tidpunkten har databasen värdena "Chai Tea", kategorin Drycker, leverantören Exotic Liquids och så vidare för just den här produkten. GridView på Sams skärm visar dock fortfarande produktnamnet på den redigerbara GridView-raden som "Chai". Några sekunder efter att Jisuns ändringar har checkats in uppdaterar Sam kategorin till Condiments och klickar på Uppdatera. Detta resulterar i en UPDATE instruktion som skickas till databasen som anger produktnamnet till "Chai", CategoryID motsvarande kategori-ID för drycker och så vidare. Jisuns ändringar av produktnamnet har skrivits över. Bild 1 visar den här serien med händelser grafiskt.

När två användare samtidigt uppdaterar en post finns det potential för en användares ändringar att skriva över de andra

Figur 1: När två användare samtidigt uppdaterar en post finns det risk för att en användares ändringar skriver över den andra (klicka för att visa bilden i full storlek)

På samma sätt, när två användare besöker en sida, kan en användare vara mitt uppe i att uppdatera en post när den tas bort av en annan användare. Eller, under tiden mellan att en användare laddar in en sida och att de klickar på Ta bort-knappen, kan en annan användare ha ändrat innehållet i posten.

Det finns tre strategier för samtidighetskontroll:

  • Gör Ingenting -if användare som samtidigt ändrar samma post, låt den senaste ändringen vinna (standardbeteendet)
  • Optimistisk samtidighet - anta att även om det kan finnas samtidighetskonflikter då och då, kommer den stora majoriteten av tiden sådana konflikter inte att uppstå; Om en konflikt uppstår bör du därför bara informera användaren om att deras ändringar inte kan sparas eftersom en annan användare har ändrat samma data
  • Pessimistisk samtidighet – anta att samtidighetskonflikter är vanliga och att användarna inte tolererar att deras ändringar inte sparas på grund av en annan användares samtidiga aktivitet. När en användare börjar uppdatera en post låser du den därför, vilket hindrar andra användare från att redigera eller ta bort posten tills användaren genomför sina ändringar

Alla våra självstudier hittills har använt standardstrategin för konfliktlösning vid samtidighet - det vill säga att vi har låtit den senaste skrivningen gälla. I den här handledningen ska vi gå igenom hur du implementerar optimistisk samtidighetskontroll.

Anmärkning

Vi tittar inte på pessimistiska samtidighetsexempel i den här självstudieserien. Pessimistisk samtidighet används sällan eftersom sådana lås, om de inte avstängs korrekt, kan hindra andra användare från att uppdatera data. Om en användare till exempel låser en post för redigering och sedan lämnar för dagen innan den låss upp, kommer ingen annan användare att kunna uppdatera posten förrän den ursprungliga användaren returnerar och slutför sin uppdatering. I situationer där pessimistisk samtidighet används finns det därför vanligtvis en tidsgräns som, om den nås, avbryter låset. Webbplatser för biljettförsäljning, som under en kort period spärrar en viss sittplats och förhindrar andra användare från att boka medan användaren slutför beställningsprocessen, är ett exempel på pessimistisk samtidighetskontroll.

Steg 1: Översikt över hur optimistisk samtidighet implementeras

Optimistisk samtidighetskontroll fungerar genom att säkerställa att posten som uppdateras eller tas bort har samma värden som när uppdaterings- eller borttagningsprocessen startade. När du till exempel klickar på knappen Redigera i en redigerbar GridView läses postens värden från databasen och visas i Textrutor och andra webbkontroller. Dessa ursprungliga värden sparas av GridView. Senare, när användaren har gjort sina ändringar och klickar på knappen Uppdatera, skickas de ursprungliga värdena plus de nya värdena till affärslogiklagret och sedan ned till dataåtkomstlagret. Dataåtkomstlagret måste utfärda en SQL-instruktion som endast uppdaterar posten om de ursprungliga värdena som användaren började redigera är identiska med de värden som fortfarande finns kvar i databasen. Bild 2 visar den här händelsesekvensen.

För att uppdateringen eller borttagningen ska lyckas måste de ursprungliga värdena vara lika med de aktuella databasvärdena

Bild 2: För att uppdateringen eller borttagningen ska lyckas måste de ursprungliga värdena vara lika med de aktuella databasvärdena (klicka om du vill visa en bild i full storlek)

Det finns olika metoder för att implementera optimistisk samtidighet (se Peter A. Brombergsoptimistiska samtidighetsuppdateringslogik för en kort titt på ett antal alternativ). ADO.NET Typed DataSet innehåller en implementering som kan konfigureras med bara kryssmarkeringen i en kryssruta. Om du aktiverar optimistisk samtidighet för en TableAdapter i Typed DataSet utökas TableAdapters UPDATE och DELETE -uttrycken så att de innehåller en jämförelse av alla ursprungliga värden i WHERE -satsen. UPDATE Följande instruktion uppdaterar till exempel namnet och priset på en produkt endast om de aktuella databasvärdena är lika med de värden som ursprungligen hämtades när posten uppdaterades i GridView. Parametrarna @ProductName och @UnitPrice innehåller de nya värden som angetts av användaren, medan @original_ProductName och @original_UnitPrice innehåller de värden som ursprungligen lästes in i GridView när knappen Redigera klickades:

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

Anmärkning

Den här UPDATE instruktionen har förenklats för läsbarhet. I praktiken skulle kontrollen i UnitPrice-satsen vara mer komplicerad eftersom WHERE kan innehålla UnitPrice och du måste kontrollera om NULL alltid returnerar False (i stället måste du använda NULL = NULL).

Förutom att använda en annan underliggande UPDATE instruktion ändrar konfigurationen av en TableAdapter för att använda optimistisk samtidighet också signaturen för dess DB-direktmetoder. Kom ihåg från vår första handledning, Skapa ett dataåtkomstlager, att databasdirektmetoder var de som accepterar en lista med skalära värden som indataparametrar (i stället för en starkt typad DataRow- eller DataTable-instans). När du använder optimistisk samtidighet innehåller db-direkt Update() - och Delete() -metoderna även indataparametrar för de ursprungliga värdena. Dessutom måste även koden i BLL:n för användning av batchuppdateringsmönstret (metoden Update() överbelastas som accepterar DataRows och DataTables i stället för skalära värden) ändras.

I stället för att utöka våra befintliga DAL:s TableAdapters till att använda optimistisk samtidighet (vilket skulle kräva att BLL ändras för att rymma), skapar vi i stället en ny Typed DataSet med namnet NorthwindOptimisticConcurrency, till vilken vi lägger till en Products TableAdapter som använder optimistisk samtidighet. Därefter skapar vi en ProductsOptimisticConcurrencyBLL Business Logic Layer-klass som har lämpliga ändringar för att stödja optimistisk samtidighets-DAL. När det här grundarbetet har lagts är vi redo att skapa sidan ASP.NET.

Steg 2: Skapa ett dataåtkomstlager som stöder optimistisk samtidighet

Om du vill skapa en ny Typed DataSet högerklickar du på DAL mappen i App_Code mappen och lägger till en ny DataSet med namnet NorthwindOptimisticConcurrency. Som vi såg i den första självstudien lägger du till en ny TableAdapter i den typade datauppsättningen och startar automatiskt konfigurationsguiden TableAdapter. På den första skärmen uppmanas vi att ange den databas som ska anslutas till – ansluta till samma Northwind-databas med hjälp av NORTHWNDConnectionString inställningen från Web.config.

Ansluta till samma Northwind-databas

Bild 3: Anslut till samma Northwind-databas (Klicka om du vill visa en bild i full storlek)

Därefter uppmanas vi att fråga efter data: via en ad hoc SQL-instruktion, en ny lagrad procedur eller en befintlig lagrad procedur. Eftersom vi använde ad hoc SQL-frågor i vår ursprungliga DAL använder du det här alternativet även här.

Ange data som ska hämtas med hjälp av en ad hoc SQL-instruktion

Bild 4: Ange data som ska hämtas med hjälp av en ad hoc SQL-instruktion (Klicka om du vill visa en bild i full storlek)

På följande skärm anger du den SQL-fråga som ska användas för att hämta produktinformationen. Vi använder exakt samma SQL-fråga som används för Products TableAdapter från vår ursprungliga DAL, som returnerar alla Product kolumner tillsammans med produktens leverantörs- och kategorinamn:

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

Använda samma SQL-fråga från Products TableAdapter i den ursprungliga DAL

Bild 5: Använd samma SQL-fråga från Products TableAdapter i den ursprungliga DAL(Klicka om du vill visa en bild i full storlek)

Innan du går vidare till nästa skärm klickar du på knappen Avancerade alternativ. Om du vill att den här TableAdapter ska använda optimistisk samtidighetskontroll markerar du kryssrutan "Använd optimistisk samtidighet".

Aktivera optimistisk samtidighetskontroll genom att kontrollera kryssrutan

Bild 6: Aktivera optimistisk samtidighetskontroll genom att kontrollera kryssrutan "Använd optimistisk samtidighet" (klicka om du vill visa en bild i full storlek)

Slutligen anger du att TableAdapter ska använda de dataåtkomstmönster som både fyller en DataTable och returnerar en DataTable. anger också att databasdirigeringsmetoderna ska skapas. Ändra metodnamnet för mönstret Return a DataTable from GetData to GetProducts (Returnera ett DataTable-mönster från GetData till GetProducts) för att spegla de namngivningskonventioner som vi använde i vår ursprungliga DAL.

Låta TableAdapter använda alla dataåtkomstmönster

Bild 7: Låt TableAdapter använda alla dataåtkomstmönster (Klicka om du vill visa en bild i full storlek)

När du har slutfört guiden kommer DataSet Designer att inkludera en starkt typad Products DataTable och TableAdapter. Ta en stund att byta namn på DataTable från Products till ProductsOptimisticConcurrency, vilket du kan göra genom att högerklicka på datatabellens namnlist och välja Byt namn på snabbmenyn.

En DataTable och TableAdapter har lagts till i den typade datauppsättningen

Bild 8: En DataTable och TableAdapter har lagts till i den typade datauppsättningen (klicka om du vill visa en bild i full storlek)

Om du vill se skillnaderna mellan UPDATE och DELETE-frågorna i ProductsOptimisticConcurrency TableAdapter (som använder optimistisk samtidighet) och i Products TableAdapter (som inte gör det), klicka på TableAdapter och gå till fönstret för Egenskaper. I underegenskaperna DeleteCommandUpdateCommand och CommandText kan du se den faktiska SQL-syntax som skickas till databasen när DAL:s uppdaterings- eller borttagningsrelaterade metoder anropas. ProductsOptimisticConcurrency För TableAdapter är instruktionen DELETE som används:

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

Däremot är instruktionen DELETE för Product TableAdapter i vår ursprungliga DAL mycket enklare.

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

Så ser du att klausulen WHERE i DELETE-satsen för TableAdapter som använder optimistisk samtidighet inkluderar en jämförelse mellan var och en av Product tabellens befintliga kolumnvärden och de ursprungliga värdena när GridView (eller DetailsView eller FormView) senast uppdaterades. Eftersom alla andra fält än ProductID, ProductNameoch Discontinued kan ha NULL värden, inkluderas ytterligare parametrar och kontroller för att korrekt jämföra NULL värden i WHERE -satsen.

Vi kommer inte att lägga till några ytterligare DataTables i det optimistiska samtidighetsaktiverade DataSetet för den här självstudien, eftersom vår ASP.NET-sida endast kommer att uppdatera och ta bort produktinformation. Vi behöver dock fortfarande lägga GetProductByProductID(productID) till metoden i ProductsOptimisticConcurrency TableAdapter.

Det gör du genom att högerklicka på TabellAdapter-namnlisten (området precis ovanför metodnamnen Fill och GetProducts ) och välja Lägg till fråga på snabbmenyn. Då startas konfigurationsguiden för TableAdapter-frågor. Precis som med tableAdapter-konfigurationen GetProductByProductID(productID) väljer du att skapa metoden med hjälp av en ad hoc SQL-instruktion (se bild 4). GetProductByProductID(productID) Eftersom metoden returnerar information om en viss produkt anger du att den här frågan är en SELECT frågetyp som returnerar rader.

Markera frågetypen som en

Bild 9: Markera frågetypen som en "SELECT som returnerar rader" (Klicka om du vill visa en bild i full storlek)

På nästa skärm uppmanas vi att använda SQL-frågan med TableAdapter-standardfrågan förinläst. Utöka den befintliga frågan så att den innehåller -satsen WHERE ProductID = @ProductID, enligt bild 10.

Lägg till en WHERE-sats i den förinstallerade frågan för att returnera en specifik produktpost

Bild 10: Lägg till en WHERE sats i den förinstallerade frågan för att returnera en specifik produktpost (Klicka om du vill visa en bild i full storlek)

Ändra slutligen de genererade metodnamnen till FillByProductID och GetProductByProductID.

Byt namn på metoderna till FillByProductID och GetProductByProductID

Bild 11: Byt namn på metoderna till FillByProductID och GetProductByProductID (Klicka om du vill visa en bild i full storlek)

När guiden är klar innehåller TableAdapter nu två metoder för att hämta data: GetProducts(), som returnerar alla produkter och GetProductByProductID(productID), som returnerar den angivna produkten.

Steg 3: Skapa ett affärslogikskikt för optimistiska Concurrency-Enabled DAL

Vår befintliga ProductsBLL klass har exempel på hur du använder både batchuppdateringen och db-direktmönstren. Både AddProduct metoden och UpdateProduct överlagringarna använder batchuppdateringsmönstret och skickar in en ProductRow instans till TableAdapter-metoden Update. Metoden DeleteProduct använder å andra sidan db-direktmönstret och anropar TableAdapter-metoden Delete(productID) .

Med den nya ProductsOptimisticConcurrency TableAdapter kräver db-direktmetoderna nu att de ursprungliga värdena också skickas in. Metoden förväntar sig till exempel nu tio indataparametrar: originalet Delete, ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitPrice, UnitsInStock, UnitsOnOrder, och ReorderLevel. Den använder värdena för de ytterligare indataparametrarna i WHERE-klause i DELETE-satsen som skickas till databasen, och tar bara bort den angivna posten om databasens aktuella värden överensstämmer med de ursprungliga.

Även om metodsignaturen för TableAdapter-metoden som används i batchuppdateringsmönstret inte har ändrats, har koden som behövs för att registrera de ursprungliga och nya värdena gjort det. I stället för att försöka använda den optimistiska samtidighetsaktiverade DAL med vår befintliga ProductsBLL klass skapar vi därför en ny Business Logic Layer-klass för att arbeta med vår nya DAL.

Lägg till en klass med namnet ProductsOptimisticConcurrencyBLL i BLL mappen i App_Code mappen.

Lägg till klassen ProductsOptimisticConcurrencyBLL i BLL-mappen

Bild 12: Lägg till klassen i ProductsOptimisticConcurrencyBLL BLL-mappen

Lägg sedan till följande kod i ProductsOptimisticConcurrencyBLL klassen:

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
    private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
    protected ProductsOptimisticConcurrencyTableAdapter Adapter
    {
        get
        {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
}

Observera instruktionen using NorthwindOptimisticConcurrencyTableAdapters ovanför början av klassdeklarationen. Namnområdet NorthwindOptimisticConcurrencyTableAdapters innehåller ProductsOptimisticConcurrencyTableAdapter klassen, som tillhandahåller DAL-metoderna. Innan klassdeklarationen System.ComponentModel.DataObject hittar du även attributet, som instruerar Visual Studio att inkludera den här klassen i listrutan i ObjectDataSource-guiden.

Egenskapen ProductsOptimisticConcurrencyBLL's Adapter ger snabb åtkomst till en instans av ProductsOptimisticConcurrencyTableAdapter klassen och följer mönstret som används i våra ursprungliga BLL-klasser (ProductsBLL, CategoriesBLLoch så vidare). Slutligen anropar den GetProducts()-metoden helt enkelt DAL:s GetProducts()-metod och returnerar ett ProductsOptimisticConcurrencyDataTable-objekt fyllt med en ProductsOptimisticConcurrencyRow-instans för varje produktpost i databasen.

Ta bort en produkt genom att använda DB Direct-mönstret med optimistisk samtidighet

När du använder db-direktmönstret mot en DAL som använder optimistisk samtidighet måste metoderna skickas de nya och ursprungliga värdena. För att ta bort finns det inga nya värden, så endast de ursprungliga värdena behöver skickas in. I vår BLL måste vi sedan acceptera alla ursprungliga parametrar som indataparametrar. Nu ska vi låta DeleteProduct metoden i ProductsOptimisticConcurrencyBLL klassen använda DB-direktmetoden. Det innebär att den här metoden måste ta in alla tio produktdatafälten som indataparametrar och skicka dessa till DAL, som du ser i följande kod:

[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Delete, true)]
public bool DeleteProduct
    (int original_productID, string original_productName,
    int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued)
{
    int rowsAffected = 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;
}

Om de ursprungliga värdena – de värden som senast lästes in i GridView (eller DetailsView eller FormView) – skiljer sig från värdena i databasen när användaren klickar på knappen WHERE Ta bort matchas inte satsen med någon databaspost och inga poster påverkas. Därför kommer TableAdapter-metodens Delete returnera 0 och BLL-metodens DeleteProduct returnera false.

Uppdatering av en produkt med batchuppdateringsmönstret och optimistisk samtidighet

Som tidigare nämnts har TableAdapter-metoden för batchuppdateringsmönstret Update samma metodsignatur oavsett om optimistisk samtidighet används eller inte. Update Metoden förväntar sig nämligen en DataRow, en matris med DataRows, en DataTable eller en Typed DataSet. Det finns inga ytterligare indataparametrar för att ange de ursprungliga värdena. Detta är möjligt eftersom DataTable håller reda på de ursprungliga och ändrade värdena för sina DataRow(ar). När DAL utfärdar sin UPDATE -instruktion fylls parametrarna @original_ColumnName i med DataRows ursprungliga värden, medan parametrarna @ColumnName fylls i med DataRows ändrade värden.

ProductsBLL I klassen (som använder vår ursprungliga, icke-optimistiska samtidighets-DAL) utför vår kod följande händelsesekvens när du använder batchuppdateringsmönstret för att uppdatera produktinformation:

  1. Läs den aktuella databasproduktinformationen i en ProductRow instans med tableAdapter-metoden GetProductByProductID(productID)
  2. Tilldela de nya värdena till instansen ProductRow från steg 1
  3. Anropa TableAdapter-metoden Update och skicka in instansen ProductRow

Den här stegsekvensen stöder dock inte optimistisk samtidighet korrekt eftersom den ProductRow ifyllda i steg 1 fylls i direkt från databasen, vilket innebär att de ursprungliga värdena som används av DataRow är de som för närvarande finns i databasen och inte de som var bundna till GridView i början av redigeringsprocessen. När vi i stället använder en optimistisk samtidighetsaktiverad DAL måste vi ändra UpdateProduct metodöverlagringarna för att använda följande steg:

  1. Läs den aktuella databasproduktinformationen i en ProductsOptimisticConcurrencyRow instans med tableAdapter-metoden GetProductByProductID(productID)
  2. Tilldela de ursprungliga värdena till instansen ProductsOptimisticConcurrencyRow från steg 1
  3. Anropa instansens ProductsOptimisticConcurrencyRowAcceptChanges() metod, som instruerar DataRow att dess aktuella värden är de "ursprungliga" värdena
  4. Tilldela de nya värdena till instansen ProductsOptimisticConcurrencyRow
  5. Anropa TableAdapter-metoden Update och skicka in instansen ProductsOptimisticConcurrencyRow

Steg 1 läser in alla aktuella databasvärden för den angivna produktposten. Det här steget är överflödigt i överbelastningen UpdateProduct som uppdaterar alla produktkolumner (eftersom dessa värden skrivs över i steg 2), men är viktigt för de överlagringar där endast en delmängd av kolumnvärdena skickas in som indataparametrar. När de ursprungliga värdena har tilldelats till instansen ProductsOptimisticConcurrencyRowAcceptChanges() anropas metoden, vilket markerar de aktuella DataRow-värdena som de ursprungliga värden som ska användas i parametrarna @original_ColumnName i -instruktionen UPDATE . Därefter tilldelas de nya parametervärdena till ProductsOptimisticConcurrencyRow och slutligen anropas Update-metoden, där datauppsättningen skickas in.

Följande kod visar överbelastningen UpdateProduct som accepterar alla produktdatafält som indataparametrar. Även om den inte visas här innehåller klassen ProductsOptimisticConcurrencyBLL som ingår i nedladdningen för den här handledningen även en UpdateProduct överlagring som bara accepterar produktens namn och pris som indataparametrar.

protected void AssignAllProductValues
    (NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product,
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued)
{
    product.ProductName = productName;
    if (supplierID == null)
        product.SetSupplierIDNull();
    else
        product.SupplierID = supplierID.Value;
    if (categoryID == null)
        product.SetCategoryIDNull();
    else
        product.CategoryID = categoryID.Value;
    if (quantityPerUnit == null)
        product.SetQuantityPerUnitNull();
    else
        product.QuantityPerUnit = quantityPerUnit;
    if (unitPrice == null)
        product.SetUnitPriceNull();
    else
        product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null)
        product.SetUnitsInStockNull();
    else
        product.UnitsInStock = unitsInStock.Value;
    if (unitsOnOrder == null)
        product.SetUnitsOnOrderNull();
    else
        product.UnitsOnOrder = unitsOnOrder.Value;
    if (reorderLevel == null)
        product.SetReorderLevelNull();
    else
        product.ReorderLevel = reorderLevel.Value;
    product.Discontinued = discontinued;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(
    // new parameter values
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued, int productID,
    // original parameter values
    string original_productName, int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued,
    int original_productID)
{
    // STEP 1: Read in the current database product information
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products =
        Adapter.GetProductByProductID(original_productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product = 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
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

Steg 4: Skicka de ursprungliga och nya värdena från sidan ASP.NET till BLL-metoderna

När DAL och BLL är klara är allt som återstår att skapa en ASP.NET sida som kan använda den optimistiska samtidighetslogik som är inbyggd i systemet. Mer specifikt måste datawebbkontrollen (GridView, DetailsView eller FormView) komma ihåg sina ursprungliga värden och ObjectDataSource måste skicka båda uppsättningarna med värden till affärslogiklagret. Dessutom måste sidan ASP.NET konfigureras för att korrekt hantera samtidighetsöverträdelser.

Börja med att öppna OptimisticConcurrency.aspx sidan i EditInsertDelete mappen och lägga till en GridView i designern. Ställ sedan in dess ID egenskap på ProductsGrid. Från GridViews smarta tagg väljer du att skapa en ny ObjectDataSource med namnet ProductsOptimisticConcurrencyDataSource. Eftersom vi vill att ObjectDataSource ska använda DAL som stöder optimistisk samtidighet, konfigurerar du det för att använda ProductsOptimisticConcurrencyBLL-objektet.

Låt ObjectDataSource använda ProductsOptimisticConcurrencyBLL-objektet

Bild 13: Be ObjectDataSource använda ProductsOptimisticConcurrencyBLL objektet (klicka om du vill visa en bild i full storlek)

Välj GetProducts, UpdateProduct och DeleteProduct metoderna från listrutorna i guiden. För metoden UpdateProduct använder du den överlagring som accepterar alla produktens datafält.

Konfigurera ObjectDataSource-kontrollens egenskaper

När du har slutfört guiden bör ObjectDataSources deklarativa markering se ut så här:

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

Som du ser innehåller DeleteParameters-samlingen en instans för var och en av de tio indataparametrarna i Parameter-klassens ProductsOptimisticConcurrencyBLL-metod. UpdateParameters På samma sätt innehåller samlingen en instans för var och en Parameter av indataparametrarna i UpdateProduct.

För de tidigare självstudierna som omfattade dataändring skulle vi ta bort egenskapen ObjectDataSource OldValuesParameterFormatString just nu, eftersom den här egenskapen anger att BLL-metoden förväntar sig att de gamla (eller ursprungliga) värdena skickas in samt de nya värdena. Dessutom anger det här egenskapsvärdet indataparameternamnen för de ursprungliga värdena. Eftersom vi skickar de ursprungliga värdena till BLL ska du inte ta bort den här egenskapen.

Anmärkning

Värdet för OldValuesParameterFormatString egenskapen måste mappas till indataparameternamnen i den BLL som förväntar sig de ursprungliga värdena. Eftersom vi gav dessa parametrar original_productNamenamnet , original_supplierIDoch så vidare, kan du lämna egenskapsvärdet OldValuesParameterFormatString som original_{0}. Men om BLL-metodernas indataparametrar hade namn som old_productName, old_supplierIDoch så vidare, skulle du behöva uppdatera OldValuesParameterFormatString egenskapen till old_{0}.

Det finns en sista egenskapsinställning som måste göras för att ObjectDataSource ska kunna skicka de ursprungliga värdena korrekt till BLL-metoderna. ObjectDataSource har en ConflictDetection-egenskap som kan tilldelas till ett av två värden:

  • OverwriteChanges – standardvärdet. skickar inte de ursprungliga värdena till BLL-metodernas ursprungliga indataparametrar
  • CompareAllValues - skickar de ursprungliga värdena till BLL-metoderna; välj det här alternativet när du använder optimistisk samtidighet

Ta en stund att ange egenskapen ConflictDetection till CompareAllValues.

Konfigurera GridViews egenskaper och fält

Med ObjectDataSources egenskaper korrekt konfigurerade ska vi fokusera på att konfigurera GridView. Eftersom vi vill att GridView ska ha stöd för redigering och borttagning klickar du först på kryssrutorna Aktivera redigering och Aktivera borttagning från GridViews smarta tagg. Detta lägger till ett Kommandofält vars ShowEditButton och ShowDeleteButton båda är inställda på true.

När GridView är bunden till ProductsOptimisticConcurrencyDataSource ObjectDataSource innehåller den ett fält för varje datafält i produkten. Även om en sådan GridView kan redigeras är användarupplevelsen allt annat än acceptabel. CategoryID och SupplierID BoundFields kommer att renderas som textrutor, vilket kräver att användaren anger lämplig kategori och leverantör som ID-nummer. Det kommer inte att finnas någon formatering för de numeriska fälten och inga verifieringskontroller för att säkerställa att produktens namn har angetts och att enhetspriset, enheter i lager, enheter i ordning och omordningsnivåvärden både är korrekta numeriska värden och är större än eller lika med noll.

Som vi diskuterade i självstudierna Lägga till verifieringskontroller i redigerings- och infogningsgränssnitt och Anpassa datamodifieringsgränssnittet kan användargränssnittet anpassas genom att ersätta BoundFields med TemplateFields. Jag har ändrat detta GridView och dess redigeringsgränssnitt på följande sätt:

  • Tog bort ProductID, SupplierName, och CategoryName BoundFields
  • Konverterade ProductName BoundField till ett TemplateField och lade till en RequiredFieldValidation-kontroll.
  • Konverterade CategoryID och SupplierID BoundFields till TemplateFields och justerade redigeringsgränssnittet för att använda rullgardinslistor i stället för textfält. I dessa TemplateFields ItemTemplates visas datafälten CategoryName och SupplierName.
  • Konverterade UnitPrice, UnitsInStock, UnitsOnOrder och ReorderLevel BoundFields till TemplateFields och lade till CompareValidator-kontroller.

Eftersom vi redan har undersökt hur du utför dessa uppgifter i tidigare självstudier, ska jag bara lista den slutliga deklarativa syntaxen här och lämna implementeringen som praxis.

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

Vi är mycket nära att ha ett fullt fungerande exempel. Det finns dock några subtiliteter som kommer att krypa upp och orsaka oss problem. Dessutom behöver vi fortfarande ett gränssnitt som varnar användaren när en samtidighetsöverträdelse har inträffat.

Anmärkning

För att en datawebbkontroll ska kunna skicka de ursprungliga värdena korrekt till ObjectDataSource (som sedan skickas till BLL) är det viktigt att GridView-egenskapen EnableViewState är inställd på true (standardvärdet). Om du inaktiverar visningstillstånd går de ursprungliga värdena förlorade vid efteråterställning.

Skicka rätt ursprungliga värden till ObjectDataSource

Det finns ett par problem med hur GridView har konfigurerats. Om ObjectDataSource-egenskapen ConflictDetection är inställd på CompareAllValues (som vår är), när metoderna Update() eller Delete() i ObjectDataSource anropas av GridView (eller DetailsView eller FormView), försöker ObjectDataSource kopiera GridView's ursprungliga värden till sina lämpliga Parameter-instanser. Se bild 2 för en grafisk representation av den här processen.

Mer specifikt tilldelas GridViews ursprungliga värden i tvåvägsdatabindningsinstruktionerna varje gång data är bundna till GridView. Därför är det viktigt att de ursprungliga värdena som krävs samlas in via dubbelriktad databindning och att de tillhandahålls i konvertibelt format.

För att se varför detta är viktigt kan du ta en stund att besöka vår sida i en webbläsare. Som förväntat listar GridView varje produkt med knappen Redigera och Ta bort i kolumnen längst till vänster.

Produkterna visas i en GridView

Bild 14: Produkterna visas i en GridView (Klicka om du vill visa en bild i full storlek)

Om du klickar på knappen Ta bort för en produkt genereras en FormatException .

Försöka ta bort en produkt resulterar i en FormatException

Bild 15: Försöker ta bort eventuella produktresultat i en FormatException (Klicka om du vill visa en bild i full storlek)

FormatException Utlöses när ObjectDataSource försöker läsa i det ursprungliga UnitPrice värdet. ItemTemplate Eftersom har UnitPrice formaterats som en valuta (<%# Bind("UnitPrice", "{0:C}") %>), innehåller den en valutasymbol, som $ 19,95. FormatException Inträffar när ObjectDataSource försöker konvertera den här strängen till en decimal. För att kringgå det här problemet har vi ett antal alternativ:

  • Ta bort valutaformateringen från ItemTemplate. Det innebär att i stället för att använda <%# Bind("UnitPrice", "{0:C}") %>använder du helt enkelt <%# Bind("UnitPrice") %>. Nackdelen med detta är att priset inte längre är formaterat.
  • Visa UnitPrice formaterat som valuta i ItemTemplate, men använd Eval-nyckelordet för att göra detta. Kom ihåg att Eval utför enkelriktad databindning. Vi måste fortfarande ange UnitPrice värdet för de ursprungliga värdena, så vi behöver fortfarande en tvåvägs-databinding-instruktion i ItemTemplate, men detta kan placeras i en etikettwebbkontroll vars Visible egenskap är inställd på false. Vi kan använda följande markering i 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>
  • Ta bort valutaformateringen från ItemTemplate, med hjälp av <%# Bind("UnitPrice") %>. I GridViews RowDataBound händelsehanterare får du programmatiskt åtkomst till den etikettwebbkontroll där UnitPrice värdet visas och anger dess Text egenskap till den formaterade versionen.
  • Lämna UnitPrice formaterat som valuta. I GridViews RowDeleting händelsehanterare ersätter du det befintliga ursprungliga UnitPrice värdet (19,95 USD) med ett faktiskt decimalvärde med .Decimal.Parse Vi såg hur man kan uppnå något liknande i RowUpdating händelsehanteraren i handledningen Hantera BLL- och DAL-Level-undantag i en ASP.NET-sida.

I mitt exempel valde jag att använda den andra metoden genom att lägga till en dold webbkontroll för etikett vars Text egenskap är dubbelriktad databindning till det oformaterade UnitPrice värdet.

När du har löst problemet kan du prova att klicka på knappen Ta bort för en produkt igen. Den här gången får du en InvalidOperationException när ObjectDataSource försöker anropa BLL-metoden UpdateProduct .

ObjectDataSource kan inte hitta en metod med de indataparametrar som den vill skicka

Bild 16: ObjectDataSource kan inte hitta en metod med de indataparametrar som den vill skicka (klicka om du vill visa en bild i full storlek)

När du tittar på undantagets meddelande är det tydligt att ObjectDataSource vill anropa en BLL-metod DeleteProduct som innehåller original_CategoryName och original_SupplierName indataparametrar. Det beror på att ItemTemplate s för CategoryID och SupplierID TemplateFields för närvarande innehåller tvåvägsbindningsinstruktioner med datafälten CategoryName och SupplierName . I stället måste vi inkludera Bind -satser med datafälten CategoryID och SupplierID . För att åstadkomma detta ersätter du de befintliga bindningsinstruktionerna med Eval -instruktioner och lägger sedan till dolda etikettkontroller vars Text egenskaper är bundna till datafälten CategoryID och SupplierID med hjälp av dubbelriktad databindning enligt nedan:

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

Med dessa ändringar kan vi nu ta bort och redigera produktinformation! I steg 5 tittar vi på hur du kontrollerar att samtidighetsöverträdelser identifieras. Men för tillfället kan det ta några minuter att försöka uppdatera och ta bort några poster för att säkerställa att uppdatering och borttagning för en enskild användare fungerar som förväntat.

Steg 5: Testa stödet för optimistisk samtidighet

För att verifiera att samtidighetsöverträdelser identifieras (i stället för att data skrivs över i blindhet) måste vi öppna två webbläsarfönster på den här sidan. I båda webbläsarinstanserna klickar du på knappen Redigera för Chai. I bara en av webbläsarna ändrar du sedan namnet till "Chai Tea" och klickar på Uppdatera. Uppdateringen bör lyckas och returnera GridView till dess förredigeringstillstånd, med "Chai Tea" som det nya produktnamnet.

I den andra webbläsarfönsterinstansen visas dock produktnamnet TextBox fortfarande "Chai". I det andra webbläsarfönstret uppdaterar du UnitPrice till 25.00. Utan stöd för optimistisk samtidig bearbetning skulle produktnamnet ändras tillbaka till "Chai" om du klickade på uppdatera i det andra webbläsarfönstret, vilket skrevs över de ändringar som gjordes av det första webbläsarfönstret. Optimistisk samtidighetskontroll används, men om du klickar på uppdateringsknappen i den andra webbläsarinstansen resulterar det i en DBConcurrencyException.

När en samtidighetsöverträdelse identifieras genereras en DBConcurrencyException

Bild 17: När en samtidighetsöverträdelse identifieras utlöses en DBConcurrencyException (Klicka om du vill visa en bild i full storlek)

DBConcurrencyException Genereras endast när DAL:s batchuppdateringsmönster används. Db-direktmönstret skapar inget undantag, det anger bara att inga rader påverkades. För att illustrera detta returnerar du båda webbläsarinstansernas GridView till deras förredigeringstillstånd. I den första webbläsarinstansen klickar du sedan på knappen Redigera och ändrar produktnamnet från "Chai Tea" tillbaka till "Chai" och klickar på Uppdatera. I det andra webbläsarfönstret klickar du på knappen Ta bort för Chai.

När du klickar på Ta bort uppdateras sidan, GridView anropar ObjectDataSource-metoden Delete() och ObjectDataSource anropar ProductsOptimisticConcurrencyBLL-klassens DeleteProduct-metod och skickar vidare de ursprungliga värdena. Det ursprungliga ProductName värdet för den andra webbläsarinstansen är "Chai Tea", som inte matchar det aktuella ProductName värdet i databasen. Därför påverkar instruktionen DELETE som utfärdas till databasen noll rader eftersom det inte finns någon post i databasen som uppfyller WHERE villkoret. Metoden DeleteProduct returnerar false och ObjectDataSources data återställs till GridView.

Från slutanvändarens perspektiv, klicka på knappen Ta bort för Chai Tea i det andra webbläsarfönstret fick skärmen att blinka och när du kommer tillbaka är produkten fortfarande där, även om den nu är listad som "Chai" (produktnamnsändringen som gjordes av den första webbläsarinstansen). Om användaren klickar på knappen Ta bort igen lyckas borttagningen, eftersom GridViews ursprungliga ProductName värde ("Chai") nu matchar värdet i databasen.

I båda dessa fall är användarupplevelsen långt ifrån idealisk. Vi vill uppenbarligen inte visa användaren den nitty-gritty informationen om DBConcurrencyException undantaget när du använder batchuppdateringsmönstret. Och beteendet när du använder db-direktmönstret är något förvirrande eftersom användarkommandot misslyckades, men det fanns ingen exakt indikation på varför.

För att åtgärda dessa två problem kan vi skapa etikettwebbkontroller på sidan som ger en förklaring till varför en uppdatering eller borttagning misslyckades. För batchuppdateringsmönstret kan vi avgöra om ett DBConcurrencyException undantag inträffade i GridViews händelsehanterare på efternivå och visa varningsetiketten efter behov. För db-direktmetoden kan vi undersöka returvärdet för BLL-metoden (vilket är true om en rad påverkades, false annars) och visa ett informationsmeddelande efter behov.

Steg 6: Lägga till informationsmeddelanden och visa dem inför en samtidighetsöverträdelse

När en samtidighetsöverträdelse inträffar beror beteendet som visas på om DAL:s batchuppdatering eller db-direktmönstret användes. I vår handledning använder vi båda mönstren: batchuppdateringsmönstret används för uppdateringar och DB-direktmönstret används för att ta bort. För att komma igång ska vi lägga till två etikettwebbkontroller på vår sida som förklarar att en samtidighetsöverträdelse inträffade när data skulle tas bort eller uppdateras. Ange etikettkontrollens Visible och EnableViewState egenskaperna till false. Detta gör att de döljs vid varje sidbesök förutom för de specifika sidbesök där deras Visible egenskap är programmatiskt inställd på 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." />

Förutom att ange egenskaperna Visible, , och EnabledViewState har jag också ställt in Text egenskapen på CssClass, vilket gör att etiketten visas i ett stort, rött, kursivt, fetstilt Warningteckensnitt. Den här CSS-klassen Warning definierades och lades till i Styles.css under självstudiekursen Undersök de händelser som är associerade med att infoga, uppdatera och radera.

När du har lagt till dessa etiketter bör designern i Visual Studio se ut ungefär som bild 18.

Två etikettkontroller har lagts till på sidan

Bild 18: Två etikettkontroller har lagts till på sidan (klicka om du vill visa en bild i full storlek)

Med de här etikettwebbkontrollerna på plats är vi redo att undersöka hur vi ska avgöra när en samtidighetsöverträdelse har inträffat. Då kan lämplig etikettsegenskap anges till Visibleoch visa informationsmeddelandettrue.

Hantera samtidighetsöverträdelser vid uppdatering

Låt oss först titta på hur du hanterar samtidighetsöverträdelser när du använder batchuppdateringsmönstret. Eftersom sådana överträdelser med batchuppdateringsmönstret orsakar ett DBConcurrencyException undantagsfel måste vi lägga till kod på vår ASP.NET sida för att avgöra om ett DBConcurrencyException undantag inträffade under uppdateringsprocessen. I så fall bör vi visa ett meddelande till användaren som förklarar att deras ändringar inte har sparats eftersom en annan användare hade ändrat samma data mellan när de började redigera posten och när de klickade på knappen Uppdatera.

Som vi såg i självstudien Hantering av BLL- och DAL-Level-undantag i en ASP.NET page kan sådana undantag identifieras och ignoreras i datawebbkontrollens händelsehanterare efter nivå. Därför måste vi skapa en händelsehanterare för GridView-händelsen RowUpdated som kontrollerar om ett DBConcurrencyException undantag har genererats. Den här händelsehanteraren skickas en referens till alla undantag som uppstod under uppdateringsprocessen, vilket visas i händelsehanterarkoden nedan:

protected void ProductsGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.Exception != null && e.Exception.InnerException != null)
    {
        if (e.Exception.InnerException is System.Data.DBConcurrencyException)
        {
            // Display the warning message and note that the
            // exception has been handled...
            UpdateConflictMessage.Visible = true;
            e.ExceptionHandled = true;
        }
    }
}

Vid ett DBConcurrencyException undantag visar den här händelsehanteraren Label-kontrollen och anger att undantaget har hanterats. När denna kod används och en samtidighetsöverträdelse inträffar vid uppdatering av en post, går användarens ändringar förlorade, eftersom de skulle ha skrivit över en annan användares modifieringar samtidigt. I synnerhet returneras GridView till dess förredigeringstillstånd och binds till aktuella databasdata. Då uppdateras GridView-raden med den andra användarens ändringar, som tidigare inte var synliga. Dessutom kommer Etikett-kontrollen att förklara för användaren vad som just hände. Den här händelsesekvensen beskrivs i bild 19.

En användares uppdateringar går förlorade inför en samtidighetsöverträdelse

Bild 19: En användares uppdateringar förloras på grund av en konkurrenskonflikt (klicka för att visa bilden i full storlek)

Anmärkning

I stället för att returnera GridView till förredigeringstillståndet kan vi också lämna GridView i redigeringstillståndet genom att ange KeepInEditMode egenskapen för det skickade GridViewUpdatedEventArgs objektet till true. Om du använder den här metoden bör du dock vara säker på att binda om data till GridView (genom att anropa dess DataBind() metod) så att den andra användarens värden läses in i redigeringsgränssnittet. Koden som är tillgänglig för nedladdning med den här handledningen har dessa två kodrader i RowUpdated händelsehanteraren kommenterade. Ta helt enkelt bort kommentarerna från dessa kodrader så att GridView förblir i redigeringsläge efter en samtidighetsöverträdelse.

Svara på samtidighetsöverträdelser vid borttagning

Med db-direktmönstret finns det inget undantag som uppstår vid en samtidighetsöverträdelse. I stället påverkar databassatsen helt enkelt inga poster, eftersom WHERE-satsen inte matchar någon post. Alla datamodifieringsmetoder som skapats i BLL har utformats så att de returnerar ett booleskt värde som anger om de påverkade exakt en post eller inte. För att avgöra om en samtidighetsöverträdelse inträffade när en post tas bort kan vi därför undersöka returvärdet för BLL-metoden DeleteProduct .

Returvärdet för en BLL-metod kan undersökas i ObjectDataSources händelsehanterare på efternivå via ReturnValue egenskapen för det objekt som ObjectDataSourceStatusEventArgs skickades till händelsehanteraren. Eftersom vi är intresserade av att fastställa returvärdet från DeleteProduct metoden måste vi skapa en händelsehanterare för ObjectDataSource-händelsen Deleted . Egenskapen ReturnValue är av typen object och kan vara null om ett undantag uppstod och metoden avbröts innan den kunde returnera ett värde. Därför bör vi först se till att egenskapen ReturnValue inte null är och är ett booleskt värde. Förutsatt att den här kontrollen godkänns visar vi DeleteConflictMessage etikettkontrollen om ReturnValue är false. Detta kan åstadkommas med hjälp av följande kod:

protected void ProductsOptimisticConcurrencyDataSource_Deleted(
    object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.ReturnValue != null && e.ReturnValue is bool)
    {
        bool deleteReturnValue = (bool)e.ReturnValue;
        if (deleteReturnValue == false)
        {
            // No row was deleted, display the warning message
            DeleteConflictMessage.Visible = true;
        }
    }
}

Vid en överträdelse av samtidigheten avbryts användarens borttagningsbegäran. GridView uppdateras och visar de ändringar som har gjorts för posten mellan den tidpunkt då användaren läste in sidan och när han eller hon klickade på knappen Ta bort. När en sådan överträdelse inträffar DeleteConflictMessage visas etiketten som förklarar vad som just hände (se bild 20).

En användares radering avbryts vid en samtidighetsöverträdelse

Bild 20: En användares borttagning avbryts på grund av en samtidighetsöverträdelse (klicka för att visa bilden i full storlek)

Sammanfattning

Det finns möjligheter till samtidighetsöverträdelser i varje program som gör att flera samtidiga användare kan uppdatera eller ta bort data. Om sådana överträdelser inte beaktas, när två användare samtidigt uppdaterar samma data "vinner" den som gör den sista ändringen och skriver över den andra användarens ändringar. Utvecklare kan också implementera antingen optimistisk eller pessimistisk samtidighetskontroll. Optimistisk samtidighetskontroll förutsätter att samtidighetsöverträdelser är ovanliga och helt enkelt inte tillåter ett uppdaterings- eller borttagningskommando som skulle utgöra en samtidighetsöverträdelse. Pessimistisk samtidighetskontroll förutsätter att samtidighetsöverträdelser är vanliga och att helt enkelt avvisa en användares uppdaterings- eller borttagningskommando är inte acceptabelt. Med pessimistisk samtidighetskontroll innebär uppdatering av en post att den låses, vilket hindrar andra användare från att ändra eller ta bort posten när den är låst.

Typed DataSet i .NET innehåller funktioner för att stödja optimistisk samtidighetskontroll. I synnerhet omfattar de UPDATE och DELETE-instruktioner som utfärdas till databasen alla tabellens kolumner, vilket säkerställer att uppdateringen eller borttagningen endast sker om postens aktuella data matchar de ursprungliga data som användaren hade när de utförde sin uppdatering eller borttagning. När DAL har konfigurerats för att stödja optimistisk samtidighet måste BLL-metoderna uppdateras. Dessutom måste den ASP.NET sidan som anropar ned till BLL konfigureras så att ObjectDataSource hämtar de ursprungliga värdena från datawebbkontrollen och skickar dem till BLL:n.

Som vi såg i den här självstudien innebär implementering av optimistisk samtidighetskontroll i en ASP.NET webbapp att DAL och BLL uppdateras och att stöd läggs till på sidan ASP.NET. Om det här tillagda arbetet är en klok investering av din tid och ansträngning beror på hur du använder det. Om du sällan har samtidiga användare som uppdaterar data, eller om de data som de uppdaterar skiljer sig från varandra, är samtidighetskontroll inte ett nyckelproblem. Men om du rutinmässigt har flera användare på webbplatsen som arbetar med samma data kan samtidighetskontroll förhindra att en användares uppdateringar eller borttagningar oavsiktligt skriver över en annans.

Lycka till med programmerandet!

Om författaren

Scott Mitchell, författare till sju ASP/ASP.NET-böcker och grundare av 4GuysFromRolla.com, har arbetat med Microsofts webbtekniker sedan 1998. Scott arbetar som oberoende konsult, tränare och författare. Hans senaste bok är Sams Teach Yourself ASP.NET 2.0 på 24 timmar. Han kan nås på mitchell@4GuysFromRolla.com.