Share via


Implementazione della concorrenza ottimistica (VB)

di Scott Mitchell

Scarica il PDF

Per un'applicazione Web che consente a più utenti di modificare i dati, c'è il rischio che due utenti possano modificare contemporaneamente gli stessi dati. In questa esercitazione verrà implementato il controllo di concorrenza ottimistica per gestire questo rischio.

Introduzione

Per le applicazioni Web che consentono solo agli utenti di visualizzare i dati o per quelli che includono solo un singolo utente che può modificare i dati, non esiste alcuna minaccia di due utenti simultanei sovrascrivendo accidentalmente le modifiche di un altro. Per le applicazioni Web che consentono a più utenti di aggiornare o eliminare i dati, tuttavia, è possibile che le modifiche di un utente siano in contrasto con un altro utente simultaneo. Senza alcun criterio di concorrenza, quando due utenti modificano simultaneamente un singolo record, l'utente che esegue il commit delle modifiche l'ultima eseguirà l'override delle modifiche apportate dal primo.

Si supponga, ad esempio, che due utenti, Jisun e Sam, visitassero entrambe una pagina dell'applicazione che consente ai visitatori di aggiornare ed eliminare i prodotti tramite un controllo GridView. Entrambi fare clic sul pulsante Modifica in GridView contemporaneamente. Jisun modifica il nome del prodotto in "Chai Tea" e fa clic sul pulsante Aggiorna. Il risultato netto è un'istruzione UPDATE inviata al database, che imposta tutti i campi aggiornabili del prodotto (anche se Jisun ha aggiornato un solo campo, ProductName). In questo momento, il database ha i valori "Chai Tea", la categoria Bevande, il fornitore Esotico Liquids e così via per questo particolare prodotto. Tuttavia, gridView nella schermata di Sam mostra ancora il nome del prodotto nella riga GridView modificabile come "Chai". Alcuni secondi dopo il commit delle modifiche di Jisun, Sam aggiorna la categoria nei condimenti e fa clic su Aggiorna. Ciò comporta un'istruzione UPDATE inviata al database che imposta il nome del prodotto su "Chai", sull'ID CategoryID categoria Bevande corrispondente e così via. Le modifiche apportate a Jisun al nome del prodotto sono state sovrascritte. La figura 1 illustra graficamente questa serie di eventi.

Quando due utenti aggiornano simultaneamente un record c'è potenziale per le modifiche di un utente alla sovrascrivitura dell'altro

Figura 1: quando due utenti aggiornano simultaneamente un record, è possibile che le modifiche di un utente sovrascrivono l'altra (fare clic per visualizzare l'immagine full-size)

Analogamente, quando due utenti visitano una pagina, un utente potrebbe trovarsi nel corso dell'aggiornamento di un record quando viene eliminato da un altro utente. In alternativa, tra quando un utente carica una pagina e quando fa clic sul pulsante Elimina, un altro utente potrebbe aver modificato il contenuto di tale record.

Sono disponibili tre strategie di controllo concorrenza :

  • Do Nothing -if gli utenti simultanei stanno modificando lo stesso record, lasciare che l'ultimo commit win (il comportamento predefinito)
  • Concorrenza ottimistica : si supponga che, mentre ci saranno conflitti di concorrenza ogni ora e poi, la maggior parte del tempo tali conflitti non si verificheranno; pertanto, se si verifica un conflitto, informa semplicemente l'utente che le modifiche non possono essere salvate perché un altro utente ha modificato gli stessi dati
  • Concorrenza pessimistica : si supponga che i conflitti di concorrenza siano comuni e che gli utenti non tollerano che le modifiche non siano state salvate a causa dell'attività simultanea di un altro utente; pertanto, quando un utente avvia l'aggiornamento di un record, bloccarlo, impedendo così ad altri utenti di modificare o eliminare tale record fino a quando l'utente esegue il commit delle modifiche

Tutte le esercitazioni finora hanno usato la strategia di risoluzione della concorrenza predefinita, ovvero l'ultima vittoria di scrittura. In questa esercitazione verrà illustrato come implementare il controllo di concorrenza ottimistica.

Nota

In questa serie di esercitazioni non verranno esaminati esempi di concorrenza pessimistici. La concorrenza pessimistica viene usata raramente perché tali blocchi, se non vengono riscriuti correttamente, possono impedire ad altri utenti di aggiornare i dati. Ad esempio, se un utente blocca un record per la modifica e quindi lascia per il giorno prima di sbloccarlo, nessun altro utente sarà in grado di aggiornare tale record fino a quando l'utente originale non restituisce e completa l'aggiornamento. Pertanto, in situazioni in cui viene usata la concorrenza pessimistica, si verifica in genere un timeout che, se raggiunto, annulla il blocco. I siti Web di vendita dei ticket, che bloccano una determinata posizione a sedere per breve periodo mentre l'utente completa il processo di ordine, è un esempio di controllo di concorrenza pessimistico.

Passaggio 1: Esaminare come viene implementata la concorrenza ottimistica

Il controllo di concorrenza ottimistica funziona assicurandosi che il record aggiornato o eliminato abbia gli stessi valori di quando è stato avviato l'aggiornamento o l'eliminazione del processo. Ad esempio, quando si fa clic sul pulsante Modifica in un controllo GridView modificabile, i valori del record vengono letti dal database e visualizzati in Caselle di testo e in altri controlli Web. Questi valori originali vengono salvati da GridView. Successivamente, dopo che l'utente apporta le modifiche e fa clic sul pulsante Aggiorna, i valori originali e i nuovi valori vengono inviati al livello di logica di business e quindi al livello di accesso ai dati. Il livello di accesso ai dati deve emettere un'istruzione SQL che aggiornerà solo il record se i valori originali che l'utente ha avviato la modifica sono identici ai valori ancora presenti nel database. La figura 2 illustra questa sequenza di eventi.

Per eseguire l'aggiornamento o l'eliminazione, i valori originali devono essere uguali ai valori correnti del database

Figura 2: Per l'aggiornamento o l'eliminazione, i valori originali devono essere uguali ai valori del database corrente (fare clic per visualizzare l'immagine a dimensioni complete)

Esistono vari approcci all'implementazione della concorrenza ottimistica (vedere Peter A. Bromberg'sOptimistic Concurrency Update Logic per una breve analisi di una serie di opzioni). Il ADO.NET Typed DataSet fornisce un'implementazione che può essere configurata solo con il segno di spunta di una casella di controllo. L'abilitazione UPDATE della concorrenza ottimistica per un TableAdapter nell'oggetto Typed DataSet aumenta le istruzioni e DELETE di TableAdapter per includere un confronto di tutti i valori originali nella WHERE clausola . L'istruzione seguente UPDATE , ad esempio, aggiorna il nome e il prezzo di un prodotto solo se i valori correnti del database sono uguali ai valori recuperati originariamente durante l'aggiornamento del record in GridView. I @ProductName parametri e @UnitPrice contengono i nuovi valori immessi dall'utente, mentre @original_ProductName e @original_UnitPrice contengono i valori originariamente caricati in GridView quando è stato fatto clic sul pulsante Modifica:

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

Nota

Questa UPDATE istruzione è stata semplificata per la leggibilità. In pratica, il UnitPrice controllo nella WHERE clausola sarebbe più coinvolto poiché UnitPrice può contenere NULL s e controllare se NULL = NULL restituisce sempre False (invece è necessario usare IS NULL).

Oltre a usare un'istruzione sottostante UPDATE diversa, la configurazione di un TableAdapter per l'uso della concorrenza ottimistica modifica anche la firma dei metodi diretti del database. Richiamare dalla prima esercitazione la creazione di un livello di accesso ai dati, ovvero i metodi diretti del database che accettano un elenco di valori scalari come parametri di input (anziché un'istanza di DataRow o DataTable fortemente tipizzata). Quando si usa la concorrenza ottimistica, i metodi diretti Update() e Delete() del database includono anche i parametri di input per i valori originali. Inoltre, il codice in BLL per l'uso del modello di aggiornamento batch (gli Update() overload del metodo che accettano DataRows e DataTables anziché i valori scalari) devono essere modificati anche.

Anziché estendere i TableAdapter esistenti di DAL per usare la concorrenza ottimistica (che richiederebbe la modifica del BLL in modo da adattare), verrà invece creato un nuovo Set di dati tipizzato denominato NorthwindOptimisticConcurrency, a cui si aggiungerà un Products TableAdapter che usa la concorrenza ottimistica. In seguito, verrà creata una ProductsOptimisticConcurrencyBLL classe Livello di logica di business che include le modifiche appropriate per supportare la concorrenza ottimistica DAL. Dopo aver posato questo lavoro, saremo pronti per creare la pagina ASP.NET.

Passaggio 2: Creazione di un livello di accesso ai dati che supporta la concorrenza ottimistica

Per creare un nuovo DataSet tipizzato, fare clic con il pulsante destro del mouse sulla cartella all'interno della DALApp_Code cartella e aggiungere un nuovo Oggetto DataSet denominato NorthwindOptimisticConcurrency. Come illustrato nella prima esercitazione, in questo modo si aggiungerà un nuovo TableAdapter al DataSet tipizzato, avviando automaticamente la Configurazione guidata TableAdapter. Nella prima schermata viene richiesto di specificare il database a cui connettersi, connettersi allo stesso database Northwind usando l'impostazione NORTHWNDConnectionString da Web.config.

Connettersi allo stesso database Northwind

Figura 3: Connettersi allo stesso database Northwind (Fare clic per visualizzare l'immagine full-size)

Verrà quindi richiesto come eseguire query sui dati: tramite un'istruzione SQL ad hoc, una nuova stored procedure o una stored procedure esistente. Poiché sono state usate query SQL ad hoc nel dal dal originale, usare questa opzione anche qui.

Specificare i dati da recuperare usando un'istruzione SQL ad hoc

Figura 4: Specificare i dati da recuperare usando un'istruzione SQL ad hoc (fare clic per visualizzare l'immagine full-size)

Nella schermata seguente immettere la query SQL da usare per recuperare le informazioni sul prodotto. Si userà la stessa query SQL utilizzata per TableAdapter Products dall'originale DAL, che restituisce tutte le colonne insieme ai nomi dei fornitori e delle Product categorie del prodotto:

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

Usare la stessa query SQL da Products TableAdapter nel DAL originale

Figura 5: Usare la stessa query SQL da Products TableAdapter nel DAL originale (fare clic per visualizzare l'immagine a dimensioni complete)

Prima di passare alla schermata successiva, fare clic sul pulsante Opzioni avanzate. Per avere questo controllo TableAdapter che usa il controllo di concorrenza ottimistica, selezionare semplicemente la casella di controllo "Usa concorrenza ottimistica".

Abilitare il controllo di concorrenza ottimistica controllando la casella di controllo

Figura 6: Abilitare il controllo di concorrenza ottimistica controllando la casella di controllo "Usa concorrenza ottimistica" (fare clic per visualizzare l'immagine full-size)

Infine, indicare che TableAdapter deve usare i modelli di accesso ai dati che riempiono una tabella dati e restituiscono una tabella dati; indicare anche che i metodi diretti del database devono essere creati. Modificare il nome del metodo per il modello Return a DataTable da GetData a GetProducts, in modo da eseguire il mirroring delle convenzioni di denominazione usate nel DAL originale.

Usare tutti i modelli di accesso ai dati di TableAdapter

Figura 7: Usare tutti i modelli di accesso ai dati (fare clic per visualizzare l'immagine full-size)

Al termine della procedura guidata, l'Designer DataSet includerà una tabella dati fortemente tipizzata Products e TableAdapter. Per rinominare DataTable da Products a ProductsOptimisticConcurrency, è possibile fare clic con il pulsante destro del mouse sulla barra del titolo di DataTable e scegliere Rinomina dal menu di scelta rapida.

Un oggetto DataTable e TableAdapter sono stati aggiunti al dataset tipizzato

Figura 8: Una tabella dati e TableAdapter sono stati aggiunti al dataset tipizzato (fare clic per visualizzare l'immagine full-size)

Per visualizzare le differenze tra le UPDATE query e DELETE tra TableAdapter (che usa la ProductsOptimisticConcurrency concorrenza ottimistica) e Products TableAdapter (che non è), fare clic su TableAdapter e passare alla Finestra Proprietà. DeleteCommand Nelle proprietà delle proprietà e UpdateCommandCommandText è possibile visualizzare la sintassi SQL effettiva inviata al database quando vengono richiamati i metodi correlati all'aggiornamento o all'eliminazione di DAL. Per l'istruzione ProductsOptimisticConcurrency TableAdapter DELETE usata è:

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

Mentre l'istruzione DELETE per Product TableAdapter nell'originale DAL è molto più semplice:

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

Come si può notare, la clausola nell'istruzione DELETE per TableAdapter che usa la WHERE concorrenza ottimistica include un confronto tra i valori di Product colonna esistenti della tabella e i valori originali al momento in cui GridView (o DetailsView o FormView) è stato popolato. Poiché tutti i campi diversi da ProductID, ProductNamee Discontinued possono avere NULL valori, sono inclusi parametri e controlli aggiuntivi per confrontare NULL correttamente i valori nella WHERE clausola .

Per questa esercitazione non verranno aggiunti altri DataTables all'oggetto DataSet abilitato per la concorrenza ottimistica per questa esercitazione, poiché la pagina ASP.NET fornirà solo l'aggiornamento e l'eliminazione delle informazioni sul prodotto. Tuttavia, è comunque necessario aggiungere il metodo all'oggetto GetProductByProductID(productID)ProductsOptimisticConcurrency TableAdapter.

A questo scopo, fare clic con il pulsante destro del mouse sulla barra del titolo di TableAdapter (l'area sopra i Fill nomi dei metodi e GetProducts ) e scegliere Aggiungi query dal menu di scelta rapida. Verrà avviata la Configurazione guidata query TableAdapter. Come per la configurazione iniziale di TableAdapter, scegliere di creare il GetProductByProductID(productID) metodo usando un'istruzione SQL ad hoc (vedere la figura 4). Poiché il metodo restituisce informazioni su un determinato prodotto, indica che questa GetProductByProductID(productID) query è un SELECT tipo di query che restituisce righe.

Contrassegnare il tipo di query come

Figura 9: Contrassegnare il tipo di query come "SELECT che restituisce righe" (Fare clic per visualizzare l'immagine full-size)

Nella schermata successiva viene richiesto di usare la query SQL con la query predefinita di TableAdapter pre-caricata. Aumentare la query esistente per includere la clausola WHERE ProductID = @ProductID, come illustrato nella figura 10.

Aggiungere una clausola WHERE alla query precaricata per restituire un record di prodotto specifico

Figura 10: Aggiungere una WHERE clausola alla query precaricata per restituire un record prodotto specifico (fare clic per visualizzare un'immagine full-size)

Infine, modificare i nomi dei metodi generati in FillByProductID e GetProductByProductID.

Rinominare i metodi in FillByProductID e GetProductByProductID

Figura 11: Rinominare i metodi in FillByProductID e GetProductByProductID (Fare clic per visualizzare l'immagine full-size)

Dopo aver completato questa procedura guidata, TableAdapter contiene ora due metodi per il recupero dei dati: GetProducts(), che restituisce tutti i prodotti e GetProductByProductID(productID), che restituisce il prodotto specificato.

Passaggio 3: Creazione di un livello di logica di business per il Concurrency-Enabled DAL ottimistico

La classe esistente ProductsBLL include esempi di uso sia dei modelli diretti di aggiornamento batch che di database. Il AddProduct metodo e UpdateProduct gli overload usano entrambi il modello di aggiornamento batch, passando un'istanza ProductRow al metodo Update di TableAdapter. Il DeleteProduct metodo, d'altra parte, usa il modello diretto db, chiamando il metodo TableAdapter Delete(productID) .

Con il nuovo ProductsOptimisticConcurrency TableAdapter, i metodi diretti del database richiedono ora che i valori originali vengano passati anche. Ad esempio, il metodo prevede ora dieci parametri di input: l'originale DeleteProductID, ProductNameQuantityPerUnitCategoryIDSupplierIDUnitsInStockUnitsOnOrderUnitPrice, ReorderLevel, e .Discontinued Usa questi valori aggiuntivi dei parametri di input nella WHERE clausola dell'istruzione DELETE inviata al database, eliminando solo il record specificato se i valori correnti del database vengono mappati a quelli originali.

Mentre la firma del metodo per il metodo TableAdapter usato nel modello di Update aggiornamento batch non è stata modificata, il codice necessario per registrare i valori originali e nuovi ha. Pertanto, anziché tentare di usare il servizio di concorrenza ottimistica abilitato per la concorrenza con la classe esistente ProductsBLL , verrà creata una nuova classe Livello logica di business per l'uso del nuovo DAL.

Aggiungere una classe denominata ProductsOptimisticConcurrencyBLL alla cartella all'interno della BLLApp_Code cartella.

Aggiungere la classe ProductsOptimisticConcurrencyBLL alla cartella BLL

Figura 12: Aggiungere la ProductsOptimisticConcurrencyBLL classe alla cartella BLL

Aggiungere quindi il codice seguente alla ProductsOptimisticConcurrencyBLL classe:

Imports NorthwindOptimisticConcurrencyTableAdapters
<System.ComponentModel.DataObject()> _
Public Class ProductsOptimisticConcurrencyBLL
    Private _productsAdapter As ProductsOptimisticConcurrencyTableAdapter = Nothing
    Protected ReadOnly Property Adapter() As ProductsOptimisticConcurrencyTableAdapter
        Get
            If _productsAdapter Is Nothing Then
                _productsAdapter = New ProductsOptimisticConcurrencyTableAdapter()
            End If
            Return _productsAdapter
        End Get
    End Property
    <System.ComponentModel.DataObjectMethodAttribute _
    (System.ComponentModel.DataObjectMethodType.Select, True)> _
    Public Function GetProducts() As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable
        Return Adapter.GetProducts()
    End Function
End Class

Prendere nota dell'istruzione using NorthwindOptimisticConcurrencyTableAdapters sopra l'inizio della dichiarazione di classe. Lo NorthwindOptimisticConcurrencyTableAdapters spazio dei nomi contiene la ProductsOptimisticConcurrencyTableAdapter classe, che fornisce i metodi di DAL. Prima della dichiarazione di classe si troverà anche l'attributo System.ComponentModel.DataObject , che indica a Visual Studio di includere questa classe nell'elenco a discesa della procedura guidata ObjectDataSource.

La ProductsOptimisticConcurrencyBLLproprietà dell'oggetto Adapter fornisce accesso rapido a un'istanza della ProductsOptimisticConcurrencyTableAdapter classe e segue il modello usato nelle classi BLL originali (ProductsBLL, CategoriesBLLe così via). Infine, il GetProducts() metodo chiama semplicemente il metodo nel metodo di GetProducts() DAL e restituisce un oggetto popolato con un'istanza ProductsOptimisticConcurrencyDataTableProductsOptimisticConcurrencyRow per ogni record prodotto nel database.

Eliminazione di un prodotto usando il modello diretto DB con concorrenza ottimistica

Quando si usa il modello diretto del database rispetto a un DAL che usa la concorrenza ottimistica, i metodi devono essere passati ai valori nuovi e originali. Per l'eliminazione, non sono presenti nuovi valori, quindi solo i valori originali devono essere passati. Nel BLL, quindi, è necessario accettare tutti i parametri originali come parametri di input. È possibile usare il metodo nella ProductsOptimisticConcurrencyBLL classe usando il DeleteProduct metodo diretto DB. Ciò significa che questo metodo deve accettare tutti i dieci campi dati del prodotto come parametri di input e passare questi a DAL, come illustrato nel codice seguente:

<System.ComponentModel.DataObjectMethodAttribute _
(System.ComponentModel.DataObjectMethodType.Delete, True)> _
Public Function DeleteProduct( _
    ByVal original_productID As Integer, ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean) _
    As Boolean
    Dim rowsAffected As Integer = Adapter.Delete(
                                    original_productID, _
                                    original_productName, _
                                    original_supplierID, _
                                    original_categoryID, _
                                    original_quantityPerUnit, _
                                    original_unitPrice, _
                                    original_unitsInStock, _
                                    original_unitsOnOrder, _
                                    original_reorderLevel, _
                                    original_discontinued)
    ' Return true if precisely one row was deleted, otherwise false
    Return rowsAffected = 1
End Function

Se i valori originali, ovvero i valori che sono stati caricati nell'ultimo caricamento in GridView (o DetailsView o FormView) differiscono dai valori nel database quando l'utente fa clic sul pulsante Elimina la WHERE clausola non corrisponderà a alcun record di database e non verranno interessati record. Di conseguenza, il metodo TableAdapter Delete restituirà 0 e il metodo BLL DeleteProduct restituirà false.

Aggiornamento di un prodotto usando il modello di aggiornamento batch con concorrenza ottimistica

Come indicato in precedenza, il metodo TableAdapter per il modello di Update aggiornamento batch ha la stessa firma del metodo indipendentemente dal fatto che venga usata o meno la concorrenza ottimistica. In genere, il Update metodo prevede un DataRow, una matrice di DataRows, un oggetto DataTable o un DataSet tipizzato. Non sono disponibili parametri di input aggiuntivi per specificare i valori originali. Ciò è possibile perché DataTable tiene traccia dei valori originali e modificati per i relativi DataRow(s). Quando il dal rilascia UPDATE l'istruzione, i parametri vengono popolati con i valori originali di DataRow, mentre i @original_ColumnName parametri vengono popolati con i @ColumnName valori modificati di DataRow.

ProductsBLL Nella classe (che usa la concorrenza originale, non ottimistica DAL), quando si usa il modello di aggiornamento batch per aggiornare le informazioni sul prodotto, il codice esegue la sequenza di eventi seguente:

  1. Leggere le informazioni sul prodotto del database corrente in un'istanza ProductRow usando il metodo TableAdapter GetProductByProductID(productID)
  2. Assegnare i nuovi valori all'istanza ProductRow dal passaggio 1
  3. Chiamare il metodo TableAdapter, passando l'istanza UpdateProductRow

Questa sequenza di passaggi, tuttavia, non supporta correttamente la concorrenza ottimistica perché il ProductRow popolamento popolato nel passaggio 1 viene popolato direttamente dal database, significa che i valori originali usati da DataRow sono quelli attualmente presenti nel database e non quelli associati a GridView all'inizio del processo di modifica. Quando si usa invece un dal dal abilitato per la concorrenza ottimistica, è necessario modificare gli overload del UpdateProduct metodo per usare la procedura seguente:

  1. Leggere le informazioni sul prodotto del database corrente in un'istanza ProductsOptimisticConcurrencyRow usando il metodo TableAdapter GetProductByProductID(productID)
  2. Assegnare i valori originali all'istanza ProductsOptimisticConcurrencyRow dal passaggio 1
  3. Chiamare il ProductsOptimisticConcurrencyRow metodo dell'istanza AcceptChanges() , che indica a DataRow che i relativi valori correnti sono quelli "originali"
  4. Assegnare i nuovi valori all'istanza ProductsOptimisticConcurrencyRow
  5. Chiamare il metodo TableAdapter, passando l'istanza UpdateProductsOptimisticConcurrencyRow

Il passaggio 1 legge in tutti i valori di database correnti per il record di prodotto specificato. Questo passaggio è superfluo nell'overload UpdateProduct che aggiorna tutte le colonne del prodotto (poiché questi valori vengono sovrascritti nel passaggio 2), ma è essenziale per tali overload in cui vengono passati solo un subset dei valori di colonna come parametri di input. Dopo aver assegnato i valori originali all'istanza ProductsOptimisticConcurrencyRow , il AcceptChanges() metodo viene chiamato, che contrassegna i valori di DataRow correnti come valori originali da utilizzare nei @original_ColumnName parametri dell'istruzione UPDATE . Successivamente, i nuovi valori dei parametri vengono assegnati all'oggetto ProductsOptimisticConcurrencyRow e, infine, viene richiamato il Update metodo, passando in DataRow.

Il codice seguente mostra l'overload UpdateProduct che accetta tutti i campi dati del prodotto come parametri di input. Anche se non illustrato qui, la ProductsOptimisticConcurrencyBLL classe inclusa nel download per questa esercitazione contiene anche un UpdateProduct overload che accetta solo il nome e il prezzo del prodotto come parametri di input.

Protected Sub AssignAllProductValues( _
    ByVal product As NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow, _
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean)
    product.ProductName = productName
    If Not supplierID.HasValue Then
        product.SetSupplierIDNull()
    Else
        product.SupplierID = supplierID.Value
    End If
    If Not categoryID.HasValue Then
        product.SetCategoryIDNull()
    Else
        product.CategoryID = categoryID.Value
    End If
    If quantityPerUnit Is Nothing Then
        product.SetQuantityPerUnitNull()
    Else
        product.QuantityPerUnit = quantityPerUnit
    End If
    If Not unitPrice.HasValue Then
        product.SetUnitPriceNull()
    Else
        product.UnitPrice = unitPrice.Value
    End If
    If Not unitsInStock.HasValue Then
        product.SetUnitsInStockNull()
    Else
        product.UnitsInStock = unitsInStock.Value
    End If
    If Not unitsOnOrder.HasValue Then
        product.SetUnitsOnOrderNull()
    Else
        product.UnitsOnOrder = unitsOnOrder.Value
    End If
    If Not reorderLevel.HasValue Then
        product.SetReorderLevelNull()
    Else
        product.ReorderLevel = reorderLevel.Value
    End If
    product.Discontinued = discontinued
End Sub
<System.ComponentModel.DataObjectMethodAttribute( _
System.ComponentModel.DataObjectMethodType.Update, True)> _
Public Function UpdateProduct(
    ByVal productName As String, ByVal supplierID As Nullable(Of Integer), _
    ByVal categoryID As Nullable(Of Integer), ByVal quantityPerUnit As String, _
    ByVal unitPrice As Nullable(Of Decimal), ByVal unitsInStock As Nullable(Of Short), _
    ByVal unitsOnOrder As Nullable(Of Short), ByVal reorderLevel As Nullable(Of Short), _
    ByVal discontinued As Boolean, ByVal productID As Integer, _
    _
    ByVal original_productName As String, _
    ByVal original_supplierID As Nullable(Of Integer), _
    ByVal original_categoryID As Nullable(Of Integer), _
    ByVal original_quantityPerUnit As String, _
    ByVal original_unitPrice As Nullable(Of Decimal), _
    ByVal original_unitsInStock As Nullable(Of Short), _
    ByVal original_unitsOnOrder As Nullable(Of Short), _
    ByVal original_reorderLevel As Nullable(Of Short), _
    ByVal original_discontinued As Boolean, _
    ByVal original_productID As Integer) _
    As Boolean
    'STEP 1: Read in the current database product information
    Dim products As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable = _
        Adapter.GetProductByProductID(original_productID)
    If products.Count = 0 Then
        ' no matching record found, return false
        Return False
    End If
    Dim product As _
        NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow = products(0)
    'STEP 2: Assign the original values to the product instance
    AssignAllProductValues( _
        product, original_productName, original_supplierID, _
        original_categoryID, original_quantityPerUnit, original_unitPrice, _
        original_unitsInStock, original_unitsOnOrder, original_reorderLevel, _
        original_discontinued)
    'STEP 3: Accept the changes
    product.AcceptChanges()
    'STEP 4: Assign the new values to the product instance
    AssignAllProductValues( _
        product, productName, supplierID, categoryID, quantityPerUnit, unitPrice, _
        unitsInStock, unitsOnOrder, reorderLevel, discontinued)
    'STEP 5: Update the product record
    Dim rowsAffected As Integer = Adapter.Update(product)
    ' Return true if precisely one row was updated, otherwise false
    Return rowsAffected = 1
End Function

Passaggio 4: Passaggio dei valori originali e nuovi dalla pagina ASP.NET ai metodi BLL

Con il completamento di DAL e BLL, tutto ciò che rimane consiste nel creare una pagina di ASP.NET che può utilizzare la logica di concorrenza ottimistica incorporata nel sistema. In particolare, il controllo Web dati (GridView, DetailsView o FormView) deve ricordare i relativi valori originali e ObjectDataSource deve passare entrambi i set di valori al livello di logica di business. Inoltre, la pagina ASP.NET deve essere configurata per gestire correttamente le violazioni di concorrenza.

Iniziare aprendo la OptimisticConcurrency.aspx pagina nella EditInsertDelete cartella e aggiungendo gridView alla Designer, impostandone la ID proprietà su ProductsGrid. Dallo smart tag di GridView scegliere di creare un nuovo oggetto ObjectDataSource denominato ProductsOptimisticConcurrencyDataSource. Poiché si vuole che objectDataSource usi il dal servizio di gestione dati che supporta la concorrenza ottimistica, configurarlo per l'uso dell'oggetto ProductsOptimisticConcurrencyBLL .

Usare ObjectDataSource l'oggetto ProductsOptimisticConcurrencyBLL

Figura 13: Usare ObjectDataSource (Fare clic per visualizzare l'immagineProductsOptimisticConcurrencyBLL full-size)

Scegliere i GetProductsmetodi , UpdateProducte DeleteProduct dagli elenchi a discesa nella procedura guidata. Per il metodo UpdateProduct, usare l'overload che accetta tutti i campi dati del prodotto.

Configurazione delle proprietà del controllo ObjectDataSource

Al termine della procedura guidata, il markup dichiarativo di ObjectDataSource dovrebbe essere simile al seguente:

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

Come si può notare, la DeleteParameters raccolta contiene un'istanza Parameter per ognuno dei dieci parametri di input nel ProductsOptimisticConcurrencyBLL metodo della DeleteProduct classe. Analogamente, la raccolta contiene un'istanza UpdateParametersParameter per ognuno dei parametri di input in UpdateProduct.

Per le esercitazioni precedenti che comportano la modifica dei dati, la proprietà ObjectDataSource OldValuesParameterFormatString verrà rimossa in questo punto, poiché questa proprietà indica che il metodo BLL prevede che i valori precedenti (o originali) vengano passati oltre ai nuovi valori. Inoltre, questo valore della proprietà indica i nomi dei parametri di input per i valori originali. Poiché i valori originali vengono passati al BLL, non rimuovere questa proprietà.

Nota

Il valore della proprietà deve eseguire il mapping ai nomi dei parametri di OldValuesParameterFormatString input nel BLL che prevedono i valori originali. Poiché questi parametri sono stati denominati original_productName, original_supplierIDe così via, è possibile lasciare il valore della OldValuesParameterFormatString proprietà come original_{0}. Se, tuttavia, i parametri di input dei metodi BLL hanno nomi come old_productName, old_supplierIDe così via, è necessario aggiornare la OldValuesParameterFormatString proprietà a old_{0}.

È disponibile un'impostazione finale della proprietà che deve essere effettuata per consentire al metodo ObjectDataSource di passare correttamente i valori originali ai metodi BLL. ObjectDataSource ha una proprietà ConflictDetection che può essere assegnata a uno dei due valori:

  • OverwriteChanges - valore predefinito; non invia i valori originali ai parametri di input originali dei metodi BLL
  • CompareAllValues - invia i valori originali ai metodi BLL; scegliere questa opzione quando si usa la concorrenza ottimistica

Prendere un momento per impostare la ConflictDetection proprietà su CompareAllValues.

Configurazione delle proprietà e dei campi di GridView

Con le proprietà ObjectDataSource configurate correttamente, si rivolgerà l'attenzione alla configurazione di GridView. Prima di tutto, poiché gridView supporta la modifica e l'eliminazione, fare clic sulla casella di controllo Abilita modifica e Abilita eliminazione dallo smart tag di GridView. Verrà aggiunto un CommandField il cui ShowEditButton oggetto e ShowDeleteButton sono entrambi impostati su true.

Se associato a ProductsOptimisticConcurrencyDataSource ObjectDataSource, GridView contiene un campo per ognuno dei campi dati del prodotto. Anche se tale gridView può essere modificato, l'esperienza utente è qualsiasi cosa ma accettabile. SupplierID E CategoryID BoundFields eseguirà il rendering come TextBoxes, richiedendo all'utente di immettere la categoria e il fornitore appropriati come numeri ID. Non vi sarà alcuna formattazione per i campi numerici e nessun controllo di convalida per assicurarsi che il nome del prodotto sia stato fornito e che il prezzo unitario, le unità in magazzino, le unità in ordine e i valori di livello di riordinamento siano entrambi valori numerici appropriati e siano maggiori o uguali a zero.

Come illustrato nell'esercitazione Aggiunta di controlli di convalida alle interfacce di modifica e inserimento epersonalizzazione dell'interfaccia di modifica dei dati , l'interfaccia utente può essere personalizzata sostituendo BoundFields con TemplateFields. Sono stato modificato gridView e la relativa interfaccia di modifica nei modi seguenti:

  • Rimosso , ProductIDSupplierNamee CategoryName BoundFields
  • Convertire BoundField ProductName in un modelloField e aggiungere un controllo RequiredFieldValidation.
  • Convertire e CategoryIDSupplierID BoundFields in TemplateFields e modificare l'interfaccia di modifica per usare DropDownLists anziché TextBoxes. In questi campi TemplateFields vengono ItemTemplatesvisualizzati i CategoryName campi dati e SupplierName .
  • Convertito i UnitPricecontrolli , UnitsInStock, UnitsOnOrdere ReorderLevel BoundFields in TemplateFields e aggiunti i controlli CompareValidator.

Poiché è già stato esaminato come eseguire queste attività nelle esercitazioni precedenti, verrà elencata solo la sintassi dichiarativa finale qui e lasciare l'implementazione come pratica.

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

Siamo molto vicini ad avere un esempio completamente funzionante. Tuttavia, ci sono alcune sottigliezze che si inquieteranno e ci causeranno problemi. È inoltre necessaria un'interfaccia che avvisa l'utente quando si è verificata una violazione della concorrenza.

Nota

Affinché un controllo Web dati passi correttamente i valori originali a ObjectDataSource (che vengono quindi passati al BLL), è fondamentale che la proprietà di EnableViewState GridView sia impostata su true (impostazione predefinita). Se si disabilita lo stato di visualizzazione, i valori originali vengono persi al postback.

Passaggio dei valori originali corretti a ObjectDataSource

Ci sono un paio di problemi con il modo in cui GridView è stato configurato. Se la proprietà ObjectDataSource ConflictDetection è impostata su CompareAllValues (come avviene con il nostro), quando i metodi o Delete() di ObjectDataSource vengono richiamati da GridView (o DetailsView o FormView), ObjectDataSource tenta di copiare i valori originali di Update() GridView nelle istanze appropriateParameter. Fare riferimento alla figura 2 per una rappresentazione grafica di questo processo.

In particolare, ai valori originali di GridView vengono assegnati i valori nelle istruzioni databinding bidirezionali ogni volta che i dati sono associati a GridView. Di conseguenza, è essenziale che tutti i valori originali richiesti vengano acquisiti tramite databinding bidirezionale e che vengano forniti in un formato convertibile.

Per vedere perché questo è importante, prendere un momento per visitare la nostra pagina in un browser. Come previsto, GridView elenca ogni prodotto con un pulsante Modifica ed Elimina nella colonna più a sinistra.

I prodotti sono elencati in un controllo GridView

Figura 14: I prodotti sono elencati in un controllo GridView (fare clic per visualizzare l'immagine a dimensione intera)

Se si fa clic sul pulsante Elimina per qualsiasi prodotto, viene generata un'eccezione FormatException .

Tentativo di eliminare i risultati di un prodotto in un'eccezione FormatException

Figura 15: Tentativo di eliminare i risultati di un prodotto in un oggetto FormatException (fare clic per visualizzare un'immagine di dimensioni intere)

Viene FormatException generato quando ObjectDataSource tenta di leggere nel valore originale UnitPrice . Poiché l'oggetto ItemTemplateUnitPrice è formattato come valuta (<%# Bind("UnitPrice", "{0:C}") %>), include un simbolo di valuta, ad esempio $19,95. Si FormatException verifica quando ObjectDataSource tenta di convertire questa stringa in un oggetto decimal. Per aggirare questo problema, sono disponibili diverse opzioni:

  • Rimuovere la formattazione della valuta da ItemTemplate. Invece di usare <%# Bind("UnitPrice", "{0:C}") %>, è sufficiente usare <%# Bind("UnitPrice") %>. Lo svantaggio di questo è che il prezzo non è più formattato.
  • Visualizzare la UnitPrice classe formattata come valuta in ItemTemplate, ma usare la Eval parola chiave per eseguire questa operazione. Tenere presente che Eval esegue l'associazione dati unidirezionale. È comunque necessario specificare il UnitPrice valore per i valori originali, quindi sarà comunque necessaria un'istruzione databinding bidirezionale in ItemTemplate, ma può essere inserita in un controllo Web Label la cui Visible proprietà è impostata su false. È possibile usare il markup seguente in ItemTemplate:
<ItemTemplate>
    <asp:Label ID="DummyUnitPrice" runat="server"
        Text='<%# Bind("UnitPrice") %>' Visible="false"></asp:Label>
    <asp:Label ID="Label4" runat="server"
        Text='<%# Eval("UnitPrice", "{0:C}") %>'></asp:Label>
</ItemTemplate>
  • Rimuovere la formattazione della valuta da ItemTemplate, usando <%# Bind("UnitPrice") %>. Nel gestore eventi di GridView accedere a livello di RowDataBound codice al controllo Web Etichetta all'interno del quale viene visualizzato il valore e impostarne Text la UnitPrice proprietà sulla versione formattata.
  • Lasciare formattato UnitPrice come valuta. Nel gestore eventi di RowDeleting GridView sostituire il valore originale UnitPrice esistente ($19,95) con un valore decimale effettivo usando Decimal.Parse. È stato illustrato come eseguire un'operazione simile nel RowUpdating gestore eventi nell'esercitazione Sulla gestione di BLL- e DAL-Level eccezioni in un'esercitazione ASP.NET Page .

Per l'esempio scelto di procedere con il secondo approccio, aggiungendo un controllo Web Etichetta nascosta la cui Text proprietà è dati bidirezionali associati al valore non formattato UnitPrice .

Dopo aver risolto questo problema, provare a fare di nuovo clic sul pulsante Elimina per qualsiasi prodotto. Questa volta si otterrà un oggetto InvalidOperationException quando ObjectDataSource tenta di richiamare il metodo BLL UpdateProduct .

ObjectDataSource non è in grado di trovare un metodo con i parametri di input da inviare

Figura 16: ObjectDataSource non è in grado di trovare un metodo con i parametri di input da inviare (fare clic per visualizzare l'immagine a dimensione intera)

Esaminando il messaggio dell'eccezione, è chiaro che ObjectDataSource vuole richiamare un metodo BLL DeleteProduct che include original_CategoryName i parametri di input e original_SupplierName . Ciò è dovuto al fatto che gli ItemTemplate oggetti per CategoryID e SupplierID TemplateFields contengono attualmente istruzioni Bind bidirezionali con i CategoryName campi dati e SupplierName . È invece necessario includere Bind istruzioni con i CategoryID campi dati e SupplierID . A tale scopo, sostituire le istruzioni Bind esistenti con Eval istruzioni e quindi aggiungere controlli Label nascosti le CategoryID cui Text proprietà sono associate ai campi dati e SupplierID usando il databinding bidirezionale, come illustrato di seguito:

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

Con queste modifiche, ora siamo in grado di eliminare e modificare correttamente le informazioni sul prodotto. Nel passaggio 5 si esaminerà come verificare che vengano rilevate violazioni della concorrenza. Per il momento, tuttavia, è necessario attendere alcuni minuti per provare ad aggiornare ed eliminare alcuni record per assicurarsi che l'aggiornamento e l'eliminazione per un singolo utente funzioni come previsto.

Passaggio 5: Test del supporto della concorrenza ottimistica

Per verificare che vengano rilevate violazioni della concorrenza (invece di comportare la sovrascrittura dei dati), è necessario aprire due finestre del browser in questa pagina. In entrambe le istanze del browser fare clic sul pulsante Modifica per Chai. Quindi, in uno solo dei browser, modificare il nome in "Chai Tea" e fare clic su Aggiorna. L'aggiornamento dovrebbe avere esito positivo e restituire GridView allo stato di pre-modifica, con "Chai Tea" come nuovo nome del prodotto.

Nell'altra istanza della finestra del browser, tuttavia, il nome del prodotto TextBox visualizza ancora "Chai". In questa seconda finestra del browser aggiornare in UnitPrice25.00. Senza il supporto della concorrenza ottimistica, facendo clic sull'aggiornamento nella seconda istanza del browser il nome del prodotto verrebbe modificato in "Chai", sovrascrivendo così le modifiche apportate dalla prima istanza del browser. Con la concorrenza ottimistica usata, tuttavia, facendo clic sul pulsante Aggiorna nella seconda istanza del browser viene generata un'eccezione DBConcurrencyException.

Quando viene rilevata una violazione della concorrenza, viene generata un'eccezione DBConcurrencyException

Figura 17: Quando viene rilevata una violazione della concorrenza, viene generata un'eccezione DBConcurrencyException (fare clic per visualizzare un'immagine a dimensione intera)

Viene DBConcurrencyException generata solo quando viene utilizzato il modello di aggiornamento batch di DAL. Il modello diretto del database non genera un'eccezione, ma indica semplicemente che non sono state interessate righe. Per illustrare questo problema, restituire gridView di entrambe le istanze del browser allo stato di pre-modifica. Quindi, nella prima istanza del browser fare clic sul pulsante Modifica e modificare il nome del prodotto da "Chai Tea" a "Chai" e fare clic su Aggiorna. Nella seconda finestra del browser fare clic sul pulsante Elimina per Chai.

Quando si fa clic su Elimina, la pagina esegue il postback, GridView richiama il metodo ObjectDataSource Delete() e ObjectDataSource chiama ProductsOptimisticConcurrencyBLL il metodo della DeleteProduct classe passando i valori originali. Il valore originale ProductName per la seconda istanza del browser è "Chai Tea", che non corrisponde al valore corrente ProductName nel database. Di conseguenza, l'istruzione DELETE rilasciata al database influisce su zero righe perché non è presente alcun record nel database che la WHERE clausola soddisfa. Il DeleteProduct metodo restituisce false e i dati di ObjectDataSource vengono rimbalzati in GridView.

Dal punto di vista dell'utente finale, facendo clic sul pulsante Elimina per Chai Tea nella seconda finestra del browser ha causato il flashing dello schermo e, al ritorno, il prodotto è ancora presente, anche se ora è elencato come "Chai" (la modifica del nome del prodotto apportata dalla prima istanza del browser). Se l'utente fa di nuovo clic sul pulsante Elimina, l'opzione Elimina avrà esito positivo, perché il valore originale ProductName di GridView ("Chai") corrisponde ora al valore nel database.

In entrambi questi casi, l'esperienza utente è lontana dall'ideale. È chiaro che non si vogliono mostrare all'utente i dettagli nitty-gritty dell'eccezione DBConcurrencyException quando si usa il modello di aggiornamento batch. E il comportamento quando si usa il modello diretto del database è un po 'confuso come il comando degli utenti non è riuscito, ma non c'era alcuna indicazione precisa del motivo.

Per risolvere questi due problemi, è possibile creare controlli Web etichetta nella pagina che forniscono una spiegazione del motivo per cui un aggiornamento o un'eliminazione non è riuscita. Per il modello di aggiornamento batch, è possibile determinare se si è verificata o meno un'eccezione DBConcurrencyException nel gestore eventi post-livello di GridView, visualizzando l'etichetta di avviso in base alle esigenze. Per il metodo diretto del database, è possibile esaminare il valore restituito del metodo BLL ,ovvero true se una riga è interessata, in caso contrario, false e visualizzare un messaggio informativo in base alle esigenze.

Passaggio 6: Aggiunta di messaggi informativi e visualizzazione di tali messaggi in caso di violazione della concorrenza

Quando si verifica una violazione della concorrenza, il comportamento esposto dipende dal fatto che sia stato usato il modello diretto di aggiornamento batch o database DAL. L'esercitazione usa entrambi i modelli, con il modello di aggiornamento batch usato per l'aggiornamento e il modello diretto del database usato per l'eliminazione. Per iniziare, aggiungere due controlli Web etichetta alla pagina che spiegano che si è verificata una violazione della concorrenza quando si tenta di eliminare o aggiornare i dati. Impostare le proprietà e del Visible controllo Etichetta su false. In questo modo le proprietà verranno nascoste in ogni pagina, ad eccezione di quelle specifiche visite di pagina in cui la proprietà Visible è impostata a livello di codice su true.EnableViewState

<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." />

Oltre a impostare Visiblele relative proprietà , EnabledViewStatee Text , ho anche impostato la CssClass proprietà su Warning, che fa sì che l'etichetta venga visualizzata in un tipo di carattere grande, rosso, corsivo, grassetto. Questa classe CSS Warning è stata definita e aggiunta a Styles.css di nuovo nell'esercitazione Analisi degli eventi associati all'inserimento, all'aggiornamento e all'eliminazione .

Dopo aver aggiunto queste etichette, il Designer in Visual Studio dovrebbe essere simile alla figura 18.

Alla pagina sono stati aggiunti due controlli etichetta

Figura 18: Sono stati aggiunti due controlli etichetta alla pagina (fare clic per visualizzare l'immagine a dimensione intera)

Con questi controlli Web Etichetta, è possibile esaminare come determinare quando si è verificata una violazione della concorrenza, a quel punto la proprietà dell'etichetta Visible appropriata può essere impostata su true, visualizzando il messaggio informativo.

Gestione delle violazioni di concorrenza durante l'aggiornamento

Si esamini prima di tutto come gestire le violazioni di concorrenza quando si usa il modello di aggiornamento batch. Poiché tali violazioni con il modello di aggiornamento batch causano la generazione di un'eccezione DBConcurrencyException , è necessario aggiungere codice alla pagina ASP.NET per determinare se si è verificata un'eccezione DBConcurrencyException durante il processo di aggiornamento. In tal caso, dovrebbe essere visualizzato un messaggio all'utente che spiega che le modifiche non sono state salvate perché un altro utente ha modificato gli stessi dati tra quando ha iniziato a modificare il record e quando ha fatto clic sul pulsante Aggiorna.

Come illustrato nell'esercitazione Sulla gestione delle eccezioni BLL e DAL-Level in un'esercitazione sulla pagina di ASP.NET , tali eccezioni possono essere rilevate e eliminate nei gestori eventi post-livello del controllo Web dei dati. È quindi necessario creare un gestore eventi per l'evento di RowUpdated GridView che controlla se è stata generata un'eccezione DBConcurrencyException . Questo gestore eventi viene passato un riferimento a qualsiasi eccezione generata durante il processo di aggiornamento, come illustrato nel codice del gestore eventi seguente:

Protected Sub ProductsGrid_RowUpdated _
        (ByVal sender As Object, ByVal e As GridViewUpdatedEventArgs) _
        Handles ProductsGrid.RowUpdated
    If e.Exception IsNot Nothing AndAlso e.Exception.InnerException IsNot Nothing Then
        If TypeOf e.Exception.InnerException Is System.Data.DBConcurrencyException Then
            ' Display the warning message and note that the exception has
            ' been handled...
            UpdateConflictMessage.Visible = True
            e.ExceptionHandled = True
        End If
    End If
End Sub

In caso di DBConcurrencyException eccezione, questo gestore eventi visualizza il UpdateConflictMessage controllo Etichetta e indica che l'eccezione è stata gestita. Con questo codice sul posto, quando si verifica una violazione della concorrenza durante l'aggiornamento di un record, le modifiche dell'utente andranno perse, poiché le modifiche di un altro utente sarebbero state sovrascritte contemporaneamente. In particolare, GridView viene restituito allo stato di pre-modifica e associato ai dati del database correnti. Verrà aggiornata la riga GridView con le modifiche dell'altro utente, che in precedenza non erano visibili. Inoltre, il UpdateConflictMessage controllo Etichetta spiegherebbe all'utente ciò che è appena accaduto. Questa sequenza di eventi è dettagliata nella figura 19.

Un utente Aggiornamenti viene perso in caso di violazione di concorrenza

Figura 19: I Aggiornamenti di un utente vengono persi in caso di violazione di concorrenza (fare clic per visualizzare l'immagine a dimensione intera)

Nota

In alternativa, invece di restituire GridView allo stato di pre-modifica, è possibile lasciare GridView nello stato di modifica impostando la KeepInEditMode proprietà dell'oggetto passato GridViewUpdatedEventArgs su true. Se si accetta questo approccio, tuttavia, assicurarsi di riassociare i dati a GridView (richiamandone DataBind() il metodo) in modo che i valori dell'altro utente vengano caricati nell'interfaccia di modifica. Il codice disponibile per il download con questa esercitazione include queste due righe di codice nel RowUpdated gestore eventi impostato come commento. Rimuovere semplicemente il commento da queste righe di codice per fare in modo che GridView rimanga in modalità di modifica dopo una violazione della concorrenza.

Risposta alle violazioni di concorrenza durante l'eliminazione

Con il modello diretto del database, non viene generata alcuna eccezione in caso di violazione della concorrenza. Al contrario, l'istruzione del database non influisce semplicemente su alcun record, perché la clausola WHERE non corrisponde ad alcun record. Tutti i metodi di modifica dei dati creati nel BLL sono stati progettati in modo che restituiscano un valore booleano che indica se hanno o meno interessato esattamente un record. Pertanto, per determinare se si è verificata una violazione della concorrenza durante l'eliminazione di un record, è possibile esaminare il valore restituito del metodo BLL DeleteProduct .

Il valore restituito per un metodo BLL può essere esaminato nei gestori eventi post-livello di ObjectDataSource tramite la ReturnValue proprietà dell'oggetto ObjectDataSourceStatusEventArgs passato nel gestore eventi. Poiché si è interessati a determinare il valore restituito dal DeleteProduct metodo , è necessario creare un gestore eventi per l'evento ObjectDataSource Deleted . La ReturnValue proprietà è di tipo object e può essere null se è stata generata un'eccezione e il metodo è stato interrotto prima di poter restituire un valore. Pertanto, è prima necessario assicurarsi che la ReturnValue proprietà non null sia e sia un valore booleano. Supponendo che questo controllo venga superato, viene visualizzato il DeleteConflictMessage controllo Etichetta se è ReturnValuefalse. A tale scopo, usare il codice seguente:

Protected Sub ProductsOptimisticConcurrencyDataSource_Deleted _
        (ByVal sender As Object, ByVal e As ObjectDataSourceStatusEventArgs) _
        Handles ProductsOptimisticConcurrencyDataSource.Deleted
    If e.ReturnValue IsNot Nothing AndAlso TypeOf e.ReturnValue Is Boolean Then
        Dim deleteReturnValue As Boolean = CType(e.ReturnValue, Boolean)
        If deleteReturnValue = False Then
            ' No row was deleted, display the warning message
            DeleteConflictMessage.Visible = True
        End If
    End If
End Sub

In caso di violazione della concorrenza, la richiesta di eliminazione dell'utente viene annullata. GridView viene aggiornato, che mostra le modifiche apportate per tale record tra il momento in cui l'utente ha caricato la pagina e quando ha fatto clic sul pulsante Elimina. Quando si verifica una violazione di questo tipo, viene visualizzata l'etichetta DeleteConflictMessage , che spiega cosa è successo (vedere la figura 20).

L'eliminazione di un utente viene annullata in caso di violazione di concorrenza

Figura 20: L'eliminazione di un utente viene annullata in Caso di violazione di concorrenza (fare clic per visualizzare l'immagine a dimensione intera)

Riepilogo

Le opportunità di violazioni della concorrenza esistono in ogni applicazione che consente a più utenti simultanei di aggiornare o eliminare i dati. Se tali violazioni non vengono rilevate, quando due utenti aggiornano contemporaneamente gli stessi dati che ottengono nell'ultima scrittura "wins", sovrascrivendo le modifiche apportate dall'altro utente. In alternativa, gli sviluppatori possono implementare il controllo della concorrenza ottimistica o pessimistica. Il controllo della concorrenza ottimistica presuppone che le violazioni della concorrenza non siano frequenti e semplicemente non consentano un comando di aggiornamento o eliminazione che costituirebbe una violazione della concorrenza. Il controllo della concorrenza pessimistico presuppone che le violazioni della concorrenza siano frequenti e che il semplice rifiuto del comando di aggiornamento o eliminazione di un utente non sia accettabile. Con il controllo della concorrenza pessimistica, l'aggiornamento di un record comporta il blocco, impedendo così ad altri utenti di modificare o eliminare il record mentre è bloccato.

Il set di dati tipizzato in .NET offre funzionalità per il supporto del controllo della concorrenza ottimistica. In particolare, le UPDATE istruzioni e DELETE rilasciate al database includono tutte le colonne della tabella, assicurando in tal modo che l'aggiornamento o l'eliminazione si verificherà solo se i dati correnti del record corrispondono ai dati originali che l'utente aveva durante l'esecuzione dell'aggiornamento o dell'eliminazione. Dopo aver configurato DAL per supportare la concorrenza ottimistica, è necessario aggiornare i metodi BLL. Inoltre, la pagina di ASP.NET che chiama il BLL deve essere configurata in modo che ObjectDataSource recuperi i valori originali dal controllo Web dei dati e li passi al BLL.

Come illustrato in questa esercitazione, l'implementazione del controllo della concorrenza ottimistica in un'applicazione Web ASP.NET comporta l'aggiornamento di DAL e BLL e l'aggiunta del supporto nella pagina ASP.NET. Il fatto che questo lavoro aggiunto sia un investimento saggio del tempo e dell'impegno dipende dall'applicazione. Se raramente si dispone di utenti simultanei che aggiornano i dati o i dati che stanno aggiornando sono diversi l'uno dall'altro, il controllo della concorrenza non è un problema chiave. Se, tuttavia, si hanno più utenti nel sito che usano gli stessi dati, il controllo della concorrenza può impedire agli aggiornamenti o alle eliminazioni di un utente di sovrascrivere involontariamente altri utenti.

Buon programmatori!

Informazioni sull'autore

Scott Mitchell, autore di sette libri ASP/ASP.NET e fondatore di 4GuysFromRolla.com, lavora con le tecnologie Web Microsoft dal 1998. Scott lavora come consulente indipendente, formatore e scrittore. Il suo ultimo libro è Sams Teach Yourself ASP.NET 2.0 in 24 ore. Può essere raggiunto all'indirizzo mitchell@4GuysFromRolla.com. o tramite il suo blog, disponibile all'indirizzo http://ScottOnWriting.NET.