Implementace optimistického řízení souběžnosti (C#)
U webové aplikace, která umožňuje úpravu dat více uživatelům, existuje riziko, že dva uživatelé můžou upravovat stejná data současně. V tomto kurzu implementujeme optimistické řízení souběžnosti, abychom toto riziko zvládli.
Úvod
U webových aplikací, které umožňují jenom uživatelům zobrazit data, nebo u aplikací, které obsahují jenom jednoho uživatele, který může data upravovat, nehrožuje, že by se dva souběžní uživatelé omylem přepsaly navzájem. U webových aplikací, které umožňují více uživatelům aktualizovat nebo odstraňovat data, je však možné, že úpravy jednoho uživatele budou kolidovat s dalšími souběžnými uživateli. Bez použití zásad souběžnosti, když dva uživatelé současně upravují jeden záznam, uživatel, který změny potvrdí jako poslední, přepíše změny provedené prvním uživatelem.
Představte si například, že dva uživatelé, Jisun a Sam, navštívili stránku v naší aplikaci, která návštěvníkům umožňovala aktualizovat a odstraňovat produkty prostřednictvím ovládacího prvku GridView. Oba kliknou na tlačítko Upravit v zobrazení GridView přibližně ve stejnou dobu. Jisun změní název produktu na Chai Tea a klikne na tlačítko Aktualizovat. Čistý výsledek je příkaz UPDATE
, který se odešle do databáze, který nastaví všechna aktualizovatelná pole produktu (i když Jisun aktualizoval pouze jedno pole, ProductName
). V tomto okamžiku má databáze hodnoty "Čaj Chai", kategorii Nápoje, dodavatel Exotické kapaliny a tak dále pro tento konkrétní produkt. Nicméně, GridView na obrazovce Sam stále zobrazuje název produktu v upravitelném řádku GridView jako "Chai". Několik sekund po potvrzení změn Jisun sam aktualizuje kategorii na Condiments a klikne na Aktualizovat. Výsledkem je UPDATE
odeslání příkazu do databáze, který nastaví název produktu na "Chai" CategoryID
, id odpovídající kategorie Nápoje atd. Změny názvu produktu společnosti Jisun byly přepsány. Obrázek 1 graficky znázorňuje tuto řadu událostí.
Obrázek 1: Když dva uživatelé současně aktualizují záznam, jeden uživatel může změnit ostatní uživatele (kliknutím zobrazíte obrázek v plné velikosti)
Podobně platí, že když stránku navštíví dva uživatelé, může být jeden uživatel uprostřed aktualizace záznamu, když ho odstraní jiný uživatel. Nebo mezi tím, když uživatel načte stránku a klikne na tlačítko Odstranit, může obsah tohoto záznamu změnit jiný uživatel.
K dispozici jsou tři strategie řízení souběžnosti :
- Nedělat nic – pokud souběžní uživatelé upravují stejný záznam, nechte poslední potvrzení vyhrát (výchozí chování).
- Optimistická souběžnost – předpokládejme, že i když občas může docházet ke konfliktům souběžnosti, v drtivé většině případů k takovým konfliktům nedojde; proto pokud dojde ke konfliktu, jednoduše informujte uživatele, že jeho změny nelze uložit, protože jiný uživatel upravil stejná data.
- Pesimistická souběžnost – předpokládejme, že konflikty souběžnosti jsou běžné a že uživatelé nebudou tolerovat oznámení, že jejich změny nebyly uloženy kvůli souběžné aktivitě jiného uživatele; proto, když jeden uživatel začne aktualizovat záznam, zamkněte ho, čímž zabráníte všem ostatním uživatelům v úpravách nebo odstranění záznamu, dokud uživatel nezapíše své úpravy.
Všechny naše kurzy dosud používaly výchozí strategii řešení souběžnosti – konkrétně jsme nechali poslední zápis win. V tomto kurzu prozkoumáme, jak implementovat optimistické řízení souběžnosti.
Poznámka
V této sérii kurzů se nebudeme dívat na příklady pesimistické souběžnosti. Pesimistická souběžnost se používá jen zřídka, protože takové zámky, pokud nejsou správně zrušené, můžou ostatním uživatelům zabránit v aktualizaci dat. Pokud například uživatel zamkne záznam pro úpravy a potom odejde na den před jeho odemknutím, žádný jiný uživatel nebude moct tento záznam aktualizovat, dokud původní uživatel nevrátí a nedokončí aktualizaci. Proto v situacích, kdy se používá pesimistická souběžnost, obvykle dochází k vypršení časového limitu, který v případě dosažení zámku zruší. Příkladem pesimistické kontroly souběžnosti jsou weby prodeje lístků, které na krátkou dobu zamknou konkrétní místo k sezení, zatímco uživatel dokončí proces objednávky.
Krok 1: Jak se implementuje optimistická souběžnost
Optimistické řízení souběžnosti funguje tak, že zajišťuje, aby aktualizovaný nebo odstraněný záznam měl stejné hodnoty jako při zahájení procesu aktualizace nebo odstraňování. Když například kliknete na tlačítko Upravit v upravitelném objektu GridView, hodnoty záznamu se načtou z databáze a zobrazí se v textových polích a dalších webových ovládacích prvcích. Tyto původní hodnoty jsou uloženy objektem GridView. Později, jakmile uživatel provede změny a klikne na tlačítko Aktualizovat, se původní hodnoty a nové hodnoty odešlou do vrstvy obchodní logiky a pak dolů do vrstvy přístupu k datům. Vrstva přístupu k datům musí vydat příkaz SQL, který aktualizuje záznam pouze v případě, že původní hodnoty, které uživatel začal upravovat, jsou stejné jako hodnoty, které jsou stále v databázi. Obrázek 2 znázorňuje tuto posloupnost událostí.
Obrázek 2: Pro úspěšné aktualizace nebo odstranění musí být původní hodnoty stejné jako aktuální hodnoty databáze (kliknutím zobrazíte obrázek v plné velikosti)
Existují různé přístupy k implementaci optimistické souběžnosti (podívejte se na řadu možností v článku Optimistic concurrency v článku Peter A. Bromberg). Sada ADO.NET Typed DataSet poskytuje jednu implementaci, kterou lze nakonfigurovat pouze zaškrtnutím políčka. Povolení optimistické souběžnosti pro TableAdapter v Typed DataSet rozšíří příkazy TableAdapter UPDATE
a DELETE
tak, aby zahrnovaly porovnání všech původních hodnot v klauzuli WHERE
. Následující UPDATE
příkaz například aktualizuje název a cenu produktu pouze v případě, že aktuální hodnoty databáze jsou rovny hodnotám, které byly původně načteny při aktualizaci záznamu v GridView. Parametry @ProductName
a @UnitPrice
obsahují nové hodnoty zadané uživatelem, zatímco @original_ProductName
a @original_UnitPrice
obsahují hodnoty, které byly původně načteny do GridView při kliknutí na tlačítko Upravit:
UPDATE Products SET
ProductName = @ProductName,
UnitPrice = @UnitPrice
WHERE
ProductID = @original_ProductID AND
ProductName = @original_ProductName AND
UnitPrice = @original_UnitPrice
Poznámka
Tento UPDATE
příkaz byl zjednodušen pro čitelnost. V praxi by kontrola v klauzuli byla více zapojena, UnitPrice
protože UnitPrice
může obsahovat NULL
s a kontrola, jestli NULL = NULL
vždy vrátí hodnotu False (místo toho musíte použít IS NULL
).WHERE
Kromě použití jiného podkladového UPDATE
příkazu konfigurace objektu TableAdapter tak, aby používal optimistickou souběžnost, také upraví podpis přímých metod databáze. Vzpomeňte si z našeho prvního kurzu Vytvoření vrstvy přístupu k datům, že přímé metody databáze byly ty, které jako vstupní parametry přijímají seznam skalárních hodnot (místo instance DataRow nebo DataTable se silným typem). Při použití optimistické souběžnosti zahrnují přímé Update()
a Delete()
metody databáze také vstupní parametry pro původní hodnoty. Kromě toho je nutné změnit také kód v BLL pro použití vzoru dávkové aktualizace ( Update()
přetížení metody, která přijímají DataRows a DataTables místo skalárních hodnot).
Místo toho, abychom stávající objekty TableAdapter dal rozšířili tak, aby používali optimistickou souběžnost (což by vyžadovalo změnu BLL tak, aby vyhovovalo), vytvoříme novou typovou datovou sadu s názvem NorthwindOptimisticConcurrency
, do které přidáme Products
objekt TableAdapter využívající optimistickou souběžnost. Potom vytvoříme ProductsOptimisticConcurrencyBLL
třídu vrstvy obchodní logiky, která bude mít odpovídající úpravy pro podporu optimistické souběžnosti DAL. Po položení tohoto základu budeme připraveni vytvořit ASP.NET stránku.
Krok 2: Vytvoření vrstvy přístupu k datům, která podporuje optimistickou souběžnost
Pokud chcete vytvořit novou typovou datovou sadu, klikněte pravým tlačítkem na DAL
složku ve App_Code
složce a přidejte novou datovou sadu s názvem NorthwindOptimisticConcurrency
. Jak jsme viděli v prvním kurzu, přidá se do sady Typed DataSet nový objekt TableAdapter a automaticky se spustí Průvodce konfigurací objektu TableAdapter. Na první obrazovce se zobrazí výzva k zadání databáze, ke které se má připojit – připojte se ke stejné databázi Northwind pomocí NORTHWNDConnectionString
nastavení z Web.config
.
Obrázek 3: Připojení k databázi Same Northwind (kliknutím zobrazíte obrázek v plné velikosti)
Dále se zobrazí výzva k dotazování na data: prostřednictvím ad hoc příkazu SQL, nové uložené procedury nebo existující uložené procedury. Vzhledem k tomu, že jsme v původním DAL použili ad hoc dotazy SQL, použijte tuto možnost i tady.
Obrázek 4: Určení dat, která se mají načíst pomocí ad hoc příkazu SQL (kliknutím zobrazíte obrázek v plné velikosti)
Na následující obrazovce zadejte dotaz SQL, který se má použít k načtení informací o produktu. Použijeme úplně stejný dotaz SQL, který se používá pro Products
TableAdapter z původního Product
DAL, který vrátí všechny sloupce spolu s názvy dodavatelů a kategorií produktu:
SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
(SELECT CategoryName FROM Categories
WHERE Categories.CategoryID = Products.CategoryID)
as CategoryName,
(SELECT CompanyName FROM Suppliers
WHERE Suppliers.SupplierID = Products.SupplierID)
as SupplierName
FROM Products
Obrázek 5: Použití stejného dotazu SQL z Products
TableAdapter v původní dal (kliknutím zobrazíte obrázek v plné velikosti)
Před přechodem na další obrazovku klikněte na tlačítko Upřesnit možnosti. Pokud chcete, aby tento objekt TableAdapter používal řízení optimistické souběžnosti, jednoduše zaškrtněte políčko Použít optimistickou souběžnost.
Obrázek 6: Povolení řízení optimistické souběžnosti zaškrtnutím políčka "Použít optimistickou souběžnost" (kliknutím zobrazíte obrázek v plné velikosti)
Nakonec uveďte, že tableAdapter by měl používat vzory přístupu k datům, které vyplní DataTable a vrátí DataTable; také uveďte, že by se měly vytvořit přímé metody databáze. Změňte název metody pro vzor Return a DataTable z GetData na GetProducts, aby se zrcadlily konvence vytváření názvů, které jsme použili v naší původní dal.
Obrázek 7: Nechte, aby tableAdapter využíval všechny vzory přístupu k datům (kliknutím zobrazíte obrázek v plné velikosti)
Po dokončení průvodce bude Designer DataSet obsahovat dataTable se silným typem Products
a TableAdapter. Udělejte chvíli a přejmenujte datatable z Products
na ProductsOptimisticConcurrency
, což můžete udělat tak, že kliknete pravým tlačítkem na záhlaví DataTable a v místní nabídce zvolíte Přejmenovat.
Obrázek 8: DataTable a TableAdapter byly přidány do typed dataset (Kliknutím zobrazíte obrázek v plné velikosti)
Pokud chcete zobrazit rozdíly mezi UPDATE
dotazy a DELETE
mezi ProductsOptimisticConcurrency
objektem TableAdapter (který používá optimistickou souběžnost) a objektem TableAdapter produktů (který ne), klikněte na objekt TableAdapter a přejděte na okno Vlastnosti. DeleteCommand
V podprodukcích vlastností CommandText
a UpdateCommand
můžete vidět skutečnou syntaxi SQL, která se odesílá do databáze při vyvolání metod aktualizace nebo odstranění souvisejících s dal. ProductsOptimisticConcurrency
Pro TableAdapter se DELETE
používá příkaz:
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))
Vzhledem k tomu, že DELETE
příkaz product TableAdapter v našem původním DAL je mnohem jednodušší:
DELETE FROM [Products] WHERE (([ProductID] = @Original_ProductID))
Jak vidíte, WHERE
klauzule v DELETE
příkazu tableAdapter, která používá optimistickou souběžnost, obsahuje porovnání mezi hodnotami existujících sloupců každé tabulky Product
a původními hodnotami v době posledního naplnění Objekt GridView (nebo DetailsView nebo FormView). Vzhledem k tomu, že všechna pole kromě , a mohou obsahovat NULL
hodnoty, jsou zahrnuty další parametry a kontroly pro správné porovnání NULL
hodnot v klauzuliWHERE
.Discontinued
ProductName
ProductID
Do sady Dat s podporou optimistické souběžnosti pro účely tohoto kurzu nebudeme přidávat žádné další tabulky DataTables, protože naše ASP.NET stránka bude poskytovat pouze aktualizace a odstraňování informací o produktu. Stále však potřebujeme přidat metodu GetProductByProductID(productID)
do ProductsOptimisticConcurrency
TableAdapter.
Chcete-li toho dosáhnout, klikněte pravým tlačítkem na záhlaví objektu TableAdapter (oblast přímo nad Fill
názvy metod a GetProducts
) a v místní nabídce zvolte Přidat dotaz. Tím se spustí Průvodce konfigurací dotazu tableadapter. Stejně jako u počáteční konfigurace objektu TableAdapter se rozhodnete vytvořit metodu GetProductByProductID(productID)
pomocí ad hoc příkazu SQL (viz obrázek 4). Vzhledem k tomu, GetProductByProductID(productID)
že metoda vrací informace o konkrétním produktu, indikujte, že tento dotaz je SELECT
typ dotazu, který vrací řádky.
Obrázek 9: Označení typu dotazu jako "SELECT
, který vrací řádky" (kliknutím zobrazíte obrázek v plné velikosti)
Na další obrazovce se zobrazí výzva k použití dotazu SQL s předem načteným výchozím dotazem tableAdapter. Rozšiřte existující dotaz tak, aby zahrnoval klauzuli WHERE ProductID = @ProductID
, jak je znázorněno na obrázku 10.
Obrázek 10: Přidání WHERE
klauzule do předem načteného dotazu pro vrácení určitého záznamu o produktu (kliknutím zobrazíte obrázek v plné velikosti)
Nakonec změňte vygenerované názvy metod na FillByProductID
a GetProductByProductID
.
Obrázek 11: Přejmenujte metody na FillByProductID
a GetProductByProductID
(kliknutím zobrazíte obrázek v plné velikosti)
Po dokončení tohoto průvodce nyní objekt TableAdapter obsahuje dvě metody pro načtení dat: GetProducts()
, která vrací všechny produkty; a GetProductByProductID(productID)
, které vrací zadaný produkt.
Krok 3: Vytvoření vrstvy obchodní logiky pro optimistickou Concurrency-Enabled DAL
Naše stávající ProductsBLL
třída obsahuje příklady použití vzorců dávkové aktualizace i přímé databáze. Metoda AddProduct
i UpdateProduct
přetížení používají vzor dávkové aktualizace a předávají ProductRow
instanci metodě Update objektu TableAdapter. Metoda DeleteProduct
na druhé straně používá přímý vzor databáze a volá metodu TableAdapter Delete(productID)
.
S novým ProductsOptimisticConcurrency
objektem TableAdapter nyní přímé metody databáze vyžadují, aby byly předány také původní hodnoty. Metoda teď například Delete
očekává deset vstupních parametrů: původní ProductID
parametry , , ProductName
, SupplierID
, CategoryID
, QuantityPerUnit
, UnitsOnOrder
ReorderLevel
UnitPrice
UnitsInStock
a .Discontinued
Používá tyto další hodnoty vstupních DELETE
parametrů v WHERE
klauzuli příkazu odeslaného do databáze a odstraní zadaný záznam pouze v případě, že se aktuální hodnoty databáze mapují na původní hodnoty.
I když se podpis metody pro metodu TableAdapter Update
použitou ve vzoru dávkové aktualizace nezměnil, kód potřebný k zaznamenání původních a nových hodnot ano. Proto místo toho, abychom se pokusili použít dal s optimistickou souběžností s naší stávající ProductsBLL
třídou, vytvoříme novou třídu Vrstvy obchodní logiky pro práci s naším novým DAL.
Do složky ve App_Code
složce přidejte třídu s názvem .ProductsOptimisticConcurrencyBLL
BLL
Obrázek 12: Přidání ProductsOptimisticConcurrencyBLL
třídy do složky BLL
Dále do třídy přidejte následující kód ProductsOptimisticConcurrencyBLL
:
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();
}
}
Všimněte si příkazu using NorthwindOptimisticConcurrencyTableAdapters
nad začátkem deklarace třídy. Obor NorthwindOptimisticConcurrencyTableAdapters
názvů obsahuje ProductsOptimisticConcurrencyTableAdapter
třídu , která poskytuje metody DAL. Také před deklarací třídy najdete System.ComponentModel.DataObject
atribut , který sadě Visual Studio dává pokyn, aby tuto třídu zahrnula do rozevíracího seznamu průvodce ObjectDataSource.
Adapter
Vlastnost ProductsOptimisticConcurrencyBLL
poskytuje rychlý přístup k instanci ProductsOptimisticConcurrencyTableAdapter
třídy a řídí se vzorem použitým v našich původních třídách BLL (ProductsBLL
, CategoriesBLL
atd.). GetProducts()
Nakonec metoda jednoduše zavolá metodu DAL GetProducts()
a vrátí ProductsOptimisticConcurrencyDataTable
objekt naplněný ProductsOptimisticConcurrencyRow
instancí pro každý záznam produktu v databázi.
Odstranění produktu s využitím modelu přímé databáze s optimistickou souběžností
Při použití modelu DB direct proti dal, který používá optimistickou souběžnost, je nutné metodám předat nové a původní hodnoty. Pro odstranění neexistují žádné nové hodnoty, takže je potřeba předat pouze původní hodnoty. V naší BLL pak musíme přijmout všechny původní parametry jako vstupní parametry. Pojďme, aby DeleteProduct
metoda ve ProductsOptimisticConcurrencyBLL
třídě používala přímou metodu DATABÁZE. To znamená, že tato metoda musí jako vstupní parametry převzít všech deset polí s daty o produktech a předat je do DAL, jak je znázorněno v následujícím kódu:
[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;
}
Pokud se původní hodnoty ( hodnoty, které byly naposledy načteny do objektu GridView) (nebo DetailsView nebo FormView) – liší od hodnot v databázi, když uživatel klikne na tlačítko Odstranit, WHERE
nebude klauzule odpovídat žádnému záznamu databáze a nebude to mít vliv na žádné záznamy. Proto metoda TableAdapter Delete
vrátí 0
a metoda BLL DeleteProduct
vrátí false
.
Aktualizace produktu pomocí vzoru dávkové aktualizace s optimistickou souběžností
Jak bylo uvedeno dříve, metoda TableAdapter Update
pro vzor dávkové aktualizace má stejný podpis metody bez ohledu na to, zda je nebo není použit optimistická souběžnost. Update
Konkrétně metoda očekává DataRow, pole DataRows, DataTable nebo Typed DataSet. Pro zadání původních hodnot nejsou k dispozici žádné další vstupní parametry. To je možné, protože DataTable uchovává informace o původních a upravených hodnotách pro své dataRow(s). Když DAL vydá svůj UPDATE
příkaz, @original_ColumnName
parametry se naplní původními hodnotami DataRow, zatímco @ColumnName
parametry se naplní upravenými hodnotami DataRow.
ProductsBLL
Ve třídě (která používá původní neoptimalizační souběžnost DAL) náš kód při použití vzoru dávkové aktualizace k aktualizaci informací o produktu provádí následující posloupnost událostí:
- Načtení informací o produktu aktuální databáze do
ProductRow
instance pomocí metody TableAdapterGetProductByProductID(productID)
- Přiřazení nových hodnot k
ProductRow
instanci z kroku 1 - Volání metody TableAdapter
Update
a předání instanceProductRow
Tato posloupnost kroků však nebude správně podporovat optimistickou souběžnost, protože ProductRow
naplněný v kroku 1 je naplněný přímo z databáze, což znamená, že původní hodnoty používané objektem DataRow jsou ty, které aktuálně existují v databázi, a ne hodnoty, které byly vázané na GridView na začátku procesu úprav. Místo toho při použití dal s povolenou optimistickou souběžností musíme změnit UpdateProduct
přetížení metody tak, aby používala následující kroky:
- Načtení informací o produktu aktuální databáze do
ProductsOptimisticConcurrencyRow
instance pomocí metody TableAdapterGetProductByProductID(productID)
- Přiřazení původních hodnot k
ProductsOptimisticConcurrencyRow
instanci z kroku 1 ProductsOptimisticConcurrencyRow
Volání metody instanceAcceptChanges()
, která dává objektu DataRow pokyn, že aktuální hodnoty jsou původní hodnoty.- Přiřazení nových hodnot k
ProductsOptimisticConcurrencyRow
instanci - Volání metody TableAdapter
Update
a předání instanceProductsOptimisticConcurrencyRow
Krok 1 načte všechny aktuální hodnoty databáze pro zadaný záznam produktu. Tento krok je nadbytečný v UpdateProduct
přetížení, které aktualizuje všechny sloupce produktu (protože tyto hodnoty jsou přepsány v kroku 2), ale je nezbytný pro přetížení, kde se jako vstupní parametry předává pouze podmnožina hodnot sloupců. Jakmile jsou původní hodnoty přiřazeny instanci ProductsOptimisticConcurrencyRow
, AcceptChanges()
je volána metoda, která označí aktuální hodnoty DataRow jako původní hodnoty, které se mají použít v parametrech @original_ColumnName
v UPDATE
příkazu . Dále jsou nové hodnoty parametrů přiřazeny k ProductsOptimisticConcurrencyRow
a nakonec Update
je vyvolána metoda, která předává DataRow.
Následující kód ukazuje UpdateProduct
přetížení, které přijímá všechna pole dat produktu jako vstupní parametry. I když se zde nezobrazuje, ProductsOptimisticConcurrencyBLL
třída zahrnutá ve stažení pro tento kurz obsahuje UpdateProduct
také přetížení, které jako vstupní parametry přijímá pouze název produktu a cenu.
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;
}
Krok 4: Předání původních a nových hodnot ze stránky ASP.NET metodám BLL
Po dokončení DAL a BLL zbývá jen vytvořit ASP.NET stránku, která může využívat logiku optimistické souběžnosti integrovanou v systému. Konkrétně data Web ovládací prvek (GridView, DetailsView nebo FormView) si musí pamatovat své původní hodnoty a ObjectDataSource musí předat obě sady hodnot do vrstvy obchodní logiky. Kromě toho musí být stránka ASP.NET nakonfigurovaná tak, aby řádně zpracovávala narušení souběžnosti.
Začněte otevřením OptimisticConcurrency.aspx
stránky ve EditInsertDelete
složce a přidáním Objektu GridView do Designer nastavením jeho ID
vlastnosti na ProductsGrid
. Z inteligentní značky GridView zvolte vytvoření nového objektu ObjectDataSource s názvem ProductsOptimisticConcurrencyDataSource
. Vzhledem k tomu, že chceme, aby objekt ObjectDataSource používal dal, který podporuje optimistickou souběžnost, nakonfigurujte ho ProductsOptimisticConcurrencyBLL
tak, aby používal objekt .
Obrázek 13: Nechte objekt ObjectDataSource použít ProductsOptimisticConcurrencyBLL
objekt (kliknutím zobrazíte obrázek v plné velikosti)
V průvodci GetProducts
vyberte metody , UpdateProduct
a DeleteProduct
z rozevíracích seznamů. Pro metodu UpdateProduct použijte přetížení, které přijímá všechna datová pole produktu.
Konfigurace vlastností ovládacího prvku ObjectDataSource
Po dokončení průvodce by deklarativní kód ObjectDataSource měl vypadat takto:
<asp:ObjectDataSource ID="ProductsOptimisticConcurrencyDataSource" runat="server"
DeleteMethod="DeleteProduct" OldValuesParameterFormatString="original_{0}"
SelectMethod="GetProducts" TypeName="ProductsOptimisticConcurrencyBLL"
UpdateMethod="UpdateProduct">
<DeleteParameters>
<asp:Parameter Name="original_productID" Type="Int32" />
<asp:Parameter Name="original_productName" Type="String" />
<asp:Parameter Name="original_supplierID" Type="Int32" />
<asp:Parameter Name="original_categoryID" Type="Int32" />
<asp:Parameter Name="original_quantityPerUnit" Type="String" />
<asp:Parameter Name="original_unitPrice" Type="Decimal" />
<asp:Parameter Name="original_unitsInStock" Type="Int16" />
<asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
<asp:Parameter Name="original_reorderLevel" Type="Int16" />
<asp:Parameter Name="original_discontinued" Type="Boolean" />
</DeleteParameters>
<UpdateParameters>
<asp:Parameter Name="productName" Type="String" />
<asp:Parameter Name="supplierID" Type="Int32" />
<asp:Parameter Name="categoryID" Type="Int32" />
<asp:Parameter Name="quantityPerUnit" Type="String" />
<asp:Parameter Name="unitPrice" Type="Decimal" />
<asp:Parameter Name="unitsInStock" Type="Int16" />
<asp:Parameter Name="unitsOnOrder" Type="Int16" />
<asp:Parameter Name="reorderLevel" Type="Int16" />
<asp:Parameter Name="discontinued" Type="Boolean" />
<asp:Parameter Name="productID" Type="Int32" />
<asp:Parameter Name="original_productName" Type="String" />
<asp:Parameter Name="original_supplierID" Type="Int32" />
<asp:Parameter Name="original_categoryID" Type="Int32" />
<asp:Parameter Name="original_quantityPerUnit" Type="String" />
<asp:Parameter Name="original_unitPrice" Type="Decimal" />
<asp:Parameter Name="original_unitsInStock" Type="Int16" />
<asp:Parameter Name="original_unitsOnOrder" Type="Int16" />
<asp:Parameter Name="original_reorderLevel" Type="Int16" />
<asp:Parameter Name="original_discontinued" Type="Boolean" />
<asp:Parameter Name="original_productID" Type="Int32" />
</UpdateParameters>
</asp:ObjectDataSource>
Jak vidíte, DeleteParameters
kolekce obsahuje Parameter
instanci pro každý z deseti vstupních parametrů v ProductsOptimisticConcurrencyBLL
metodě třídy DeleteProduct
. UpdateParameters
Podobně kolekce obsahuje Parameter
instanci pro každý vstupní parametr v UpdateProduct
souboru .
V předchozích kurzech, které zahrnovaly úpravy dat, bychom v tuto chvíli odebrali vlastnost ObjectDataSource OldValuesParameterFormatString
, protože tato vlastnost označuje, že metoda BLL očekává předání starých (nebo původních) hodnot i nových hodnot. Tato hodnota vlastnosti navíc označuje názvy vstupních parametrů pro původní hodnoty. Vzhledem k tomu, že do BLL předáváme původní hodnoty, tuto vlastnost neodstraňujte .
Poznámka
Hodnota vlastnosti se musí mapovat na názvy vstupních OldValuesParameterFormatString
parametrů v BLL, které očekávají původní hodnoty. Vzhledem k tomu, original_supplierID
že jsme tyto parametry original_productName
pojmenovali , atd., můžete ponechat OldValuesParameterFormatString
hodnotu vlastnosti jako original_{0}
. Pokud by však vstupní parametry metod BLL měly názvy jako old_productName
, old_supplierID
atd., museli byste vlastnost aktualizovat OldValuesParameterFormatString
na old_{0}
.
Existuje jedno poslední nastavení vlastnosti, které je potřeba provést, aby ObjectDataSource správně předal původní hodnoty metodám BLL. ObjectDataSource má vlastnost ConflictDetection , kterou lze přiřadit k jedné ze dvou hodnot:
OverwriteChanges
- výchozí hodnota; neodesílá původní hodnoty původním vstupním parametrům metod BLL.CompareAllValues
- odesílá původní hodnoty metodám BLL; tuto možnost zvolte při použití optimistické souběžnosti.
Nastavte ConflictDetection
vlastnost na CompareAllValues
.
Konfigurace vlastností a polí objektu GridView
Když jsou vlastnosti ObjectDataSource správně nakonfigurované, obraťme pozornost na nastavení Objektu GridView. Protože chceme, aby objekt GridView podporoval úpravy a odstraňování, klikněte na zaškrtávací políčka Povolit úpravy a Povolit odstranění u inteligentní značky GridView. Tím se přidá commandfield, jehož ShowEditButton
a ShowDeleteButton
jsou nastavené na true
.
Při vázání na ProductsOptimisticConcurrencyDataSource
ObjectDataSource GridView obsahuje pole pro každé z datových polí produktu. I když takový GridView lze upravovat, uživatelské prostředí je cokoli, jen ne přijatelné. Pole CategoryID
a SupplierID
BoundField se vykreslí jako textová pole, která vyžadují, aby uživatel zadal příslušnou kategorii a dodavatele jako identifikační čísla. Číselná pole nebudou formátována a nebudou k dispozici žádné ověřovací ovládací prvky, které by zajistily, že byl zadán název produktu a že jednotková cena, skladové jednotky, jednotky v objednávce a hodnoty na úrovni změny objednávky jsou správné číselné hodnoty a jsou větší než nebo rovny nule.
Jak jsme probrali v kurzech Přidání ověřovacích ovládacích prvků do úprav a vkládání rozhraní a Přizpůsobení rozhraní pro úpravy dat , uživatelské rozhraní lze přizpůsobit nahrazením BoundFields templateFields. Upravil(a) jsem tento GridView a jeho rozhraní pro úpravy následujícími způsoby:
- Odebrání
ProductID
vázaných polí ,SupplierName
aCategoryName
- Převeďte
ProductName
BoundField na TemplateField a přidali ovládací prvek RequiredFieldValidation. - Převeďte
CategoryID
objekty aSupplierID
BoundFields na TemplateFields a upravili jste rozhraní pro úpravy tak, aby místo textových polí používalo DropDownLists. V těchto polích šablon jsouItemTemplates
zobrazenaCategoryName
datová pole aSupplierName
. - Převeďte pole
UnitPrice
,UnitsInStock
,UnitsOnOrder
aReorderLevel
BoundFields na TemplateFields a přidali ovládací prvky CompareValidator.
Vzhledem k tomu, že jsme už v předchozích kurzech prozkoumali, jak tyto úlohy provést, vypíšeme tady jenom konečnou deklarativní syntaxi a nechám implementaci jako praktickou.
<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>
Jsme velmi blízko k tomu, abychom měli plně funkční příklad. Existuje však několik jemností, které se vynoří a způsobí nám problémy. Kromě toho stále potřebujeme rozhraní, které uživatele upozorní, když dojde k narušení souběžnosti.
Poznámka
Aby ovládací prvek data Web správně předával původní hodnoty objectDataSource (které jsou pak předány do BLL), je důležité, aby vlastnost GridView EnableViewState
je nastavena na true
(výchozí). Pokud zakážete stav zobrazení, původní hodnoty se při zpětném odeslání ztratí.
Předání správných původních hodnot do ObjectDataSource
Existuje několik problémů se způsobem konfigurace objektu GridView. Pokud objectDataSource ConflictDetection
vlastnost je nastavena na CompareAllValues
(stejně jako naše), když ObjectDataSource Update()
Delete()
nebo metody jsou vyvolány GridView (nebo DetailsView nebo FormView), ObjectDataSource se pokusí zkopírovat původní hodnoty GridView do příslušných Parameter
instancí. Grafické znázornění tohoto procesu najdete na obrázku 2.
Konkrétně původní hodnoty GridView jsou přiřazeny hodnoty v obousměrných příkazech databinding pokaždé, když jsou data vázána na GridView. Proto je nezbytné, aby všechny požadované původní hodnoty byly zachyceny pomocí obousměrné vazby dat a aby byly poskytovány v konvertibilním formátu.
Pokud chcete zjistit, proč je to důležité, navštivte naši stránku v prohlížeči. Podle očekávání gridView zobrazí seznam jednotlivých produktů s tlačítky Upravit a Odstranit ve sloupci úplně vlevo.
Obrázek 14: Produkty jsou uvedené v zobrazení GridView (kliknutím zobrazíte obrázek v plné velikosti)
Pokud kliknete na tlačítko Odstranit u libovolného produktu, vyvolá se FormatException
příkaz .
Obrázek 15: Pokus o odstranění všech výsledků produktu v souboru (kliknutím zobrazíte obrázek vFormatException
plné velikosti)
Je FormatException
vyvolána, když ObjectDataSource se pokusí číst v původní UnitPrice
hodnotě. ItemTemplate
Protože má UnitPrice
formát měna (<%# Bind("UnitPrice", "{0:C}") %>
), obsahuje symbol měny, například 19,95 USD. Dojde FormatException
k tomu, když se ObjectDataSource pokusí převést tento řetězec na decimal
. Abychom tento problém obešli, máme několik možností:
- Odeberte formátování měny z objektu
ItemTemplate
. To znamená, že místo použití<%# Bind("UnitPrice", "{0:C}") %>
použijte jednoduše .<%# Bind("UnitPrice") %>
Nevýhodou je, že cena už není formátovaná. - Zobrazí formátovanou
UnitPrice
jako měnu vItemTemplate
souboru , ale k tomu použijteEval
klíčové slovo. Vzpomeňte si, žeEval
provádí jednosměrné vazby dat. Stále potřebujeme zadatUnitPrice
hodnotu pro původní hodnoty, takže budeme stále potřebovat obousměrný příkaz databinding vItemTemplate
, ale ten lze umístit do ovládacího prvku Label Web, jehožVisible
vlastnost je nastavena nafalse
. V šabloně ItemTemplate můžeme použít následující kód:
<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>
- Pomocí příkazu odeberte formátování měny z objektu
ItemTemplate
<%# Bind("UnitPrice") %>
. V obslužné rutiněRowDataBound
události objektu GridView programově přejděte k ovládacímu prvku Label Web, ve kterémUnitPrice
je hodnota zobrazena, a nastavte jehoText
vlastnost na formátovanou verzi. - Ponechte formátovanou
UnitPrice
jako měnu. V obslužné rutině události GridViewRowDeleting
nahraďte existující původníUnitPrice
hodnotu ($ 19,95) skutečnou desetinnou hodnotou pomocíDecimal.Parse
. Viděli jsme, jak provést něco podobnéhoRowUpdating
v obslužné rutině události v kurzu Zpracování výjimek BLL a DAL-Level v ASP.NET Stránce .
Pro můj příklad jsem se rozhodl použít druhý přístup, přidání skrytého ovládacího prvku Label Web, jehož Text
vlastnost je obousměrná data svázaná s neformátovanou UnitPrice
hodnotou.
Po vyřešení tohoto problému zkuste u libovolného produktu znovu kliknout na tlačítko Odstranit. Tentokrát se zobrazí, InvalidOperationException
když se ObjectDataSource pokusí vyvolat metodu BLL UpdateProduct
.
Obrázek 16: Objekt ObjectDataSource nemůže najít metodu se vstupními parametry, které chce odeslat (kliknutím zobrazíte obrázek v plné velikosti)
Při pohledu na zprávu o výjimce je jasné, že ObjectDataSource chce vyvolat metodu BLL DeleteProduct
, která zahrnuje original_CategoryName
a original_SupplierName
vstupní parametry. Důvodem je to, že ItemTemplate
s pro CategoryID
pole a SupplierID
TemplateFields aktuálně obsahují obousměrné příkazy Bind s datovými CategoryName
poli a SupplierName
. Místo toho musíme do datových polí a SupplierID
zahrnout Bind
příkazyCategoryID
. Chcete-li toho dosáhnout, nahraďte existující příkazy Bind příkazy Eval
a pak přidejte skryté ovládací prvky Popisek, jejichž Text
vlastnosti jsou vázány k CategoryID
datovým polím a SupplierID
pomocí obousměrné vazby dat, jak je znázorněno níže:
<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>
Díky těmto změnám teď můžeme úspěšně odstranit a upravit informace o produktu. V kroku 5 se podíváme na to, jak ověřit, že jsou zjištěna porušení souběžnosti. Prozatím ale trvá několik minut, než se pokusíte aktualizovat a odstranit několik záznamů, abyste zajistili, že aktualizace a odstranění pro jednoho uživatele fungují podle očekávání.
Krok 5: Testování podpory optimistické souběžnosti
Abychom ověřili, že jsou zjištěna porušení souběžnosti (místo toho, aby data byla naslepo přepsána), musíme na této stránce otevřít dvě okna prohlížeče. V obou případech prohlížeče klikněte na tlačítko Upravit pro Chai. Pak v jednom z prohlížečů změňte název na "Chai Tea" a klikněte na Aktualizovat. Aktualizace by měla být úspěšná a vrátit objekt GridView do stavu předběžné úpravy s názvem nového produktu "Chai Tea".
V druhé instanci okna prohlížeče však název produktu TextBox stále zobrazuje "Chai". V tomto druhém okně prohlížeče aktualizujte na UnitPrice
25.00
. Bez podpory optimistické souběžnosti by kliknutí na aktualizovat v druhé instanci prohlížeče změnilo název produktu zpět na "Chai", čímž by se přepsaly změny provedené první instancí prohlížeče. Při použití optimistické souběžnosti však kliknutí na tlačítko Aktualizovat v druhé instanci prohlížeče způsobí dbConcurrencyException.
Obrázek 17: Když se zjistí narušení souběžnosti, DBConcurrencyException
vyvolá se (kliknutím zobrazíte obrázek v plné velikosti)
Vyvolá DBConcurrencyException
se pouze při použití modelu dávkové aktualizace DAL. Model přímé databáze nevyvolá výjimku, pouze značí, že nebyly ovlivněny žádné řádky. Chcete-li to ilustrovat, vraťte zobrazení GridView obou instancí prohlížeče do jejich stavu před úpravami. Potom v prvním prohlížeči klikněte na tlačítko Upravit a změňte název produktu z "Chai Tea" zpět na "Chai" a klikněte na Aktualizovat. V druhém okně prohlížeče klikněte na tlačítko Odstranit pro Chai.
Po kliknutí na Odstranit se stránka vrátí zpět, GridView vyvolá objectDataSource Delete()
metodu a ObjectDataSource zavolá dolů do ProductsOptimisticConcurrencyBLL
metody třídy DeleteProduct
a předá původní hodnoty. Původní ProductName
hodnota pro druhou instanci prohlížeče je "Chai Tea", která neodpovídá aktuální ProductName
hodnotě v databázi. DELETE
Proto příkaz vydaný databázi ovlivňuje nula řádků, protože v databázi není žádný záznam, který by klauzule WHERE
splňovala. Metoda DeleteProduct
vrátí false
a data ObjectDataSource se vrátí do Objektu GridView.
Z pohledu koncového uživatele kliknutí na tlačítko Odstranit pro Chai Tea v druhém okně prohlížeče způsobilo, že obrazovka bliká a po návratu se produkt stále nachází, i když je nyní uvedený jako "Chai" (změna názvu produktu provedená první instancí prohlížeče). Pokud uživatel klikne znovu na tlačítko Odstranit, odstranění bude úspěšné, protože původní ProductName
hodnota GridView ("Chai") teď odpovídá hodnotě v databázi.
V obou těchto případech není uživatelské prostředí zdaleka ideální. Je zřejmé, že nechceme uživateli zobrazovat podrobnosti o výjimce DBConcurrencyException
při použití vzoru dávkové aktualizace. Chování při použití modelu přímé databáze je poněkud matoucí, protože příkaz users selhal, ale nebylo jasné, proč.
Abychom tyto dva problémy napravili, můžeme na stránce vytvořit ovládací prvky Label Web, které poskytují vysvětlení, proč aktualizace nebo odstranění selhala. V případě vzoru dávkové aktualizace můžeme určit, zda došlo k výjimce DBConcurrencyException
v obslužné rutině události po úrovni GridView, a podle potřeby se zobrazí popisek upozornění. U přímé metody databáze můžeme prozkoumat návratovou hodnotu metody BLL (což je, pokud byl true
ovlivněn jeden řádek, false
jinak) a podle potřeby zobrazit informační zprávu.
Krok 6: Přidání informačních zpráv a jejich zobrazení tváří v tvář porušení souběžnosti
Pokud dojde k narušení souběžnosti, projevované chování závisí na tom, zda byla použita dávková aktualizace dal nebo přímý vzor databáze. Náš kurz používá oba vzory, přičemž vzor dávkové aktualizace se používá k aktualizaci a model přímé databáze použitý k odstranění. Abychom mohli začít, přidáme na naši stránku dva ovládací prvky Label Web, které vysvětlují, že při pokusu o odstranění nebo aktualizaci dat došlo k narušení souběžnosti. Nastavte vlastnosti a EnableViewState
vlastnosti ovládacího prvku Visible
Popisek na false
hodnotu . To způsobí jejich skrytí při každé návštěvě stránky s výjimkou konkrétních návštěv stránek, kde je jejich Visible
vlastnost programově nastavená na true
.
<asp:Label ID="DeleteConflictMessage" runat="server" Visible="False"
EnableViewState="False" CssClass="Warning"
Text="The record you attempted to delete has been modified by another user
since you last visited this page. Your delete was cancelled to allow
you to review the other user's changes and determine if you want to
continue deleting this record." />
<asp:Label ID="UpdateConflictMessage" runat="server" Visible="False"
EnableViewState="False" CssClass="Warning"
Text="The record you attempted to update has been modified by another user
since you started the update process. Your changes have been replaced
with the current values. Please review the existing values and make
any needed changes." />
Kromě nastavení vlastností Visible
, EnabledViewState
a Text
jsem také nastavil CssClass
vlastnost na Warning
, což způsobí, že se popisek zobrazí velkým červeným, kurzívou a tučným písmem. Tato třída CSS Warning
byla definována a přidána do Styles.css zpět v kurzu Zkoumání událostí souvisejících s vkládáním, aktualizací a odstraňováním .
Po přidání těchto popisků by Designer v sadě Visual Studio měly vypadat podobně jako na obrázku 18.
Obrázek 18: Na stránku byly přidány dva ovládací prvky Popisek (kliknutím zobrazíte obrázek v plné velikosti)
Po použití těchto ovládacích prvků Label Web jsme připraveni prozkoumat, jak zjistit, kdy došlo k narušení souběžnosti, a v tomto okamžiku je možné nastavit odpovídající vlastnost Popisek Visible
na true
hodnotu , která zobrazí informační zprávu.
Zpracování porušení souběžnosti při aktualizaci
Nejprve se podíváme na to, jak řešit porušení souběžnosti při použití vzoru dávkové aktualizace. Vzhledem k tomu, že taková porušení se vzorem dávkové aktualizace způsobí DBConcurrencyException
výjimku, musíme na stránku ASP.NET přidat kód, abychom zjistili, jestli během procesu aktualizace nedošlo k výjimce DBConcurrencyException
. Pokud ano, měli bychom uživateli zobrazit zprávu s vysvětlením, že změny nebyly uloženy, protože jiný uživatel změnil stejná data mezi tím, kdy začal upravovat záznam a kdy klikl na tlačítko Aktualizovat.
Jak jsme viděli v kurzu Zpracování výjimek BLL a DAL-Level v kurzu ASP.NET Page , takové výjimky je možné detekovat a potlačit v obslužných rutinách událostí po úrovni datového webového ovládacího prvku. Proto musíme vytvořit obslužnou rutinu události pro událost GridView RowUpdated
, která zkontroluje, jestli DBConcurrencyException
byla vyvolána výjimka. Této obslužné rutině události se předá odkaz na jakoukoli výjimku, která byla vyvolána během procesu aktualizace, jak je znázorněno v následujícím kódu obslužné rutiny události:
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;
}
}
}
Tváří v tvář výjimce DBConcurrencyException
tato obslužná rutina události zobrazí UpdateConflictMessage
ovládací prvek Popisek a označuje, že výjimka byla zpracována. Když je tento kód na místě, dojde při aktualizaci záznamu k narušení souběžnosti, dojde ke ztrátě změn uživatele, protože by současně přepsal změny jiného uživatele. Konkrétně gridView se vrátí do stavu předběžné úpravy a je vázán na aktuální data databáze. Tím se aktualizuje řádek GridView o změny ostatních uživatelů, které dříve nebyly viditelné. Ovládací prvek Popisek navíc uživateli vysvětlí, UpdateConflictMessage
co se právě stalo. Tato posloupnost událostí je podrobně popsána na obrázku 19.
Obrázek 19: Uživatelské Aktualizace jsou ztraceny při porušení souběžnosti (kliknutím zobrazíte obrázek v plné velikosti)
Poznámka
Alternativně, místo vrácení GridView do stavu před úpravami, bychom mohli ponechat GridView ve stavu úprav nastavením KeepInEditMode
vlastnosti předaného GridViewUpdatedEventArgs
objektu na true. Pokud však použijete tento přístup, nezapomeňte znovu připojit data k GridView (vyvoláním jeho DataBind()
metody), aby hodnoty druhého uživatele byly načteny do rozhraní pro úpravy. Kód dostupný ke stažení v tomto kurzu obsahuje tyto dva řádky kódu v RowUpdated
obslužné rutině události zakomentované. Jednoduše odkomentujte tyto řádky kódu, aby objekt GridView zůstal po narušení souběžnosti v režimu úprav.
Reakce na porušení souběžnosti při odstraňování
S přímým vzorem databáze není vyvolána žádná výjimka v případě porušení souběžnosti. Místo toho databázový příkaz jednoduše neovlivňuje žádné záznamy, protože klauzule WHERE neodpovídá žádnému záznamu. Všechny metody úpravy dat vytvořené v BLL byly navrženy tak, aby vracely logickou hodnotu označující, jestli mají nebo nemají vliv právě na jeden záznam. Abychom tedy zjistili, jestli při odstraňování záznamu nedošlo k narušení souběžnosti, můžeme prozkoumat návratovou hodnotu metody BLL DeleteProduct
.
Návratovou hodnotu pro metodu BLL lze prozkoumat v obslužných rutinách událostí po úrovni ObjectDataSource prostřednictvím ReturnValue
vlastnosti objektu ObjectDataSourceStatusEventArgs
předaného obslužné rutině události. Vzhledem k tomu, že nás zajímá určení návratové hodnoty z DeleteProduct
metody, potřebujeme pro událost ObjectDataSource Deleted
vytvořit obslužnou rutinu události. Vlastnost ReturnValue
je typu object
a může být null
, pokud byla vyvolána výjimka a metoda byla přerušena před vrácením hodnoty. Proto bychom se měli nejprve ujistit, že ReturnValue
vlastnost není null
a je logická hodnota. Za předpokladu, že tato kontrola projde, zobrazíme DeleteConflictMessage
ovládací prvek Popisek, ReturnValue
pokud je false
. To lze provést pomocí následujícího kódu:
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;
}
}
}
V případě porušení souběžnosti je žádost uživatele o odstranění zrušena. Objekt GridView se aktualizuje a zobrazuje změny, ke kterým došlo u daného záznamu mezi načtením stránky uživatelem a kliknutím na tlačítko Odstranit. Když se takové porušení zjistí, DeleteConflictMessage
zobrazí se popisek, který vysvětluje, co se právě stalo (viz Obrázek 20).
Obrázek 20: Odstranění uživatele je zrušeno při porušení souběžnosti (kliknutím zobrazíte obrázek v plné velikosti)
Souhrn
Příležitosti k narušení souběžnosti existují v každé aplikaci, která umožňuje více souběžných uživatelů aktualizovat nebo odstranit data. Pokud se taková porušení neúčtují, když dva uživatelé současně aktualizují stejná data, kdo se při posledním zápisu "vyhraje", přepíšou změny provedené druhým uživatelem. Alternativně můžou vývojáři implementovat optimistické nebo pesimistické řízení souběžnosti. Řízení optimistické souběžnosti předpokládá, že porušení souběžnosti nejsou častá, a jednoduše zakáže příkaz pro aktualizaci nebo odstranění, který by představoval porušení souběžnosti. Pesimistické řízení souběžnosti předpokládá, že narušení souběžnosti jsou častá, a jednoduše odmítnout příkaz pro aktualizaci nebo odstranění jednoho uživatele není přijatelné. Při pesimistickém řízení souběžnosti zahrnuje aktualizace záznamu jeho uzamčení, čímž zabrání ostatním uživatelům v úpravách nebo odstranění záznamu, když je uzamčený.
Typed DataSet v .NET poskytuje funkce pro podporu optimistického řízení souběžnosti. Konkrétně příkazy a DELETE
vydané pro databázi zahrnují všechny sloupce tabulky, což zajišťuje, že k aktualizaci nebo odstranění dojde pouze v případě, UPDATE
že aktuální data záznamu odpovídají původním datům, která uživatel měl při aktualizaci nebo odstranění. Jakmile je dal nakonfigurovaný tak, aby podporoval optimistickou souběžnost, je potřeba aktualizovat metody BLL. Kromě toho ASP.NET stránka, která volá do BLL musí být nakonfigurována tak, aby ObjectDataSource načítá původní hodnoty z jeho data webového ovládacího prvku a předá je dolů do BLL.
Jak jsme viděli v tomto kurzu, implementace řízení optimistické souběžnosti ve webové aplikaci ASP.NET zahrnuje aktualizaci DAL a BLL a přidání podpory na stránce ASP.NET. To, jestli tato přidaná práce představuje moudrou investici vašeho času a úsilí, závisí na vaší aplikaci. Pokud nemáte souběžné uživatele, kteří aktualizují data, nebo pokud se data, která aktualizují, liší, není řízení souběžnosti klíčovým problémem. Pokud ale na webu pravidelně pracuje se stejnými daty více uživatelů, může řízení souběžnosti pomoct zabránit tomu, aby aktualizace nebo odstranění jednoho uživatele neúmyslně přepsaly jiné.
Všechno nejlepší na programování!
O autorovi
Scott Mitchell, autor sedmi knih o ASP/ASP.NET a zakladatel 4GuysFromRolla.com, pracuje s webovými technologiemi Microsoftu od roku 1998. Scott pracuje jako nezávislý konzultant, školitel a spisovatel. Jeho nejnovější kniha je Sams Teach Yourself ASP.NET 2.0 in 24 Hours. Můžete ho zastihnout na mitchell@4GuysFromRolla.comadrese . nebo prostřednictvím jeho blogu, který najdete na adrese http://ScottOnWriting.NET.