Condividi tramite


Implementazione della concorrenza ottimistica (VB)

di Scott Mitchell

Scaricare il PDF

Per un'applicazione Web che consente a più utenti di modificare i dati, esiste il rischio che due utenti modifichino contemporaneamente gli stessi dati. In questa esercitazione verrà implementato il controllo della 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 per due utenti simultanei sovrascrivendo accidentalmente le modifiche di un altro utente. Per le applicazioni Web che consentono a più utenti di aggiornare o eliminare i dati, tuttavia, esiste il potenziale per le modifiche di un utente in conflitto con un altro utente simultaneo. Senza alcun criterio di concorrenza in atto, quando due utenti modificano contemporaneamente un singolo record, l'utente che esegue il commit delle sue modifiche per ultimo sovrascriverà le modifiche apportate dal primo.

Si supponga, ad esempio, che due utenti, Jisun e Sam, visitassero entrambe una pagina nell'applicazione che consentiva ai visitatori di aggiornare ed eliminare i prodotti tramite un controllo GridView. Entrambi fare clic sul pulsante Modifica in GridView nello stesso momento. 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). A questo punto, il database ha i valori "Chai Tea", la categoria Beverages, il fornitore Exotic Liquids e così via per questo particolare prodotto. Tuttavia, la GridView nella schermata di Sam mostra ancora il nome del prodotto nella riga GridView modificabile come "Chai". Pochi secondi dopo il commit delle modifiche di Jisun, Sam aggiorna la categoria a Condimenti e fa clic su Aggiorna. Questo comporta un'istruzione UPDATE inviata al database che imposta il nome del prodotto su "Chai", l'ID CategoryID della categoria Beverages corrispondente, e così via. Le modifiche al nome del prodotto apportate da Jisun sono state sovrascritte. La figura 1 illustra graficamente questa serie di eventi.

Quando due utenti aggiornano simultaneamente un record, è possibile che le modifiche di un utente vengano sovrascritte dall'altro

Figura 1: Quando due utenti aggiornano simultaneamente un record, è possibile che le modifiche di un utente sovrascrivono le altre (fare clic per visualizzare l'immagine a dimensione intera)

Analogamente, quando due utenti visitano una pagina, un utente potrebbe trovarsi durante l'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 del record.

Sono disponibili tre strategie di controllo della concorrenza :

  • Non fare nulla -if gli utenti concorrenti stanno modificando lo stesso record, lasciare che l'ultimo commit vinca (comportamento predefinito)
  • Concorrenza ottimistica : presupporre che, sebbene ci siano conflitti di concorrenza ogni ora e poi, la maggior parte del tempo tali conflitti non si verificheranno; pertanto, se si verifica un conflitto, è sufficiente informare 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 tollerino che le modifiche non siano state salvate a causa dell'attività simultanea di un altro utente; pertanto, quando un utente inizia ad aggiornare un record, bloccarlo, impedendo così ad altri utenti di modificare o eliminare tale record fino a quando l'utente non esegue il commit delle modifiche

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

Annotazioni

In questa serie di esercitazioni non verranno esaminati esempi di concorrenza pessimistica. La concorrenza pessimistica viene usata raramente perché tali blocchi, se non vengono riabiliti 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, è in genere presente un timeout che, se raggiunto, annulla il blocco. I siti Web di vendita dei biglietti, che bloccano una determinata posizione di posti a sedere per breve periodo mentre l'utente completa il processo di ordine, è un esempio di controllo di concorrenza pessimistico.

Passaggio 1: Esaminare il modo in cui viene implementata la concorrenza ottimistica

Il controllo della concorrenza ottimistica funziona verificando che il record da aggiornare o eliminare abbia gli stessi valori di quando è stato avviato il processo di aggiornamento o eliminazione. 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 più i nuovi valori vengono inviati al livello della logica di business e quindi al livello di accesso ai dati. Il livello di accesso ai dati deve emettere un'istruzione SQL che aggiornerà il record solo se i valori originali avviati dall'utente sono identici ai valori ancora presenti nel database. La figura 2 illustra questa sequenza di eventi.

Affinché l'aggiornamento o l'eliminazione abbia esito positivo, i valori originali devono essere uguali ai valori correnti del database

Figura 2: Affinché l'aggiornamento o l'eliminazione abbia esito positivo, i valori originali devono essere uguali ai valori correnti del database (fare clic per visualizzare l'immagine a dimensione intera)

Esistono diversi approcci all'implementazione della concorrenza ottimistica (vedere la Logica di aggiornamento con concorrenza ottimistica di Peter A. Bromberg per una breve panoramica delle diverse opzioni). Il set di dati tipizzato ADO.NET fornisce un'implementazione che può essere configurata solo con il segno di spunta di una casella di controllo. L'abilitazione della concorrenza ottimistica per un TableAdapter in un DataSet tipizzato aumenta le istruzioni UPDATE e DELETE del TableAdapter per includere un confronto di tutti i valori originali nella clausola WHERE. 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

Annotazioni

Questa UPDATE istruzione è stata semplificata per la leggibilità. In pratica, il UnitPrice controllo nella WHERE clausola sarebbe più coinvolto perché 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. Come illustrato nella prima esercitazione, Creazione di un livello di accesso ai dati, i metodi diretti del database sono quelli che accettano un elenco di valori scalari come parametri di input (anziché un'istanza DataRow o DataTable fortemente tipizzata). Quando si usa la concorrenza ottimistica, anche i metodi diretti Update() e Delete() di database includono parametri di input per i valori originali. Inoltre, il codice nel BLL per l'uso del modello di aggiornamento batch (gli overload del metodo Update() che accettano DataRows e DataTables anziché i valori scalari) devono essere modificati anch'essi.

Invece di estendere i TableAdapter esistenti di DAL per usare la concorrenza ottimistica (che richiederebbe la modifica del valore BLL per adattarlo), si creerà invece un nuovo dataset tipizzato denominato NorthwindOptimisticConcurrency, a cui si aggiungerà un Products TableAdapter che usa la concorrenza ottimistica. Successivamente si creerà una ProductsOptimisticConcurrencyBLL classe livello di logica di business con le modifiche appropriate per supportare la concorrenza ottimistica DAL. Una volta gettate queste basi, 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 DAL cartella all'interno della App_Code cartella e aggiungere un nuovo dataset denominato NorthwindOptimisticConcurrency. Come illustrato nella prima esercitazione, in questo modo verrà aggiunto 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.

Connetti allo stesso database Northwind

Figura 3: Connettersi allo stesso database Northwind (fare clic per visualizzare l'immagine a dimensione intera)

Ci viene chiesto come interrogare i dati: utilizzando un'istruzione SQL ad hoc, una nuova stored procedure, o una stored procedure esistente. Poiché sono state usate query SQL ad hoc nel DAL originale, utilizzare 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 un'immagine a dimensione intera)

Nella schermata seguente immettere la query SQL da usare per recuperare le informazioni sul prodotto. Utilizziamo ora la stessa query SQL usata per il TableAdapter Products dal nostro DAL originale, la quale restituisce tutte le colonne Product insieme ai nomi dei fornitori e delle 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 di Products TableAdapter nel file DAL originale

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

Prima di passare alla schermata successiva, fare clic sul pulsante Opzioni avanzate. Per fare in modo che tableAdapter usi il controllo della concorrenza ottimistica, basta selezionare la casella di controllo "Usa concorrenza ottimistica".

Abilitare il controllo della concorrenza ottimistica controllando la casella di controllo

Figura 6: Abilitare il controllo della concorrenza ottimistica controllando la casella di controllo "Usa concorrenza ottimistica" (fare clic per visualizzare l'immagine a dimensione intera)

Infine, indicare che TableAdapter deve usare i modelli di accesso ai dati che riempiono una DataTable e restituiscono una DataTable. indicare anche che devono essere creati i metodi diretti del database. 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 modello DAL originale.

Fare in modo che TableAdapter usi tutti i modelli di accesso ai dati

Figura 7: Fare clic su TableAdapter per utilizzare tutti i modelli di accesso ai dati (fare clic per visualizzare l'immagine a dimensione intera)

Al termine della procedura guidata, il Progettista di DataSet includerà un Products DataTable fortemente tipizzato e un TableAdapter. Prenditi un momento per rinominare la DataTable da Products a ProductsOptimisticConcurrency, cosa che puoi fare facendo clic con il pulsante destro del mouse sulla barra del titolo di DataTable e scegliendo Rinomina dal menu di scelta rapida.

Un oggetto DataTable e TableAdapter sono stati aggiunti al dataset tipizzato

Figura 8: Un oggetto DataTable e TableAdapter sono stati aggiunti al dataset tipizzato (fare clic per visualizzare l'immagine a dimensione intera)

Per visualizzare le differenze tra le query UPDATE e DELETE tra il TableAdapter ProductsOptimisticConcurrency (che usa la concorrenza ottimistica) e il TableAdapter dei Prodotti (che non lo fa), fare clic sul TableAdapter e accedere alla finestra delle Proprietà. Nelle proprietà DeleteCommand e UpdateCommand, nelle rispettive proprietà secondarie CommandText, è possibile visualizzare la sintassi SQL effettiva inviata al database quando vengono richiamati i metodi di aggiornamento o eliminazione correlati al DAL. Per TableAdapter, l'istruzione ProductsOptimisticConcurrencyDELETE 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))

L'istruzione DELETE per il Product TableAdapter nel nostro DAL originale è molto più semplice.

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

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

Non verranno aggiunte altre tabelle DataTable al DataSet concurrency ottimistico per questa esercitazione, perché la pagina ASP.NET fornirà solo informazioni sull'aggiornamento e l'eliminazione dei prodotti. Tuttavia, è comunque necessario aggiungere il GetProductByProductID(productID) metodo al ProductsOptimisticConcurrency TableAdapter.

A tale scopo, fare clic con il pulsante destro del mouse sulla barra del titolo di TableAdapter (l'area sopra i nomi dei Fill metodi e GetProducts ) e scegliere Aggiungi query dal menu di scelta rapida. Questa azione avvierà il wizard di configurazione delle query di 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 GetProductByProductID(productID) metodo restituisce informazioni su un determinato prodotto, indicare che questa 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 a dimensione intera)

Nella schermata successiva ci viene richiesto di fornire la query SQL, con la query predefinita di TableAdapter già precaricata. 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 a dimensione intera)

Modificare infine 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 a dimensione intera)

Al termine della 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 business logic per il DAL ottimistico Concurrency-Enabled

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

Con il nuovo ProductsOptimisticConcurrency TableAdapter, i metodi diretti del database richiedono ora che vengano passati anche i valori originali. Ad esempio, il Delete metodo prevede ora dieci parametri di input: originaleProductID, ProductName, SupplierIDCategoryID, QuantityPerUnit, UnitPrice, , UnitsInStockUnitsOnOrder, 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.

Anche se la firma del metodo del TableAdapter Update usato nella procedura di aggiornamento batch non è stata modificata, il codice necessario per registrare i valori originali e nuovi è cambiato. Pertanto, invece di tentare di usare il DAL abilitato al concurrency ottimistico con la classe esistente ProductsBLL, creiamo una nuova classe del Livello Logico di Business per lavorare con il nostro nuovo DAL.

Aggiungere una classe denominata ProductsOptimisticConcurrencyBLL alla BLL cartella all'interno della App_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

Si noti l'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 proprietà ProductsOptimisticConcurrencyBLL di Adapter fornisce accesso rapido a un'istanza della classe ProductsOptimisticConcurrencyTableAdapter e segue il modello usato nelle classi BLL originali (ProductsBLL, CategoriesBLL, e così via). Infine, il GetProducts() metodo chiama semplicemente il metodo del GetProducts() e restituisce un ProductsOptimisticConcurrencyDataTable oggetto popolato con un'istanza di ProductsOptimisticConcurrencyRow per ogni record di prodotto nel database.

Eliminazione di un prodotto tramite il modello diretto del database con concorrenza ottimistica

Quando si utilizza il modello diretto del database con un DAL che usa la concorrenza ottimistica, ai metodi devono essere passati i valori nuovi e originali. Per l'eliminazione, non sono presenti nuovi valori, quindi è necessario passare solo i valori originali. Nel BLL, quindi, è necessario accettare tutti i parametri originali come parametri di input. Si supponga di avere il DeleteProduct metodo nella ProductsOptimisticConcurrencyBLL classe che usa il metodo diretto del database. Ciò significa che questo metodo deve accettare tutti e dieci i campi dati del prodotto come parametri di input e passarli 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 caricati per l'ultima volta in GridView (o DetailsView o FormView) differiscono dai valori del database quando l'utente fa clic sul pulsante Elimina, la WHERE clausola non corrisponderà ad alcun record di database e non saranno interessati alcun record. Di conseguenza, il metodo di Delete TableAdapter restituirà 0 e il metodo BLL DeleteProduct restituirà false.

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

Come indicato in precedenza, il metodo Update del TableAdapter per il modello di aggiornamento batch ha la stessa firma del metodo, a prescindere dal fatto che si utilizzi 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é il DataTable tiene traccia dei valori originali e modificati per le sue righe dei dati. Quando il DAL rilascia UPDATE l'istruzione, i @original_ColumnName parametri vengono popolati con i valori originali di DataRow, mentre i @ColumnName parametri vengono popolati con i valori modificati di DataRow.

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

  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 del ProductRow Passo 1
  3. Chiamare il metodo del TableAdapter Update e passare l'istanza ProductRow

Questa sequenza di passaggi, tuttavia, non supporterà correttamente la concorrenza ottimistica perché il ProductRow popolamento nel passaggio 1 viene popolato direttamente dal database, ovvero 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 invece si usa un DAL abilitato per la concorrenza ottimistica, è necessario modificare gli overload del metodo UpdateProduct per usare la procedura la 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 del ProductsOptimisticConcurrencyRow passaggio 1
  3. Chiama il metodo ProductsOptimisticConcurrencyRow dell'istanza AcceptChanges(), che istruisce il DataRow che i suoi valori correnti sono quelli "originali"
  4. Assegna i nuovi valori all'istanza ProductsOptimisticConcurrencyRow
  5. Chiamare il metodo del TableAdapter Update e passare l'istanza ProductsOptimisticConcurrencyRow

Il passaggio 1 legge tutti i valori correnti del database 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 gli overload in cui solo un subset dei valori di colonna viene passato come parametri di input. Dopo aver assegnato i valori originali all'istanza ProductsOptimisticConcurrencyRow , viene chiamato il AcceptChanges() metodo , che contrassegna i valori dataRow correnti come valori originali da usare nei @original_ColumnName parametri dell'istruzione UPDATE . Successivamente, i nuovi valori dei parametri vengono assegnati a ProductsOptimisticConcurrencyRow e, infine, viene richiamato il metodo Update, passando il DataRow.

Il codice seguente illustra 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 ASP.NET che può usare la logica di concorrenza ottimistica incorporata nel sistema. In particolare, il controllo Web dati (GridView, DetailsView o FormView) deve ricordare i valori originali e ObjectDataSource deve passare entrambi i set di valori al livello della logica di business. Inoltre, la pagina ASP.NET deve essere configurata per gestire correttamente le violazioni della concorrenza.

Per iniziare, apri la pagina OptimisticConcurrency.aspx nella cartella EditInsertDelete e aggiungi un GridView al Designer, impostando la sua proprietà ID su ProductsGrid. Dallo smart tag di GridView scegliere di creare un nuovo ObjectDataSource denominato ProductsOptimisticConcurrencyDataSource. Poiché si desidera che ObjectDataSource utilizzi il DAL che supporta la concorrenza ottimistica, configurarlo per l'uso dell'oggetto ProductsOptimisticConcurrencyBLL.

Fare in modo che ObjectDataSource usi l'oggetto ProductsOptimisticConcurrencyBLL

Figura 13: Usa l'oggetto ProductsOptimisticConcurrencyBLL con ObjectDataSource (Fare clic per visualizzare l'immagine a dimensione intera)

Scegliere i GetProducts, UpdateProduct e DeleteProduct metodi 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

Dopo aver completato la 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 raccolta contiene un'istanza DeleteParameters per ognuno dei dieci parametri di input nel metodo Parameter della classe ProductsOptimisticConcurrencyBLL. Analogamente, la raccolta contiene un'istanza UpdateParametersParameter per ognuno dei parametri di input in UpdateProduct.

Per le esercitazioni precedenti che hanno comportato la modifica dei dati, la proprietà ObjectDataSource OldValuesParameterFormatString verrà rimossa a questo punto, poiché questa proprietà indica che il metodo BLL prevede che i valori precedenti (o originali) vengano passati e i nuovi valori. Inoltre, questo valore della proprietà indica i nomi dei parametri di input per i valori originali. Poiché si passano i valori originali nel BLL, non rimuovere questa proprietà.

Annotazioni

Il valore della OldValuesParameterFormatString proprietà deve essere mappato ai nomi dei parametri di 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}.

Esiste un'impostazione finale della proprietà che deve essere eseguita affinché ObjectDataSource passi correttamente i valori originali ai metodi BLL. ObjectDataSource ha una proprietà ConflictDetection che può essere assegnata a uno dei due valori seguenti:

  • OverwriteChanges - il 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

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

Configurazione delle proprietà e dei campi di GridView

Con le proprietà di ObjectDataSource configurate correttamente, diamo un'occhiata alla configurazione di GridView. In primo luogo, poiché si vuole che GridView supporti la modifica e l'eliminazione, fare clic sulle caselle di controllo Abilita modifica e Abilita eliminazione dallo smart tag di GridView. Verrà aggiunto un oggetto CommandField il cui ShowEditButton 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 è possibile modificare un controllo GridView di questo tipo, l'esperienza utente è tutt'altro che accettabile. I "BoundFields" CategoryID e SupplierID saranno resi come TextBoxes, richiedendo all'utente di immettere la categoria e il fornitore appropriati come numeri ID. Non ci 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 del livello di riordinamento siano sia valori numerici appropriati che siano maggiori o uguali a zero.

Come discusso nei tutorial sull'aggiunta di controlli di convalida alle interfacce di modifica e inserimento e sulla personalizzazione dell'interfaccia di modifica dei dati, l'interfaccia utente può essere personalizzata sostituendo i BoundFields con i TemplateFields. Ho modificato questo controllo GridView e la relativa interfaccia di modifica nei modi seguenti:

  • Rimozione di ProductID, SupplierNamee CategoryName BoundFields
  • Convertito BoundField ProductName in un oggetto TemplateField e aggiunto un controllo RequiredFieldValidation.
  • Convertiti i CategoryID e SupplierID BoundFields in TemplateFields e modificata l'interfaccia di modifica in modo da usare i DropDownLists anziché i TextBoxes. In questi TemplateFields ItemTemplates, vengono visualizzati i campi dati CategoryName e SupplierName.
  • Convertito i UnitPrice, UnitsInStock, UnitsOnOrder e ReorderLevel BoundFields in TemplateFields e aggiunto i controlli CompareValidator.

Poiché abbiamo già esaminato come svolgere queste attività nelle esercitazioni precedenti, elencherò solo la sintassi dichiarativa finale qui e lascerò l'implementazione come esercitazione.

<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 sfumature che emergeranno e ci causeranno problemi. Inoltre, è ancora necessaria un'interfaccia che avvisa l'utente quando si è verificata una violazione della concorrenza.

Annotazioni

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à ConflictDetection di ObjectDataSource è impostata su CompareAllValues (come nel nostro caso), quando i metodi Update() o Delete() di ObjectDataSource vengono richiamati da GridView (o DetailsView o FormView), ObjectDataSource tenta di copiare i valori originali di GridView nelle sue istanze appropriate Parameter. Per una rappresentazione grafica di questo processo, vedere la figura 2.

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

Per vedere perché questo è importante, dedicare 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 una GridView

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

Se fai clic sul pulsante Elimina per un prodotto, viene generata un'eccezione FormatException.

Tentativo di eliminare qualsiasi prodotto comporta un'eccezione FormatException

Figura 15: Tentativo di eliminare i risultati di un prodotto in un FormatException (fare clic per visualizzare un'immagine a dimensione intera)

Viene generato FormatException quando ObjectDataSource tenta di leggere il valore originale UnitPrice. Poiché l'oggetto ItemTemplateUnitPrice è formattato come valuta (<%# Bind("UnitPrice", "{0:C}") %>), include un simbolo di valuta, ad esempio $19,95. L'errore FormatException si verifica quando ObjectDataSource tenta di convertire questa stringa in un elemento 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 UnitPrice formattato come valuta in ItemTemplate, ma usare la parola chiave Eval 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 questa operazione 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, utilizzando <%# Bind("UnitPrice") %>. Nel gestore eventi di GridView, accedere programmaticamente al controllo Web Label all'interno del quale viene visualizzato il valore e impostare la proprietà RowDataBound sulla versione formattata.
  • Lascia UnitPrice formattato come valuta. Nel gestore eventi di RowDeleting GridView sostituire il valore originale UnitPrice esistente ($19,95) con un valore decimale effettivo usando Decimal.Parse. Abbiamo visto come eseguire un'operazione simile nel gestore di eventi nella lezione RowUpdatingGestione delle eccezioni BLL e DAL-Level in una pagina ASP.NET.

Per il mio esempio, ho scelto di utilizzare il secondo approccio, aggiungendo un controllo Web Etichetta nascosto la cui proprietà è associata ai dati bidirezionali al valore non formattato.

Dopo aver risolto questo problema, provare a fare clic sul pulsante Elimina per qualsiasi prodotto. Questa volta riceverai un InvalidOperationException quando l'ObjectDataSource tenterà di invocare il metodo UpdateProduct del BLL.

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 parametri di input e original_SupplierName . Ciò è dovuto al fatto che i ItemTemplate per i TemplateFields CategoryID e SupplierID attualmente contengono istruzioni di Binding bidirezionale con i campi dati CategoryName e SupplierName. Invece, dobbiamo includere Bind istruzioni con i campi dati CategoryID e SupplierID. A tale scopo, sostituire le istruzioni Bind esistenti con Eval istruzioni e quindi aggiungere controlli Label nascosti le cui Text proprietà sono associate ai CategoryID 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 possibile eliminare e modificare correttamente le informazioni sul prodotto. Nel passaggio 5 verrà illustrato come verificare che vengano rilevate violazioni della concorrenza. Tuttavia, per il momento, provare ad aggiornare ed eliminare alcuni record per assicurarsi che l'aggiornamento e l'eliminazione per un singolo utente funzionino 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, però, il nome del prodotto TextBox continua a mostrare "Chai". In questa seconda finestra del browser, aggiornare UnitPrice in 25.00. Senza il supporto della concorrenza ottimistica, facendo clic sull'aggiornamento nella seconda istanza del browser il nome del prodotto viene nuovamente 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 restituita un'eccezione DBConcurrencyException.

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

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

Viene generato DBConcurrencyException solo quando viene utilizzato il modello di aggiornamento batch di DAL. Il modello diretto del database non genera un'eccezione, indica semplicemente che nessuna riga è stata interessata. Per illustrare questo, restituire la GridView di entrambe le istanze del browser allo stato precedente alla 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 Delete() di ObjectDataSource e ObjectDataSource chiama il metodo ProductsOptimisticConcurrencyBLL della classe DeleteProduct, 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. Pertanto, l'istruzione DELETE rilasciata al database influisce su zero righe poiché 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 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. Chiaramente non si vuole 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 perché il comando degli utenti non è riuscito, ma non c'è un'indicazione precisa del motivo.

Per risolvere questi due problemi, è possibile creare controlli Web di etichetta nella pagina che forniscano una spiegazione del motivo per cui un aggiornamento o un'eliminazione non sono riusciti. 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 in caso di violazione di concorrenza

Quando si verifica una violazione della concorrenza, il comportamento dipende dal fatto che sia stato utilizzato l'aggiornamento batch del DAL o il modello diretto del DB. 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, aggiungiamo due controlli etichetta Web alla nostra pagina per spiegare che si è verificata una violazione della concorrenza quando si tenta di eliminare o aggiornare i dati. Impostare le proprietà Visible e EnableViewState del controllo Etichetta su false; in questo modo, il controllo verrà nascosto in ogni visita di pagina, ad eccezione di quelle particolari visite in cui la sua proprietà Visible è impostata programmaticamente su 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." />

Oltre a impostare le proprietà Visible, EnabledViewState e Text, ho anche impostato la proprietà CssClass su Warning, che fa sì che la etichetta venga visualizzata in un carattere grande, rosso, corsivo, grassetto. Questa classe CSS Warning è stata definita e aggiunta a Styles.css nel corso dell'esercitazione Analisi degli eventi associati all'inserimento, all'aggiornamento e all'eliminazione.

Dopo aver aggiunto queste etichette, la finestra di progettazione 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 Label, siamo pronti a esaminare come determinare quando si verifica una violazione di concorrenza, a quel punto la proprietà del Label appropriato può essere impostata su Visible, visualizzando il messaggio informativo.

Gestione delle violazioni di concorrenza durante l'aggiornamento

Si esaminerà 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 una 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 gridView RowUpdated che controlla se è stata generata un'eccezione DBConcurrencyException . In questo gestore eventi viene passato un riferimento a qualsiasi eccezione che si è verificata 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 presenza di un'eccezione DBConcurrencyException, questo gestore eventi visualizza il controllo Label UpdateConflictMessage 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 correnti del database. Verrà aggiornata la riga GridView con le modifiche dell'altro utente, che in precedenza non erano visibili. Inoltre, il UpdateConflictMessage controllo Etichetta spiega all'utente cosa è successo. Questa sequenza di eventi è dettagliata nella figura 19.

Gli aggiornamenti di un utente vengono persi in caso di violazione della concorrenza

Figura 19: Gli aggiornamenti di un utente vengono persi a causa di una violazione della concorrenza (fare clic per visualizzare l'immagine a schermo intero)

Annotazioni

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 scaricabile con questo tutorial ha queste due righe di codice nel gestore eventi commentate; basta rimuovere il commento da queste righe di codice per mantenere il GridView 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. In realtà, l'istruzione di database non influisce su nessun record, poiché la clausola WHERE non corrisponde a nessun 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 interessato o meno esattamente un record. Pertanto, per determinare se si è verificata una violazione di concorrenza durante l'eliminazione DeleteProduct di un record, è possibile esaminare il valore restituito del metodo BLL.

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 Deleted ObjectDataSource. 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, dovremmo prima assicurarci che la ReturnValue proprietà non sia null e sia un valore booleano. Supponendo che questo controllo venga superato, viene mostrato il controllo DeleteConflictMessage dell'etichetta se ReturnValue è false. 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 al 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 è accaduto (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 faccia a una violazione di concorrenza (fare clic per visualizzare l'immagine a dimensione intera)

Riassunto

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, chi effettua l'ultima modifica ha la meglio, sovrascrivendo le modifiche dell'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 di 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 pessimistico, 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 supportare il controllo della concorrenza ottimistica. In particolare, le istruzioni UPDATE 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 quando ha eseguito l'aggiornamento o l'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 relativo controllo Web 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 hanno 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 regolarmente più utenti nel sito che usano gli stessi dati, il controllo della concorrenza può aiutare a impedire agli aggiornamenti o alle eliminazioni di un utente di sovrascrivere involontariamente un altro.

Buon programmatori!

Informazioni sull'autore

Scott Mitchell, autore di sette libri ASP/ASP.NET e fondatore di 4GuysFromRolla.com, ha lavorato 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 a mitchell@4GuysFromRolla.com.