Implementace optimistického řízení souběžnosti (C#)

Scott Mitchell

Stáhnout PDF

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

Když dva uživatelé současně aktualizují záznam, může dojít k tomu, že jeden uživatel může přepsat ostatní uživatele.

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

Aby byla aktualizace nebo odstranění úspěšná, musí být původní hodnoty rovny aktuálním hodnotám databáze.

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.

Připojení k databázi Same Northwind

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.

Určení dat, která se mají načíst, pomocí ad hoc příkazu SQL

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

Použití stejného dotazu SQL z tabulky Products TableAdapter v původní dal

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.

Povolte řízení optimistické souběžnosti zaškrtnutím políčka 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.

Nechte tableadapter využívat všechny vzory přístupu k datům.

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.

DataTable a TableAdapter byly přidány do zadané datové sady.

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

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.

Označení typu dotazu jako

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.

Přidání klauzule WHERE do předem načteného dotazu pro vrácení určitého záznamu o produktu

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.

Přejmenujte metody 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í ProductIDparametry , , ProductName, SupplierID, CategoryID, QuantityPerUnit, UnitsOnOrderReorderLevelUnitPriceUnitsInStocka .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 .ProductsOptimisticConcurrencyBLLBLL

Přidání třídy ProductsOptimisticConcurrencyBLL do složky 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 ProductsOptimisticConcurrencyBLLposkytuje 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, CategoriesBLLatd.). 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í:

  1. Načtení informací o produktu aktuální databáze do ProductRow instance pomocí metody TableAdapter GetProductByProductID(productID)
  2. Přiřazení nových hodnot k ProductRow instanci z kroku 1
  3. Volání metody TableAdapter Update a předání instance ProductRow

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:

  1. Načtení informací o produktu aktuální databáze do ProductsOptimisticConcurrencyRow instance pomocí metody TableAdapter GetProductByProductID(productID)
  2. Přiřazení původních hodnot k ProductsOptimisticConcurrencyRow instanci z kroku 1
  3. ProductsOptimisticConcurrencyRow Volání metody instanceAcceptChanges(), která dává objektu DataRow pokyn, že aktuální hodnoty jsou původní hodnoty.
  4. Přiřazení nových hodnot k ProductsOptimisticConcurrencyRow instanci
  5. Volání metody TableAdapter Update a předání instance ProductsOptimisticConcurrencyRow

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 .

Nechte objekt ObjectDataSource použít objekt ProductsOptimisticConcurrencyBLL.

Obrázek 13: Nechte objekt ObjectDataSource použít ProductsOptimisticConcurrencyBLL objekt (kliknutím zobrazíte obrázek v plné velikosti)

V průvodci GetProductsvyberte metody , UpdateProducta 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 UpdateProductsouboru .

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_productNamepojmenovali , 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_supplierIDatd., 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í ProductIDvázaných polí , SupplierNamea CategoryName
  • Převeďte ProductName BoundField na TemplateField a přidali ovládací prvek RequiredFieldValidation.
  • Převeďte CategoryID objekty a SupplierID 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 jsou ItemTemplateszobrazena CategoryName datová pole a SupplierName .
  • Převeďte pole UnitPrice, UnitsInStock, UnitsOnOrdera ReorderLevel 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.

Produkty jsou uvedeny v zobrazení GridView.

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 .

Pokus o odstranění všech výsledků produktu ve funkci FormatException

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 v ItemTemplatesouboru , ale k tomu použijte Eval klíčové slovo. Vzpomeňte si, že Eval provádí jednosměrné vazby dat. Stále potřebujeme zadat UnitPrice hodnotu pro původní hodnoty, takže budeme stále potřebovat obousměrný příkaz databinding v ItemTemplate, ale ten lze umístit do ovládacího prvku Label Web, jehož Visible vlastnost je nastavena na false. 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ém UnitPrice je hodnota zobrazena, a nastavte jeho Text vlastnost na formátovanou verzi.
  • Ponechte formátovanou UnitPrice jako měnu. V obslužné rutině události GridView RowDeleting 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ého RowUpdating 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 .

ObjectDataSource nemůže najít metodu se vstupními parametry, které chce odeslat.

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

Při zjištění porušení souběžnosti je vyvolána výjimka 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 falsehodnotu . 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, EnabledViewStatea 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.

Na stránku byly přidány dva ovládací prvky Popisek.

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 truehodnotu , 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.

Při porušení souběžnosti dochází ke ztrátě Aktualizace uživatele

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

Odstranění uživatele v důsledku porušení souběžnosti je zrušeno

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.