Anteckning
Åtkomst till den här sidan kräver auktorisering. Du kan prova att logga in eller ändra kataloger.
Åtkomst till den här sidan kräver auktorisering. Du kan prova att ändra kataloger.
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.
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.
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
.
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.
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
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".
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.
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.
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 DeleteCommand
UpdateCommand
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
, ProductName
och 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.
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.
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
.
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.
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
, CategoriesBLL
och 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:
- Läs den aktuella databasproduktinformationen i en
ProductRow
instans med tableAdapter-metodenGetProductByProductID(productID)
- Tilldela de nya värdena till instansen
ProductRow
från steg 1 - Anropa TableAdapter-metoden
Update
och skicka in instansenProductRow
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:
- Läs den aktuella databasproduktinformationen i en
ProductsOptimisticConcurrencyRow
instans med tableAdapter-metodenGetProductByProductID(productID)
- Tilldela de ursprungliga värdena till instansen
ProductsOptimisticConcurrencyRow
från steg 1 - Anropa instansens
ProductsOptimisticConcurrencyRow
AcceptChanges()
metod, som instruerar DataRow att dess aktuella värden är de "ursprungliga" värdena - Tilldela de nya värdena till instansen
ProductsOptimisticConcurrencyRow
- Anropa TableAdapter-metoden
Update
och skicka in instansenProductsOptimisticConcurrencyRow
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 ProductsOptimisticConcurrencyRow
AcceptChanges()
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.
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_productName
namnet , original_supplierID
och så vidare, kan du lämna egenskapsvärdet OldValuesParameterFormatString
som original_{0}
. Men om BLL-metodernas indataparametrar hade namn som old_productName
, old_supplierID
och 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
, ochCategoryName
BoundFields - Konverterade
ProductName
BoundField till ett TemplateField och lade till en RequiredFieldValidation-kontroll. - Konverterade
CategoryID
ochSupplierID
BoundFields till TemplateFields och justerade redigeringsgränssnittet för att använda rullgardinslistor i stället för textfält. I dessa TemplateFieldsItemTemplates
visas datafältenCategoryName
ochSupplierName
. - Konverterade
UnitPrice
,UnitsInStock
,UnitsOnOrder
ochReorderLevel
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.
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
.
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 iItemTemplate
, men användEval
-nyckelordet för att göra detta. Kom ihåg attEval
utför enkelriktad databindning. Vi måste fortfarande angeUnitPrice
värdet för de ursprungliga värdena, så vi behöver fortfarande en tvåvägs-databinding-instruktion iItemTemplate
, men detta kan placeras i en etikettwebbkontroll varsVisible
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 GridViewsRowDataBound
händelsehanterare får du programmatiskt åtkomst till den etikettwebbkontroll därUnitPrice
värdet visas och anger dessText
egenskap till den formaterade versionen. - Lämna
UnitPrice
formaterat som valuta. I GridViewsRowDeleting
händelsehanterare ersätter du det befintliga ursprungligaUnitPrice
värdet (19,95 USD) med ett faktiskt decimalvärde med .Decimal.Parse
Vi såg hur man kan uppnå något liknande iRowUpdating
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
.
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.
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 Warning
teckensnitt. 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.
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 Visible
och 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.
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).
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.