Partager via


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

par Scott Mitchell

Télécharger le PDF

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

Présentation

Pour les applications web qui autorisent uniquement les utilisateurs à afficher des données ou pour ceux qui incluent uniquement un seul utilisateur qui peut modifier des données, il n’existe aucune menace pour deux utilisateurs simultanés qui remplacent accidentellement les modifications apportées par les uns aux autres. 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 les autres utilisateurs simultanés. Sans stratégie d’accès concurrentiel en place, lorsque deux utilisateurs modifient simultanément un enregistrement unique, l’utilisateur qui valide ses modifications en dernier remplace les modifications apportées par le premier.

Par exemple, imaginez que deux utilisateurs, Jisun et Sam, ont tous deux visité une page dans notre application qui permettait aux visiteurs de mettre à jour et de supprimer les produits par le biais d’un contrôle GridView. Les deux cliquez sur le bouton Modifier dans GridView 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 Des liquides exotiques, et ainsi de suite pour ce produit particulier. Toutefois, le GridView sur l'écran de Sam affiche toujours le nom du produit dans la ligne modifiable du GridView « Chai ». Quelques secondes après la validation des modifications de Jisun, Sam met à jour la catégorie sur Condiments et clique sur Mettre à jour. Cela entraîne l’envoi d’une UPDATE instruction à la base de données qui définit le nom du produit à « Chai », à la catégorie Boissons correspondante avec l'ID CategoryID, et ainsi de suite. Les modifications de Jisun au nom du produit ont été remplacées. La figure 1 illustre graphiquement cette série d’événements.

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

Figure 1 : Lorsque deux utilisateurs mettent simultanément à jour un enregistrement, il y a un risque qu'un utilisateur écrase les modifications de 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 lorsqu’il clique sur le bouton Supprimer, un autre utilisateur peut avoir modifié le contenu de cet enregistrement.

Il existe trois stratégies de contrôle d’accès concurrentiel disponibles :

  • Do Nothing -if utilisateurs simultanés modifient le même enregistrement, laissez la dernière validation gagner (comportement par défaut)
  • Accès concurrentiel optimiste : supposons qu'il peut y avoir des conflits d'accès concurrentiel de temps en temps, mais que la plupart du temps ces conflits ne se produisent pas. Par conséquent, si un conflit se produit, informez simplement 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éreraient pas que leurs modifications ne soient pas 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 tous les autres utilisateurs de modifier ou de supprimer cet enregistrement jusqu’à ce que l’utilisateur valide ses modifications

Jusqu’à présent, tous nos tutoriels ont utilisé la stratégie de résolution des conflits de concurrence par défaut, à savoir nous avons laissé le dernier écrit l'emporter. Dans ce tutoriel, nous allons examiner l'implémentation d'un contrôle de concurrence optimiste.

Remarque

Nous n’examinerons pas les exemples d’accès concurrentiel pessimiste dans cette série de tutoriels. La concurrence pessimiste est rarement utilisée, car ces verrous, s’ils ne sont pas correctement arrêtés, peuvent empêcher d’autres utilisateurs de mettre à jour les données. Par exemple, si un utilisateur verrouille un enregistrement pour modification, puis quitte le jour avant de le déverrouiller, aucun autre utilisateur ne pourra mettre à jour cet enregistrement jusqu’à ce que l’utilisateur d’origine retourne et termine 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’expiration qui, s’il est atteint, annule le verrou. Les sites web de vente de billets, qui verrouillent un emplacement de siège particulier pendant une courte période pendant que l’utilisateur termine le processus de commande, est un exemple de contrôle d’accès concurrentiel pessimiste.

Étape 1 : Analyser comment la concurrence optimiste est implémentée

Le contrôle d’accès concurrentiel optimiste fonctionne en veillant à ce que l’enregistrement mis à jour ou supprimé ait 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 d’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 clique sur le bouton Mettre à jour, les valeurs d’origine ainsi que les nouvelles valeurs sont envoyées à la couche logique métier, puis vers le bas jusqu’à 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 actuelles de la base de données (cliquez pour afficher l’image de taille complète)

Il existe différentes approches pour implémenter l’accès concurrentiel optimiste (consultez la logique de mise à jour d’accès concurrentiel optimiste de Peter A. Bromberg pour un bref aperçu de plusieurs options). L’ADO.NET Typed DataSet fournit une implémentation qui peut être configurée avec uniquement la coche d’une case. L’activation de l’accès concurrentiel optimiste pour un TableAdapter dans l’ensemble de données typé augmente les instructions de UPDATE et de DELETE du TableAdapter pour inclure une comparaison de toutes les valeurs d’origine dans la clause WHERE. 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 paramètres @ProductName et @UnitPrice contiennent les nouvelles valeurs entrées par l’utilisateur, tandis que les paramètres @original_ProductName et @original_UnitPrice contiennent les valeurs qui ont été initialement chargées dans le GridView lorsque le bouton Modifier a été cliqué :

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

Remarque

Cette UPDATE instruction a été simplifiée pour la lisibilité. Dans la pratique, la UnitPrice vérification dans la WHERE clause nécessiterait plus d'efforts, car UnitPrice peut contenir NULL et vérifier si NULL = NULL retourne toujours False (au lieu de cela, vous devez utiliser IS NULL).

Outre l’utilisation d’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 vers la 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 en tant que paramètres d’entrée (plutôt qu’une instance DataRow ou DataTable fortement typée). Lorsque vous utilisez la concurrence optimiste, les méthodes directes de la base de données Update() et Delete() incluent des paramètres d'entrée pour les valeurs d'origine également. De plus, le code de la BLL pour utiliser le modèle de mise à jour par lots (les Update() surcharges de méthode qui acceptent DataRows et DataTables plutôt que les valeurs scalaires) doivent également être modifiés.

Plutôt que d’étendre les 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 jeu de données typé nommé NorthwindOptimisticConcurrency, auquel nous ajouterons un Products TableAdapter qui utilise l’accès concurrentiel 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 que ce travail de terrain a été posé, nous serons prêts à créer la page ASP.NET.

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

Pour créer un Jeu de données typé, cliquez avec le bouton droit sur le DAL dossier dans le App_Code dossier et ajoutez un nouveau DataSet nommé NorthwindOptimisticConcurrency. Comme nous l'avons vu dans le premier tutoriel, cela ajoutera un nouveau TableAdapter au jeu de données typé, et lancera automatiquement l'Assistant de configuration du 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 à partir 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 (cliquez pour afficher l’image de taille complète)

Ensuite, nous sommes invités à savoir comment interroger les données : via une instruction SQL ad hoc, une nouvelle procédure stockée ou 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 (cliquez pour afficher l’image de taille complète)

Dans l’écran suivant, entrez la requête SQL à utiliser pour récupérer les informations du produit. Utilisons exactement la même requête SQL utilisée pour le TableAdapter Products de notre DAL d’origine, qui retourne toutes les colonnes ainsi que les noms de fournisseurs et de catégories Product 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 du TableAdapter de la table Products dans le DAL d'origine

Figure 5 : Utiliser la même requête SQL à partir de TableAdapter Products dans le dal d’origine (cliquez pour afficher l’image de taille complète)

Avant de passer à l’écran suivant, cliquez sur le bouton Options avancées. Pour que le TableAdapter utilise le contrôle de concurrence optimiste, cochez simplement la case « Utiliser la concurrence optimiste ».

Activer le contrôle de concurrence optimiste en cochant la case « Utiliser la concurrence optimiste »

Figure 6 : Activer le contrôle de concurrence optimiste en cochant la case « Utiliser la concurrence optimiste » (Cliquez pour afficher l’image en taille réelle)

Enfin, indiquez que TableAdapter doit utiliser les modèles d’accès aux données qui permettent de remplir et de retourner un DataTable ; indiquez é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 motif DataTable de GetData à GetProducts afin de mettre en miroir les conventions d’affectation de noms que nous avons utilisées dans notre DAL d’origine.

Faire en sorte que le TableAdapter utilise tous les types de modèles d'accès aux données

Figure 7 : Faire en sorte que le TableAdapter utilise tous les modèles d'accès aux données (cliquez pour voir l’image en taille réelle)

Une fois l’Assistant terminé, le Concepteur de DataSet inclut un DataTable structuré Products et un TableAdapter. Prenez un moment pour renommer le DataTable de Products à ProductsOptimisticConcurrency, ce que vous pouvez faire en cliquant avec le bouton droit sur la barre de titre du DataTable et en choisissant Renommer dans le menu contextuel.

Un DataTable et le TableAdapter ont été ajoutés au jeu de données typé

Figure 8 : Un DataTable et TableAdapter ont été ajoutés à l’ensemble de données typé (cliquez pour afficher l’image de taille complète)

Pour voir les différences entre les requêtes UPDATE et DELETE entre le TableAdapter ProductsOptimisticConcurrency (qui utilise la concurrence optimiste) et le TableAdapter des produits (qui ne le fait pas), cliquez sur le TableAdapter et ouvrez la fenêtre Propriétés. Dans les sous-propriétés DeleteCommand des propriétés UpdateCommand et CommandText, vous pouvez voir la syntaxe SQL réelle envoyée à la base de données lorsque les méthodes liées à la mise à jour ou à la suppression du DAL sont appelées. Pour TableAdapter, ProductsOptimisticConcurrency l’instruction DELETE utilisée est la suivante :

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 de Product TableAdapter dans notre DAL d’origine est beaucoup plus simple :

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

Comme vous pouvez le voir, la WHERE clause de l’instruction DELETE pour 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ée 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 dans la WHERE clause.

Nous n’ajoutons pas de DataTables supplémentaires à l’ensemble de données avec accès concurrentiel optimiste pour ce didacticiel, car notre page ASP.NET fournira uniquement la mise à jour et la suppression des informations sur le produit. Toutefois, nous devons toujours ajouter la GetProductByProductID(productID) méthode à ProductsOptimisticConcurrency TableAdapter.

Pour ce faire, cliquez avec le bouton droit sur la barre de titre de TableAdapter (la zone située juste au-dessus des noms des méthodes Fill et GetProducts) puis choisissez Ajouter une requête dans le menu contextuel. Cela lance l’Assistant Configuration des requêtes du TableAdapter. Comme avec la configuration initiale de TableAdapter, choisissez de créer la méthode à l’aide GetProductByProductID(productID) d’une instruction SQL ad hoc (voir la 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.

Marquer le type de requête en tant que « SELECT qui retourne des lignes »

Figure 9 : Marquer le type de requête en tant que «SELECT qui retourne des lignes » (Cliquez pour afficher l’image de taille complète)

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 renvoyer un enregistrement de produit spécifique (cliquez pour afficher l’image de taille complète)

Enfin, remplacez les noms de méthode générés par FillByProductID et GetProductByProductIDpar .

Renommer les méthodes en FillByProductID et GetProductByProductID

Figure 11 : Renommer les méthodes vers FillByProductID et GetProductByProductID (Cliquez pour afficher l’image de taille complète)

Avec cet Assistant terminé, TableAdapter contient maintenant 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 l'Concurrency-Enabled DAL optimiste

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

Avec la nouvelle 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 maintenant dix paramètres d’entrée : l’original ProductID, , ProductName, SupplierIDCategoryID, QuantityPerUnit, , UnitPrice, UnitsInStock, , UnitsOnOrderReorderLevel, et Discontinued. 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 correspondent aux valeurs d’origine.

Même si la signature de méthode pour la méthode du TableAdapter utilisée dans le modèle de mise à jour par lots Update n’a pas changé, le code nécessaire pour enregistrer les valeurs d’origine et les nouvelles valeurs a changé. Par conséquent, plutôt que d’essayer d’utiliser le DAL avec accès concurrentiel optimiste avec notre classe existante ProductsBLL , nous allons créer une classe de couche logique métier pour utiliser notre nouveau 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 :

using System;
using System.Data;
using System.Configuration;
using System.Web;
using System.Web.Security;
using System.Web.UI;
using System.Web.UI.WebControls;
using System.Web.UI.WebControls.WebParts;
using System.Web.UI.HtmlControls;
using NorthwindOptimisticConcurrencyTableAdapters;
[System.ComponentModel.DataObject]
public class ProductsOptimisticConcurrencyBLL
{
    private ProductsOptimisticConcurrencyTableAdapter _productsAdapter = null;
    protected ProductsOptimisticConcurrencyTableAdapter Adapter
    {
        get
        {
            if (_productsAdapter == null)
                _productsAdapter = new ProductsOptimisticConcurrencyTableAdapter();
            return _productsAdapter;
        }
    }
    [System.ComponentModel.DataObjectMethodAttribute
    (System.ComponentModel.DataObjectMethodType.Select, true)]
    public NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable GetProducts()
    {
        return Adapter.GetProducts();
    }
}

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 de 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 propriété ProductsOptimisticConcurrencyBLL de Adapter fournit un accès rapide à une instance de la classe ProductsOptimisticConcurrencyTableAdapter, et suit le modèle utilisé dans nos classes BLL d’origine (ProductsBLL, CategoriesBLL, etc.). Enfin, la méthode GetProducts() appelle simplement la méthode GetProducts() de la couche DAL et retourne un objet ProductsOptimisticConcurrencyDataTable rempli avec une instance ProductsOptimisticConcurrencyRow pour chaque enregistrement de produit dans la base de données.

Suppression d’un produit à l’aide de l’utilisation directe de la base de données avec concurrence optimiste

Lorsque vous utilisez le modèle direct de base de données contre une DAL qui utilise la concurrence optimiste, les méthodes doivent recevoir les valeurs nouvelles et originales. Pour la suppression, il n’y a pas de nouvelles valeurs, de sorte que seules les valeurs d’origine doivent être transmises. Dans notre BLL, nous devons accepter tous les paramètres d’origine comme paramètres d’entrée. Supposons que la DeleteProduct méthode de la ProductsOptimisticConcurrencyBLL classe utilise la 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 bool DeleteProduct
    (int original_productID, string original_productName,
    int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued)
{
    int rowsAffected = Adapter.Delete(original_productID,
                                      original_productName,
                                      original_supplierID,
                                      original_categoryID,
                                      original_quantityPerUnit,
                                      original_unitPrice,
                                      original_unitsInStock,
                                      original_unitsOnOrder,
                                      original_reorderLevel,
                                      original_discontinued);
    // Return true if precisely one row was deleted, otherwise false
    return rowsAffected == 1;
}

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 correspond à 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 schéma de mise à jour par lot avec concurrence optimiste

Comme indiqué précédemment, la méthode TableAdapter associée au modèle de mise à jour par lots avec Update conserve 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 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 renseignés 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 original et non optimiste), 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 actuelles du produit de base de données dans une ProductRow instance à l’aide de la méthode TableAdapter GetProductByProductID(productID)
  2. Affecter les nouvelles valeurs à l’instance à partir de l’étape ProductRow 1
  3. Appelez la méthode Update du TableAdapter, en passant l’instance ProductRow

Toutefois, cette séquence d'étapes ne prend pas correctement en charge l'accès concurrentiel optimiste, car les valeurs de ProductRow à l'étape 1 sont directement issues de la base de données. Ainsi, les valeurs initiales utilisées par DataRow sont celles qui existent actuellement dans la base de données, et non celles qui étaient liées à GridView au début du processus de modification. Au lieu de cela, lors de l'utilisation d'un DAL avec accès concurrentiel optimiste, nous devons modifier des UpdateProduct surcharges de méthode pour utiliser les étapes suivantes :

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

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 est essentielle pour ces surcharges où seuls un sous-ensemble des valeurs de colonne sont passés en tant que paramètres d’entrée. Une fois que les valeurs d’origine ont été affectées à l’instance ProductsOptimisticConcurrencyRow , la AcceptChanges() méthode est appelée, qui marque les valeurs DataRow actuelles comme valeurs d’origine à utiliser dans les @original_ColumnName paramètres de l’instruction UPDATE . Ensuite, les nouvelles valeurs de paramètre sont affectées à la ProductsOptimisticConcurrencyRow méthode et, 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 du 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 didacticiel contient également une UpdateProduct surcharge qui accepte uniquement le nom et le prix du produit comme paramètres d’entrée.

protected void AssignAllProductValues
    (NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product,
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued)
{
    product.ProductName = productName;
    if (supplierID == null)
        product.SetSupplierIDNull();
    else
        product.SupplierID = supplierID.Value;
    if (categoryID == null)
        product.SetCategoryIDNull();
    else
        product.CategoryID = categoryID.Value;
    if (quantityPerUnit == null)
        product.SetQuantityPerUnitNull();
    else
        product.QuantityPerUnit = quantityPerUnit;
    if (unitPrice == null)
        product.SetUnitPriceNull();
    else
        product.UnitPrice = unitPrice.Value;
    if (unitsInStock == null)
        product.SetUnitsInStockNull();
    else
        product.UnitsInStock = unitsInStock.Value;
    if (unitsOnOrder == null)
        product.SetUnitsOnOrderNull();
    else
        product.UnitsOnOrder = unitsOnOrder.Value;
    if (reorderLevel == null)
        product.SetReorderLevelNull();
    else
        product.ReorderLevel = reorderLevel.Value;
    product.Discontinued = discontinued;
}
[System.ComponentModel.DataObjectMethodAttribute
(System.ComponentModel.DataObjectMethodType.Update, true)]
public bool UpdateProduct(
    // new parameter values
    string productName, int? supplierID, int? categoryID, string quantityPerUnit,
    decimal? unitPrice, short? unitsInStock, short? unitsOnOrder,
    short? reorderLevel, bool discontinued, int productID,
    // original parameter values
    string original_productName, int? original_supplierID, int? original_categoryID,
    string original_quantityPerUnit, decimal? original_unitPrice,
    short? original_unitsInStock, short? original_unitsOnOrder,
    short? original_reorderLevel, bool original_discontinued,
    int original_productID)
{
    // STEP 1: Read in the current database product information
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyDataTable products =
        Adapter.GetProductByProductID(original_productID);
    if (products.Count == 0)
        // no matching record found, return false
        return false;
    NorthwindOptimisticConcurrency.ProductsOptimisticConcurrencyRow product = products[0];
    // STEP 2: Assign the original values to the product instance
    AssignAllProductValues(product, original_productName, original_supplierID,
        original_categoryID, original_quantityPerUnit, original_unitPrice,
        original_unitsInStock, original_unitsOnOrder, original_reorderLevel,
        original_discontinued);
    // STEP 3: Accept the changes
    product.AcceptChanges();
    // STEP 4: Assign the new values to the product instance
    AssignAllProductValues(product, productName, supplierID, categoryID,
        quantityPerUnit, unitPrice, unitsInStock, unitsOnOrder, reorderLevel,
        discontinued);
    // STEP 5: Update the product record
    int rowsAffected = Adapter.Update(product);
    // Return true if precisely one row was updated, otherwise false
    return rowsAffected == 1;
}

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

Une fois le DAL et le BLL terminés, il ne reste qu'à créer une page ASP.NET capable d'utiliser la logique de concurrence 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 passer 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.

Ouvrez d'abord la page OptimisticConcurrency.aspx dans le dossier EditInsertDelete, puis ajoutez un GridView à l'interface de création et définissez sa propriété ID sur ProductsGrid. À partir de l'étiquette intelligente de GridView, choisissez de créer une nouvelle ObjectDataSource nommée ProductsOptimisticConcurrencyDataSource. Comme nous voulons que cette ObjectDataSource utilise le DAL qui prend en charge la concurrence optimiste, configurez-le pour utiliser l’objet ProductsOptimisticConcurrencyBLL.

Faites en sorte que l'ObjectDataSource utilise l'objet ProductsOptimisticConcurrencyBLL

Figure 13 : Faire utiliser par ObjectDataSource l'objet (ProductsOptimisticConcurrencyBLLCliquez pour afficher l’image de taille complète)

Choisissez les méthodes GetProducts, UpdateProduct et 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 de 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 une Parameter instance pour chacun des dix paramètres d’entrée de la méthode ProductsOptimisticConcurrencyBLL de la classe DeleteProduct. De même, la UpdateParameters collection contient une Parameter instance pour chacun des paramètres d’entrée dans UpdateProduct.

Pour ces didacticiels 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 attend que les anciennes valeurs (ou d’origine) soient passées 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 à la BLL, ne supprimez pas cette propriété.

Remarque

La valeur de la OldValuesParameterFormatString propriété doit correspondre aux noms des paramètres d’entrée dans la BLL qui attendent les valeurs d’origine. Étant donné que nous avons nommé ces paramètres original_productName, original_supplierIDet ainsi de suite, vous pouvez conserver la valeur de propriété OldValuesParameterFormatString comme original_{0}. Toutefois, si les paramètres d’entrée des méthodes BLL avaient des noms tels que old_productName, old_supplierID, et ainsi de suite, vous devriez mettre à jour la propriété OldValuesParameterFormatString vers old_{0}.

Il existe un paramètre de propriété final qui doit être effectué pour que ObjectDataSource passe 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 - transmet les valeurs initiales aux méthodes BLL ; optez pour cette option lors de l'utilisation de la concurrence optimiste

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

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

Avec les propriétés d’ObjectDataSource correctement configurées, nous allons attirer l’attention sur 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 ajoutera 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 zones de texte et SupplierID BoundFields s’affichent en tant que TextBox, ce qui oblige l’utilisateur à entrer la catégorie et le fournisseur appropriés en tant que numéros d’ID. Il n’y aura pas de 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 l’ajout de contrôles de validation aux interfaces d’édition et d’insertion et la personnalisation des didacticiels sur 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 de la manière suivante :

  • Suppression des champs liés ProductID, SupplierName et CategoryName BoundFields
  • Converti le ProductName BoundField en TemplateField et ajouté un contrôle RequiredFieldValidation.
  • A converti CategoryID et SupplierID BoundFields en TemplateFields et a ajusté l’interface d’édition pour utiliser des DropDownLists plutôt que des TextBoxes. Dans ces TemplateFields' ItemTemplates, les champs de données CategoryName et SupplierName sont affichés.
  • Converti les UnitPrice, UnitsInStock, UnitsOnOrder et ReorderLevel BoundFields en TemplateFields et ajouté des contrôles CompareValidator.

Étant donné que nous avons déjà examiné comment accomplir ces tâches dans les didacticiels précédents, je vais simplement répertorier la syntaxe déclarative finale ici et laisser l’implémentation en tant que 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 entièrement fonctionnel. Cependant, il y a quelques subtilités qui vont apparaître 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.

Remarque

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

Passage des valeurs d’origine correctes à ObjectDataSource

Il existe quelques problèmes lors de la configuration de GridView. Si la propriété ConflictDetection d'ObjectDataSource est définie sur CompareAllValues (comme la nôtre), lorsque les méthodes Update() ou Delete() de l'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ées Parameter. Reportez-vous à la figure 2 pour obtenir une représentation graphique de ce processus.

Plus précisément, les valeurs d’origine de la GridView sont assignées aux valeurs des instructions de liaison de données bidirectionnelles chaque fois que les données sont liées à la GridView. Par conséquent, il est essentiel que les valeurs d’origine requises soient toutes capturées par le biais d’une 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 (cliquez pour afficher l’image de taille complète)

Si vous cliquez sur le bouton Supprimer pour n’importe quel produit, une exception FormatException est lancée.

La suppression de n'importe quel produit entraîne une exception de type FormatException

Figure 15 : Tentative de suppression de n'importe quel produit entraîne un FormatException (cliquez pour voir l'image en grande taille)

FormatException est déclenché quand ObjectDataSource tente de lire la valeur originale UnitPrice. Étant donné que la ItemTemplate est formatée comme une devise UnitPrice (<%# Bind("UnitPrice", "{0:C}") %>), elle inclut un symbole monétaire, comme 19,95 $. Le FormatException se produit lorsque l'ObjectDataSource tente de convertir cette chaîne en un decimal. Pour contourner ce problème, nous avons plusieurs options :

  • Enlevez la mise en forme monétaire du ItemTemplate. Autrement dit, au lieu d’utiliser <%# Bind("UnitPrice", "{0:C}") %>, il suffit d’utiliser <%# Bind("UnitPrice") %>. L'inconvénient, c'est que le prix n'est plus correctement formaté.
  • Affichez UnitPrice formatée comme une devise dans ItemTemplate, mais utilisez le mot clé Eval pour cela. Souvenez-vous qu’il Eval effectue une liaison de données unidirectionnelle. Nous devons toujours fournir la UnitPrice valeur des valeurs d’origine. Nous aurons donc toujours besoin d’une instruction de liaison de données bidirectionnelle dans le ItemTemplate, qui peut être placée dans un contrôle Label Web avec la propriété Visible définie sur false. Nous pourrions 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 de ItemTemplate en utilisant <%# Bind("UnitPrice") %>. Dans le gestionnaire d’événements RowDataBound de GridView, 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 la UnitPrice formatée comme une devise. Dans le gestionnaire d’événements RowDeleting de GridView, remplacez la valeur d’origine UnitPrice existante ($19,95) par une valeur décimale réelle à l’aide Decimal.Parsede . Nous avons vu comment accomplir quelque chose de similaire dans le RowUpdating gestionnaire d’événements dans le didacticiel sur la gestion des exceptions BLL et DAL-Level dans une page ASP.NET.

Pour mon exemple, j'ai choisi d'adopter la deuxième approche, en ajoutant un contrôle Web Label masqué dont la propriété est liée de manière bidirectionnelle aux données à la valeur non mise 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 obtiendrez un InvalidOperationException lorsque l’ObjectDataSource tentera d’appeler la méthode UpdateProduct de BLL.

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

Figure 16 : ObjectDataSource ne peut pas trouver de méthode avec les paramètres d’entrée qu’il souhaite envoyer (cliquez pour afficher l’image de taille complète)

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

Avec ces modifications, nous sommes maintenant en mesure de supprimer et de modifier les informations du produit ! À l’étape 5, nous verrons comment vérifier si les violations de concurrence sont détectées. Toutefois, pour l’instant, essayez de mettre à jour et de supprimer quelques enregistrements pour vous assurer que la mise à jour et la suppression d’un seul utilisateur fonctionne comme prévu.

Étape 5 : Test de la prise en charge de la concurrence optimiste

Pour vérifier que les 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 ramener le GridView à son état avant modification, avec « Chai Tea » comme nouveau nom de produit.

Dans l’autre instance de la fenêtre du navigateur, toutefois, le nom de produit TextBox affiche toujours « Chai ». Dans cette deuxième fenêtre de navigateur, mettez à jour le UnitPrice à 25.00. Sans prise en charge de l'accès concurrentiel optimiste, cliquer sur la mise à jour dans la deuxième instance de navigateur remettrait le nom du produit à « Chai », annulant ainsi les modifications effectuées par la première instance du navigateur. Lorsqu'un accès concurrent optimiste est utilisé, toutefois, cliquer sur le bouton Mettre à jour dans la deuxième instance de navigateur entraîne une DBConcurrencyException.

Lorsqu'une violation de concurrence est détectée, une exception DBConcurrencyException liée à la concurrence est levée

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

Le DBConcurrencyException n'est levé que 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, renvoyez gridView des deux instances de navigateur à leur état de préversion. Ensuite, dans la première instance du navigateur, cliquez sur le bouton Modifier et remplacez le nom du produit par « Chai Tea » par « Chai », puis cliquez sur Mettre à jour. Dans la deuxième fenêtre du navigateur, cliquez sur le bouton Supprimer pour Chai.

Lorsque vous cliquez sur Supprimer, la page se rafraîchit, le GridView appelle la méthode Delete() de l'ObjectDataSource, et l'ObjectDataSource appelle la méthode ProductsOptimisticConcurrencyBLL de la classe DeleteProduct, en passant les valeurs d'origine. La valeur d’origine ProductName de la deuxième instance du 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 à la base de données n'affecte aucune ligne, puisqu'il n'existe aucun enregistrement dans la base de données qui satisfasse la clause WHERE. La DeleteProduct méthode retourne false et les données d’ObjectDataSource sont renvoyées à GridView.

Du point de vue de l’utilisateur final, en cliquant sur le bouton Supprimer pour Chai Tea dans la deuxième fenêtre du navigateur, l’écran s’est flashé et, lors de son retour, le produit est toujours là, bien qu’il soit répertorié comme « Chai » (la modification du nom du produit effectuée par la première instance du navigateur). Si l’utilisateur clique à nouveau sur le bouton Supprimer, la suppression réussit, car la valeur d’origine ProductName de GridView (« Chai ») correspond désormais à la valeur de la base de données.

Dans ces deux cas, l’expérience utilisateur est loin d’être idéale. Nous ne voulons pas afficher clairement à 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 un peu déroutant car la commande utilisateurs a échoué, mais il n’y a pas eu d’indication précise de pourquoi.

Pour résoudre ces deux problèmes, nous pouvons créer des contrôles Web Label sur la page qui fournissent une explication sur la raison 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 de post-niveau de GridView, 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 selon les besoins.

Étape 6 : Ajouter des messages d'information et les afficher en cas de violation de concurrence

Lorsqu’une violation de concurrence se produit, le comportement observé dépend de l’utilisation du modèle de mise à jour par lot ou du modèle direct de la couche d'accès aux données (DAL) de la base de données. Notre tutoriel utilise les deux modèles, avec le modèle de mise à jour par lots 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 Web Label à 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 Visible et EnableViewState du contrôle Label sur false; ceci les fera être masqués lors de chaque visite de page, sauf pour les visites de page particulières où leur propriété Visible est programmatiquement définie sur 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." />

En plus de définir leurs propriétés Visible, EnabledViewState et Text, j’ai également défini la propriété CssClass à Warning, ce qui entraîne l’affichage des étiquettes dans une police grande, rouge, italique et en 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 Labels, le concepteur dans Visual Studio doit 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 de taille complète)

Avec ces contrôles d'étiquette Web en place, nous sommes prêts à examiner comment déterminer quand une violation de concurrence s’est produite, moment où la propriété d’étiquette Visible appropriée peut être définie sur true, affichant le message d’information.

Gestion des violations de simultanéité lors de la mise à jour

Examinons tout 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 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 avait modifié les mêmes données entre le moment où il a commencé à modifier l’enregistrement et lorsqu’il a cliqué sur le bouton Mettre à jour.

Comme nous l’avons vu dans la gestion des exceptions BLL et DAL-Level dans un didacticiel 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 RowUpdated gridView qui vérifie si une DBConcurrencyException exception a été levée. Ce gestionnaire d’événements reçoit une référence à toute exception levée au cours du processus de mise à jour, comme indiqué dans le code du gestionnaire d’événements ci-dessous :

protected void ProductsGrid_RowUpdated(object sender, GridViewUpdatedEventArgs e)
{
    if (e.Exception != null && e.Exception.InnerException != null)
    {
        if (e.Exception.InnerException is System.Data.DBConcurrencyException)
        {
            // Display the warning message and note that the
            // exception has been handled...
            UpdateConflictMessage.Visible = true;
            e.ExceptionHandled = true;
        }
    }
}

Face à une exception, ce gestionnaire d’événements DBConcurrencyException affiche le contrôle d'étiquette UpdateConflictMessage et indique que l’exception est prise en charge. Avec 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 elles auraient remplacé les modifications d’un autre utilisateur en même temps. En particulier, le GridView est retourné à son état avant modification et lié aux données actuelles de la base de données. Cela met à jour la ligne GridView avec les modifications de l’autre utilisateur, qui n’ont pas été visibles précédemment. En outre, le UpdateConflictMessage contrôle Label explique à l’utilisateur ce qui vient d’arriver. Cette séquence d’événements est détaillée dans la figure 19.

Mises à jour utilisateur perdues face à une violation de la concurrence

Figure 19 : Les mises à jour d’un utilisateur sont perdues face à une violation d’accès concurrentiel (cliquez pour afficher l’image de taille complète)

Remarque

Au lieu de renvoyer GridView à l’état de pré-édition, nous pourrions laisser GridView dans son état d’édition en définissant la KeepInEditMode propriété de l’objet passé GridViewUpdatedEventArgs sur true. Toutefois, si vous utilisez cette approche, veillez à rebiner les données à GridView (en appelant sa DataBind() méthode) afin que les valeurs de l’autre utilisateur soient chargées dans l’interface d’édition. Le code disponible en téléchargement avec ce didacticiel comporte ces deux lignes de code dans le RowUpdated gestionnaire d’événements commentées ; supprimez simplement les marques de commentaire de ces lignes de code pour que GridView reste en mode édition après une violation d’accès concurrentiel.

Répondre 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 face à une violation de concurrence. 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 de données créées dans la BLL ont été conçues afin qu’elles retournent une valeur booléenne indiquant si elles ont affecté précisément un enregistrement. Par conséquent, pour déterminer si une violation de concurrence s’est produite lors de la suppression d’un enregistrement, nous pouvons examiner la valeur de retour de la méthode BLL DeleteProduct .

La valeur de retour d’une méthode BLL peut être examinée dans les gestionnaires d’événements de post-niveau de ObjectDataSource via la ReturnValue propriété de l’objet ObjectDataSourceStatusEventArgs passé dans le gestionnaire d’événements. Étant donné que nous sommes intéressés à 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 que la méthode a été interrompue avant de pouvoir retourner une valeur. Par conséquent, nous devons d’abord nous assurer que la ReturnValue propriété n’est pas null et qu’elle est une valeur booléenne. En supposant que cette vérification réussit, nous affichons le DeleteConflictMessage contrôle Label si la valeur ReturnValue est false. Pour ce faire, utilisez le code suivant :

protected void ProductsOptimisticConcurrencyDataSource_Deleted(
    object sender, ObjectDataSourceStatusEventArgs e)
{
    if (e.ReturnValue != null && e.ReturnValue is bool)
    {
        bool deleteReturnValue = (bool)e.ReturnValue;
        if (deleteReturnValue == false)
        {
            // No row was deleted, display the warning message
            DeleteConflictMessage.Visible = true;
        }
    }
}

Face à une violation de concurrence, 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 s’affiche, expliquant ce qui vient d’arriver (voir la figure 20).

La suppression d’un utilisateur est annulée en cas de violation de concurrence

Figure 20 : L'annulation de la suppression par l'utilisateur en raison d'une violation de concurrence (cliquez pour afficher l’image en taille réelle)

Résumé

Les opportunités de violations de concurrence existent dans chaque application qui permet à plusieurs utilisateurs simultanés de mettre à jour ou de supprimer des données. Si de telles violations ne sont pas prises en compte, lorsque deux utilisateurs mettent à jour les mêmes données simultanément, celui qui effectue la dernière écriture prévaut, et ses modifications remplacent celles de l'autre utilisateur. Les développeurs peuvent également implémenter un contrôle de la concurrence optimiste ou pessimiste. Le contrôle d’accès concurrentiel optimiste suppose que les violations d’accès concurrentiel sont peu fréquentes et interdisent simplement une commande de mise à jour ou de suppression qui constituerait une violation de concurrence. Le contrôle d’accès concurrentiel pessimiste suppose que les violations de concurrence sont fréquentes et que rejeter simplement la mise à jour ou la suppression d’un utilisateur n’est pas acceptable. Avec le contrôle d’accès concurrentiel pessimiste, la mise à jour d’un enregistrement implique de le verrouiller, empêchant ainsi tout autre utilisateur 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 de concurrence optimiste. Les instructions UPDATE et DELETE envoyées à la base de données contiennent toutes les colonnes de la table, ce qui garantit que la mise à jour ou la suppression ne se produira que si les données actuelles du registre correspondent aux données d'origine que l'utilisateur avait lorsqu'il a procédé à la mise à jour ou à la suppression. Une fois que le DAL a été configuré pour prendre en charge la concurrence optimiste, les méthodes du BLL doivent être mises à jour. En outre, la page ASP.NET qui appelle dans la BLL doit être configurée afin que ObjectDataSource récupère les valeurs d’origine de son contrôle Web de données et les transmet à la 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 de DAL et BLL et l’ajout de la prise en charge dans la page ASP.NET. Que ce travail ajouté soit ou non un investissement judicieux de votre temps et de votre effort dépend de votre application. Si vous avez rarement des utilisateurs simultanés qui mettent à jour les 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é. Si, toutefois, vous avez régulièrement plusieurs utilisateurs sur votre site qui travaillent 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 les autres.

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 en tant que consultant indépendant, formateur et écrivain. Son dernier livre est Sams Teach Yourself ASP.NET 2.0 en 24 heures. On peut le joindre à mitchell@4GuysFromRolla.com.