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 propre à 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 quattre 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 application (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).
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.
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 temps 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 oeuvre 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 :
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 entraîner 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, querys, 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 à jours, 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 à jours, 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 à jours, 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 à jours, 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 toute 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é !
-
Dans l'arborescence, faites un clic droit (menu contextuel) à l'endroit où vous désirez créer le service,
Add - New Item
-
Sélectionnez « Domain Service Class », saisissez un nom de fichier (Referenciel.cs) et validez !
-
Dans cet écran,
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).
PS : 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 ! -
Le résultat est :
-
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 :
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 authenfié (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.
-
Comme pour le fichier Referentiel.cs,
faites un clic droit à l'endroit où vous désirez créer le service, Add - New Item, -
Sélectionnez "Domain Service Class" comme type
et entrez "RefCustomer.cs" comme nom de fichier.
Puis validez ! -
Suit alors l'écran qui définit ce que contient le fichier :
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 reviendront par la suite. -
Ajoutez le mot clé partial dans la déclaration de la classe,
public class Referentiel : LinqToEntities... => public partial class Referentiel : LinqToEntities... -
Supprimez tout attribut décorant la classe.
Le fichier de code donne alors ceci :
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éé, sur-fixé
.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)
#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 champs, champ indispensable, etc.).
Dans l'arborescence, nous retrouvons alors ceci :
Attention : côté données, dans la version du PDC (nov 2009) RIA ne supporte pas les PK (clé primaire) multi-composites 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 champs 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.
- Générer le projet Web.
- Générer le projet Silverlight.
Si d'aventure, la curiosité vous pique;), je vous invite à déployer toute l'arborescence du projet silverlight.
-
Faites un clic sur le projet Silverlight,
-
Faites un clic sur l'icone "Show All Files"
-
Éventuellement sur "Refresh" (on ne sait jamais)
L'arborescence affiche alors les répertoires et fichiers qui ne font pas partie du projet.
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 ?
A chaque build du projet Silverlight, ce répertoire et le contenu qui s'y trouve est régénéré.
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 :
monContexte =
new
Referentiel
(
);
//
Où
Referentiel
est
le
nom
de
la
classe
DomainService<BR>
- Création de la requête Linq d'interrogation :
query =
monContexte.
GetCustomersQuery
(
).
Where
(
cust =
>
cust.
ContactName.
StartsWith
(
"
A
"
));
-
Exécution et récupération du résultat (s'il y a lieu). PS : 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).
monContexte.
Load<
Customers>
(
query,
loadOp =
>
{
//
Code
de
l'expression
lambda
}
,
false
);
//
...
//
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ées
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'ajouterais que RIA a la capacité de détecter les objets et propriétés modifiées, 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 sont 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é :
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és 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
PS : 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 :
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 par é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.
-
Tester si une erreur est présente.
-
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". - Gérer l'erreur.
Le code ci dessous trace le cheminement :
//
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) :
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;
}
}
}
}
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;
}
}
}
}
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;
}
}
}
}
A l'utilisation de ces extensions, le code se résume donc à ceci :
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 noeud customErrors contenu dans le fichier web.config de l'application
web qui héberge les DomainServices.
<
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).
[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 champs 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.
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 quattre étapes.
-
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. -
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 (Click sur le bouton), le code récupère le contenu du DataContext du bouton qui a généré l'évènement. -
Récupération de la valeur sélectionnée.
- 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.
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é 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 stoqué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 :
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, nico-pyright et jacques_jean. Sans oublier Caro-Line (pour le dernier "tuto"
=> "tutoriel") :).