-
Highcharts : Une librairie Javascript pour intégrer très facilement des graphiques dans une page web (y compris en partant du contenu d'une table html) et qui fonctionne aussi bien avec jQuery que MooTools.
-
The CSS3 :not() selector : Des exemples pour comprendre comment utiliser le sélecteur :not() dans les feuilles de style
vendredi 12 mars 2010
Blogmarks du 11/03/2010
Par Michel le vendredi 12 mars 2010, 08:19
jeudi 11 mars 2010
Faire une liste Html.RadioButton
Par Michel le jeudi 11 mars 2010, 00:04
Un bouton radio ne s'utilisant jamais seul, on doit donc coder plusieurs helpers Html.RadioButton() quand on veut permettre à l'utilisateur de faire un choix entre plusieurs options :
<% using (Html.BeginForm()) { %>
<fieldset>
<p>
Couleur ?
<%= Html.RadioButton("Couleur", "R", false) %>Rouge
<%= Html.RadioButton("Couleur", "V", true) %>Vert
<%= Html.RadioButton("Couleur", "B", false) %>Bleu
</p>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
<% } %>
Ce code ASP.NET permet d'afficher la page ci-dessous :

Si on consulte le code source de cette page, on trouve le code HTML suivant :
<form action="/Home/Edit" method="post">
<fieldset>
<p>
Couleur ?
<input id="Couleur" name="Couleur" type="radio" value="R" />Rouge
<input checked="checked" id="Couleur" name="Couleur" type="radio" value="V" />Vert
<input id="Couleur" name="Couleur" type="radio" value="B" />Bleu
</p>
<p>
<input type="submit" value="Save" />
</p>
</fieldset>
</form>
Il faut cliquer sur un des 3 boutons radio pour sélectionner une option. Une fois qu'une option a été sélectionnée, le fait de cliquer sur un autre bouton radio va sélectionner une nouvelle option et automatiquement dé-sélectionner l'option qui était sélectionnée auparavant. Les 3 boutons radio sont liés entre eux et on ne peut sélectionner qu'une seule option à la fois parce qu'au niveau du code HTML, les 3 <input type="radio" ...> ont le même attribut name.
Pour l'instant, si on veut sélectionner une option, il est obligatoire de bien viser et de cliquer pile sur le petit rond du bouton radio. Dans la pratique, il faudrait faire en sorte que quand on clique sur le libellé de l'option, cela permette aussi de sélectionner cette option.
Pour faire ça, il faut associer le libellé au bouton radio. En HTML, on fait ça en utilisant une balise "label" pour définir le libellé et on relie ce label au bouton radio avec un attribut "for" qui pointe vers l'identifiant du bouton radio :
<input id="Couleur" name="Couleur" type="radio" value="R" /><label for="Couleur">Rouge</label>
Avec le helper Html.RadioButton() de ASP.NET MVC, il suffit donc de faire ça, mais le problème c'est que ça n'est pas si facile que ça...
Pour commencer, si on re-regarde le code HTML généré, on s'aperçoit que les 3 boutons radio ont le même identifiant ! Outre le fait que c'est pas normal du tout, ça nous complique la vie pour associer chaque libellé au bon bouton radio (puisque l'attribut "for" a besoin de l'identifiant du contrôle associé). Il va donc falloir trouver un moyen pour définir un identifiant unique pour chaque bouton radio. Heureusement, c'est prévu : le helper Html.RadioButton() accepte un objet htmlAttributes qui va nous permettre de faire ça.
Couleur ?
<%= Html.RadioButton("Couleur", "R", false, new { @id = "Red" })%><label for="Red">Rouge</label>
<%= Html.RadioButton("Couleur", "V", true, new { @id = "Green" })%><label for="Green">Vert</label>
<%= Html.RadioButton("Couleur", "B", false, new { @id = "Blue" })%><label for="Blue">Bleu</label>
Et ce coup-ci, on peut constater au niveau du code HTML généré que les 3 boutons radio n'ont plus à partager le même identifiant :
Couleur ? <input id="Red" name="Couleur" type="radio" value="R" /><label for="Red">Rouge</label> <input checked="checked" id="Green" name="Couleur" type="radio" value="V" /><label for="Green">Vert</label> <input id="Blue" name="Couleur" type="radio" value="B" /><label for="Blue">Bleu</label>
Et maintenant, il est possible de cliquer sur le nom d'une couleur pour automatiquement sélectionner le bouton radio qui lui est associé.
mercredi 10 mars 2010
Blogmarks du 09/03/2010
Par Michel le mercredi 10 mars 2010, 08:30
-
Put Your Views (and Pages) On a Diet : Une technique pour alléger le source des vues sous ASP.NET MVC. Pas forcément très intéressant, mais explique aussi comment se débarrasser des commandes «%@ Import namespace="Xxxxxx.Helpers" %» en ajoutant une section «namespaces» dans le fichier Web.config situé dans le sous-répertoire Views.
mardi 9 mars 2010
Blogmarks du 08/03/2010
Par Michel le mardi 9 mars 2010, 09:53
-
ASP.NET MVC Best Practices : Quelques-unes des meilleures pratiques à suivre pour faciliter la maintenance des applications développées avec ASP.NET MVC
mercredi 3 mars 2010
ASP.NET MVC c'est pas forcément LINQ to SQL
Par Michel le mercredi 3 mars 2010, 21:49 - Traductions
Ceci est la traduction du billet "ASP.NET MVC is not all about Linq to SQL" de Mike Brind
La plupart des exemples destinés à illustrer l'utilisation de ASP.NET MVC sont tous basés sur l'utilisation de LINQ to SQL ou d'Entity Framework pour gérer l'accès aux données. Les forums sur www.asp.net regorgent de questions pour savoir s'il existe des alternatives et en effet, il est possible de faire autrement. Cet article va montrer comment utiliser une couche d'accès aux données en ADO.NET pur afin de gérer le contenu dynamique d'une application de type CRUD toute simple.
Pour les besoins de cette démonstration, je vais énormément emprunter à la série d'articles qu'Imar Spaanjaar a consacré à la réalisation d'applications web multi-couches en ASP.NET 2.0. Je vous recommande fortement de lire ces articles ou au minimum les deux premiers afin de vous familiariser avec les rudiments d'une approche multi-couches lors de la conception d'une application ASP.NET. Je vais reprendre quasiment telles quelles 3 des principales couches de son application : Business Objects, Business Logic et Data Access dont la construction est expliquée de façon très détaillée dans les articles d'Imar. Par conséquent, cet article se contentera d'étudier à quoi servent ces couches sans chercher à entrer dans le détail de leur code.
Pour commencer, nous allons jeter un coup d'œil à l'application présentée par Imar. Il s'agit d'une application toute simple où on retrouve les opérations CRUD typiques. Elle permet aux utilisateurs de gérer des contacts, avec leurs adresses, numéros de téléphone et adresses e-mail, et leur offre la possibilité de créer, lire, mettre à jour et supprimer l'une ou l'autre de ces entités.

Les entités gérées par l'application sont ContactPersons, PhoneNumbers, Addresses and EmailAddresses. Elles sont toutes définies dans la couche des objets métiers (BO = Business Objects) de l'application. Dans l'exemple d'origine, chacune de ces classe contient uniquement des propriétés publiques (avec leurs méthodes get et set) et elles ne gèrent absolument aucun traitement. Ceux-ci sont gérés au sein de la couche des règles métiers (BLL = Business Logic Layer) dans des classes NomEntitéManager. Il existe une correspondance une-à-une entre une entité et sa classe Manager associée. Chacune des classes Manager contient des méthodes pour retrouver une instance de l'entité ou une collection d'entités, pour enregistrer une entité (ajout ou modification) et pour supprimer une entité. Cette partie de l'application peut aussi être utilisée pour gérer la validation, les autorisations, etc... Mais il n'y en a pas dans l'exemple pour qu'il reste suffisamment compréhensible. Si vous souhaitez voir comment mettre en œuvre des règles de validation dans la couche métier, vous pouvez vous reporter à la seconde série d'articles dans laquelle Imar présente les évolutions apportées à son application, y compris sa migration sous ASP.NET 3.5.
La dernière couche est la couche d'accès aux données (DAL = Data Access Layer). Cette couche comprend elle aussi des classes qui ont une correspondance une-à-une avec les classes Manager dans la couche BLL. En pratique, les méthodes de la couche BLL appellent les méthodes correspondantes dans la couche DAL. Les méthodes de la DAL sont les seules de l'application qui ont à connaitre le mécanisme utilisé pour conserver (stocker) les entités. Pour notre exemple, il s'agit d'une base de données SQL Server Express. Par conséquent, les classes de la DAL utilisent ADO.NET et les classes SqlClient. Le principe derrière cette approche est que si jamais vous souhaitez changer le mécanisme employé pour le stockage (pour utiliser XML, Oracle, un Web service ou même LINQ to SQL ou tout autre ORM), vous n'aurez qu'à remplacer cette couche DAL. Du moment que votre nouvelle DAL exposera des méthodes avec les signatures attendues par la couche BLL, tout devrait continuer à fonctionner sans qu'il soit nécessaire de faire la moindre modification dans le reste de l'application. Pour être sûr que la nouvelle DAL respecte bien les mêmes signatures de méthode que la DAL actuelle, le plus simple est de définir des Interfaces (ce qui pourrait faire un très bon sujet d'article).
Architecture MVC
Il existe tout un tas de très bons articles qui expliquent ce qu'est l'architecture MVC, c'est pourquoi ce billet ne va pas entrer trop dans le détail. Pour une présentation plus détaillée, je vous recommande de consulter directement les tutoriels sur ASP.NET MVC sur le site de Microsoft (ou leurs traductions en français sur le site Developpez). Très succinctement, le M est pour le Modèle, dans lequel on va retrouver les couches BO, BLL et DAL. Le V est pour les Vues, qui correspond à toute la partie interface utilisateur (ce que nos utilisateurs verront). Et le C est pour les Contrôleurs. Le rôle des contrôleurs est de coordonner les réponses de l'application aux demandes faites par les utilisateurs. Si un utilisateur clique sur un bouton qui pointe vers une URL spécifique, cette demande est mappée à une action du contrôleur (c'est à dire une méthode dans la classe contrôleur) qui a alors la responsabilité de gérer tous les traitements nécessaires pour répondre à cette demande et renvoyer une réponse à l'utilisateur, généralement sous la forme d'une nouvelle Vue ou d'une mise à jour de la vue en cours.
Après avoir créé une nouvelle application MVC dans Visual Studio puis supprimé les vues et les contrôleurs créés par défaut, la première chose que j'ai faite a été de copier les fichiers correspondant aux couches BO, BLL et DAL depuis l'application d'Imar dans le dossier Models de ma nouvelle application. J'ai également copié la base de données SQL Server du répertoire App_Data depuis l'application d'origine dans le dossier App_Data de l'application MVC puis j'ai fait de même pour le fichier Style.ccs que j'ai placé dans le dossier Content.

J'ai aussi changé quelques petits trucs. Il faut ajouter la chaine de connexion à la base de données dans le fichier Web.config de l'application MVC. Même si ce n'était pas vraiment indispensable, j'ai aussi modifié les namespaces au niveau des fichiers copiés puis un peu mis à jour quelques morceaux du code de la DAL pour utiliser des fonctionnalités du C# 3.0. Une fois tout ça fini, j'ai fait un rapide Ctrl + Maj + F5 pour vérifier que le projet compilait. Par la suite, je n'aurait pas à revenir sur ces fichiers, à part pour quelques méthodes de la DAL et les méthodes correspondantes dans la BLL, comme nous le verrons par la suite.
Les Contrôleurs
Ayant supprimé les contrôleurs créés par défaut par Visual Studio, j'ai ajouté quatre contrôleurs, un pour chaque entité. ce qui me donne donc au final les contrôleurs suivants : ContactController, PhoneController, AddressController et EmailController.

Chacun de ces contrôleurs sera chargé de coordoner 4 actions : List, Add, Edit et Delete. Par conséquent, la première chose à faire est de définir la route pour ces différentes actions au niveau du fichier Global.asax.cs :
public static void RegisterRoutes(RouteCollection routes)
{
routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
routes.MapRoute(
"Default",
"{controller}/{action}/{id}",
new { controller = "Contact", action = "List", id = "" }
);
}
La page par défaut de l'application sera chargée d'afficher la liste des contacts. Les données sur les contacts sont obtenues grâce à la méthode GetList() dans la classe ContactPersonManager de la BLL. Par conséquent, le code source pour l'action List() du contrôleur est le suivant :
public ActionResult List()
{
var model = ContactPersonManager.GetList();
return View(model);
}
Les vues fortement typées
Je vais utiliser des vues fortement typées tout au long de l'application. D'une part parce que cela permet de profiter de l'intellisense dans le code source des vues et d'autre part parce qu'elles ne dépendent pas du ViewData qui est source d'erreur à cause de l'utilisation de chaines pour indexer les différentes valeurs. Pour faire un peu de ménage, j'ai ajouté quelques namespaces de type ContactManagerMVC.Xxxxx dans la section <namespaces> du fichier Web.Config. Voici donc les namespaces que j'utilise par rapport à ceux définis dans le code d'Imar :
<namespaces> <add namespace="System.Web.Mvc"/> <add namespace="System.Web.Mvc.Ajax"/> <add namespace="System.Web.Mvc.Html"/> <add namespace="System.Web.Routing"/> <add namespace="System.Linq"/> <add namespace="System.Collections.Generic"/> <add namespace="ContactManagerMVC.Views.ViewModels"/> <add namespace="ContactManagerMVC.Models.BusinessObject"/> <add namespace="ContactManagerMVC.Models.BusinessObject.Collections"/> <add namespace="ContactManagerMVC.Models.BusinessLogic"/> <add namespace="ContactManagerMVC.Models.DataAccess"/> <add namespace="ContactManagerMVC.Models.Enums"/> </namespaces>
Cela a pour effet de les rendre disponibles dans toute l'application et d'éviter d'avoir à saisir les types complets à l'intérieur des vues. L'objet renvoyé par la méthode GetList() est de type ContactPersonList qui est défini dans le dossier Collections à l'intérieur de la couche BO. Il s'agit tout simplement d'une collection d'objets ContactPerson. La déclaration de page en première ligne de la vues List est la suivante :
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<ContactPersonList>" %>
Vous pouvez aussi voir que j'utilise une page maître. Dans celle-ci, j'ai fait référence au fichier css récupéré du code d'Imar. Le code html qui gère l'affichage des objets ContactPerson de la collection est le suivant :
<table class="table">
<tr>
<th scope="col">Id</th>
<th scope="col">Full Name</th>
<th scope="col">Date of Birth</th>
<th scope="col">Type</th>
<th scope="col"> </th>
<th scope="col"> </th>
<th scope="col"> </th>
<th scope="col"> </th>
<th scope="col"> </th>
</tr>
<%
if (Model != null)
{
foreach (var person in Model)
{%>
<tr>
<td><%= person.Id %></td>
<td><%= person.FullName %></td>
<td><%= person.DateOfBirth.ToString("d") %></td>
<td><%= person.Type %></td>
<td title="address/list" class="link">Addresses</td>
<td title="email/list" class="link">Email</td>
<td title="phone/list" class="link">Phone Numbers</td>
<td title="contact/edit" class="link">Edit</td>
<td title="contact/delete" class="link">Delete</td>
</tr>
<%
}
}else{%>
<tr>
<td colspan="9">No Contacts Yet</td>
</tr>
<% }%>
</table>
Vous pouvez constater au premier coup d'oeil l'avantage d'avoir des vues fortement typées. L'objet Model est du type ContactPersonList, ce qui fait que chaque élément est du type ContactPerson et que ses propriétés sont disponibles sans qu'il soit nécessaire de caster le Model en ContactPersonList. Si on avait dû faire un cast et qu'on ait fait une erreur, cette erreur de casting n'aurait été détectée qu'au moment de l'exécution, ce qui est pour le moins embêtant.
J'ai un petit peu triché pour le code html. J'aurai pu demander à générer un contenu de type "List" quand j'ai créé la vue, ce qui m'aurait permis d'avoir un squelette de page approprié pour les listes. Mais je ne l'ai pas fait. Je voulais obtenir quelque chose qui puisse fonctionner plus simplement avec la css d'Imar. J'ai donc lancé son application sur mon PC et une fois qu'elle s'est affichée dans mon navigateur, j'ai affiché la source et copié le html à partir de celle-ci. Imar utilise des GridViews dans son application webforms et par conséquent il y a pas mal de css et autre qui est automatiquement inséré dans le html généré. J'ai donc nettoyé un peu cette soupe et défini les styles correspondant à la table dans le fichier Site.css. J'en ai également profité pour ajouter un peu de css pour les éléments <th> et <td> comme vous le verrez si vous téléchargez mon application.
J'ai aussi ajouté des attributs "title" aux cellules de la table qui contiennent les liens vers les autres actions dans l'application d'origine. Je ne souhaitais pas que toute la page soit repostée pour afficher les adresses ou les numéros de téléphone ou lorsque l'utilisateur veut modifier ou supprimer un enregistrement existant, mais que le site utilise de l'Ajax pour tout cela. Ces attributs "title" vont jouer un rôle clé dans la façon dont je compte gérer Ajax dans mon application. Et pour finir, la classe css "link" me sert pour que le texte agisse comme un lien avec du souligné et un pointeur de type main lorsque la souris passe dessus.
Utiliser jQuery pour la partie Ajax
Avant de jeter un coup d'œil au gros morceau de script qui gère les fonctionnalités Ajax de l'application, voici trois lignes supplémentaires de html que j'ai ajouté en bas de la vue List :
<input type="button" id="addContact" name="addContact" value="Add Contact" /> <div id="details"></div> <div id="dialog" title="Confirmation Required">Are you sure about this?</div>
La première ligne affiche le bouton qui permet aux utilisateurs de créer un nouveau contact. La deuxième ligne contient une balise <div> vide qui sert de conteneur pour du contenu à venir. La troisième ligne est destinée à la boite de confirmation en jQuery qui servira pour demander aux utilisateurs de valider le fait qu'ils souhaitent supprimer un contact.
Pour que tout cela puisse fonctionner, il faut encore ajouter 3 fichiers dans la page maître : un pour insérer la librairie jQuery en elle-même et les deux autres pour certaines fonctionnalités de la librairie jQuery.UI (pour gérer une boite de dialogue modale et saisir des dates à l'aide d'un calendrier) :
<script src="../../Scripts/jquery-1.3.2.min.js" type="text/javascript"></script> <script src="../../Scripts/ui.core.min.js" type="text/javascript"></script> <script src="../../Scripts/jquery-ui.min.js" type="text/javascript"></script>
Voici ce que donne la vue List quand on y accède :

Et voici le source jQuery complet nécessaire pour gérer entièrement cette vue List :
<script type="text/javascript">
$(function() {
// row colours
$('tr:even').css('background-color', '#EFF3FB');
$('tr:odd').css('background-color', '#FFFFFF');
// selected row managment
$('tr').click(function() {
$('tr').each(function() {
$(this).removeClass('SelectedRowStyle');
});
$(this).addClass('SelectedRowStyle');
});
// hide the dialog div
$('#dialog').hide();
// set up ajax to prevent caching of results in IE
$.ajaxSetup({ cache: false });
// add an onclick handler to items with the "link" css class
$('.link').live('click', function(event) {
var id = $.trim($('td:first', $(this).parents('tr')).text());
var loc = $(this).attr('title');
// check to ensure the link is not a delete link
if (loc.lastIndexOf('delete') == -1) {
$.get(loc + '/' + id, function(data) {
$('#details').html(data);
});
// if it is, show the modal dialog
} else {
$('#dialog').dialog({
buttons: {
'Confirm': function() {
window.location.href = loc + '/' + id;
},
'Cancel': function() {
$(this).dialog('close');
}
}
});
$('#dialog').dialog('open');
}
});
// add an onclick event handler to the add contact button
$('#addContact').click(function() {
$.get('Contact/Add', function(data) {
$('#details').html(data);
});
});
});
</script>
Cela peut paraitre un peu compliqué, mais comme pour tout avec jQuery c'est en fait assez simple. J'ai séparé du mieux possible les différentes parties du code à l'aide de commentaires pour essayer de faciliter sa compréhension :
- La première chose que ce script effectue, c'est de remplacer la fonctionnalité AlternatingRowColor qui existe au niveau des contrôles serveurs quand on utilise les webforms. Au lieu de cela, le script applique un style css aux lignes de la table une fois que la table a été affichée.
- Puis j'ai ajouté un peu de code pour mettre en évidence la ligne en cours de sélection, comme Imar l'avait fait dans l'application d'origine.
- La <div> créée pour gérer le message de confirmation de la boite de dialogue est ensuite masquée pour qu'elle ne s'affiche pas sous la liste
- La ligne
$.ajaxSetup({ cache: false });sert à éviter que IE mette en cache les informations. Si vous ne faites pas cela, vous allez vous creuser les méninges en vous demandant pourquoi vos mises à jours et vos suppressions ne sont pas répercutées à l'affichage alors qu'elles ont bien été prises en compte dans la base de données.
Le dernier gros bloc de code est assez intéressant. Il utilise la méthode .live() qui fait en sorte que les gestionnaires d'évènements sont bien attachés à tous les éléments correspondants, qu'ils existent déjà à cet instant précis ou pas encore. Par exemple, lorsque l'utilisateur clique sur le lien "Adresses", le résultat est l'affichage d'une autre table qui liste les numéros de téléphone correspondant :

Vous pouvez voir que cette table contient des liens "Edit" et "Delete". Si je n'avais pas utilisé la méthode .live(), les gestionnaires d'évènements n'auraient pas été attachés à ces deux liens. Le gestionnaire d'évènement est attaché aux cellules ayant la classe css "link". Il commence par récupérer la valeur de l'identifiant pour l'enregistrement. Dans le cas de la table des contacts, il s'agira du champ ContactPersonId (puisque c'est ce que contient la première cellule de la ligne). Pour les sous-listes, il s'agira de l'identifiant du numéro de téléphone ou de l'adresse email. Nous avons besoin de ces identifiants pour les faire passer aux actions du contrôleur chargées de gérer la modification, la suppression ou l'affichage des sous-listes. Nous verrons cela un peu plus loin.
Vous devez maintenant commencer à comprendre pourquoi j'ai ajouté des attributs "title" aux cellules de la table. Ils contiennent la route qu'il faut appeler après avoir construit l'url complète en lui ajoutant l'identifiant. Ensuite, j'effectue un contrôle pour voir si la route contient le mot "delete". Si ce n'est pas le cas, la requête est lancée et son résultat affiché dans la balise <div> masquée. S'il s'agit d'un lien pour supprimer un enregistrement, la boite de confirmation modale est affichée avant de supprimer l'enregistrement, ce qui donne ainsi la possibilité à l'utilisateur de changer d'avis.
Et pour finir, j'attache un gestionnaire d'évènement au clic sur le bouton "Add Contact". Mais nous verrons ça dans la partie suivante.
Vous voyez ? Je vous avais bien dit que c'était simple !
Ajouter un contact
Quand on ajoute des enregistrements dans une application ASP.NET, la pratique habituelle est de proposer à l'utilisateur un formulaire contenant une série de zones de saisies dans lesquelles il peut entrer des données. La plupart des zones de saisie pour un objet ContactPerson sont basiques : nom, prénom, date de naissance. Mais une d'entre elle n'est pas si évidente : le type. Cette valeur doit être obtenue à partir de l'énumération (ami, collègue, etc...) définie au niveau de la classe PersonType.cs dans le dossier Models\Enums. Nous devons donc faire en sorte de proposer à l'utilisateur un choix restreint de valeurs possibles. Pour cela, une DropDownList fera parfaitement l'affaire. Cependant, cette série de valeurs ne fait pas parti de l'objet ContactPerson existant. Il va donc falloir présenter une version personnalisée de cet objet ContactPerson à la vue pour pouvoir gérer cela. Et c'est là que les ViewModels personnalisés entrent en jeu.
J'ai vu qu'il y avait débat pour savoir où est-ce qu'il fallait placer les ViewModels dans l'application. Certains pensent qu'ils font parti du modèle. A mon avis, ils sont plutôt liés aux vues. Ils n'ont vraiment de sens que dans le cadre des applications MVC et ils ne sont pas vraiment réutilisables, et donc ils ne devraient pas se retrouver dans le modèle. C'est pourquoi j'ai choisi de créer un répertoire ViewModels à l'intérieur du répertoire Views. Puis j'ai créé le fichier ContactPersonViewModel.cs en saisissant le code source suivant :
using System;
using System.Collections.Generic;
using System.Web.Mvc;
namespace ContactManagerMVC.Views.ViewModels
{
public class ContactPersonViewModel
{
public int Id { get; set; }
public string FirstName { get; set; }
public string MiddleName { get; set; }
public string LastName { get; set; }
public DateTime DateOfBirth { get; set; }
public IEnumerable<SelectListItem> Type { get; set; }
}
}
Si vous regardez la dernière propriété de cette classe, vous pouvez constater que j'ai défini Type comme étant une collection de IEnumerable<SelectListItem>. C'est ce qui sera rattaché à la liste déroulante au niveau de la vue.
Au niveau du contrôleur, il y a besoin de deux actions pour gérer l'ajout. La première action est décoré avec l'attribut [AcceptVerbs(HttpVerbs.Get)] et la seconde avec l'attribut [AcceptVerbs(HttpVerbs.Post)]. Le but de la première action est de renvoyer le formulaire destiné à saisir le contact alors que la seconde doit gérer les valeurs qui sont envoyées lorsque le formulaire est posté :
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Add()
{
var personTypes = Enum.GetValues(typeof (PersonType))
.Cast<PersonType>()
.Select(p => new
{
ID = p, Name = p.ToString()
});
var model = new ContactPersonViewModel
{
Type = new SelectList(personTypes, "ID", "Name")
};
return PartialView(model);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Add(ContactPerson person)
{
ContactPersonManager.Save(person);
return RedirectToAction("List");
}
Les premières lignes dans la première action sont chargées de transférer les valeurs de l'énumération ContactType dans un tableau, chaque élément étant ensuite casté en objet anonyme avec une propriété ID et une propriété Name. ID est la valeur d'énumération, et Name la valeur constante qui va de pair avec l'énumération. Un objet ContactPersonViewModel est ensuite instancié et sa propriété Type est initialisé avec un objet SelectList auquel on transmet un IEnumerable des objets anonymes obtenus précédemment et on indique quels champs utiliser pour la valeur et pour le libellé.
Pour ajouter un contact, j'ai créé une vue partielle fortement typée et choisi le type ContactPersonViewModel. Le code pour la vue partielle est le suivant :
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ContactPersonViewModel>" %>
<script type="text/javascript">
$(function() {
$('#DateOfBirth').datepicker({ dateFormat: 'yy/mm/dd' });
});
</script>
<% using (Html.BeginForm("Add", "Contact", FormMethod.Post)) {%>
<table>
<tr>
<td class="LabelCell">Name</td>
<td><%= Html.TextBox("FirstName") %></td>
</tr>
<tr>
<td class="LabelCell">Middle Name</td>
<td><%= Html.TextBox("MiddleName") %></td>
</tr>
<tr>v
<td class="LabelCell">Last Name</td>
<td><%= Html.TextBox("LastName") %></td>
</tr>
<tr>
<td class="LabelCell">Date of Birth</td>
<td><%= Html.TextBox("DateOfBirth", String.Empty)%></td>
</tr>
<tr>
<td class="LabelCell">Type</td>
<td><%=Html.DropDownList("Type")%>
</td>
</tr>
<tr>
<td class="LabelCell"></td>
<td><input type="submit" name="submit" id="submit" value="Save" /></td>
</tr>
</table>
<% } %>
Le jQuery au début de cette vue sert à associer un datepicker jQuery.UI à la zone de saisie DateOfBirth. Au niveau du html helper qui défini la zone de saisie DateOfBirth, le second paramètre nous assure que par défaut, sa valeur sera vide. Et pour le reste, toutes les zone de saisie prennent le même nom que la propriété correspondante de ContactPerson que nous voulons saisir. Cela nous permet d'être sûr qu'au moment du post, la correspondance entre les zones de saisie et le modèle fonctionnera de façon correcte. L'enum pour le ContactType est lui aussi automatiquement lié pour nous par MVC :

La méthode action qui répond à la requête POST est alors capable de faire correspondre les valeurs du formulaire aux propriété d'un objet ContactPerson avant d'appeler la méthode Save() de la BLL en trois fois rien de code avant de renvoyer l'utilisateur vers l'action List.
Modifier un contact
Cette fois encore, il existe deux actions dans le contrôleur pour gérer les modifications : une pour la requête GET initiale et une autre pour la requête POST lorsque le formulaire est validé :
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Edit(int id)
{
var personTypes = Enum.GetValues(typeof (PersonType))
.Cast<PersonType>()
.Select(p => new { ID = p, Name = p.ToString() });
var contactPerson = ContactPersonManager.GetItem(id);
var model = new ContactPersonViewModel
{
Id = id,
FirstName = contactPerson.FirstName,
MiddleName = contactPerson.MiddleName,
LastName = contactPerson.LastName,
DateOfBirth = contactPerson.DateOfBirth,
Type = new SelectList(personTypes, "ID", "Name", contactPerson.Type)
};
return PartialView(model);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(ContactPerson person)
{
ContactPersonManager.Save(person);
return RedirectToAction("List");
}
Nous avons déjà vu comment jQuery est utilisé pour appeler l'action Edit en lui faisant passer l'ID du contact à modifier. Cet identifiant est utilisé pour retrouver les informations sur ce contact dans la base de données en passant comme d'habitude par la BLL qui fait ensuite appel à la DAL. L'objet ContactPersonViewModel est construit à partir des informations du contact et complété par une SelectList comme pour l'action Add. Mais cette fois, le constructeur de SelectList utilise un quatrième paramètre pour définir la valeur actuelle de la propriété Type pour le contact à modifier. Ce dernier paramètre correspond à la valeur qui sera pré-sélectionné dans la liste déroulante.
Le code de la vue partielle Edit est lui aussi quasiment identique à celui de la vue Add :
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<ContactPersonViewModel>" %>
<script type="text/javascript">
$(function() {
$('#DateOfBirth').datepicker({dateFormat: 'yy/mm/dd'});
});
</script>
<% using (Html.BeginForm("Edit", "Contact", FormMethod.Post)) {%>
<table>
<tr>
<td class="LabelCell">Name</td>
<td><%= Html.TextBox("FirstName") %></td>
</tr>
<tr>
<td class="LabelCell">Middle Name</td>
<td><%= Html.TextBox("MiddleName") %></td>
</tr>
<tr>
<td class="LabelCell">Last Name</td>
<td><%= Html.TextBox("LastName") %></td>
</tr>
<tr>
<td class="LabelCell">Date of Birth</td>
<td><%= Html.TextBox("DateOfBirth", Model.DateOfBirth.ToString("yyyy/MM/dd")) %></td>
</tr>
<tr>
<td class="LabelCell">Type</td>
<td><%= Html.DropDownList("Type")%></td>
</tr>
<tr>
<td class="LabelCell"><%= Html.Hidden("Id") %></td>
<td><input type="submit" name="submit" id="submit" value="Save" /></td>
</tr>
</table>
<% } %>
Les principales différences viennent du fait que la zone DateOfBirth contient une chaîne pour formater la date et l'afficher de façon plus conviviale. Et juste avant le bouton de validation du formulaire, il y a maintenant un helper Html.Hidden() qui sert pour conserver l'identifiant du contact en cours de modification. Et bien sûr, le post du formulaire pointe vers une action différente du contrôleur. Il y aurait sans doute des avantages à combiner les formulaires (et les actions) Add et Edit et à se baser sur un drapeau pour savoir dans quel mode on se trouve (Add ou Edit) afin de pouvoir présenter tel ou tel code html au niveau de la vue et effectuer tel ou tel traitement au niveau de l'action. Cela permettrait d'éviter pas mal de répétitions. Mais j'ai préféré les garder séparés pour que l'application de démonstration reste le plus claire possible.
Supprimer un contact
L'action de suppression est assez simple et elle n'a pas besoin d'avoir une vue correspondante. Une fois la suppression réalisée, elle se contente de rediriger l'utilisateur sur l'action List :
public ActionResult Delete(int id)
{
ContactPersonManager.Delete(id);
return RedirectToAction("List");
}
J'ai fait quelques modifications par rapport au code d'origine des couches BLL et DAL. Au départ, la méthode ContactPersonManager.Delete() utilisait une instance du contact à supprimer. Et au niveau de la méthode Delete() dans la DAL, il n'y avait que l'identifiant de ce contact qui était utilisé (et donc nécessaire). Comme je ne vois pas l'intérêt de passer un objet complet quand on n'a besoin que de son identifiant, j'ai modifié ces deux méthodes pour qu'elles fonctionnent avec un entier. L'autre avantage, c'est que cela simplifie le code puisqu'avant il fallait instancier un objet ContactPerson juste pour pouvoir le supprimer.
Quand on clique sur un lien "Delete", le code jQuery déclenche l'affichage de la boite de dialogue pour confirmer la suppression.

Si l'utilisateur clique sur le bouton "Cancel", rien ne se passe (à part la fermeture de la boite de dialogue). Par contre, s'il clique sur le bouton "Confirm", l'url qui a été construite par le jQuery est appelée afin de pointer sur l'action Delete du contrôleur.
Gérer les collections
Toutes les collections (PhoneNumberList, EmailAddressList et AddressList) sont gérées exactement de la même manière. Par conséquence, je n'ai qu'à en choisir une (EmailAddressList en l'occurrence) pour illustrer leur fonctionnement. Vous pourrez aussi consulter les sources de l'application pour voir exactement comment les autres fonctionnent.
Pour commencer, nous allons voir comment afficher les adresses email associées au contact sélectionné. Pour cela, il y a besoin d'avoir une action List au niveau du contrôleur :
public ActionResult List(int id)
{
var model = new EmailAddressListViewModel
{
EmailAddresses = EmailAddressManager.GetList(id),
ContactPersonId = id
};
return PartialView(model);
}
Cette méthode action prend l'identifiant du contact (souvenez-vous, il est obtenu à partir de la première cellule dans la ligne qui a été cliquée) puis renvoie un autre ViewModel personnalisé (EmailAddressListViewModel). C'est grâce à ça que l'identifiant du contact peut être transmis à la vue :
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<EmailAddressListViewModel>" %>
<script type="text/javascript">
$(function() {
$('#add').click(function() {
$.get('Email/Add/<%= Model.ContactPersonId %>', function(data) {
$('#details').html(data);
});
});
});
</script>
<table class="table">
<tr>
<th scope="col">Contact Person Id</th>
<th scope="col">Email</th>
<th scope="col">Type</th>
<th scope="col"> </th>
<th scope="col"> </th>
</tr>
<%if(Model.EmailAddresses != null)
{foreach (var email in Model.EmailAddresses) {%>
<tr>
<td><%= email.Id %></td>
<td><%= email.Email %></td>
<td><%= email.Type %></td>
<td title="email/edit" class="link">Edit</td>
<td title="email/delete" class="link">Delete</td>
</tr>
<%}
}else
{%>
<tr>
<td colspan="9">No email addresses for this contact</td>
</tr>
<%}%>
</table>
<input type="button" name="add" value="Add Email" id="add" />
Vous pouvez voir que la propriété ContactPersonId est nécessaire pour utiliser la méthode d'action Add. Nous devons être certain que nous allons ajouter le nouvel élément à la collection en le reliant au bon contact. En ce qui concerne les méthodes d'action Edit et Delete, elles fonctionnent exactement de la même façon que pour l'objet ContactPerson : l'identifiant de l'élément qui doit être mis à jour ou supprimé est passé via l'URL et les cellules de la table sont complétées par un attribut "title" ce qui leur permet d'être prises en compte par la méthode jQuery .live() qu'on avait mis en place au niveau de la vue List pour les contacts.
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Add(int id)
{
var contactTypes = Enum.GetValues(typeof(ContactType))
.Cast<ContactType>()
.Select(c => new
{
Id = c,
Name = c.ToString()
});
var model = new EmailAddressViewModel
{
ContactPersonId = id,
Type = new SelectList(contactTypes, "ID", "Name")
};
return PartialView("Add", model);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Add(EmailAddress emailAddress)
{
emailAddress.Id = -1;
EmailAddressManager.Save(emailAddress);
return RedirectToAction("List", new {id = emailAddress.ContactPersonId});
}
Une ViewModel personnalisée a été créé exprès pour afficher les objets EmailAddress existants dans le but de les modifier ou d'en ajouter. Celle-ci possède le même type de propriété pour associer la collection IEnumerable<SelectListItem> à la liste déroulante des types de contacts. Ces méthodes se distinguent de leur équivalent dans ContactController par ce qu'elles renvoient. La première renvoie le formulaire Add sous forme de vue partielle alors que la seconde redirige vers l'action List du contrôleur, ce qui a pour effet de rafraichir l'affichage de la collection une fois qu'elle a été mise à jour (et c'est aussi la raison pour laquelle nous avions spécifié l'option "cache" à faux dans le script jQuery).
En ce qui concerne l'enregistrement d'un élément de la collection, nous devons définir sa propriété Id à -1 avant d'effectuer le Save. Ceci est nécessaire pour que la procédure stockée "Upsert" soit correctement capable de déterminer s'il faut ajouter ou modifier l'élément. En effet, la propriété Id de l'élément est initialisée par défaut par le système de routage ce qui fait que dans la pratique elle va contenir la valeur de ContactPerson.Id. Par conséquent, si nous ne l'initialisions pas explicitement à -1, la procédure "Upsert" ne chercherait pas à créer un nouvel enregistrement pour l'objet EmailAddress, mais plutôt à modifier l'élément EmailAddress dont l'identifiant serait en fait l'identifiant du contact en cours. Ceci est dû au fait qu'Imar a choisi d'utiliser une approche de type "Upsert" pour modifier (UPdate) et ajouter (inSERT) des données avec une seule procédure.
Quoiqu'il en soit, voici le code de la vue partielle destinée à ajouter une adresse email :
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<EmailAddressViewModel>" %>
<script type="text/javascript">
$(function() {
$('#save').click(function() {
$.ajax({
type: "POST",
url: $("#AddEmail").attr('action'),
data: $("#AddEmail").serialize(),
dataType: "text/plain",
success: function(response) {
$("#details").html(response);
}
});
});
});
</script>
<% using(Html.BeginForm("Add", "Email", FormMethod.Post, new { id = "AddEmail" })) {%>
<table class="table">
<tr>
<td>Email:</td>
<td><%= Html.TextBox("Email")%></td>
</tr>
<tr>
<td>Type:</td>
<td><%= Html.DropDownList("Type") %></td>
</tr>
<tr>
<td><%= Html.Hidden("ContactPersonId") %></td>
<td><input type="button" name="save" id="save" value="Save" /></td>
</tr>
</table>
<% } %>
Dans ce cas particulier, le code jQuery est chargé d'envoyer le formulaire par Ajax. Si vous faites attention, vous pouvez constater que ce code est rattaché à un bouton html et pas à un input type="submit". Il va d'abord sérialiser le contenu des champs du formulaire puis faire une requête pour atteindre l'action Add() décoré avec l'attribut [AcceptVerbs(HttpVerbs.Post)].
Modifier et supprimer des adresses email
Pour la modification des objets EmailAddress nous avons besoin du même genre d'actions et de vues que celles auxquelles nous avons eu à faire jusqu'à présent. Il faut ajouter deux actions au niveau du contrôleur, une pour le GET et l'autre pour le POST :
[AcceptVerbs(HttpVerbs.Get)]
public ActionResult Edit(int id)
{
var emailAddress = EmailAddressManager.GetItem(id);
var contactTypes = Enum.GetValues(typeof(ContactType))
.Cast<ContactType>()
.Select(c => new
{
Id = c,
Name = c.ToString()
});
var model = new EmailAddressViewModel
{
Type = new SelectList(contactTypes, "ID", "Name", emailAddress.Type),
Email = emailAddress.Email,
ContactPersonId = emailAddress.ContactPersonId,
Id = emailAddress.Id
};
return View(model);
}
[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Edit(EmailAddress emailAddress)
{
EmailAddressManager.Save(emailAddress);
return RedirectToAction("List", "Email", new { id = emailAddress.ContactPersonId });
}
Puis la vue partielle pour la modification qui doit commencer à devenir familière :
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<EmailAddressViewModel>" %>
<script type="text/javascript">
$(function() {
$('#save').click(function() {
$.ajax({
type: "POST",
url: $("#EditEmail").attr('action'),
data: $("#EditEmail").serialize(),
dataType: "text/plain",
success: function(response) {
$("#details").html(response);
}
});
});
});
</script>
<% using(Html.BeginForm("Edit", "Email", FormMethod.Post, new { id = "EditEmail" })) {%>
<table class="table">
<tr>
<td>Email:</td>
<td><%= Html.TextBox("Email")%></td>
</tr>
<tr>
<td>Type:</td>
<td><%= Html.DropDownList("Type") %></td>
</tr>
<tr>
<td><%= Html.Hidden("ContactPersonId") %><%= Html.Hidden("Id") %></td>
<td><input type="button" name="save" id="save" value="Save" /></td>
</tr>
</table>
<% } %>
Une fois encore, c'est presque le même chose que pour la vue Add, si ce n'est la présence d'un champ caché pour stocker la valeur EmailAddress.Id afin de pouvoir ensuite mettre à jour la bonne adresse email.
En ce qui concerne l'action Delete, il n'y a pas vraiment besoin d'explications supplémentaires :
public ActionResult Delete(int id)
{
EmailAddressManager.Delete(id);
return RedirectToAction("List", "Contact");
}
Conclusion
Le but de ce petit exercice était de démontrer qu'il est parfaitement possible de réaliser des applications MVC sans recourir à LINQ to SQL ou Entity Framework. Je suis parti d'une application de type Web Forms déjà développée en ASP.NET 2.0. Le fait qu'elle soit dès le départ très bien structurée en couches m'a permis de réutiliser ses couches Business Objects, Business Logic et Data access avec peu ou pas de modification. La couche DAL d'accès aux données DAL utilise toujours ADO.NET et fait appel à des procédures stockées de base de données SQL Server.
Tout au long de cet exercice, j'ai présenté comment utiliser des vues fortement typées basées sur des ViewModels personnalisés et comment une petite dose de jQuery peut grandement améliorer l'interface utilisateur. Bien entendu, cette application n'est en aucun cas l'application idéale et elle est loin d'être prête pour une utilisation réelle. Il resterait beaucoup de choses à faire pour l'améliorer, comme par exemple refactoriser les vues et les actions liées aux opérations d'ajout et de modification. D'autre part, elle est totalement dépourvue de toute forme de validation. Les suppressions renvoient toujours à la page d'accueil, alors qu'il serait plus pratique pour l'utilisateur qu'après avoir supprimé un téléphone, un email ou une adresse on ré-affiche directement la sous-liste correspondante mise à jour. Cela impliquerait de faire passer l'identifiant ContactPersonId à l'action Delete() et devrait être relativement facile à réaliser.
Blogmarks du 02/03/2010
Par Michel le mercredi 3 mars 2010, 07:24
-
Less Framework : An HTML5-powered CSS framework for building smart website layouts for varying screen widths. Intéressant.
vendredi 26 février 2010
Blogmarks du 25/02/2010
Par Michel le vendredi 26 février 2010, 09:26
-
Mercurial integration with Visual Studio : Présentation de Mercurial VSS qui permet d'intégrer Mercurial dans Visual Studio 2008 ou 2010
jeudi 25 février 2010
Blogmarks du 24/02/2010
Par Michel le jeudi 25 février 2010, 08:10
-
Hg Init: a Mercurial tutorial : Un tutoriel en 6 parties par Joel Spolsky destiné à nous enseigner les éléments clés de Mercurial pour se guérir de Subversion, connaitre les bases de Mercurial, fonctionner en équipe, réparer les bêtises, fusionner les différentes évolutions et organiser les dépôts Mercurial.
-
Guide de programmation C# : Documentation en français sur Visual Studio 2008 : les fonctionnalités du langage C# et les fonctions accessibles en C# via le .NET Framework.
mercredi 24 février 2010
Blogmarks du 23/02/2010
Par Michel le mercredi 24 février 2010, 12:01
-
LINQ Audit Trail : Quelques méthodes d'extensions pour auditer automatiquement les requêtes LINQ to SQL et savoir quelles sont les tables affectées, les champs mis à jour (ancienne et nouvelle valeur), qui a fait la modification...
mardi 23 février 2010
Convertir SQL en LINQ : FROM et SELECT
Par Michel le mardi 23 février 2010, 22:51 - Traductions
Ceci est une traduction assez libre de la série de billets rédigés par Bill Horst pour apprendre comment passer du langage SQL au langage LINQ et plus spécialement en VB LINQ. Pour ma part, j’ai adapté les explications et les exemples de codes pour cibler la syntaxe C# du langage LINQ.
Après un premier billet pour comparer SQL et LINQ, ce deuxième billet de la série va présenter comment gérer des clauses plus spécifiques du SQL, en commençant par deux des clauses les plus importantes : FROM et SELECT.
La clause FROM
Une commande SQL SELECT débute toujours par une clause SELECT suivie d’une clause FROM. Dans le cas de LINQ, une expression commence toujours par une clause « from » (ou « aggregate » comme nous le verrons par la suite). La clause SQL FROM de base indique la table sur laquelle va porter la requête, et de façon similaire, la clause LINQ from indique l’objet sur lequel va porter la requête (ClientListe dans notre exemple).
Cet objet peut représenter des données en mémoire, des données d’une table SQL ou des données XML. Dans les exemples à venir, j’utilise des données « en mémoire » pour que le code soit suffisamment simple. En plus de cet objet sur lequel porte la requête, la clause LINQ from contient toujours un identifiant pour la « ligne » courante (Contact dans notre exemple) qui sert d’alias pour représenter cette ligne.
Avec SQL, il suffit de faire « SELECT * » pour sélectionner toutes les colonnes de la table. En ce qui concerne LINQ, il suffit d’utiliser la clause « select alias » pour renvoyer tous les membres de l’objet requêté :
SQL :
SELECT * FROM ClientTable
LINQ :
from Contact in ClientListe select Contact
Utiliser un alias dans le FROM
En SQL, il est possible de définir un alias sur une table au niveau de la partie FROM de la requête. Cela permet ensuite de faire référence aux colonnes de la table source par l’intermédiaire de cet alias. Comme nous l’avons vu dans le paragraphe précédent, l’identifiant qui est défini au niveau de la clause from en LINQ sert lui aussi essentiellement à cela :
SQL :
SELECT Contact.ClientID, Contact.Telephone FROM ClientTable Contact
LINQ :
from Contact in ClientListe
select new { Contact.ClientID, Contact.Telephone }
La clause SELECT
En SQL, les commandes SELECT contiennent une liste des valeurs à sélectionner parmi les informations disponibles (Nom, Telephone, Ville…). De la même façon, le langage LINQ vous permet lui aussi de sélectionner certains des membres pour renvoyer un objet anonyme constitué à partir de cette sélection.
Il n’est absolument pas nécessaire que les membres que vous définissez correspondent à l’objet que vous avez spécifié au niveau de la clause from. Il peut en effet s’agir de n’importe quelle expression C# valide (comme par exemple « 3 + 4 »). S’il n’est pas possible de déduire le nom d’un membre que vous avez sélectionné, vous devrez utiliser un alias (voir le paragraphe suivant).
SQL :
SELECT Nom, ClientID FROM ClientTable Contact
LINQ :
from Contact in ClientListe
select new { Contact.Nom, Contact.ClientID }
Utiliser un alias dans le SELECT
Toujours en SQL, il est possible de définir des alias pour les différents éléments que vous sélectionnez au niveau de la clause SELECT, ce qui permet ensuite d’utiliser ces alias pour faire référence à ces éléments. De la même façon, LINQ vous permet de donner un nom aux éléments que vous sélectionnez, ce qui vous permettra par la suite d’utiliser ce nom faire référence à ces éléments lorsque vous exploiterez le résultat de la requête LINQ.
SQL :
SELECT Nom NomContact, ClientID ContactID FROM ClientTable Contact
LINQ :
from Contact in ClientListe
select new { NomContact = Contact.Nom, ContactID = Contact.ClientID }
Dans le prochain billet, je présenterai les équivalents des clauses DISTINCT, WHERE et ORDER BY pour le langage LINQ.
lundi 22 février 2010
Convertir SQL en LINQ : Introduction
Par Michel le lundi 22 février 2010, 19:32 - Traductions
Ceci est une traduction assez libre de la série de billets rédigés par Bill Horst pour apprendre comment passer du langage SQL au langage LINQ et plus spécialement en VB LINQ. Pour ma part, j’ai adapté les explications et les exemples de codes pour cibler la syntaxe C# du langage LINQ.
Comme vous le savez sans doute déjà, les commandes LINQ permettent d’écrire des requêtes en C# en utilisant une syntaxe inspirée de SQL. Cependant, la syntaxe de LINQ ne correspond pas exactement à la syntaxe du SQL. Par conséquent, si vous avez déjà travaillé en SQL ou que vous êtes habitués à écrire des requêtes SQL, vous allez sans doute chercher à transformer vos requêtes SQL en requêtes LINQ.
Ce billet va être le premier d’une série consacrée à la façon de traduire du SQL en LINQ. Dans cette introduction, je souhaite présenter ce qui différencie LINQ de SQL puis j’aborderai les particularités de chaque syntaxe dans la suite de la série.
Pré-requis
Les exemples de code SQL que je vais utiliser seront basés sur une table ClientTable et une table CommandeTable. Pour les exemples C#, je m’appuierai sur des objets ClientListe et les objets CommandeListe, tous deux de type IEnumerable. Et j’utiliserai deux classes, Client et Commande qui sont définies de la façon suivante :
class Client
{
public int ClientID;
public string Nom;
public string Telephone;
public string Adresse;
public string Ville;
public string CodePostal;
public string Region;
}
class Commande
{
public int CommandeID;
public int ClientID;
public Single Cout;
public string Telephone;
public DateTime DateCommande;
public string Livraison;
public string NomArticle;
}
Syntaxe de base
LINQ gère l’équivalent des commandes SQL SELECT, mais pas les autres types de commandes SQL telles que CREATE, INSERT, UPDATE ou DELETE. On peut considérer que la syntaxe de base d’une requête SQL SELECT est constituée d’une série de « clauses » débutant par une clause SELECT :
sqlSelectClause [ sqlClause1 [ sqlClause2 [ ... ] ] ]
Il peut exister un certain nombre de différences en fonction des nombreuses versions de SQL, mais on a toujours plus ou moins la même syntaxe générale :
SELECT Nom NomContact, ClientID ContactID FROM ClientTable ORDER BY Nom
La syntaxe de base pour une expression LINQ consiste également en une série de « clauses » qui débute par la clause « from » (ou éventuellement une clause « aggregate » comme nous le verrons plus tard) :
linqFromClause [ linqClause1 [ linqClause2 [ ... ] ] ]
Ce qui donne par exemple :
from Contact in ClientListe
orderby Contact.Nom
select new { NomContact = Contact.Nom, ContactID = Contact.ClientID }
J’ai employé le terme « expression LINQ » ci-dessus parce qu’à proprement parler les requêtes LINQ ne sont pas des commandes complètes. Alors qu’une requête SQL se suffit à elle-même, si on se place d’un point de vue syntaxique, une requête LINQ n’est que l’équivalent d’une expression telle que « 3 * 4 ». Cela ne constitue pas une commande complète et il faut donc « faire » quelque chose avec. Une requête LINQ peut apparaitre dans du code C# tel que celui-ci :
var ContactsTries = from Contact in ClientListe
orderby Contact.Nom
select new { NomContact = Contact.Nom, ContactID = Contact.ClientID };
Chaque clause d’une requête LINQ porte sur un objet de type IEnumerable<T> et renvoie un nouvel objet de type IEnumerable<T> (sans que le type T retourné soit nécessairement du même type que le T initial). En général, les clauses de la requête sont analogues aux clauses SQL (SELECT ou ORDER BY par exemple) et vous pouvez habituellement traduire votre requête SQL en LINQ en faisant du « clause à clause ». Dans les exemples ci-dessus, même si les clauses apparaissent dans un ordre légèrement différent, vous pouvez malgré tout voir qu’elles sont assez semblables.
Conclusion
Après cette introduction assez généraliste, j’aborderai des aspects plus spécifiques au cours des billets suivants. Mon objectif tout au long de ces petits tutoriels consacrés à la traduction du SQL en LINQ est de présenter les points suivants :
- FROM et SELECT
- DISTINCT, WHERE et ORDER BY
- Les fonctions (scalaires et agrégations)
- GROUP BY et HAVING
- Les jointures
- UNION, TOP et sous-requêtes
Blogmarks du 19/02/2010
Par Michel le lundi 22 février 2010, 10:47
-
Linksgrid : Un portail Web 2.0 open-source développé avec Asp.Net MVC 1.0, Microsoft Ajax et jQuery
-
MSDN Magazine Digital Downloads : Les archives des revues MSDN Magazine téléchargeables au format PDF depuis janvier 2003.
vendredi 19 février 2010
Afficher le SQL généré par NHibernate
Par Michel le vendredi 19 février 2010, 08:06
Problème
Quand on débute avec un ORM (et avec NHibernate en l'occurrence), il n'est pas très très simple de comprendre ce qui va se passer au niveau de la base de données quand on fait tel ou tel truc avec NHibernate et encore moins quand on s'essaie à faire des requêtes LINQ to NHibernate. Pour quelqu'un qui comme moi écrit du SQL depuis des années, l'idéal ce serait donc d'arriver à voir de mes propres yeux le code SQL que fabrique NHibernate.
Au début, j'ai donc eu la réaction classique et je me suis demandé pourquoi diable je ne parvenais pas à voir le source des requêtes SQL générées par NHibernate alors que j'avais pourtant bien initialisé la propriété "show_sql" à "true" :
<property name="show_sql">true</property>
Puis j'ai finalement remarqué que ça c'était bon pour quand je faisais des tests unitaires ou de petits essais sous forme d'applications consoles, mais que ça n'était pas prévu pour les applications web (et même pas du tout puisqu'il n'y avait rien).
Qu'à cela ne tienne, la réponse est dans Google. Mais là, le problème c'est qu'il y a beaucoup de réponses. Trop même. Et la plupart du temps il y a surtout tout un tas d'explications sur le fonctionnement de NHibernate et de log4net, la façon dont NHibernate utilise log4net, les différentes manipulations et configurations à faire pour qu'ils travaillent ensemble dans de bonnes conditions, comment sélectionner ce qu'on veut loguer...
Enfin beaucoup trop de trucs à assimiler quand comme moi on veut juste savoir à quoi ressemble le code SQL généré par NHibernate et qu'on n'a pas le temps et les moyens de s'intéresser aux coulisses.
Mais comme je suis persistant persévérant, j'ai quand même réussi
à trouver la une solution et même qu'après un ou deux essais j'ai
réussi à isoler ce qu'il était nécessaire et indispensable de faire pour que
cela fonctionne de façon satisfaisante :
- il faut bien utiliser log4net
- il n'y a pas énormément de trucs à faire
Solution
La première phase pour utiliser log4net nécessite de le configurer au niveau
du fichier Web.config. Il faut commencer par ajouter la ligne suivante à
l'intérieur de la section <configSections> afin de déclarer
le nom de la section de configuration qui sera dédiée à log4net :
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
Ensuite, il faut ajouter cette section <log4net> (juste
après la section <configSections>) dans le Web.config pour
configurer réellement log4net. Comme pour l'instant mon seul objectif est de
pouvoir afficher les requêtes SQL au fur et à mesure que NHibernate les génère,
je peux me contenter d'insérer les quelques lignes suivantes :
<log4net>
<appender name="TraceAppender" type="log4net.Appender.TraceAppender">
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%message%newline" />
</layout>
</appender>
<logger name="NHibernate.SQL" additivity="false">
<level value="ALL"/>
<appender-ref ref="TraceAppender" />
</logger>
</log4net>
En gros, ça sert à dire que NHibernate doit loguer les requêtes SQL qu'il génère et que log4net doit afficher ces logs dans la fenêtre de débugage de Visual Studio (c'est peut être pas exactement ça, mais le résultat sera bientôt là).
Une fois cette première phase de configuration terminée, il reste à activer log4net. Pour cela, il faut tout d'abord ajouter la librairie log4net.dll dans les références du projet. Ceci fait, il reste à "démarrer" log4net, ou plus exactement à lui indiquer qu'il doit se paramétrer en utilisant la configuration définie dans le fichier Web.config.
Pour ça, il suffit d'ajouter une ligne
log4net.Config.XmlConfigurator.Configure(); au niveau de la
procédure Application_Start dans le Global.axax.cs (et peut-être un using
log4net; en début de fichier) :
using log4net;
...
protected void Application_Start()
{
log4net.Config.XmlConfigurator.Configure();
RegisterRoutes(RouteTable.Routes);
}
Et c'est tout ! En fait, on n'a même pas besoin d'avoir initialisé la propriété "show_sql" à "true" pour que ça marche :

Pour aller plus loin
Trois liens intéressants parmi tant d'autres qui m'ont permis d'arriver à cette solution :
- Logging NHibernate SQL with log4net in ASP.NET explique comment enregistrer les requêtes SQL de NHibernate dans un fichier de logs : pas tout à fait ce que je cherche donc, mais malgré tout bien détaillé et suffisamment clair
- Displaying NHibernate SQL in Visual Studio’s Output Window fait quasiment ce que je veux mais ne n'explique pas tout ce qu'il faut faire pour que ça marche
- log4net Config Examples présente des exemples de configuration pour tous les types de sorties des logs : base de données, fichier, smtp...
jeudi 18 février 2010
Suite des essais LINQ to NHibernate
Par Michel le jeudi 18 février 2010, 08:55
Cette fois-ci je vais me contenter de refaire le même genre de tests que lors de mes premiers essais avec LINQ to NHibernate d'hier. Mais aujourd'hui, au lieu de partir d'un contact pour accéder au groupe auquel il est lié, je vais explorer l'autre côté de la relation entre les deux tables et partir d'un groupe puis accéder aux contacts qui lui sont rattachés.
Essai numéro 1
Je commence doucement avec une requête LINQ toute simple :
var linq1 = (from g in _session.Linq<Group>()
where g.Id == id
select g).FirstOrDefault();
Sans trop de surprise (maintenant que j'ai quelques essais au compteur), NHibernate génère la requête SQL suivante :
SELECT top 1
this_.Id as Id0_0_,
this_.Name as Name0_0_
FROM Groups this_
WHERE this_.Id = @p0; // @p0 = 1
Puis j'essaie d'accéder à la propriété Contacts de l'objet Group que vient de renvoyer la première requête LINQ :
int count = linq1.Contacts.Count();
NHibernate génère alors une seconde requête SQL afin de charger la liste des contacts associés au groupe :
SELECT contacts0_.groupId as groupId1_,
contacts0_.Id as Id1_,
contacts0_.Id as Id1_0_,
contacts0_.FirstName as FirstName1_0_,
contacts0_.LastName as LastName1_0_,
contacts0_.Phone as Phone1_0_,
contacts0_.Email as Email1_0_,
contacts0_.groupId as groupId1_0_
FROM Contacts contacts0_
WHERE contacts0_.groupId=@p0; // @p0 = 1
Jusqu'ici, c'est tout pareil par rapport aux résultats que que j'avais obtenus au cours de mon premier essai dans l'autre sens.
Essai numéro 2
Comme lors de mes premiers tests, ce deuxième essai va consister à utiliser la méthode Extend("Entité") pour vérifier que cela fonctionne à peu près de la même façon que pour aller du contact vers le groupe. Ce qui me donne donc la requête LINQ suivante :
var linq2 = (from g in _session.Linq<Group>().Expand("Contacts")
where g.Id == id
select g).FirstOrDefault();
Et comme je pouvais presque m'y attendre, cela a eu pour effet de faire apparaitre une jointure avec la table Contacts dans la requête SQL qui est générée par NHibernate :
SELECT top 1
this_.Id as Id0_1_,
this_.Name as Name0_1_,
contacts2_.groupId as groupId3_,
contacts2_.Id as Id3_,
contacts2_.Id as Id1_0_,
contacts2_.FirstName as FirstName1_0_,
contacts2_.LastName as LastName1_0_,
contacts2_.Phone as Phone1_0_,
contacts2_.Email as Email1_0_,
contacts2_.groupId as groupId1_0_
FROM Groups this_
left outer join Contacts contacts2_ on this_.Id=contacts2_.groupId
WHERE this_.Id = @p0; // @p0 = 1
Et comme hier, le fait d'accéder à la liste des contacts de l'objet Group renvoyé n'a pas d'impact et NHibernate ne génère pas une nouvelle requête SQL :
int count2 = linq2.Contacts.Count();
Jusqu'ici, tout va toujours bien puisque NHibernate se comporte toujours rigoureusement de la même façon dans les deux sens de la relation.
Interlude
Pour rester le plus synchro possible avec mon billet précédent, voici le fichier de mapping NHibernate que j'ai défini pour les groupes :
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="ContactManager.Models" assembly="ContactManager">
<class name="Group" table="Groups" dynamic-update="true">
<cache usage="read-write" />
<id name="Id" column="Id" type="integer">
<generator class="identity" />
</id>
<property name="Name" type="string" />
<bag
name="Contacts"
inverse="true"
lazy="true">
<key column="groupId" />
<one-to-many class="Contact" />
</bag>
</class>
</hibernate-mapping>
Comme dans le fichier Contact.hbm.xml d'hier, ce fichier de mapping spécifie
également la relation qui existe entre les tables Groups et Contacts. Par
rapport à la simple balise <many-to-one ... /> d'hier, il
faut utiliser un élément <bag ... /> pour représenter le
fait qu'à un groupe sont liés plusieurs contacts.
Essai numéro 3
Dans le fichier de mapping Group.hbm.xml, le fait que je fasse du
lazy-loading est pour l'instant défini de façon explicite grâce à l'attribut
lazy="false". Par conséquent, si je souhaite maintenant ne plus
faire de lazy-loading, il est donc nécessaire que je modifie le mapping
NHibernate de la façon suivante :
...
<bag
name="Contacts"
inverse="true"
lazy="false">
<key column="groupId" />
<one-to-many class="Contact" />
</bag>
...
Et désormais, si je reprend la même requête LINQ qu'au début de ce billet :
var linq3 = (from g in _session.Linq<Group>()
where g.Id == id
select g).FirstOrDefault();
Lorsque je l'exécute, je peux voir que NHibernate a cette fois-ci généré 2 requêtes SQL différentes. La première sert pour charger les informations du groupe et la seconde permet de charger les données correspondantes aux contacts qui sont rattachés au groupe :
SELECT top 1
this_.Id as Id0_0_,
this_.Name as Name0_0_
FROM Groups this_
WHERE this_.Id = @p0; // @p0 = 1
SELECT contacts0_.groupId as groupId1_,
contacts0_.Id as Id1_,
contacts0_.Id as Id1_0_,
contacts0_.FirstName as FirstName1_0_,
contacts0_.LastName as LastName1_0_,
contacts0_.Phone as Phone1_0_,
contacts0_.Email as Email1_0_,
contacts0_.groupId as groupId1_0_
FROM Contacts contacts0_
WHERE contacts0_.groupId=@p0; // @p0 = 1
Deuxième bilan
Ce qui est vraiment bien, c'est que ça fonctionne exactement de la même façon dans les deux sens ! Et donc normalement, une fois que je serai un peu mieux rodé à l'utilisation de NHibernate et de LINQ to NHibernate, je devrais grosso-modo réussir à prévoir ce qui va se passer sans trop de problème.
Par contre, j'ai encore pas mal de difficultés en ce qui concerne le
mapping. Pour l'instant je fais surtout du copié / collé de différents trucs
trouvés sur internet (dont le tutoriel Utiliser ASP.NET MVC et NHibernate) => je ne sais pas trop
(et je me demande) s'il est nécessaire / obligatoire / conseillé de définir la
relation dans les deux fichiers de mapping ou si on peut se contenter de faire
ça d'un seul côté (c'est à dire dans un seul fichier de mapping). C'est
d'ailleurs peut-être à ça que sert l'attribut
inverse="true" ? => Encore pas mal trucs à creuser pour
plus tard...
mercredi 17 février 2010
Premiers essais avec LINQ to NHibernate
Par Michel le mercredi 17 février 2010, 15:44
Etant donné que je débute avec NHibernate et LINQ to NHibernate, j'ai fait quelques essais pour comprendre ce qui se passe côté SQL quand je fais telle ou telle requête LINQ.
Essai numéro 1
Quand je fais la requête LINQ suivante :
var linq1 = (from c in _session.Linq<Contact>()
where c.Id == id
select c).FirstOrDefault();
NHibernate génère la requête SQL ci-dessous :
SELECT top 1
this_.Id as Id1_0_,
this_.FirstName as FirstName1_0_,
this_.LastName as LastName1_0_,
this_.Phone as Phone1_0_,
this_.Email as Email1_0_,
this_.groupId as groupId1_0_
FROM Contacts this_
WHERE this_.Id = @p0; // @p0 = 13
Ensuite, lorsque j'accède à une propriété liée de l'objet Contact qui a été retourné par la requête LINQ :
string NomGroupe = linq1.Group.Name;
Alors NHibernate va générer une seconde requête SQL pour charger l'objet Group associé au contact :
SELECT group0_.Id as Id0_0_,
group0_.Name as Name0_0_
FROM Groups group0_
WHERE group0_.Id=@p0; // @p0 = 1
Essai numéro 2
Après quelques recherches, j'ai trouvé qu'il était possible de reproduire la méthode Include("Entité") de ADO.NET Entity Framework en utilisant la méthode Expand(). Cela me permet donc d'écrire ma requête LINQ de la façon suivante :
var linq2 = (from c in _session.Linq<Contact>().Expand("Group")
where c.Id == id
select c).FirstOrDefault();
Et dans ce cas, NHibernate insère automatiquement une jointure avec la table Groups dans la requête SQL qu'il génère :
SELECT top 1
this_.Id as Id1_1_,
this_.FirstName as FirstName1_1_,
this_.LastName as LastName1_1_,
this_.Phone as Phone1_1_,
this_.Email as Email1_1_,
this_.groupId as groupId1_1_,
group2_.Id as Id0_0_,
group2_.Name as Name0_0_
FROM Contacts this_
inner join Groups group2_ on this_.groupId=group2_.Id
WHERE this_.Id = @p0; // @p0 = 13
Comme je suis d'un naturel méfiant, je suis allé jusqu'à vérifier ce qui se passait quand j'accédais à la propriété Group de l'objet Contact :
string NomGroupe2 = linq2.Group.Name;
Et fort heureusement il ne se passe rien et NHibernate ne génère pas de requête SQL supplémentaire.
Interlude
OK. Mais NHibernate ça ne peut pas être aussi simple que ça et il y des tas de trucs à prendre en compte. Jusqu'à maintenant, j'ai fait mes deux essais avec le fichier de mapping suivant pour les contacts :
<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="ContactManager.Models" assembly="ContactManager">
<class name="Contact" table="Contacts" dynamic-update="true">
<cache usage="read-write" />
<id name="Id" column="Id" type="integer">
<generator class="identity" />
</id>
<property name="FirstName" type="string" />
<property name="LastName" type="string" />
<property name="Phone" type="string" />
<property name="Email" type="string" />
<many-to-one name="Group" column="groupId" not-null="true" />
</class>
</hibernate-mapping>
Dans ce fichier, il y a une ligne <many-to-one name="Group"
column="groupId" not-null="true" /> qui sert à définir la relation
entre les tables Contacts et Groups :
- 1 contact appartient à 1 groupe
- 1 groupe peut contenir 0 à N contact(s)
Par défaut, cette relation "fonctionne" en mode lazy-loading, ce qui signifie que le groupe ne sera chargé que lorsqu'on en aura réellement besoin. Et au vu de mes deux premiers essais, je ne peux qu'être satisfait puisque c'est exactement comme cela que ça s'est passé.
Essai numéro 3
Et maintenant, supposons que je ne veuille plus faire de lazy-loading ?
Pour commencer, je dois ajouter lazy="false" à mon fichier de
mapping :
...
<many-to-one name="Group" column="groupId" not-null="true" lazy="false" />
...
Et ce coup-ci, quand je refais la même requête LINQ que lors de mon premier essai :
var linq3 = (from c in _session.Linq<Contact>()
where c.Id == id
select c).FirstOrDefault();
Je peux constater que NHibernate a généré directement les 2 requêtes SQL suivantes :
SELECT top 1
this_.Id as Id1_0_,
this_.FirstName as FirstName1_0_,
this_.LastName as LastName1_0_,
this_.Phone as Phone1_0_,
this_.Email as Email1_0_,
this_.groupId as groupId1_0_
FROM Contacts this_
WHERE this_.Id = @p0; // @p0 = 13
SELECT group0_.Id as Id0_0_,
group0_.Name as Name0_0_
FROM Groups group0_
WHERE group0_.Id=@p0; // @p0 = 1
Par acquit de conscience j'ai re-vérifié mon premier essai sans
lazy="false". Pas de problème : NHibernate ne génère bien que
la 1° requête SQL si je me contente de faire la requête LINQ et que je ne
cherche pas à accéder à l'objet Group.
Premier bilan
Au moins, ça commence à être un peu plus clair pour ce côté de la relation. Si tout va bien, je ferais plus tard quelques essais supplémentaires pour étudier comment ça se passe dans le cas où je pars d'un groupe et que j'accède ensuite à ses contacts.
J'ai pas trainé : la suite de mes essais avec LINQ to NHibernate !
mardi 16 février 2010
Blogmarks du 15/02/2010
Par Michel le mardi 16 février 2010, 08:10
-
Summer of NHibernate : Une série de 14 vidéos (soit une bonne quinzaine d'heures) pour se former à l'utilisation de NHibernate avec pour chaque cours le projet C# correspondant.
lundi 15 février 2010
NerdDinner (en français) : Fin !
Par Michel le lundi 15 février 2010, 19:58
Ca y est ! Ce week-end j'ai complètement terminé la traduction du tutoriel NerdDinner en français. Et finalement, j'ai réussi à tout finir beaucoup plus vite que prévu et surtout j'ai même pris de l'avance par rapport à la réalisation du tutoriel.
Vu que la partie consacrée à l'ajout de la carte ne m'emballait pas des masses, j'ai préféré m'atteler à sa traduction. Ca m'a remotivé et j'ai alors pu finir la réalisation de cette partie en me basant directement sur ma traduction plutôt que sur le tutoriel en VO.
Pour la dernière partie qui concerne les tests unitaires, j'ai donc décidé de fonctionner de la même façon et même d'aller encore plus loin en choisissant de traduire entièrement cette partie avant de passer à sa réalisation. Donc maintenant, il ne me reste plus qu'à suivre mon tutoriel en français pour terminer le projet NerdDinner et disposer d'une application totalement finie.
Pour conclure, je suis vraiment très content d'avoir réussi le défi que je m'étais fixé et d'être arrivé à traduire les presque 200 pages du tutoriel (184 en fait) sans jamais me décourager.
Même si le fait d'avoir tout traduit et aussi d'avoir réellement réécrit moi-même la totalité du code (à part les expressions régulières et les procédures SQL Server) m'a demandé beaucoup de temps, je suis convaincu que ça m'a permis de bien mieux comprendre comment utiliser ASP.NET MVC, en tout cas beaucoup mieux que si je m'étais contenté de lire vite fait le tutoriel et de faire un peu de copier / coller pour le réaliser.
L'autre avantage c'est que j'ai fait pas mal de progrès en matière de traduction et que vers la fin j'ai atteint un rythme tout à fait honorable. Au début, j'ai beaucoup utilisé Google Traduction pour dégrossir le plus gros du boulot et disposer d'un point de départ rassurant. Mais dans les derniers temps, je comptais beaucoup plus sur l'excellent site WordReference pour m'aider à comprendre certaines expressions ou à trouver la traduction la plus adaptée.
Blogmarks du 12/02/2010
Par Michel le lundi 15 février 2010, 10:25
-
Using TABLE INHERITS in Oracle and PostgreSQL to provide consistency : Un article qui explique comment définir une structure de table de base puis de la réutiliser dans toutes les autres tables. Utilisable pour avoir automatiquement certaines colonnes dans toutes les tables : identifiant, date création, date modification, utilisateur qui a fait la création, utilisateur qui a fait la dernière modification...
mercredi 10 février 2010
Blogmarks du 09/02/2010
Par Michel le mercredi 10 février 2010, 08:22
-
101 LINQ Samples : 101 exemples de requêtes LINQ toutes prêtes pour s'initier et se perfectionner petit à petit à l'utilisation de LINQ
lundi 8 février 2010
Blogmarks du 05/02/2010
Par Michel le lundi 8 février 2010, 10:15
-
NHibernate - Native SQL : Le chapitre de la documentation officielle de NHibernate consacré à l'utilisation de requêtes SQL écrites "en dur" dans le code.
-
Hibernate - Native SQL : Le chapitre de la documentation officielle de Hibernate consacré à l'utilisation de requêtes SQL écrites "en dur" dans le code.
« billets précédents - page 1 de 30
