Partager via


Implémentation de l’accès concurrentiel optimiste (VB)

par Scott Mitchell

Télécharger le PDF

Pour une application web qui permet à plusieurs utilisateurs de modifier des données, il existe le risque que deux utilisateurs modifient les mêmes données en même temps. Dans ce tutoriel, nous allons implémenter un contrôle d’accès concurrentiel optimiste pour gérer ce risque.

Introduction

Pour les applications web qui permettent uniquement aux utilisateurs d’afficher des données, ou pour celles qui n’incluent qu’un seul utilisateur capable de modifier des données, il n’y a pas de menace que deux utilisateurs simultanés se remplacent accidentellement par les modifications de l’autre. Toutefois, pour les applications web qui permettent à plusieurs utilisateurs de mettre à jour ou de supprimer des données, il est possible que les modifications d’un utilisateur entrent en conflit avec celles d’un autre utilisateur simultané. Sans aucune stratégie d’accès concurrentiel en place, lorsque deux utilisateurs modifient simultanément un seul enregistrement, l’utilisateur qui valide ses dernières modifications remplace les modifications apportées par le premier.

Par exemple, imaginez que deux utilisateurs, Jisun et Sam, consultaient une page de notre application qui permettait aux visiteurs de mettre à jour et de supprimer les produits via un contrôle GridView. Tous deux cliquez sur le bouton Modifier dans GridView à peu près en même temps. Jisun remplace le nom du produit par « Chai Tea » et clique sur le bouton Mettre à jour. Le résultat net est une UPDATE instruction envoyée à la base de données, qui définit tous les champs pouvant être mis à jour du produit (même si Jisun n’a mis à jour qu’un seul champ, ProductName). À ce stade, la base de données a les valeurs « Chai Tea », la catégorie Boissons, le fournisseur Liquides exotiques, et ainsi de suite pour ce produit particulier. Toutefois, l’écran GridView de Sam affiche toujours le nom du produit dans la ligne GridView modifiable sous la forme « Chai ». Quelques secondes après la validation des modifications de Jisun, Sam met à jour la catégorie vers Condiments et clique sur Mettre à jour. Il en résulte une UPDATE instruction envoyée à la base de données qui définit le nom du produit sur « Chai », le CategoryID sur l’ID de catégorie Boissons correspondant, et ainsi de suite. Les modifications apportées par Jisun au nom du produit ont été remplacées. La figure 1 représente graphiquement cette série d’événements.

Lorsque deux utilisateurs mettent à jour simultanément un enregistrement, il est possible qu’un utilisateur modifie pour remplacer l’autre

Figure 1 : Lorsque deux utilisateurs mettent à jour simultanément un enregistrement, il est possible qu’un utilisateur change de remplacer l’autre (cliquez pour afficher l’image en taille réelle)

De même, lorsque deux utilisateurs visitent une page, un utilisateur peut être en train de mettre à jour un enregistrement lorsqu’il est supprimé par un autre utilisateur. Ou, entre le moment où un utilisateur charge une page et qu’il clique sur le bouton Supprimer, un autre utilisateur peut avoir modifié le contenu de cet enregistrement.

Trois stratégies de contrôle d’accès concurrentiel sont disponibles :

  • Ne rien faire : si des utilisateurs simultanés modifient le même enregistrement, laissez le dernier commit gagner (le comportement par défaut)
  • Accès concurrentiel optimiste : supposons que même s’il peut y avoir des conflits d’accès concurrentiel de temps en temps, la grande majorité du temps de tels conflits ne se produisent pas ; par conséquent, si un conflit se produit, il suffit d’informer l’utilisateur que ses modifications ne peuvent pas être enregistrées, car un autre utilisateur a modifié les mêmes données
  • Concurrence pessimiste : supposons que les conflits d’accès concurrentiel sont courants et que les utilisateurs ne tolèrent pas qu’on leur indique que leurs modifications n’ont pas été enregistrées en raison de l’activité simultanée d’un autre utilisateur ; par conséquent, lorsqu’un utilisateur commence à mettre à jour un enregistrement, verrouillez-le, empêchant ainsi d’autres utilisateurs de modifier ou de supprimer cet enregistrement jusqu’à ce que l’utilisateur valide leurs modifications

Jusqu’à présent, tous nos tutoriels ont utilisé la stratégie de résolution de concurrence par défaut, à savoir que nous avons laissé gagner la dernière écriture. Dans ce tutoriel, nous allons examiner comment implémenter un contrôle d’accès concurrentiel optimiste.

Notes

Nous n’examinerons pas d’exemples pessimistes de concurrence dans cette série de tutoriels. La concurrence pessimiste est rarement utilisée, car de tels verrous, s’ils ne sont pas correctement abandonnés, peuvent empêcher d’autres utilisateurs de mettre à jour les données. Par exemple, si un utilisateur verrouille un enregistrement pour la modification et qu’il part le jour avant de le déverrouiller, aucun autre utilisateur ne sera en mesure de mettre à jour cet enregistrement tant que l’utilisateur d’origine n’aura pas retourné et terminé sa mise à jour. Par conséquent, dans les situations où la concurrence pessimiste est utilisée, il existe généralement un délai d’attente qui, s’il est atteint, annule le verrou. Les sites web de vente de billets, qui verrouillent un emplacement particulier pour une courte période pendant que l’utilisateur termine le processus de commande, sont un exemple de contrôle d’accès concurrentiel pessimiste.

Étape 1 : Examiner la façon dont la concurrence optimiste est implémentée

Le contrôle d’accès concurrentiel optimiste fonctionne en s’assurant que l’enregistrement en cours de mise à jour ou de suppression a les mêmes valeurs que lors du démarrage du processus de mise à jour ou de suppression. Par exemple, lorsque vous cliquez sur le bouton Modifier dans un GridView modifiable, les valeurs de l’enregistrement sont lues à partir de la base de données et affichées dans textboxes et autres contrôles web. Ces valeurs d’origine sont enregistrées par GridView. Plus tard, une fois que l’utilisateur a apporté ses modifications et cliqué sur le bouton Mettre à jour, les valeurs d’origine plus les nouvelles valeurs sont envoyées à la couche logique métier, puis à la couche d’accès aux données. La couche d’accès aux données doit émettre une instruction SQL qui met à jour l’enregistrement uniquement si les valeurs d’origine que l’utilisateur a commencé à modifier sont identiques aux valeurs toujours dans la base de données. La figure 2 illustre cette séquence d’événements.

Pour que la mise à jour ou la suppression réussisse, les valeurs d’origine doivent être égales aux valeurs de base de données actuelles

Figure 2 : Pour que la mise à jour ou la suppression réussisse, les valeurs d’origine doivent être égales aux valeurs de base de données actuelles (cliquer pour afficher l’image en taille réelle)

Il existe différentes approches pour implémenter l’accès concurrentiel optimiste (voir La logique de mise à jour de la concurrence optimiste de Peter A. Bromberg pour un bref aperçu de plusieurs options). L’ADO.NET DataSet typé fournit une implémentation qui peut être configurée en cochant simplement une case à cocher. L’activation de la concurrence optimiste pour un TableAdapter dans le DataSet typé augmente les instructions et DELETE de UPDATE TableAdapter pour inclure une comparaison de toutes les valeurs d’origine dans la WHERE clause. L’instruction suivante UPDATE , par exemple, met à jour le nom et le prix d’un produit uniquement si les valeurs de base de données actuelles sont égales aux valeurs qui ont été récupérées à l’origine lors de la mise à jour de l’enregistrement dans GridView. Les @ProductName paramètres et @UnitPrice contiennent les nouvelles valeurs entrées par l’utilisateur, tandis que @original_ProductName et @original_UnitPrice contiennent les valeurs qui ont été initialement chargées dans le GridView lorsque vous avez cliqué sur le bouton Modifier :

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

Notes

Cette UPDATE instruction a été simplifiée pour plus de lisibilité. Dans la pratique, le UnitPrice case activée dans la clause serait plus impliqué dans la WHERE mesure où UnitPrice peut contenir NULL s et vérifier si NULL = NULL retourne toujours False (à la place, vous devez utiliser IS NULL).

En plus d’utiliser une autre instruction sous-jacente UPDATE , la configuration d’un TableAdapter pour utiliser l’accès concurrentiel optimiste modifie également la signature de ses méthodes directes de base de données. Rappelez-vous de notre premier tutoriel, Création d’une couche d’accès aux données, que les méthodes directes de base de données étaient celles qui acceptent une liste de valeurs scalaires comme paramètres d’entrée (plutôt qu’une instance DataRow ou DataTable fortement typée). Lorsque vous utilisez l’accès concurrentiel optimiste, le direct Update() de base de données et Delete() les méthodes incluent également des paramètres d’entrée pour les valeurs d’origine. En outre, le code dans la BLL pour l’utilisation du modèle de mise à jour par lots (les Update() surcharges de méthode qui acceptent DataRows et DataTables plutôt que les valeurs scalaires) doit également être modifié.

Plutôt que d’étendre nos TableAdapters de DAL existants pour utiliser l’accès concurrentiel optimiste (ce qui nécessiterait de modifier la BLL pour prendre en charge), créons plutôt un dataset typé nommé NorthwindOptimisticConcurrency, auquel nous allons ajouter un TableAdapter qui utilise la Products concurrence optimiste. Ensuite, nous allons créer une ProductsOptimisticConcurrencyBLL classe de couche logique métier qui a les modifications appropriées pour prendre en charge le DAL d’accès concurrentiel optimiste. Une fois ce travail de base posé, nous serons prêts à créer la page ASP.NET.

Étape 2 : Création d’une couche d’accès aux données qui prend en charge la concurrence optimiste

Pour créer un DataSet typé, cliquez avec le bouton droit sur le DAL dossier dans le App_Code dossier et ajoutez un DataSet nommé NorthwindOptimisticConcurrency. Comme nous l’avons vu dans le premier tutoriel, cela ajoute un nouveau TableAdapter au DataSet typé, en lançant automatiquement l’Assistant Configuration de TableAdapter. Dans le premier écran, nous sommes invités à spécifier la base de données à laquelle se connecter : connectez-vous à la même base de données Northwind à l’aide du NORTHWNDConnectionString paramètre de Web.config.

Se connecter à la même base de données Northwind

Figure 3 : Se connecter à la même base de données Northwind (cliquer pour afficher l’image en taille réelle)

Ensuite, nous sommes invités à savoir comment interroger les données : par le biais d’une instruction SQL ad hoc, d’une nouvelle procédure stockée ou d’une procédure stockée existante. Étant donné que nous avons utilisé des requêtes SQL ad hoc dans notre DAL d’origine, utilisez également cette option ici.

Spécifier les données à récupérer à l’aide d’une instruction SQL ad hoc

Figure 4 : Spécifier les données à récupérer à l’aide d’une instruction SQL ad hoc (cliquer pour afficher l’image en taille réelle)

Dans l’écran suivant, entrez la requête SQL à utiliser pour récupérer les informations sur le produit. Nous allons utiliser exactement la même requête SQL que celle utilisée pour le Products TableAdapter de notre DAL d’origine, qui retourne toutes les colonnes ainsi que les Product noms de fournisseur et de catégorie du produit :

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

Utiliser la même requête SQL à partir de la table ProductsAdapter dans le DAL d’origine

Figure 5 : Utiliser la même requête SQL à partir de TableAdapter Products dans le DAL d’origine (cliquer pour afficher l’image en taille réelle)

Avant de passer à l’écran suivant, cliquez sur le bouton Options avancées. Pour que ce TableAdapter utilise le contrôle d’accès concurrentiel optimiste, case activée simplement la case à cocher « Utiliser la concurrence optimiste ».

Activer le contrôle d’accès concurrentiel optimiste en vérifiant la case CheckBox « Utiliser la concurrence optimiste »

Figure 6 : Activer le contrôle d’accès concurrentiel optimiste en vérifiant la case CheckBox « Utiliser la concurrence optimiste » (cliquer pour afficher l’image en taille réelle)

Enfin, indiquez que TableAdapter doit utiliser les modèles d’accès aux données qui remplissent un DataTable et retournent un DataTable ; indiquent également que les méthodes directes de base de données doivent être créées. Modifiez le nom de la méthode pour renvoyer un modèle DataTable de GetData à GetProducts, afin de miroir les conventions de nommage que nous avons utilisées dans notre DAL d’origine.

Faites en charge à TableAdapter d’utiliser tous les modèles d’accès aux données

Figure 7 : Faire utiliser tous les modèles d’accès aux données par tableAdapter (cliquer pour afficher l’image en taille réelle)

Une fois l’Assistant terminé, l’Designer DataSet inclut un DataTable et un TableAdapter fortement typésProducts. Prenez un moment pour renommer le DataTable à partir de ProductsProductsOptimisticConcurrency, ce que vous pouvez faire en cliquant avec le bouton droit sur la barre de titre de DataTable et en choisissant Renommer dans le menu contextuel.

Un DataTable et un TableAdapter ont été ajoutés au DataSet typé

Figure 8 : Un DataTable et un TableAdapter ont été ajoutés au DataSet typé (cliquez pour afficher l’image en taille réelle)

Pour voir les différences entre UPDATE les requêtes et DELETE entre tableAdapter ProductsOptimisticConcurrency (qui utilise l’accès concurrentiel optimiste) et la table Products TableAdapter (ce qui n’est pas le cas), cliquez sur tableAdapter et accédez au Fenêtre Propriétés. Dans les DeleteCommand sous-propriétés et UpdateCommandCommandText , vous pouvez voir la syntaxe SQL réelle envoyée à la base de données lorsque les méthodes de mise à jour ou de suppression du DAL sont appelées. Pour tableAdapter ProductsOptimisticConcurrency , l’instruction DELETE utilisée est :

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

Alors que l’instruction DELETE pour le Product TableAdapter dans notre DAL d’origine est beaucoup plus simple :

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

Comme vous pouvez le voir, la clause de l’instruction WHEREDELETE De TableAdapter qui utilise l’accès concurrentiel optimiste inclut une comparaison entre chacune des valeurs de colonne existantes de la Product table et les valeurs d’origine au moment où gridView (ou DetailsView ou FormView) a été renseigné pour la dernière fois. Étant donné que tous les champs autres que ProductID, ProductNameet Discontinued peuvent avoir NULL des valeurs, des paramètres et des vérifications supplémentaires sont inclus pour comparer correctement les NULL valeurs de la WHERE clause.

Nous n’ajouterons pas de DataTables supplémentaires au DataSet optimiste avec accès concurrentiel pour ce didacticiel, car notre page ASP.NET ne fournit que la mise à jour et la suppression des informations sur le produit. Toutefois, nous devons toujours ajouter la GetProductByProductID(productID) méthode à TableAdapter ProductsOptimisticConcurrency .

Pour ce faire, cliquez avec le bouton droit sur la barre de titre de TableAdapter (la zone située juste au-dessus des noms de méthode Fill et GetProducts ) et choisissez Ajouter une requête dans le menu contextuel. L’Assistant Configuration de requête TableAdapter est alors lancé. Comme pour la configuration initiale de notre TableAdapter, choisissez de créer la méthode à l’aide GetProductByProductID(productID) d’une instruction SQL ad hoc (voir figure 4). Étant donné que la GetProductByProductID(productID) méthode retourne des informations sur un produit particulier, indiquez que cette requête est un SELECT type de requête qui retourne des lignes.

Marquez le type de requête comme « SELECT qui retourne des lignes »

Figure 9 : Marquer le type de requête comme un «SELECT qui retourne des lignes » (cliquez pour afficher l’image en taille réelle)

Dans l’écran suivant, nous sommes invités à utiliser la requête SQL, avec la requête par défaut de TableAdapter préchargée. Augmentez la requête existante pour inclure la clause WHERE ProductID = @ProductID, comme illustré dans la figure 10.

Ajouter une clause WHERE à la requête préchargée pour retourner un enregistrement de produit spécifique

Figure 10 : Ajouter une WHERE clause à la requête préchargée pour retourner un enregistrement de produit spécifique (cliquer pour afficher une image en taille réelle)

Enfin, remplacez les noms de méthodes générées par FillByProductID et GetProductByProductID.

Renommez les méthodes FillByProductID et GetProductByProductID

Figure 11 : Renommez les méthodes en FillByProductID et GetProductByProductID (Cliquez pour afficher l’image en taille réelle)

Une fois cet Assistant terminé, TableAdapter contient désormais deux méthodes pour récupérer des données : GetProducts(), qui retourne tous les produits ; et GetProductByProductID(productID), qui retourne le produit spécifié.

Étape 3 : Création d’une couche logique métier pour le dal optimiste Concurrency-Enabled

Notre classe existante ProductsBLL contient des exemples d’utilisation des modèles directs de mise à jour par lot et de base de données. La AddProduct méthode et UpdateProduct les surcharges utilisent le modèle de mise à jour par lots, en passant un ProductRow instance à la méthode Update de TableAdapter. La DeleteProduct méthode, d’autre part, utilise le modèle direct de base de données, appelant la méthode de Delete(productID) TableAdapter.

Avec le nouveau ProductsOptimisticConcurrency TableAdapter, les méthodes directes de base de données nécessitent désormais que les valeurs d’origine soient également transmises. Par exemple, la Delete méthode attend désormais dix paramètres d’entrée : , ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnitUnitPriceUnitsInStock, , UnitsOnOrder, et ReorderLevelDiscontinued. Il utilise les valeurs de ces paramètres d’entrée supplémentaires dans WHERE la clause de l’instruction DELETE envoyée à la base de données, en supprimant uniquement l’enregistrement spécifié si les valeurs actuelles de la base de données sont mappées aux valeurs d’origine.

Bien que la signature de méthode pour la méthode TableAdapter Update utilisée dans le modèle de mise à jour par lots n’ait pas changé, le code nécessaire pour enregistrer les valeurs d’origine et les nouvelles valeurs a. Par conséquent, plutôt que d’essayer d’utiliser le DAL avec accès concurrentiel optimiste avec notre classe existante ProductsBLL , créons une nouvelle classe de couche logique métier pour travailler avec notre nouvelle DAL.

Ajoutez une classe nommée ProductsOptimisticConcurrencyBLL au BLL dossier dans le App_Code dossier.

Ajouter la classe ProductsOptimisticConcurrencyBLL au dossier BLL

Figure 12 : Ajouter la ProductsOptimisticConcurrencyBLL classe au dossier BLL

Ensuite, ajoutez le code suivant à la 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

Notez l’instruction using NorthwindOptimisticConcurrencyTableAdapters au-dessus du début de la déclaration de classe. L’espace NorthwindOptimisticConcurrencyTableAdapters de noms contient la ProductsOptimisticConcurrencyTableAdapter classe, qui fournit les méthodes du DAL. Avant la déclaration de classe, vous trouverez également l’attribut System.ComponentModel.DataObject , qui indique à Visual Studio d’inclure cette classe dans la liste déroulante de l’Assistant ObjectDataSource.

La ProductsOptimisticConcurrencyBLLpropriété de Adapter fournit un accès rapide à un instance de la ProductsOptimisticConcurrencyTableAdapter classe et suit le modèle utilisé dans nos classes BLL d’origine (ProductsBLL, CategoriesBLL, etc.). Enfin, la GetProducts() méthode appelle simplement la méthode du GetProducts() DAL et retourne un ProductsOptimisticConcurrencyDataTable objet rempli avec un ProductsOptimisticConcurrencyRow instance pour chaque enregistrement de produit dans la base de données.

Suppression d’un produit à l’aide du modèle direct de base de données avec accès concurrentiel optimiste

Lorsque vous utilisez le modèle direct de base de données sur un DAL qui utilise la concurrence optimiste, les méthodes doivent être transmises aux valeurs nouvelles et d’origine. Pour la suppression, il n’y a pas de nouvelles valeurs. Par conséquent, seules les valeurs d’origine doivent être transmises. Dans notre BLL, nous devons donc accepter tous les paramètres d’origine comme paramètres d’entrée. La méthode de la DeleteProduct classe utilise la ProductsOptimisticConcurrencyBLL méthode directe de base de données. Cela signifie que cette méthode doit prendre les dix champs de données de produit en tant que paramètres d’entrée et les transmettre au DAL, comme indiqué dans le code suivant :

<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

Si les valeurs d’origine (celles qui ont été chargées pour la dernière fois dans GridView (ou DetailsView ou FormView) diffèrent des valeurs de la base de données lorsque l’utilisateur clique sur le bouton Supprimer, la WHERE clause ne correspondra à aucun enregistrement de base de données et aucun enregistrement n’est affecté. Par conséquent, la méthode de Delete TableAdapter retourne 0 et la méthode de DeleteProduct BLL retourne false.

Mise à jour d’un produit à l’aide du modèle de mise à jour par lots avec accès concurrentiel optimiste

Comme indiqué précédemment, la méthode TableAdapter Update pour le modèle de mise à jour par lots a la même signature de méthode, que l’accès concurrentiel optimiste soit utilisé ou non. À savoir, la Update méthode attend un DataRow, un tableau de DataRows, un DataTable ou un DataSet typé. Il n’existe aucun paramètre d’entrée supplémentaire pour spécifier les valeurs d’origine. Cela est possible, car le DataTable effectue le suivi des valeurs d’origine et modifiées pour ses DataRow(s). Lorsque le DAL émet son UPDATE instruction, les @original_ColumnName paramètres sont remplis avec les valeurs d’origine de DataRow, tandis que les @ColumnName paramètres sont remplis avec les valeurs modifiées de DataRow.

Dans la ProductsBLL classe (qui utilise notre DAL d’accès concurrentiel non optimiste d’origine), lors de l’utilisation du modèle de mise à jour par lots pour mettre à jour les informations sur le produit, notre code effectue la séquence d’événements suivante :

  1. Lire les informations de produit de base de données actuelles dans un ProductRow instance à l’aide de GetProductByProductID(productID) la méthode TableAdapter
  2. Affecter les nouvelles valeurs au ProductRow instance de l’étape 1
  3. Appelez la méthode de Update TableAdapter, en passant le ProductRow instance

Toutefois, cette séquence d’étapes ne prend pas correctement en charge l’accès concurrentiel optimiste, car le remplissage à l’étape ProductRow 1 est rempli directement à partir de la base de données, ce qui signifie que les valeurs d’origine utilisées par DataRow sont celles qui existent actuellement dans la base de données, et non celles qui étaient liées au GridView au début du processus de modification. Au lieu de cela, lorsque vous utilisez un DAL avec accès concurrentiel optimiste, nous devons modifier les surcharges de UpdateProduct méthode pour effectuer les étapes suivantes :

  1. Lire les informations de produit de base de données actuelles dans un ProductsOptimisticConcurrencyRow instance à l’aide de GetProductByProductID(productID) la méthode TableAdapter
  2. Affecter les valeurs d’origine au ProductsOptimisticConcurrencyRow instance de l’étape 1
  3. Appelez la ProductsOptimisticConcurrencyRow méthode du AcceptChanges() instance, qui indique au DataRow que ses valeurs actuelles sont celles « d’origine »
  4. Affectez les nouvelles valeurs au ProductsOptimisticConcurrencyRow instance
  5. Appelez la méthode de Update TableAdapter, en passant le ProductsOptimisticConcurrencyRow instance

L’étape 1 lit toutes les valeurs de base de données actuelles pour l’enregistrement de produit spécifié. Cette étape est superflue dans la UpdateProduct surcharge qui met à jour toutes les colonnes de produit (car ces valeurs sont remplacées à l’étape 2), mais elle est essentielle pour ces surcharges où seul un sous-ensemble des valeurs de colonne est transmis en tant que paramètres d’entrée. Une fois les valeurs d’origine attribuées au ProductsOptimisticConcurrencyRow instance, la AcceptChanges() méthode est appelée, ce qui marque les valeurs DataRow actuelles comme valeurs d’origine à utiliser dans les @original_ColumnName paramètres de l’instructionUPDATE. Ensuite, les nouvelles valeurs de paramètre sont affectées à et ProductsOptimisticConcurrencyRow , enfin, la Update méthode est appelée, en passant le DataRow.

Le code suivant montre la UpdateProduct surcharge qui accepte tous les champs de données de produit comme paramètres d’entrée. Bien qu’elle ne soit pas affichée ici, la ProductsOptimisticConcurrencyBLL classe incluse dans le téléchargement de ce tutoriel contient également une UpdateProduct surcharge qui accepte uniquement le nom et le prix du produit comme paramètres d’entrée.

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

Étape 4 : passage des valeurs d’origine et des nouvelles valeurs de la page ASP.NET aux méthodes BLL

Une fois le DAL et le BLL terminés, il ne reste plus qu’à créer une page ASP.NET qui peut utiliser la logique d’accès concurrentiel optimiste intégrée au système. Plus précisément, le contrôle Web de données (GridView, DetailsView ou FormView) doit mémoriser ses valeurs d’origine et ObjectDataSource doit transmettre les deux ensembles de valeurs à la couche logique métier. En outre, la page ASP.NET doit être configurée pour gérer correctement les violations d’accès concurrentiel.

Commencez par ouvrir la OptimisticConcurrency.aspx page dans le EditInsertDelete dossier et ajouter un GridView au Designer, en définissant sa ID propriété sur ProductsGrid. À partir de la balise active de GridView, choisissez de créer un ObjetDataSource nommé ProductsOptimisticConcurrencyDataSource. Étant donné que nous voulons que cet ObjetDataSource utilise le DAL qui prend en charge l’accès concurrentiel optimiste, configurez-le pour utiliser l’objet ProductsOptimisticConcurrencyBLL .

Faites en charge à ObjectDataSource d’utiliser l’objet ProductsOptimisticConcurrencyBLL

Figure 13 : Faire utiliser l’objet ProductsOptimisticConcurrencyBLL ObjectDataSource (cliquer pour afficher l’image en taille réelle)

Choisissez les GetProductsméthodes , UpdateProductet DeleteProduct dans les listes déroulantes de l’Assistant. Pour la méthode UpdateProduct, utilisez la surcharge qui accepte tous les champs de données du produit.

Configuration des propriétés du contrôle ObjectDataSource

Une fois l’Assistant terminé, le balisage déclaratif d’ObjectDataSource doit ressembler à ce qui suit :

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

Comme vous pouvez le voir, la DeleteParameters collection contient un Parameter instance pour chacun des dix paramètres d’entrée de la méthode de DeleteProduct la ProductsOptimisticConcurrencyBLL classe. De même, la UpdateParameters collection contient une Parameter instance pour chacun des paramètres d’entrée dans UpdateProduct.

Pour les tutoriels précédents qui ont impliqué la modification des données, nous allons supprimer la propriété ObjectDataSource OldValuesParameterFormatString à ce stade, car cette propriété indique que la méthode BLL s’attend à ce que les anciennes valeurs (ou d’origine) soient transmises, ainsi que les nouvelles valeurs. En outre, cette valeur de propriété indique les noms des paramètres d’entrée pour les valeurs d’origine. Étant donné que nous transmettons les valeurs d’origine dans le BLL, ne supprimez pas cette propriété.

Notes

La valeur de la OldValuesParameterFormatString propriété doit être mappé aux noms des paramètres d’entrée dans le BLL qui attendent les valeurs d’origine. Étant donné que nous avons nommé ces paramètres original_productName, original_supplierID, et ainsi de suite, vous pouvez laisser la valeur de la OldValuesParameterFormatString propriété comme original_{0}. Si, toutefois, les paramètres d’entrée des méthodes BLL avaient des noms tels que old_productName, old_supplierID, et ainsi de suite, vous devez mettre à jour la OldValuesParameterFormatString propriété vers old_{0}.

Un dernier paramètre de propriété doit être créé pour que ObjectDataSource transmette correctement les valeurs d’origine aux méthodes BLL. ObjectDataSource a une propriété ConflictDetection qui peut être affectée à l’une des deux valeurs suivantes :

  • OverwriteChanges - valeur par défaut ; n’envoie pas les valeurs d’origine aux paramètres d’entrée d’origine des méthodes BLL
  • CompareAllValues - envoie les valeurs d’origine aux méthodes BLL ; choisissez cette option lors de l’utilisation de la concurrence optimiste

Prenez un moment pour définir la ConflictDetection propriété sur CompareAllValues.

Configuration des propriétés et des champs de GridView

Une fois les propriétés d’ObjectDataSource correctement configurées, nous allons nous intéresser à la configuration de GridView. Tout d’abord, étant donné que nous voulons que GridView prend en charge la modification et la suppression, cliquez sur les cases Activer la modification et Activer la suppression à partir de la balise active de GridView. Cela ajoute un Champ de commande dont ShowEditButton et ShowDeleteButton sont tous deux définis sur true.

Lorsqu’il est lié à ProductsOptimisticConcurrencyDataSource ObjectDataSource, gridView contient un champ pour chacun des champs de données du produit. Bien qu’un tel GridView puisse être modifié, l’expérience utilisateur est tout sauf acceptable. Les CategoryID et SupplierID BoundFields s’affichent en tant que TextBoxes, ce qui oblige l’utilisateur à entrer la catégorie appropriée et le fournisseur comme numéros d’ID. Il n’y aura aucune mise en forme pour les champs numériques et aucun contrôle de validation pour s’assurer que le nom du produit a été fourni et que le prix unitaire, les unités en stock, les unités sur commande et les valeurs de niveau de réorganisation sont à la fois des valeurs numériques appropriées et sont supérieures ou égales à zéro.

Comme nous l’avons vu dans les didacticiels Ajout de contrôles de validation aux didacticiels Modification et insertion d’interfaces et Personnalisation de l’interface de modification des données , l’interface utilisateur peut être personnalisée en remplaçant BoundFields par TemplateFields. J’ai modifié ce GridView et son interface d’édition des manières suivantes :

  • Suppression des ProductIDchamps , SupplierNameet CategoryName BoundFields
  • Convertissez boundField ProductName en templateField et ajoutez un contrôle RequiredFieldValidation.
  • Convertissez et CategoryIDSupplierID BoundFields en TemplateFields, et ajustez l’interface d’édition pour utiliser DropDownLists plutôt que TextBoxes. Dans les champs de ItemTemplatesmodèles , les CategoryName champs de données et SupplierName sont affichés.
  • Convertissez , UnitPriceUnitsInStock, UnitsOnOrderet ReorderLevel BoundFields en TemplateFields et ajoutez des contrôles CompareValidator.

Étant donné que nous avons déjà examiné comment accomplir ces tâches dans les tutoriels précédents, je vais simplement répertorier la syntaxe déclarative finale ici et laisser l’implémentation comme pratique.

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

Nous sommes très proches d’avoir un exemple complet. Cependant, il y a quelques subtilités qui vont se glisser et nous causer des problèmes. En outre, nous avons toujours besoin d’une interface qui alerte l’utilisateur lorsqu’une violation d’accès concurrentiel s’est produite.

Notes

Pour qu’un contrôle Web de données passe correctement les valeurs d’origine à ObjectDataSource (qui sont ensuite passées au BLL), il est essentiel que la propriété de EnableViewState GridView soit définie sur true (valeur par défaut). Si vous désactivez l’état d’affichage, les valeurs d’origine sont perdues lors de la publication.

Transmission des valeurs d’origine correctes à ObjectDataSource

Il existe quelques problèmes avec la façon dont GridView a été configuré. Si la propriété de ConflictDetection ObjectDataSource a la valeur CompareAllValues (comme la nôtre), lorsque les méthodes ou Delete() de Update() ObjectDataSource sont appelées par gridView (ou DetailsView ou FormView), l’ObjectDataSource tente de copier les valeurs d’origine de GridView dans ses instances appropriéesParameter. Reportez-vous à la figure 2 pour obtenir une représentation graphique de ce processus.

Plus précisément, les valeurs d’origine de GridView se voient attribuer les valeurs dans les instructions de liaison de données bidirectionnel chaque fois que les données sont liées au GridView. Par conséquent, il est essentiel que les valeurs d’origine requises soient toutes capturées via la liaison de données bidirectionnel et qu’elles soient fournies dans un format convertible.

Pour voir pourquoi cela est important, prenez un moment pour visiter notre page dans un navigateur. Comme prévu, gridView répertorie chaque produit avec un bouton Modifier et Supprimer dans la colonne la plus à gauche.

Les produits sont répertoriés dans un GridView

Figure 14 : Les produits sont répertoriés dans un GridView (cliquer pour afficher une image en taille réelle)

Si vous cliquez sur le bouton Supprimer d’un produit, un FormatException est levée.

Tentative de suppression des résultats d’un produit dans une exception FormatException

Figure 15 : Tentative de suppression des résultats d’un produit dans un FormatException (cliquez pour afficher l’image en taille réelle)

le FormatException est déclenché lorsque l’ObjetDataSource tente de lire la valeur d’origine UnitPrice . Étant donné que le ItemTemplateUnitPrice a mis en forme en tant que devise (<%# Bind("UnitPrice", "{0:C}") %>), il inclut un symbole monétaire, comme 19,95 $. Le FormatException se produit lorsque l’ObjectDataSource tente de convertir cette chaîne en .decimal Pour contourner ce problème, nous avons plusieurs options :

  • Supprimez la mise en forme monétaire du ItemTemplate. Autrement dit, au lieu d’utiliser <%# Bind("UnitPrice", "{0:C}") %>, utilisez <%# Bind("UnitPrice") %>simplement . L’inconvénient est que le prix n’est plus mis en forme.
  • Affichez le UnitPrice mis en forme en tant que devise dans , ItemTemplatemais utilisez le Eval mot clé pour ce faire. Rappelez-vous qu’effectue Eval une liaison de données unidirectionnel. Nous devons toujours fournir la UnitPrice valeur pour les valeurs d’origine. Nous avons donc toujours besoin d’une instruction de liaison de données bidirectionnel dans , ItemTemplatemais cela peut être placé dans un contrôle Label Web dont Visible la propriété est définie sur false. Nous pouvons utiliser le balisage suivant dans 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>
  • Supprimez la mise en forme monétaire du , à l’aide ItemTemplate<%# Bind("UnitPrice") %>de . Dans le gestionnaire d’événements gridView RowDataBound , accédez par programmation au contrôle Label Web dans lequel la UnitPrice valeur est affichée et définissez sa Text propriété sur la version mise en forme.
  • Laissez le UnitPrice mis en forme en tant que devise. Dans le gestionnaire d’événements de RowDeleting GridView, remplacez la valeur d’origine UnitPrice existante (19,95 $) par une valeur décimale réelle à l’aide de Decimal.Parse. Nous avons vu comment effectuer quelque chose de similaire dans le RowUpdating gestionnaire d’événements dans le didacticiel Gestion des exceptions BLL- et DAL-Level dans un ASP.NET Page .

Pour mon exemple, j’ai choisi d’utiliser la deuxième approche, en ajoutant un contrôle Label Web masqué dont Text la propriété est des données bidirectionnel liées à la valeur non mise UnitPrice en forme.

Après avoir résolu ce problème, essayez de cliquer à nouveau sur le bouton Supprimer pour n’importe quel produit. Cette fois, vous obtenez un InvalidOperationException lorsque l’ObjectDataSource tente d’appeler la méthode de UpdateProduct BLL.

ObjectDataSource ne trouve pas de méthode avec les paramètres d’entrée qu’il souhaite envoyer

Figure 16 : ObjectDataSource Ne trouve pas de méthode avec les paramètres d’entrée qu’il souhaite envoyer (cliquez pour afficher l’image en taille réelle)

En examinant le message de l’exception, il est clair que ObjectDataSource souhaite appeler une méthode BLL DeleteProduct qui inclut original_CategoryName des paramètres et original_SupplierName d’entrée. Cela est dû au fait que les ItemTemplate s pour et CategoryIDSupplierID TemplateFields contiennent actuellement des instructions Bind bidirectionnel avec les CategoryName champs de données et SupplierName . Au lieu de cela, nous devons inclure des Bind instructions avec les champs de CategoryID données et SupplierID . Pour ce faire, remplacez les instructions Bind existantes par Eval des instructions, puis ajoutez des contrôles Label masqués dont Text les propriétés sont liées aux champs de données et SupplierID à l’aide de la CategoryID liaison de données bidirectionnel, comme indiqué ci-dessous :

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

Grâce à ces modifications, nous sommes maintenant en mesure de supprimer et de modifier correctement les informations sur le produit ! À l’étape 5, nous allons voir comment vérifier que des violations d’accès concurrentiel sont détectées. Mais pour l’instant, prenez quelques minutes pour essayer de mettre à jour et de supprimer quelques enregistrements afin de vous assurer que la mise à jour et la suppression pour un seul utilisateur fonctionnent comme prévu.

Étape 5 : Test de la prise en charge de l’accès concurrentiel optimiste

Pour vérifier que des violations d’accès concurrentiel sont détectées (au lieu d’entraîner un remplacement aveugle des données), nous devons ouvrir deux fenêtres de navigateur sur cette page. Dans les deux instances de navigateur, cliquez sur le bouton Modifier pour Chai. Ensuite, dans un seul des navigateurs, remplacez le nom par « Chai Tea », puis cliquez sur Mettre à jour. La mise à jour doit réussir et retourner gridView à son état de pré-édition, avec « Chai Tea » comme nom de nouveau produit.

Toutefois, dans l’autre fenêtre de navigateur, instance, le nom de produit TextBox affiche toujours « Chai ». Dans cette deuxième fenêtre de navigateur, mettez à jour le UnitPrice vers 25.00. Sans prise en charge de l’accès concurrentiel optimiste, le fait de cliquer sur la mise à jour dans le deuxième navigateur instance revient au nom du produit en « Chai », ce qui remplace les modifications apportées par le premier instance de navigateur. Avec l’accès concurrentiel optimiste, toutefois, le fait de cliquer sur le bouton Mettre à jour dans le deuxième navigateur instance entraîne une dbConcurrencyException.

Lorsqu’une violation d’accès concurrentiel est détectée, une exception DBConcurrencyException est levée.

Figure 17 : Lorsqu’une violation d’accès concurrentiel est détectée, une DBConcurrencyException est levée (cliquer pour afficher l’image en taille réelle)

le DBConcurrencyException est levée uniquement lorsque le modèle de mise à jour par lots du DAL est utilisé. Le modèle direct de base de données ne déclenche pas d’exception, il indique simplement qu’aucune ligne n’a été affectée. Pour illustrer cela, retournez GridView des deux instances de navigateur à leur état de pré-édition. Ensuite, dans le premier instance de navigateur, cliquez sur le bouton Modifier et remplacez le nom du produit de « Chai Tea » par « Chai », puis cliquez sur Mettre à jour. Dans la deuxième fenêtre de navigateur, cliquez sur le bouton Supprimer pour Chai.

Lorsque vous cliquez sur Supprimer, la page publie à nouveau, GridView appelle la méthode ObjectDataSource Delete() et l’ObjectDataSource appelle la méthode de DeleteProduct la ProductsOptimisticConcurrencyBLL classe, en transmettant les valeurs d’origine. La valeur d’origine ProductName du deuxième instance de navigateur est « Chai Tea », qui ne correspond pas à la valeur actuelle ProductName dans la base de données. Par conséquent, l’instruction DELETE émise pour la base de données affecte zéro ligne, car il n’existe aucun enregistrement dans la base de données que la WHERE clause satisfait. La DeleteProduct méthode retourne false et les données de ObjectDataSource sont renvoyées à GridView.

Du point de vue de l’utilisateur final, le fait de cliquer sur le bouton Supprimer pour Chai Tea dans la deuxième fenêtre de navigateur a fait clignoter l’écran et, à son retour, le produit est toujours là, bien qu’il soit maintenant répertorié comme « Chai » (le changement de nom de produit effectué par le premier navigateur instance). Si l’utilisateur clique à nouveau sur le bouton Supprimer, la suppression réussit, car la valeur d’origine ProductName de GridView (« Chai ») correspond maintenant à la valeur dans la base de données.

Dans les deux cas, l’expérience utilisateur est loin d’être idéale. Nous ne voulons clairement pas montrer à l’utilisateur les détails nitty-gritty de l’exception lors de l’utilisation DBConcurrencyException du modèle de mise à jour par lots. Et le comportement lors de l’utilisation du modèle direct de base de données est quelque peu déroutant, car la commande des utilisateurs a échoué, mais il n’y avait aucune indication précise de la raison.

Pour résoudre ces deux problèmes, nous pouvons créer des contrôles Label Web sur la page qui fournissent une explication de l’échec d’une mise à jour ou d’une suppression. Pour le modèle de mise à jour par lots, nous pouvons déterminer si une DBConcurrencyException exception s’est produite ou non dans le gestionnaire d’événements post-niveau de GridView, en affichant l’étiquette d’avertissement si nécessaire. Pour la méthode directe de base de données, nous pouvons examiner la valeur de retour de la méthode BLL (c’est-à-dire true si une ligne a été affectée, false sinon) et afficher un message d’information si nécessaire.

Étape 6 : Ajout de messages d’information et affichage de ceux-ci dans le visage d’une violation d’accès concurrentiel

Lorsqu’une violation d’accès concurrentiel se produit, le comportement présenté dépend de l’utilisation ou non de la mise à jour par lot du dal ou du modèle direct de base de données. Notre tutoriel utilise les deux modèles, le modèle de mise à jour par lots étant utilisé pour la mise à jour et le modèle direct de base de données utilisé pour la suppression. Pour commencer, nous allons ajouter deux contrôles Label Web à notre page qui expliquent qu’une violation d’accès concurrentiel s’est produite lors de la tentative de suppression ou de mise à jour des données. Définissez les propriétés et EnableViewState du Visible contrôle Label sur false; elles seront masquées à chaque visite de page, sauf pour les visites de page particulières où leur Visible propriété est définie truepar programmation sur .

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

En plus de définir leurs Visiblepropriétés , EnabledViewStateet Text , j’ai également défini la CssClass propriété sur Warning, ce qui entraîne l’affichage des étiquettes dans une grande police rouge, italique et gras. Cette classe CSS Warning a été définie et ajoutée à Styles.css dans le didacticiel Examen des événements associés à l’insertion, à la mise à jour et à la suppression .

Après avoir ajouté ces étiquettes, les Designer dans Visual Studio doivent ressembler à la figure 18.

Deux contrôles d’étiquette ont été ajoutés à la page

Figure 18 : Deux contrôles d’étiquette ont été ajoutés à la page (cliquez pour afficher l’image en taille réelle)

Une fois ces contrôles Label Web en place, nous sommes prêts à examiner comment déterminer quand une violation d’accès concurrentiel s’est produite, auquel moment la propriété de Visible Label appropriée peut être définie sur true, en affichant le message d’information.

Gestion des violations d’accès concurrentiel lors de la mise à jour

Examinons d’abord comment gérer les violations d’accès concurrentiel lors de l’utilisation du modèle de mise à jour par lots. Étant donné que ces violations avec le modèle de mise à jour par lots entraînent la levée d’une DBConcurrencyException exception, nous devons ajouter du code à notre page de ASP.NET pour déterminer si une DBConcurrencyException exception s’est produite pendant le processus de mise à jour. Dans ce cas, nous devons afficher un message à l’utilisateur expliquant que ses modifications n’ont pas été enregistrées, car un autre utilisateur a modifié les mêmes données entre le moment où il a commencé à modifier l’enregistrement et le moment où il a cliqué sur le bouton Mettre à jour.

Comme nous l’avons vu dans le didacticiel Gestion des exceptions BLL et DAL-Level dans un ASP.NET Page , ces exceptions peuvent être détectées et supprimées dans les gestionnaires d’événements post-niveau du contrôle Web de données. Par conséquent, nous devons créer un gestionnaire d’événements pour l’événement gridView RowUpdated qui vérifie si une DBConcurrencyException exception a été levée. Ce gestionnaire d’événements reçoit une référence à toute exception qui a été déclenchée pendant le processus de mise à jour, comme indiqué dans le code du gestionnaire d’événements ci-dessous :

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

En cas d’exception, ce gestionnaire d’événements DBConcurrencyException affiche le UpdateConflictMessage contrôle Label et indique que l’exception a été gérée. Une fois ce code en place, lorsqu’une violation d’accès concurrentiel se produit lors de la mise à jour d’un enregistrement, les modifications de l’utilisateur sont perdues, car ils auraient remplacé les modifications d’un autre utilisateur en même temps. En particulier, GridView est retourné à son état de pré-édition et lié aux données de base de données actuelles. Cette opération met à jour la ligne GridView avec les modifications de l’autre utilisateur, qui n’étaient pas visibles auparavant. En outre, le UpdateConflictMessage contrôle Label explique à l’utilisateur ce qui vient de se passer. Cette séquence d’événements est détaillée dans la figure 19.

Les Mises à jour d’un utilisateur sont perdus face à une violation d’accès concurrentiel

Figure 19 : Les Mises à jour d’un utilisateur sont perdus face à une violation d’accès concurrentiel (cliquer pour afficher une image en taille réelle)

Notes

Sinon, au lieu de retourner le GridView à l’état de pré-édition, nous pouvons laisser gridView dans son état d’édition en définissant la KeepInEditMode propriété de l’objet transmis sur GridViewUpdatedEventArgs true. Toutefois, si vous utilisez cette approche, veillez à lier les données à GridView (en appelant sa DataBind() méthode) afin que les valeurs de l’autre utilisateur soient chargées dans l’interface de modification. Le code disponible en téléchargement avec ce didacticiel contient ces deux lignes de code dans le RowUpdated gestionnaire d’événements commentées ; il suffit de supprimer les marques de commentaire de ces lignes de code pour que GridView reste en mode édition après une violation de concurrence.

Réponse aux violations d’accès concurrentiel lors de la suppression

Avec le modèle direct de base de données, aucune exception n’est levée en cas de violation d’accès concurrentiel. Au lieu de cela, l’instruction de base de données n’affecte simplement aucun enregistrement, car la clause WHERE ne correspond à aucun enregistrement. Toutes les méthodes de modification des données créées dans le BLL ont été conçues de telle sorte qu’elles retournent une valeur booléenne indiquant si elles ont ou non affecté précisément un enregistrement. Par conséquent, pour déterminer si une violation d’accès concurrentiel s’est produite lors de la suppression d’un enregistrement, nous pouvons examiner la valeur de retour de la méthode de DeleteProduct BLL.

La valeur de retour d’une méthode BLL peut être examinée dans les gestionnaires d’événements post-niveau d’ObjectDataSource via la ReturnValue propriété de l’objet ObjectDataSourceStatusEventArgs passé dans le gestionnaire d’événements. Étant donné que nous voulons déterminer la valeur de retour de la DeleteProduct méthode, nous devons créer un gestionnaire d’événements pour l’événement Deleted ObjectDataSource. La ReturnValue propriété est de type object et peut être null si une exception a été levée et si la méthode a été interrompue avant qu’elle puisse retourner une valeur. Par conséquent, nous devons d’abord nous assurer que la ReturnValue propriété n’est pas null et est une valeur booléenne. En supposant que cette case activée réussit, nous affichons le DeleteConflictMessage contrôle Label si est ReturnValuefalse. Pour ce faire, utilisez le code suivant :

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

En cas de violation d’accès concurrentiel, la demande de suppression de l’utilisateur est annulée. GridView est actualisé, montrant les modifications qui se sont produites pour cet enregistrement entre le moment où l’utilisateur a chargé la page et quand il a cliqué sur le bouton Supprimer. Lorsqu’une telle violation se produit, l’étiquette DeleteConflictMessage est affichée, expliquant ce qui vient de se produire (voir figure 20).

La suppression d’un utilisateur est annulée en face d’une violation d’accès concurrentiel

Figure 20 : La suppression d’un utilisateur est annulée dans la face d’une violation d’accès concurrentiel (cliquer pour afficher l’image en taille réelle)

Résumé

Il existe des possibilités de violations de concurrence dans chaque application qui permet à plusieurs utilisateurs simultanés de mettre à jour ou de supprimer des données. Si ces violations ne sont pas prises en compte, lorsque deux utilisateurs mettent à jour simultanément les mêmes données que celui qui obtient dans la dernière écriture « gagne », l’écriture des modifications de l’autre utilisateur change. Les développeurs peuvent également implémenter un contrôle d’accès concurrentiel optimiste ou pessimiste. Le contrôle d’accès concurrentiel optimiste part du principe que les violations d’accès concurrentiel sont peu fréquentes et interdit simplement une commande de mise à jour ou de suppression qui constituerait une violation d’accès concurrentiel. Le contrôle d’accès concurrentiel pessimiste suppose que les violations d’accès concurrentiel sont fréquentes et qu’il n’est pas acceptable de rejeter simplement la commande de mise à jour ou de suppression d’un utilisateur. Avec un contrôle d’accès concurrentiel pessimiste, la mise à jour d’un enregistrement implique son verrouillage, empêchant ainsi d’autres utilisateurs de modifier ou de supprimer l’enregistrement pendant qu’il est verrouillé.

Le DataSet typé dans .NET fournit des fonctionnalités permettant de prendre en charge le contrôle d’accès concurrentiel optimiste. En particulier, les UPDATE instructions et DELETE émises à la base de données incluent toutes les colonnes de la table, garantissant ainsi que la mise à jour ou la suppression n’aura lieu que si les données actuelles de l’enregistrement correspondent aux données d’origine dont l’utilisateur disposait lors de la mise à jour ou de la suppression. Une fois que le DAL a été configuré pour prendre en charge la concurrence optimiste, les méthodes BLL doivent être mises à jour. En outre, la page ASP.NET qui appelle vers le bas dans le BLL doit être configurée de telle sorte que ObjectDataSource récupère les valeurs d’origine de son contrôle web de données et les transmet au BLL.

Comme nous l’avons vu dans ce tutoriel, l’implémentation d’un contrôle d’accès concurrentiel optimiste dans une application web ASP.NET implique la mise à jour du DAL et du BLL et l’ajout de la prise en charge dans la page ASP.NET. Le fait que ce travail supplémentaire soit ou non un investissement judicieux de votre temps et de vos efforts dépend de votre application. Si vous avez rarement des utilisateurs simultanés mettant à jour des données, ou si les données qu’ils mettent à jour sont différentes les unes des autres, le contrôle d’accès concurrentiel n’est pas un problème clé. Toutefois, si plusieurs utilisateurs de votre site travaillent régulièrement avec les mêmes données, le contrôle d’accès concurrentiel peut aider à empêcher les mises à jour ou les suppressions d’un utilisateur de remplacer involontairement ceux d’un autre.

Bonne programmation !

À propos de l’auteur

Scott Mitchell, auteur de sept livres ASP/ASP.NET et fondateur de 4GuysFromRolla.com, travaille avec les technologies Web Microsoft depuis 1998. Scott travaille comme consultant indépendant, formateur et écrivain. Son dernier livre est Sams Teach Yourself ASP.NET 2.0 in 24 Heures. Il est accessible à l’adressemitchell@4GuysFromRolla.com . ou via son blog, qui peut être trouvé à l’adresse http://ScottOnWriting.NET.