IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

WCF RIA Services : une approche pragmatique !

Ce tutoriel a pour but de décrire les étapes à suivre pour mettre en place RIA, mais en tenant compte de l'expérience acquise au cours de ces derniers mois (réalisation de projets qui utilisent RIA).

Il aborde donc la technologie et expose les solutions mises en place pour faciliter les phases de développement (gestion des erreurs, conflits, classe partielle pour définir le « DomainService », etc.). Il s'agit, en quelque sorte, d'ébauches de « bonnes conduites » (en toute modestie) qui ont facilité le développement des logiciels.
6 commentaires Donner une note à l´article (4.5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

Ce tutoriel a pour but de décrire les étapes à suivre pour mettre en place RIA, mais en tenant compte de l'expérience acquise au cours de ces derniers mois (réalisation de projets qui utilisent RIA).

Il aborde donc la technologie et expose les solutions mises en place pour faciliter les phases de développement (gestion des erreurs, conflits, classe partielle pour définir le « DomainService », etc.). Il s'agit, en quelque sorte, d'ébauches de « bonnes conduites » (en toute modestie) qui ont facilité le développement des logiciels.

La rédaction de cet aide mémoire a été réalisée dans le cadre suivant :

  • Framework.net 3.5 sp1 ;
  • Mapping relationnel objet : « Entity Framework » ;
  • Silverlight 3 ;
  • WCF RIA Services pour le Framework 3.5 (version datant du PDC).

Les exemples éventuels liés à ce document se baseront donc sur cet environnement, néanmoins, le passage à la version 4 ne remet pas en cause ce tutoriel (logiquement).

Cliquez ici pour récupérer les sources de l'exemple.

La base de données utilisée est présente dans les sources (répertoire App_Data du projet web).
Pour exécuter le projet, vous devez monter la base de données (Northwind) dans une instance SQL Server puis modifier la chaine de connexion dans le fichier web.config.

Attention : la chaine de connexion n'est pas une chaine SQL Server classique. Il s'agit d'une chaine de connexion Entity Framework. Dès lors, la chaine de connexion à la base de données est incluse dans la chaine Entity Framework. Je vous conseille donc de ne modifier que les éléments propres à SQL Server inclus dans la chaine Entity Framework (situés vers la fin de la chaine).

N'oubliez pas de démarrer la solution par le projet web : l'application Silverlight ne peut pas démarrer toute seule.

Une dernière information : le code source et le tutoriel se complètent. Je vous conseille donc de consulter les deux simultanément.

Bonne lecture !

II. Wcf RIA Data Services : Kesako

RIA est une couche logicielle (API) qui va permettre de simplifier l'implémentation de la communication entre les tiers d'une application n-tiers (entre la partie client léger -silverlight ici- et le serveur web).

Pour ce faire, RIA s'appuie sur quatre mécanismes :

  • centralisation de la définition des règles de validation des données.
    Le mécanisme est basé sur la décoration des classes et membres par des attributs dans un fichier de métadonnées (nous y reviendrons plus tard) ;
  • le modèle de données est diffusé côté client (on ne manipule donc que des objets métiers qui correspondent au modèle Entity Framework) ;
  • proxy de communication qui utilise Linq pour requêter (ADO.NET DataServices). La requête Linq est transformée en url de requêtage pour interroger le serveur, récupérer les résultats et mapper les résultats en objets métiers ;
  • génération automatique des points de terminaison (WCF) lors du déploiement de l'application (au nombre de 3 : WsHttpBinding, BinaryHttpBinding et BasicHttpBinding).

WsHttpBinding, BinaryHttpBinding et BasicHttpBinding sont trois types de liaisons WCF.

WCF (Windows Communication Fundation) est un mécanisme qui permet aux applications de communiquer facilement ensemble.

Cette technologie présente comme avantage de séparer l'implémentation (code), des aspects techniques utilisés pour négocier la communication entre applications (WebService, Binaire, etc.).

Cette technologie repose sur un principe : ABC - Address, Binding, Contrat.

Address - Tout point de terminaison dispose d'une adresse ;

Binding - Tout point de terminaison est exploitable suivant certaines règles (protocole, sécurité, quotas de données, etc.) ;

Contrat - Tout point de terminaison est lié à un contrat de service ; 

Dans notre cas de figure :

- BasicHttpBinding définit un ensemble de règles simple reflétant un WebService de type asmx ;

- WsHttpBinding s'appuie sur BasicHttpBinding, en lui ajoutant des notions de sécurité (couche transport, session, etc.) ;

- BinaryHttpBinding permet la communication binaire entre applications. Silverlight exploite nativement ce point de terminaison ;

Ci-dessous, un résumé des bindings Wcf existant et leurs caractéristiques respectives.

Notez que seul le BasicHttpBinding ne dispose pas d'un transport sécurisé (T).

/paragraph>
Image non disponible
Cette image a été trouvée sur le blog suivant : http://bloggingabout.net/blogs/dennis/

Attention : si le serveur s'exécute sur Windows 2008, le serveur web est forcément IIS 7.x, il est indispensable que les composants Windows « Microsoft .Net Framework 3.5.x », « WCF Http Activation » et « WCF Non Http Activation » soient activés.

Image non disponible

En résumé, le client va requêter une source de données (DataContext) en utilisant Linq sur un proxy. Le proxy va se charger de traduire la requête en url, de se connecter au serveur pour la lui transmettre et enfin, de récupérer les résultats et effectuer le mapping relationnel objet (côté client).

Il n'est donc plus nécessaire d'implémenter un web Service (ou autre) pour les opérations de création, mise à jour et de récupération d'objets (dans la majorité des cas) !

It's Magic !

Grâce à ce mécanisme, le travail des équipes de développement se concentre alors sur les aspects métiers de l'application n-tiers. Cela permet de gagner du temP.-S. précieux sur l'implémentation des couches de communication entre composants applicatifs de l'application n-tiers (vécu !).

En d'autres termes, il n'est plus nécessaire de se préoccuper du moyen utilisé pour effectuer les accès aux données du SI (type web Service ou autre). RIA se charge de cela en offrant au développeur un proxy à instancier et à interroger (en Linq). RIA peut prendre également en charge les mécanismes de validation, insertion, modification, suppression de données.

III. Mise en œuvre de RIA dans un projet web Silverlight

Le tutoriel n'en parle pas (car beaucoup d'écrits existent à ce sujet), néanmoins, pour créer un projet Silverlight supportant RIA, il vous suffit d'activer la boite à cocher Wcf Ria Services lors de la création de l'application Silverlight au sein du projet web.

Il est également possible d'activer RIA après avoir créé le projet Silverlight : vous devez vous rendre dans les propriétés du projet Silverlight, et activer WCF Ria Service en précisant le projet web où se trouvent les services.

Pour l'exemple, je me base sur un modèle Entity Framework. Mais il demeure possible d'utiliser d'autres frameworks de mapping relationnel objet.

Le modèle de données de l'exemple est construit sur la Bdd Northwind (histoire de ne pas utiliser mes bases client.

Les tables retenues sont :

Image non disponible

III-A. Création du « Domain Service Class »

La classe « DomainService » est un des points clés du mécanisme. C'est elle qui, à l'exécution de l'application, va entrainer la création des points de terminaisons WCF dans l'application (aux nombres de trois comme nous l'avons vu précédemment, à savoir : WsHttpBinding, BinaryHttpBinding et BasicHttpBinding).

Une classe DomainService concerne un modèle de données. Ce qui signifie que toute les classes métier qui sont liées entre elles, et qui doivent être manipulées par la couche cliente seront déclarées au sein du même DomainService.

Il est donc fortement conseillé d'utiliser le mécanisme des classes partielles pour déclarer le DomainService (chaque fichier contient alors les méthodes, queries, etc. liées à une classe métier).

Dans le cas qui nous concerne, nous aurons donc une classe Referentiel constituée de cinq fichiers.

  • Referentiel.cs
    Ce fichier contient toutes les déclarations communes à tout le référentiel.
    Exemple :
     - méthodes pour capturer les opérations et les loguer ;
     - gestion des erreurs côté serveur ;
     - attributs de classes (exemple : EnableClientAccessAttribute qui permet d'indiquer que le DomainService est accessible depuis une application cliente) ;
     - etc.
  • RefCustomer.cs
    Ce fichier contient les déclarations propres à la gestion des objets métier « Customers ».
    Exemple :
     - déclaration indiquant que les insertions, mises à jour, suppressions sont autorisées ;
     - requêtes (Linq) personnalisées ;
     - etc.
  • RefOrders.cs
    Ce fichier contient les déclarations propres à la gestion des objets métier « Orders ».
    Exemple :
     - déclaration indiquant que les insertions, mises à jour, suppressions sont autorisées ;
     - requêtes (Linq) personnalisées ;
     - etc.
  • RefOrder_Details.cs,
    Ce fichier contient les déclarations propres à la gestion des objets métier « Order_Detail ».
    Exemple :
     - déclaration indiquant que les insertions, mises à jour, suppressions sont autorisées ;
     - requêtes (Linq) personnalisées ;
     - etc.
  • RefProducts.cs
    Ce fichier contient les déclarations propres à la gestion des objets métier « Products ».
    Exemple :
     - déclaration indiquant que les insertions, mises à jour, suppressions sont autorisées ;
     - requêtes (Linq) personnalisées ;
     - etc.

III-A-1. Création du fichier de base de la classe « Referentiel » : Referentiel.cs

Ce fichier va contenir toutes les caractéristiques et implémentations propres à la classe en elle-même.

Parmi ces éléments, nous trouverons entre autres :

  • la gestion des erreurs côté serveur ;
  • le log éventuel des opérations ;
  • les attributs qui précisent le comportement du « DomainService » ;
  • etc.

Cette classe a comme caractéristique de ne pas être liée à un type métier précis, et donc, on ne retrouve aucune caractéristique propre à un type manipulé !

Important : n'oubliez pas de recompiler le projet entre le moment où le modèle EF est créé et celui où les classes DomainServices sont créées.

En effet, le modèle de données sur lequel il est possible de créer un DomainService est contenu dans l'assembly généré !

  1. Dans l'arborescence, faites un clic droit (menu contextuel) à l'endroit où vous désirez créer le service, Add - New Item
    Image non disponible
  2. Sélectionnez « Domain Service Class », saisissez un nom de fichier (Referenciel.cs) et validez !
    Image non disponible
  3. Dans cet écran,

    Image non disponible
    vérifiez que :
     - le nom de la classe est « Referentiel » ;
     - la propriété « Enable client access » est sélectionnée ;
     - le modèle de données sur lequel se base le service est le bon (ici NorthwindEntities).
        P.-S. S'il n'apparaît pas, c'est que vous avez probablement omis de compiler le projet après la création du modèle EF !
     - aucune entité particulière n'est sélectionnée (en effet, nous créons le fichier principal de notre Référentiel).

    Et validez !
  4. Le résultat est :
    Image non disponible
  5. Dans le fichier de code, n'oubliez pas d'ajouter le mot clé partial à la déclaration de la classe,
         public class Referentiel : LinqToEntities… => public partial class Referentiel : LinqToEntities…

Le fichier de code ressemble alors à ceci :

Classe Referentiel après sa création...
Sélectionnez
namespace NorthWindRIA.Web.Metier.Services
{
    using System.Web.DomainServices.Providers;
    using System.Web.Ria;
    using NorthWindRIA.Web.Metier;
        
    // [RequiresAuthentication] => Attribut qui décommenté, entraine la nécessité d’être authentifié (au sens ASP.net) pour attaquer le service,
    /// <summary>
    /// Classe de base du Referentiel.
    /// </summary>
    [EnableClientAccess()] // Indique que le service est accessible !
    public partial class Referentiel : LinqToEntitiesDomainService<NorthwindEntities>
    {
        // contient ce qui est commun à toutes les déclarations partielles : attributs, etc.
    }
}

Concernant les attributs de classe ci-dessus :

  • [RequiresAuthentication()]
    Indique que l'utilisateur doit être authentifié (au sens ASP.net du terme) pour utiliser le service (si décommenté) ;
  • [EnableClientAccess()]
    Indique que le service est disponible depuis un client.

III-A-2. Création d'un fichier annexe de la classe « Referentiel » : gestion des Customers (RefCustomers.cs)

Le fichier RefCustomer.cs (au même titre que RefOrders.cs, RefOrder_Details.cs et RefProducts.cs) va contenir une partie de la classe Referentiel (suivant le principe de classes partielles). Ce fichier va englober toutes les méthodes, queries qui concernent la gestion des Customers (clients).

Pour créer cet élément, voici les opérations.

  1. Comme pour le fichier Referentiel.cs,
    Image non disponible faites un clic droit à l'endroit où vous désirez créer le service, Add - New Item,
  2. Sélectionnez « Domain Service Class » comme type
    Image non disponible
    et entrez « RefCustomer.cs » comme nom de fichier. 
    Puis validez !
  3. Suit alors l'écran qui définit ce que contient le fichier :
    Image non disponible
    Attention, c'est ici que les choses peuvent sembler étranges.

    -> Entrez « Referentiel » comme Domain service class name (même nom que pour le fichier Referentiel.cs).
    -> Sélectionnez EnableClientAccess, ainsi que l'entité « Customers ».
    -> Pour permettre à une application cliente d'effectuer des insertions et modifications sur les objets Customers, n'oubliez pas de sélectionner « Enable Editing ».

    Last but not least, sélectionnez « Generate associated classes for metadata ».
    Cette option (importante) demande à Visual de générer une classe de métadonnées nous permettant de fixer les règles de validation par membre (entre autres) de la classe Customer, nous y reviendrons par la suite.
  4. Ajoutez le mot clé partial dans la déclaration de la classe,
         public class Referentiel : LinqToEntities… => public partial class Referentiel : LinqToEntities…
  5. Supprimez tout attribut décorant la classe.

Le fichier de code donne alors ceci :

 
Sélectionnez
namespace NorthWindRIA.Web.Metier.Services
{
    using System.Data;
    using System.Linq;
    using System.Web.DomainServices.Providers;
    using NorthWindRIA.Web.Metier;


    // Classe partielle Referentiel (cf. Referentiel.cs pour les informations de la classe)
    // ce fichier contient la description des services dispo pour les entités Customers
    public partial class Referentiel : LinqToEntitiesDomainService<NorthwindEntities>
    {
        /// <summary>
        /// Retourne la Query de base utilisée lorsqu'un client effectue une requête de sélection
        /// </summary>
        /// <returns>Query sur les entités <see cref="Customers"/> servant de base à toutes les requêtes de sélection provenant des app clientes.</returns>
        public IQueryable<Customers> GetCustomers()
        {
            return this.ObjectContext.Customers;
        }

        /// <summary>
        /// Effectue une insertion (si absent, la tentative d'insertion d'une entité entraine une exception => car interdit)
        /// </summary>
        /// <param name="customers">Client à insérer</param>
        public void InsertCustomers(Customers customers)
        {
            this.ObjectContext.AddToCustomers(customers);
        }

        /// <summary>
        /// Effectue une modification (si absent, la tentative de mise à jour d'une entité entraine une exception => car interdit)
        /// </summary>
        /// <param name="customers">Client à modifier</param>
        public void UpdateCustomers(Customers currentCustomers)
        {
            if ((currentCustomers.EntityState == EntityState.Detached))
            {
                this.ObjectContext.AttachAsModified(currentCustomers, this.ChangeSet.GetOriginal(currentCustomers));
            }
        }

        /// <summary>
        /// Effectue une suppression (si absent, la tentative de suppression d'une entité entraine une exception => car interdit)
        /// </summary>
        /// <param name="customers">Client à supprimer</param>
        public void DeleteCustomers(Customers customers)
        {
            if ((customers.EntityState == EntityState.Detached))
            {
                this.ObjectContext.Attach(customers);
            }
            this.ObjectContext.DeleteObject(customers);
        }
    }
}
III-A-2-a. Le fichier de métadonnées

Lorsque l'on génère une classe « Domain Service » basée sur une entité (Customers ici) et que l'option « Generate associated classes for metadata » est sélectionnée,
Visual Studio génère (côté serveur) un fichier qui porte le même nom que le fichier créé, surfixé .metadata.cs (pour le fichier RefCustomers.cs => RefCustomers.metadata.cs).

Ce fichier contient du code mort (inutile d'ajouter des méthodes) : il ne sert qu'à permettre la précision des caractéristiques des membres de l'entité métier.

La validation des données est réalisée grâce aux System.ComponentModel.DataAnnotations (ce mécanisme est basé sur la réflexion).

Leur fonctionnement est le suivant : dans le code, les membres sont décorés par des attributs de validation (StringLenght,etc.). Lors de l'exécution, .net analyse (réflexion) la classe afin de connaître les règles de validation (réalisé une seule fois).

Puis, avant l'affectation de valeurs, la validation est exécutée sur la nouvelle valeur. Dans le cas où cette dernière est invalide, une System.ComponentModel.DataAnnotations.ValidationException est lancée.

Exemple : le champ ContactName du Customers ne peut dépasser 25 caractères, je peux donc décorer le champ ContactName contenu dans le fichier de métadonnées d'un StringLengthAttribute.

Lors de l'exécution, la règle appliquée sera validée dans l'application cliente et l'application serveur.

Le fichier ressemble à ceci : (intègre l'exemple du StringLengthAttribute)

 
Sélectionnez
#pragma warning disable 649    // disable compiler warnings about unassigned fields

namespace NorthWindRIA.Web.Metier
{
    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.ComponentModel.DataAnnotations;
    using System.Data.Objects.DataClasses;
    using System.Linq;
    using System.Web.DomainServices;
    using System.Web.Ria;
    using System.Web.Ria.Services;


    // The MetadataTypeAttribute identifies CustomersMetadata as the class
    // that carries additional metadata for the Customers class.
    [MetadataTypeAttribute(typeof(Customers.CustomersMetadata))]
    public partial class Customers
    {

        // This class allows you to attach custom attributes to properties
        // of the Customers class.
        //
        // For example, the following marks the Xyz property as a
        // required field and specifies the format for valid values:
        //    [Required]
        //    [RegularExpression("[A-Z][A-Za-z0-9]*")]
        //    [StringLength(32)]
        //    public string Xyz;
        internal sealed class CustomersMetadata
        {

            // Metadata classes are not meant to be instantiated.
            private CustomersMetadata()
            {
            }

            public string Address;

            public string City;

            public string CompanyName;

            [StringLength(25)] // Attribut ajouté pour limiter le nombre de caractères
            public string ContactName;

            public string ContactTitle;

            public string Country;

            public string CustomerID;

            public string Fax;

            public EntityCollection<Orders> Orders;

            public string Phone;

            public string PostalCode;

            public string Region;
        }
    }
}

#pragma warning restore 649    // re-enable compiler warnings about unassigned fields

Dans le cas où aucun attribut n'est précisé, ce sont les caractéristiques du modèle de données qui sont utilisées (longueur de chamP.-S., champ indispensable, etc.).

Dans l'arborescence, nous retrouvons alors ceci :

Image non disponible

Attention : côté données, dans la version du PDC (nov 2009) RIA ne supporte pas les PK (clé primaire) multicomposites utilisées comme PK-FK (FK = clé étrangère) dans une autre table. Donc, une bonne habitude (avec RIA) consiste à utiliser un ID propre, + une contrainte d'unicité sur les chamP.-S. pour éviter les problèmes.

III-A-3. Génération

Une fois la création du service réalisée, il ne reste qu'à générer le projet.

  1. Générer le projet web.
  2. Générer le projet Silverlight.

Si d'aventure, la curiosité vous pique;), je vous invite à déployer toute l'arborescence du projet silverlight.

Déploiement de l'arborescence

  1. Faites un clic sur le projet Silverlight,
    Image non disponible
  2. Faites un clic sur l'icône « Show All Files »
    Image non disponible
  3. Éventuellement sur « Refresh » (on ne sait jamais)
    Image non disponible

L'arborescence affiche alors les répertoires et fichiers qui ne font pas partie du projet.
Image non disponible

Le répertoire qui nous intéresse plus particulièrement est « Generated_Code ».
Nous y retrouvons les classes générées par le moteur RIA lors du Build du projet Silverlight.
Je vous invite à consulter le fichier généré : cela vous permettra de mieux comprendre la logique des classes générées. Exemple : quelles sont les opérations effectuées lors de l'affectation de valeur ?

À chaque build du projet Silverlight, ce répertoire et le contenu qui s'y trouvent sont régénérés.

IV. Charger les données

Le chargement des données demeure, somme toute, assez simple.

Il se réalise en trois étapes :

  • Instanciation du contexte de données :
Instanciation du contexte de données...
Sélectionnez
monContexte = new Referentiel(); // Où Referentiel est le nom de la classe DomainService<BR>
  • Création de la requête Linq d'interrogation :
Création de la requête Linq d'interrogation...
Sélectionnez
query = monContexte.GetCustomersQuery().Where(cust => cust.ContactName.StartsWith("A"));
  • Exécution et récupération du résultat (s'il y a lieu). P.-S. La récupération peut se réaliser par expression lambda ou par événement.
    Dans les deux cas, l'objet qui contient le résultat de la requête est de type LoadOperation. C'est donc avec ce dernier qu'il faut traiter les erreurs éventuelles, et récupérer les résultats (si nécessaire).
Exécution de la requête, et récupération des résultats par expression lambda...
Sélectionnez
monContexte.Load<Customers>(
       query,
       loadOp =>
       {
           // Code de l'expression lambda
       }, 
       false);
Exécution de la requête et récupération des résultats par événement...
Sélectionnez
// ...
     // Exécute la requête !
     var loadOp = monContexte.Load<Customers>(query, false);
     loadOp.Completed += new EventHandler(loadOp_Completed); // abonnement du delegate à la méthode loadOp_Completed

//...

// Code de la méthode abonnée au retour de l'événement...
private void loadOp_Completed(object sender, EventArgs e)
{
     var loadOp = sender as LoadOperation<Customers>; // le sender est l'objet LoadOperation !
     // Suite du code !
}

Lorsque je parle de la récupération de données, j'utilise le terme « éventuellement ». Car lors de la récupération de données, RIA met à jour les collections concernées, intrinsèques du Contexte utilisé pour effectuer la requête.

Donc, rien n'empêche d'utiliser ces ObservablesCollections (qui nous informent lorsqu'elles sont modifiées), par Binding (par exemple). C'est le cas dans l'exemple lié au tutoriel.

V. Soumettre des modifications de données

Lorsque l'on parle de soumission de modifications, on entend par là toute opération (insertion, mise à jour, suppression) effectuée entre le moment où les données sont extraites (ou depuis la dernière mise à jour) et le moment où j'effectue la demande de soumission.

La soumission de modifications n'est pas complexe en soi, mais il faut respecter une certaine logique

Lorsque j'extrais un objet du serveur, on peut voir que j'utilise un contexte de données. Ce contexte reste lié à mon objet (un objet ne pouvant être lié qu'à un contexte de données) durant la vie de l'objet (sauf si je détache l'objet du contexte).

J'ajouterai que RIA a la capacité de détecter les objets et propriétés modifiés, et dans une certaine mesure (nous le verrons plus tard), de participer à la gestion des conflits.

Revenons donc à la soumission : il suffit d'appeler la méthode SubmitChanges du contexte de données qui a extrait les données, bien évidemment, après la modification des données.

Je vous invite à consulter l'exemple fourni.
Voici sa logique :

  • le DataContext de l'écran contient le contexte de données qui extrait ces dernières ;

  • la propriété ItemsSource de la grid est liée (Binding Xaml) à l'observable collection cachée derrière la propriété Customers du DataContext.
    Donc, dès que la collection Customers change, la grid change son contenu ;
  • la propriété IsEnable du bouton de mise à jour est liée (Binding Xaml) à la propriété HasChanges du DataContext.
    Donc, lorsque l'utilisateur modifie une valeur, le contexte de données informe le bouton qu'il peut s'activer !

Lorsque l'utilisateur clique sur le bouton permettant la mise à jour, le code suivant est appelé :

 
Sélectionnez
monContexte.SubmitChanges(
     submitOp =>
     {
          // code qui traite le retour de màj avec la gestion des erreurs
          if (submitOp.HasError)
          {
               submitOp.MarkErrorAsHandled(); // évite de remonter l'exception à l'App.Application_UnhandledException

               if (submitOp.Error != null)
               {

               }
          }
     }, 
     null);

Dans ce code, je traite le retour de l'appel de mise à jour par une expression lambda. Néanmoins, à l'instar de la récupération de données, il demeure possible de réaliser cette opération de façon évènementielle. L'objet manipulé n'est pas de type LoadOperation, mais plutôt SubmitOperation.

Information : je vous invite à modifier une valeur (ajouter une lettre par exemple) puis à supprimer votre modification, vous verrez que le bouton Mettre à jour les données s'active et se désactive automatiquement. Ceci est réalisé en liant (Binding) la propriété HasChanges du contexte de données à la propriété IsEnabled du bouton (voir code Xaml).

VI. Gestion des erreurs liées à l'accès aux données

La gestion des exceptions provenant des accès aux données demeure, en définitive, assez simple à gérer.

Considérons les deux côtés applicatifs, à savoir : côté client (Silverlight) et côté Serveur.

VI-A. Côté client : Silverlight

VI-A-1. Dans le code

Lors de l'appel à la méthode Load (mais le propos vaut également pour les autres opérations disponibles), il est possible de préciser si une exception doit être propagée dans le client en cas d'exception lors de l'appel au serveur (dernier paramètre booléen des méthodes d'appel). Si l'exception est activée, se produit et n'est pas gérée par le code du développeur, cette dernière sera renvoyée à la méthode App.Application_UnhandledException.

Il y a plusieurs moyens de vérifier que les traitements n'ont pas renvoyé d'exceptions.

Considérons en deux :
- - l'expression lambda ou « CallBack method » ;
- - l'abonnement à l'événement Completed de l'objet renvoyé lors de l'appel ;

« CallBack method » ou expression lambda
P.-S. L'expression Lambda demeure ma technique favorite.

Ce moyen s'utilise le plus facilement du monde : lors de l'appel à l'opération, un des paramètres possibles est de type « Action{LoadOperation} », « Action{SubmitOperation} » ou « Action{InvokeOperation} ».
Il suffit dès lors d'utiliser une expression lambda d'exécution afin de gérer le retour.

Exemple :

 
Sélectionnez
monContexte.Load<Customers>(
    monContexte.GetCustomersQuery(),
    <BR>    // EXPRESSION LAMBDA : 
    loadOperation =>
         {
              if (loadOperation.HasError)
              {
                  MessageBox.Show(loadOperation.Error.Message);
                  loadOperation.MarkErrorAsHandled();
              }
         }, 
    // FIN D'EXPRESSION LAMBDA
    null);

Abonnement à l'événement Completed

Dans ce deuxième cas de figure, lors de l'appel à l'opération (Load, Submit ou Invoke), un objet de type LoadOperation (ou SubmitOperation, ou InvokeOperation) est retourné. Il suffit de s'abonner à l'événement « Completed » afin d'être averti de la fin du traitement (cf. ci-dessus « Exécution de la requête » et récupération des résultats événements événements…).

Dans les deux cas, l'objet qui contient les résultats de l'appel est identique, il est soit de type « LoadOperation », soit « SubmitOperation » ou encore « InvokeOperation ».

La gestion d'un retour se réalise alors en trois parties.

  1. Tester si une erreur est présente.
  2. Si c'est le cas : appeler « MarkErrorAsHandled ».
    Lorsque l'exception a été traitée, il ne faut pas oublier d'appeler la méthode « MarkErrorAsHandled() ». Cela va indiquer à RIA que l'exception a été traitée et qu'il ne faut pas la propager au reste de l'application. De même qu'il est possible de vérifier si l'exception a déjà été traitée par la propriété « IsErrorHandled ».
  3. Gérer l'erreur.

Le code ci-dessous trace le cheminement :

 
Sélectionnez
// Appel de Submit changes en passant une expression lambda appelée au retour de l'appel !
monContexte.SubmitChanges(
     submitOp => // Expression lambda !
     {
          // code qui traite le retour avec gestion des erreurs
          
          if (submitOp.HasError) // Erreur présente ?
          {
               submitOp.MarkErrorAsHandled(); // permet d'éviter de diffuser l'exception comme irrémédiable !

               if (submitOp.Error != null)
               {
                    // traitement de l'objet Erreur (type Exception)
               }
               // traitement commun à toutes les erreurs
          }
     }, 
     null);

Afin de centraliser la gestion des erreurs, dans les projets que je gère, j'ajoute trois classes d'extensions :

     - une sur le type SubmitOperation ; 

     - la seconde sur le type InvokeOperation ; 

     - et la troisième sur le type LoadOperation.

Ces classes contiennent une méthode « TraiterRetour() » qui gère de façon commune les erreurs.

Du coup, pour traiter les exceptions, je n'ai qu'à ajouter un using pointant vers le namespace (si nécessaire) qui contient ces classes, et appeler la méthode TraiterException();

Ci-dessous le code de ces trois classes d'extension (vous pouvez vous en inspirer, ou carrément les réutiliser : elles sont dans le projet) :

Classe d'extension pour le type LoadOperation
Sélectionnez
namespace NorthWindRIA
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows.Ria;

    /// <summary>
    /// Classe d'extension sur un <see cref="LoadOperation{T}"/>
    /// </summary>
    public static class LoadOperationExtender
    {
        /// <summary>
        /// Traite le retour d'un appel au serveur pour les LoadOperations.
        /// Affiche éventuellement un message d'erreur à l'utilisateur si une erreur s'est produite.
        /// </summary>
        /// <typeparam name="T">Type d'objet retourné par la LoadOperation (<see cref="Entity"/>>)</typeparam>
        /// <param name="loadOp"><see cref="LoadOperation"/> qui a été utilisée pour l'appel au serveur</param>
        /// <returns><see cref="List{T}"/>La liste est vide en cas d'erreur (non nulle)</returns>
        public static List<T> TraiteRetour<T>(this LoadOperation<T> loadOp)
            where T : Entity
        {
            List<T> returnedList = new List<T>(); // par sécurité, la liste n'est jamais nulle !

            if (loadOp.Error != null)
            {
                loadOp.MarkErrorAsHandled();

                /* Méthode statique située dans la classe App qui permet de 
                 * traiter les erreurs de façon commune à toute l'app */
                App.TraiteErreur(loadOp.Error); 
            }
            else if (loadOp.Entities == null && !loadOp.Entities.Any())
            {
                /* Méthode statique située dans la classe App qui permet de 
                 * traiter les erreurs de façon commune à toute l'app */
                App.TraiteErreur("Le serveur n'a renvoyé aucune entité de type " + typeof(T).ToString() + " ! ");
            }
            else
            {
                returnedList = new List<T>(loadOp.Entities);
            }

            return returnedList;
        }
    }
}

}
Classe d'extension pour le type SubmitOperation
Sélectionnez
namespace NorthWindRIA
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows.Ria;

    /// <summary>
    /// Extender de la classe <see cref="SubmitOperation"/>
    /// </summary>
    public static class SubmitOperationExtender
    {
        /// <summary>
        /// Traite le retour d'un appel au serveur pour les <see cref="SubmitOperation"/>.
        /// Affiche éventuellement un message d'erreur à l'utilisateur si une erreur s'est produite.
        /// </summary>
        /// <param name="submitOp"><see cref="SubmitOperation"/> qui a été utilisée pour l'appel au serveur</param>
        /// <returns>Boolean : False si le retour est en erreur</returns>
        public static Boolean TraiteRetour(this SubmitOperation submitOp)
        {
            Boolean returned = true;

            if (submitOp.Error != null)
            {
                submitOp.MarkErrorAsHandled();
                returned = false;

                /* Méthode statique située dans la classe App qui permet de 
                 * traiter les erreurs de façon commune à toute l'app */
                App.TraiteErreur(submitOp.Error);
            }

            return returned;
        }
    }
}

}
Classe d'extension pour le type InvokeOperation
Sélectionnez
namespace NorthWindRIA
{
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Windows.Ria;

    /// <summary>
    /// Classe d'extension sur un <see cref="InvokeOperation"/>
    /// </summary>
    public static class InvokeOperationExtender
    {
        /// <summary>
        /// Traite le retour d'un appel au serveur pour les <see cref="InvokeOperation"/>.
        /// Affiche éventuellement un message d'erreur à l'utilisateur si une erreur s'est produite.
        /// </summary>
        /// <param name="invokeOp"><see cref="InvokeOperation"/> qui a été utilisée pour l'appel au serveur</param>
        /// <returns>Boolean : False si le retour est en erreur</returns>
        public static Boolean TraiteRetour(this InvokeOperation invokeOp)
        {
            Boolean returned = true;

            if (invokeOp.Error != null)
            {
                invokeOp.MarkErrorAsHandled();
                returned = false;

                /* Méthode statique située dans la classe App qui permet de 
                 * traiter les erreurs de façon commune à toute l'app */
                App.TraiteErreur(invokeOp.Error); 
            }

            return returned;
        }
    }
}


}

À l'utilisation de ces extensions, le code se résume donc à ceci :

 
Sélectionnez
var results = loadOp.TraiteRetour(); // pour une LoadOperation


if(submitOp.TraiteRetour()) // pour une SubmitOperation 
{
    // ...
}
else
{
    // ...
}


if(submitOp.TraiteRetour()) // pour une InvokeOperation 
{
    // ...
}
else
{
    // ...
}

VI-A-2. Détails paramétrables

Il est possible d'améliorer le détail des exceptions qui se propagent depuis le serveur.
Il suffit de modifier le nœud customErrors contenu dans le fichier web.config de l'application web qui héberge les DomainServices.

 
Sélectionnez
     <customErrors mode="On" defaultRedirect="?" />

Si la balise « customErrors » est définie (valeur « On » ou « RemoteOnly »), et que l'application ne s'exécute pas à partir de la machine qui l'héberge, le détail du message ne sera pas propagé au client. L'exception sera toujours présente, mais disposera d'un message réduit du type « Load operation failed for query 'xxx' ? ». Le type de l'exception sera toujours System.Windows.Ria.DomainOperationException.

Important : il faut bien considérer le fait que la sécurisation de l'application dépend également du détail des informations disponibles dans les exceptions. En effet, une exception trop bavarde pourrait aider des pirates à s'introduire sur le SI.

VI-B. Côté serveur : DomainService

Côté serveur, le seul moyen de gérer les erreurs éventuelles consiste à les intercepter. Pour ce faire, la classe DomainService permet d'overrider une méthode nommée « OnError ».

Cette dernière est appelée lorsqu'une exception se produit, et ce, quel que soit le type ou l'origine de l'exception (Entity Framework, Exception « maison » suite à l'appel d'un algorithme particulier, etc.).

Cet overriding permet d'analyser et, éventuellement traiter, les exceptions renvoyées lors des appels des couches inférieures.

Exemple : Tracing des exceptions.

Cet overriding sera déclaré dans le fichier principal de notre DomainService (Referentiel.cs).

 
Sélectionnez
[EnableClientAccess()]
public partial class Referentiel : LinqToEntitiesDomainService<NorthwindEntities>
{
     // contient ce qui est commun à toutes les déclarations partielles : attributs, etc.

     /// <summary>
     /// Traite les erreurs avant de les renvoyer au client !
     /// </summary>
     /// <param name="errorInfo"></param>
     protected override void OnError(DomainServiceErrorInfo errorInfo)
     {
          base.OnError(errorInfo);
          // Traite l'erreur
     }
}

VII. Gestion des conflits de données

Lors d'une mise à jour de nos données, un conflit peut apparaître si (par malheur) la donnée concernée a été modifiée depuis notre dernière extraction.

Traditionnellement, un ensemble de règles existent pour traiter cette situation embarrassante.

Tout d'abord, nous pourrions considérer que le dernier a toujours raison : dans ce cas, inutile de faire quoi que ce soit, Entity gère très bien les choses sans activer la gestion des conflits !

Nous aurions pu, également, choisir que le plus ancien a toujours raison… (Ou pas).

Enfin, nous pouvons implémenter notre propre logique de résolution de conflits, automatique ou avec prise de décision de la part de l'utilisateur.

Dans le premier cas (automatique), nous implémenterons certainement la logique du code sur le serveur. Dans le second, il faudra remonter l'erreur au client, et laisser l'application cliente se dépêtrer avec celle-ci.

Quoi qu'il en soit, voici la logique qui permet de provoquer un conflit de données :

  • utilisateur 1 affiche les données ;
  • utilisateur 2 affiche les données ;
  • utilisateur 1 modifie un enregistrement (update, delete) ;
  • utilisateur 2 tente de modifier le même enregistrement, même champ ;
  • utilisateur 2 est averti que la donnée qu'il tente de mettre à jour est en conflit, car sa source ne correspond plus à celle actuellement en base de données.

Si nous ne choisissons pas la situation par défaut (le dernier qui a parlé a toujours raison), il est indispensable de permettre la gestion des conflits.

Car si RIA prend en compte les mécanismes de gestion concurrentielle, encore faut-il que la couche d'accès aux données soit la source de cette prise en compte.

En d'autres termes, dans EF (pour le cas qui nous concerne), il faut activer la gestion concurrentielle sur les chamP.-S. des objets manipulés.

VII-A. Côté serveur

Lorsqu'un conflit se produit, il est possible de le gérer côté serveur de telle façon que l'utilisateur n'ait aucune décision à prendre. Il nous reste donc à implémenter le code de résolution et à effectuer son appel depuis la bonne méthode. Cette dernière est l'override (comme pour la gestion des erreurs) d'une méthode nommée « ResolveChangeSet ».

Dans le cas qui nous concerne, c'est (une fois de plus) le fichier Referentiel.cs qui semble tout désigné pour accueillir cette méthode.

 
Sélectionnez
protected override bool ResolveChangeSet(ChangeSet changeSet)
{
     return base.ResolveChangeSet(changeSet); // remplacer ce code par le notre.
}

VII-B. Côté client (Silverlight)

Dans de nombreux cas de figure, l'application va mettre à contribution l'utilisateur pour résoudre le conflit (visuellement).

Avant de parcourir le code, je vous propose de suivre la logique de résolution de conflits qui sera utilisée.
Cette dernière repose sur quatre étapes.

  1. Récupération de la valeur actuellement en base de données.
    En effet, les deux autres valeurs (l'originale que l'utilisateur avait et celle modifiée par l'utilisateur) sont contenues dans l'objet en conflit.
  2. Affichage des données à l'utilisateur, pour lui permettre de choisir la donnée à conserver.
    Dans le code, les objets sont utilisés dans les DataContext des Grid affichées. Lors de la sélection (Clic sur le bouton), le code récupère le contenu du DataContext du bouton qui a généré l'événement. 
  3. Récupération de la valeur sélectionnée.
  4. SI l'objet sélectionné n'est pas celui en Bdd => enregistrement de la valeur sélectionnée.
    Par contre, on se sert de la variable qui contient l'objet en base de données, car celui-ci est déjà lié au contexte utilisé pour extraire la valeur en base de données.
    Il suffit alors d'écraser ses valeurs, puis  d'enregistrer les modifications.

Pour cette partie plus précisément, je vous conseille de consulter le code source de l'exemple (MainPage.xaml.cs \ ResolveConflict) dans son jus.

Néanmoins, vous trouverez ci-dessous le code contenant la logique de résolution.

 
Sélectionnez
private void ResolveConflict(Customers custInConflict)
{
    // Trois entités sont considérées pour la résolution :
    //  - Entité actuellement extraite, => monCustOriginal
    //  - Entité actuellement en base de données, => custActuellementEnBdd 
    //  - Entité qui a été modifiée par l'utilisateur, => monCustModifie

    Customers monCustOriginal, monCustModifie, custActuellementEnBdd, custChoisitPourSvgBdd = null;

    // Création du Contexte de données pour la résolution de conflit :
    Referentiel dataCtx = new Referentiel();

    // Récup de la version actuellement en Bdd
    var query = dataCtx.GetCustomersQuery().Where(cust => cust.CustomerID == custInConflict.CustomerID);
    dataCtx.Load<Customers>(
        query,
        loadOp =>
        {
            // Expression lambda appelée lorsque le customer actuellement en Bdd est récupéré
            var results = loadOp.TraiteRetour();

            if (results.Any()) // Le customer existe-t-il en Bdd ?
            {
                // Redirection dans les variables ad hoc => clarifier le code
                custActuellementEnBdd = results.First();
                monCustOriginal = custInConflict.GetOriginal() as Customers;
                monCustModifie = custInConflict;

                // Affichage d'un écran pour sélectionner le Customer à garder,
                CustConflictResolver win = new CustConflictResolver()
                {
                    CustomerBdd = custActuellementEnBdd,
                    CustomerOriginal = monCustOriginal,
                    CustomerSelectionne = monCustModifie
                };

                // Lorsque la fenêtre se ferme, traitement de la résolution de conflits (abonnement à l'événement Closed en utilisant une Lambda d'exécution.
                win.Closed += (sender, e) =>
                {
                    if (win.DialogResult != null && win.DialogResult.Value) // Utilisation a-t-il annulé ?
                    {
                        custChoisitPourSvgBdd = win.CustomerSelectionne; // customer sélectionné !

                        // Sauvegarde du Customer souhaité (ou rien si l'heureux élu est celui déjà en bdd)
                        if (custChoisitPourSvgBdd != custActuellementEnBdd)
                        {
                            // L'objet sur lequel on bosse est custActuellementEnBdd 
                            //, car c'est celui qui est attaché au contexte de données local dataCtx
                            custActuellementEnBdd.RemplaceMesMembresParCeuxDe(custChoisitPourSvgBdd);

                            // Enregistrement de custActuellementEnBdd modifié !
                            dataCtx.SubmitChanges(
                                submitOp =>
                                {
                                    if (submitOp.TraiteRetour())
                                    {
                                        // Pour éviter un rafraichissement complet ! on détache l'objet 
                                        // pour pouvoir le rattacher au contexte de données principal (cf. RemplaceCustomer)
                                        dataCtx.Customers.Detach(custActuellementEnBdd);
                                        this.RemplaceCustomer(custInConflict, custActuellementEnBdd);
                                    }
                                },
                                false);
                        }
                    }
                };
                win.Show();
            }
        },
        false);
}

VII-B-1. Contexte de données et entités attachées

L'exemple précédent utilise un terme que je n'ai pas encore abordé : Detach() et en l'abordant, il faut parler également de son jumeau : Attach(). Évitons les jalousies !

Pour extraire une entité, nous devons utiliser un contexte de données (le proxy qui permet d'interroger la classe Domain Service).

Lorsque l'extraction est réalisée : les entités sont liées au contexte d'extraction.

Cette liaison est comme la remorque d'une voiture : si elle n'est pas attachée, elle ne sert pas à grand-chose (hormis pour stocker le barda du garage dedans…).

Lorsque la remorque est attachée, elle devient utile (puisqu'il devient possible de l'utiliser pour déplacer des objets).

Dernière précision, une remorque ne peut être attachée qu'à un véhicule à la fois (comme pour les entités).

La méthode Detach() permet de désolidariser la remorque de la voiture, donc de détacher l'entité de son contexte. Du coup, si une opération est effectuée avec le contexte de données, cette opération ne tiendra pas compte de l'entité détachée ! (Exemple SubmitChanges ne prendra pas en compte les modifications à sauvegarder en Db pour l'entité détachée).

La méthode Attach() permet de placer le timon de la remorque sur la boule de la voiture, donc de lier l'entité à un contexte de données.

Lorsque cette liaison est effectuée : l'entité liée n'est pas marquée comme modifiée. Elle est donc considérée comme identique à la l'information stockée en base de données.

Un dernier point avant de terminer ce tutoriel : lorsqu'une propriété d'une entité est modifiée, un flag passe l'entité à HasChanges = true !

C'est sur la base de cette valeur qu'un contexte de données sait qu'au prochain SubmitChanges, il faut envoyer l'entité modifiée.

Ce mécanisme de flag ne fonctionne que si l'entité est attachée à un contexte de données.

Donc, instancier un objet, le modifier et ensuite l'attacher n'engendrera aucune modification en base de données lors du SubmitChanges().

Pour ajouter une entité, il est nécessaire d'appeler le contexte de données, la collection des entités concernées et enfin la méthode add.
Exemple :

 
Sélectionnez
monContexte.Customers.Add(nouvelleEntiteCustomer);
 monContexte.SubmitChanges(); // peut être appelé pour enregistrer plusieurs modifications en une fois !

VIII. Remerciements

Je tiens à remercier toutes les personnes qui ont permis à ce tutoriel d'exister : Adeline, Mélanie, Louis-Guillaume, Jérome, nicopyright et jacques_jean. Sans oublier Caro-Line (pour le dernier « tuto » => « tutoriel ») :).

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2010 DEVUYST Benjamin. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.