ASP.NET

Utiliser plusieurs frameworks de code avec un seul ASP.NET

Jeff Fritz

En 2001, lorsque Microsoft a lancé Microsoft .NET Framework, ainsi qu'une nouvelle technologie nommée ASP.NET, les développeurs Web l'ont adopté pour créer des sites à l'aide d'un framework reposant sur les formulaires. Ce framework, également nommé Web Forms, a su résister à l'épreuve du temps pendant 8 ans, avec des améliorations et des modifications visant à prendre en charge un environnement Web en constante évolution. La création d'une application Web à cette époque constituait un choix simple, avec une boîte de dialogue Nouveau projet qui présentait quatre options ASP.NET, comme illustré à la figure 1. La plupart d'entre nous ignoraient les projets de site Web mobile ASP.NET et de Bibliothèque de contrôles Web, et créaient uniquement des projets d'application Web ASP.NET. Si vous aviez besoin de services Web, vous pouviez ajouter un service basé sur SOAP à un site Web existant avec un fichier .asmx.

The Original New Project ASP.NET Choices in Visual C#
Figure 1 Les choix ASP.NET initiaux en Visual C# dans la boîte de dialogue Nouveau projet

Au début de l'année 2009, le paysage ASP.NET a changé considérablement avec l'apparition du modèle MVC (Model-View-Controller). Les développeurs, à qui l'on avait promis la disparition des ViewState, du cycle de vie des événements de page ou des événements de publication, se précipitèrent vers le nouveau framework. Je fus l'un d'entre eux, intrigué par le potentiel de cette technologie Web plus testable. Nous avons dû trouver des façons de justifier auprès de nos responsables et de nos centres de coûts le budget nécessaire au transfert des applications vers MVC et de nombreux développeurs ont travaillé activement pour présenter le contenu MVC dans la même application qu'une application Web Forms existante. Tout a très bien fonctionné avec MVC pendant plusieurs années, puis le Web s'est quelque peu développé. ASP.NET a alors dû évoluer à nouveau.

En 2012, Microsoft a lancé deux nouveaux frameworks qui sont venus compléter la boîte à outils ASP.NET : API Web et SignalR. Ces deux frameworks apportent quelque chose de spécial à l'environnement et chacun est unique à sa façon :

  • l'API Web offre une expérience similaire à celle de MVC pour permettre aux développeurs de fournir un contenu destiné à une interprétation automatique. Il n'y a aucune interface utilisateur et aucune transaction n'a lieu tranquillement avec REST. Les types de contenu sont négociés et l'API Web peut mettre automatiquement un contenu au format JSON ou XML, selon les en-têtes HTTP soumis à un point de terminaison de l'API Web.
  • SignalR est le nouveau modèle de distribution Web en temps réel proposé par Microsoft. Cette technologie ouvre les canaux des communications client-serveur afin de permettre des communications immédiates et riches du serveur vers le client. Le modèle de diffusion du contenu dans SignalR va à l'encontre de nos attentes habituelles puisque le serveur appelle le client afin d'interagir avec le contenu.

Observez la figure 2 qui vous permet de comparer Web Forms et MVC, ainsi que l'API Web et MVC.

Figure 2 Avantages de chaque framework de composant ASP.NET

Framework Productivité Contrôle Interface utilisateur Temps réel
Web Forms    
MVC    
API Web    
SignalR      

La productivité requiert des fonctionnalités qui vous permettent de développer et de proposer une solution rapidement. Le contrôle correspond à votre capacité d'avoir un impact sur les bits transmis sur le réseau aux utilisateurs connectés. L'interface utilisateur indique si vous pouvez utiliser le framework pour offrir une interface utilisateur complète. En dernier lieu, le facteur temps réel fait référence à la façon dont le framework présente le contenu dans des délais qui pourraient être perçus comme une mise à jour immédiate.

Aujourd'hui, en 2013, lorsque j'ouvre ma copie de Visual Studio et que je tente de démarrer un projet ASP.NET, je vois s'afficher les boîtes de dialogue illustrées à la figure 3 et à la figure 4.

New Web Project in Visual Studio 2012
Figure 3 Nouveau projet Web dans Visual Studio 2012

New Project Template Dialog in Visual Studio 2012
Figure 4 Boîte de dialogue du nouveau modèle de projet dans Visual Studio 2012

Ces fenêtres comportent des questions épineuses. Avec quel type de projet dois-je commencer ? Quel modèle me permettra d'être au plus proche de ma solution ou d'y arriver le plus rapidement possible ? Et comment procéder si je souhaite inclure certains composants de chaque modèle ? Puis-je créer une application mobile avec certains contrôles de serveur et une API Web ?

Dois-je choisir une seule approche ?

Dois-je choisir une seule approche ? La réponse sera brève : non. Vous n'avez pas besoin de sélectionner un seul de ces frameworks pour créer une application Web. Différentes techniques vous permettent d'utiliser Web Forms et MVC ensemble. En outre, contrairement à ce qui se passe avec les boîtes de dialogue présentées ici, l'API Web et SignalR peuvent facilement être ajoutés comme fonctionnalités à une application Web. N'oubliez pas que tout contenu ASP.NET est rendu par l'intermédiaire d'une série de HttpHandlers et HttpModules. Tant que les gestionnaires et les modules corrects sont référencés, vous pouvez créer une solution avec le framework de votre choix.

Ce principe est au cœur du concept « Un ASP.NET » : ne choisissez pas un seul de ces frameworks, mais créez votre solution avec les parties de chacun d'entre eux qui répondent le mieux à vos besoins. Vous avez le choix, ne vous limitez pas une seule possibilité.

Pour vous aider à voir comment cela fonctionne, voici une petite application Web que j'ai créée avec une mise en forme unifiée, un écran de recherche et un écran de création pour une liste de produits. L'écran de recherche est optimisé par Web Forms et l'API Web. Il affiche en outre des mises à jour en direct provenant de SignalR. L'écran de création sera généré automatiquement par des modèles MVC. J'utilise également une bibliothèque de contrôles tierce pour donner une belle apparence à Web Forms : RadControls Telerik pour ASP.NET AJAX. Une version d'essai de ces contrôles est disponible sur bit.ly/15o2Oab.

Configuration de l'exemple de projet et de la présentation partagée

Pour commencer, je crée un projet à l'aide de la boîte de dialogue illustrée à la figure 3. Bien que je puisse choisir une application vide ou Web Forms, l'application MVC est la solution la plus complète. Il est très intéressant de commencer par un projet MVC car vous disposez ainsi de tous les outils Visual Studio pour vous aider à configurer vos modèles, vues et contrôleurs, ainsi que de la capacité d'ajouter des objets Web Forms où vous le souhaitez dans la structure du fichier de projet. Il est possible d'ajouter des outils MVC dans une application Web existante en modifiant une partie du contenu XML dans le fichier .csproj. Ce processus peut être automatisé en installant le package NuGet nommé AddMvc3ToWebForms.

Pour configurer les contrôles Telerik à utiliser dans le cadre de ce projet, j'ai besoin de modifier Web.config de façon à ajouter les HttpHandlers et les HttpModules qui sont normalement configurés dans un projet standard RadControls Telerik. Je commence par ajouter deux lignes pour définir l'apparence d'interface utilisateur des contrôles Telerik AJAX :

<add key="Telerik.Skin" value="WebBlue" />
</appSettings>

J'ajoute ensuite le préfixe de balise Telerik :

<add tagPrefix="telerik" namespace="Telerik.Web.UI" assembly="Telerik.Web.UI" />
</controls>

J'effectue quelques ajouts minimes nécessaires aux HttpHandlers de Web.config pour les contrôles Telerik :

<add path="Telerik.Web.UI.WebResource.axd" type="Telerik.Web.UI.WebResource"
    verb="*" validate="false" />
</httpHandlers>

Et en dernier lieu, j'ajoute quelques éléments aux gestionnaires de Web.config pour les contrôles Telerik :

<system.WebServer>
  <validation validateIntegratedModeConfiguration="false" />
  <handlers>
    <remove name="Telerik_Web_UI_WebResource_axd" />
    <add name="Telerik_Web_UI_WebResource_axd"
      path="Telerik.Web.UI.WebResource.axd"
      type="Telerik.Web.UI.WebResource" verb="*" preCondition="integratedMode" />

Je crée maintenant une page de disposition pour ce projet. Pour cela je crée une page site.master Web Forms dans le dossier Vues | Partagé. Pour la présentation de ce site, j'ajoute un logo et un menu standard à toutes les pages. J'ajoute une image pour ce logo en la faisant simplement glisser dans ma présentation. Ensuite, pour ajouter un menu en cascade très intéressant à la disposition, je fais glisser un RadMenu de la boîte à outils des contrôles vers le concepteur, juste en dessous de l'image. Dans la surface du concepteur, je peux créer rapidement mon menu en cliquant avec le bouton droit de la souris sur le contrôle de menu et en sélectionnant Modifier les éléments pour afficher la fenêtre illustrée à la figure 5.

Telerik RadMenu Configuration Window
Figure 5 Fenêtre de configuration du RadMenu Telerik

Les deux éléments de menu sur lesquels je souhaite me concentrer se trouve sous Products : il s'agit de Search et New. Pour chacun de ces éléments, j'ai défini la propriété NavigateUrl et le texte de la façon suivante :

<telerik:RadMenuItem Text="Products">
  <Items>
    <telerik:RadMenuItem Text="Search" NavigateUrl="~/Product/Search" />
    <telerik:RadMenuItem Text="New" NavigateUrl="~/Product/New" />
  </Items>
</telerik:RadMenuItem>

Maintenant que le menu est configuré, je rencontre un problème là où j'ai défini ma disposition à l'aide de Web Forms et où je dois héberger mon contenu MVC. Il ne s'agit pas d'un problème difficile, et il peut tout à fait être résolu.

Combler le fossé : configuration de MVC pour utiliser une page maître Web Forms

Comme la plupart d'entre vous, je préfère opter pour la simplicité. Je souhaite partager la présentation que j'ai définie pour ce projet entre Web Forms et MVC. Une technique bien documentée et mise au point par Matt Hawley montre comment utiliser une page maître Web Forms avec des vues basées sur MVC Razor (bit.ly/ehVY3H). J'ai choisi d'utiliser cette technique dans ce projet. Pour combler ce fossé, je configure une vue Web Forms simple nommée RazorView.aspx qui référence la page maître :

<%@ Page Language="C#" AutoEventWireup="true"
  MasterPageFile="~/Views/Shared/Site.Master"
  Inherits="System.Web.Mvc.ViewPage<dynamic>" %>
<%@ Import Namespace="System.Web.Mvc" %>
<asp:Content id="bodyContent" runat="server" 
  ContentPlaceHolderID="body">
<% Html.RenderPartial((string)ViewBag._ViewName); %>
</asp:Content>

Pour que les contrôleurs MVC utilisent cette vue et permettent à leurs vues basées sur Razor d'être exécutées, j'ai besoin d'étendre chaque contrôleur pour acheminer le contenu de la vue de façon appropriée. J'utilise pour cela une méthode d'extension qui réachemine le modèle, ViewData et TempData de façon appropriée via RazorView.aspx, comme illustré à la figure 6.

Figure 6 La méthode d'extension RazorView pour réacheminer les vues MVC via une page maître Web Forms

public static ViewResult RazorView(this Controller controller,
  string viewName = null, object model = null)
{
  if (model != null)
    controller.ViewData.Model = model;
  controller.ViewBag._ViewName = !string.IsNullOrEmpty(viewName)
    ? viewName
    : controller.RouteData.GetRequiredString("action");
  return new ViewResult
  {
    ViewName = "RazorView",
    ViewData = controller.ViewData,
    TempData = controller.TempData
  };
}

Maintenant que cette méthode a été construite, je peux facilement acheminer toutes les actions MVC via la page maître. L'étape suivante consiste à configurer ProductsController afin que les produits puissent être créés.

MVC et l'écran de création de produit

La partie MVC de cette solution est une approche MVC relativement standard. J'ai défini un objet de modèle simple nommé BoardGame dans le dossier Models de mon projet, comme illustré à la figure 7.

Figure 7 L'objet BoardGame

public class BoardGame
{
  public int Id { get; set; }
  public string Name { get; set; }
  [DisplayFormat(DataFormatString="$0.00")]
  public decimal Price { get; set; }
  [Display(Name="Number of items in stock"), Range(0,10000)]
  public int NumInStock { get; set; }
}

J'utilise ensuite les outils MVC standard dans Visual Studio pour créer un ProductController vide. J'ajoute un dossier Views | Product, puis je clique avec le bouton droit sur le dossier Product et je choisis View dans le menu Add. Cette vue prendra en charge la création de nouveaux jeux de société et je vais donc la créer avec les options représentées à la figure 8.

Creating the “New” View
Figure 8 Création de la vue « New »

Grâce aux outils et aux modèles MVC, je n'ai besoin d'apporter aucune modification. La vue créée a des étiquettes et une validation, et vous pouvez utiliser ma page maître. La figure 9 montre comment définir l'action New dans le ProductController.

Figure 9 Acheminement de ProductController par l'intermédiaire de RazorView

public ActionResult New()
{
  return this.RazorView();
}
[HttpPost]
public ActionResult New(BoardGame newGame)
{
  if (!ModelState.IsValid)
  {
    return this.RazorView();
  }
  newGame.Id = _Products.Count + 1;
  _Products.Add(newGame);
  return Redirect("~/Product/Search");
}

Les développeurs MVC reconnaîtront probablement cette syntaxe puisque le seul changement consiste à retourner une RazorView au lieu d'une View. L'objet _Products est une collection statique en lecture seule de produits factices qui sont définis dans ce contrôleur (au lieu d'utiliser une base de données dans cet exemple) :

public static readonly List<BoardGame> _Products = 
  new List<BoardGame>()
{
  new BoardGame() {Id=1, Name="Chess", Price=9.99M},
  new BoardGame() {Id=2, Name="Checkers", Price=7.99M},
  new BoardGame() {Id=3, Name="Battleship", Price=8.99M},
  new BoardGame() {Id=4, Name="Backgammon", Price= 12.99M}
};

Configuration de la page de recherche basée sur Web Forms

Je souhaite que les utilisateurs puissent accéder à la page de recherche de produit avec une URL qui ne ressemble pas à une URL Web Forms et qui soit simple à utiliser. Avec le lancement d'ASP.NET 2012.2, cela peut désormais être configuré facilement. Il suffit d'ouvrir le fichier App_Start/RouteConfig.cs et d'appeler EnableFriendlyUrls pour basculer sur cette capacité :

public static void RegisterRoutes(
    RouteCollection routes)
  {
    routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
    routes.EnableFriendlyUrls();
    routes.MapRoute(
      name: "Default",
      url: "{controller}/{action}/{id}",
      defaults: new { controller = "Home", action =
        "Index", id = UrlParameter.Optional }
    );
  }

Maintenant que cette ligne a été ajoutée, ASP.NET achemine les requêtes /Product/Search vers le fichier physique qui se trouve à /Product/Search.aspx.

Je configure ensuite une page de recherche qui affiche une grille des produits actuels et de leur stock. Je crée un dossier Product dans le projet, puis j'ajoute un nouveau Web Form nommé Search.aspx. Dans ce fichier, je supprime tout balisage à l'exception de la directive @Page et je définis MasterPageFile sur le fichier Site.Master défini précédemment. Pour afficher les résultats, je choisis le RadGrid Telerik afin de pouvoir configurer et afficher rapidement les données de résultat :

<%@ Page Language="C#" AutoEventWireup="true"
  CodeBehind="Search.aspx.cs"
  Inherits="MvcApplication1.Product.Search"
  MasterPageFile="~/Views/Shared/Site.Master" %>
<asp:Content runat="server" id="main" ContentPlaceHolderID="body">
  <telerik:RadGrid ID="searchProducts" runat="server" width="500"
    AllowFilteringByColumn="True" CellSpacing="0" GridLines="None"
    AllowSorting="True">

La grille rend automatiquement des colonnes qui lui sont liées côté serveur et offre des capacités de tri et de filtrage. Toutefois, j'aimerais rendre tout cela plus dynamique. Je souhaite que les données soient fournies et gérées côté client. Dans le cadre de ce modèle, les données peuvent être transmises et liées sans aucun code côté serveur dans les Web Forms. Pour cela, il suffit d'ajouter une API Web qui fournira et effectuera les opérations de données.

Ajout de l'API Web au mélange

J'utilise le menu standard Projet | Ajouter nouveau pour ajouter un contrôleur de l'API Web nommé ProductController à un dossier nommé « api » dans mon projet. Cela me permet de différencier clairement les contrôleurs MVC et les contrôleurs API. Cette API effectuera une chose : fournir les données de la grille au format JSON et prendre en charge les requêtes OData. Pour réaliser cette opération dans une API Web, j'écris une seule méthode Get et je lui ajoute un attribut Queryable.

[Queryable]
public IQueryable<dynamic> Get(ODataQueryOptions options)
{
  return Controllers.ProductController._Products.Select(b => new
  {
    Id = b.Id,
    Name = b.Name,
    NumInStock = b.NumInStock,
    Price = b.Price.ToString("$0.00")
  }).AsQueryable();
}

Ce code retourne la collection d'objets BoardGame dans la liste statique avec un peu de mise en forme. En ajoutant [Queryable] à cette méthode et en retournant une collection susceptible d'être interrogée, le framework de l'API Web gérera et traitera automatiquement le filtre OData et les commandes de tri. La méthode doit également être configurée avec le paramètre d'entrée ODataQueryOptions afin de gérer les données de filtre soumises par la grille.

Pour configurer la grille dans Search.aspx afin d'utiliser cette nouvelle API, j'ajoute quelques paramètres clients au balisage de la page. Dans ce contrôle de grille, je définis la liaison de données cliente avec un élément ClientSettings et un paramètre DataBinding. Le paramètre DataBinding indique l'emplacement de l'API, le type de format de la réponse et le nom du contrôleur à interroger, ainsi que le format de requête OData. Avec ces paramètres et une définition des colonnes à présenter dans la grille, je peux exécuter le projet et voir la grille liée aux données dans la liste factice de données _Products, comme illustré à la figure 10.

Figure 10 Source de mise en forme complète de la grille

<telerik:RadGrid ID="searchProducts" runat="server" width="500"
  AllowFilteringByColumn="True" CellSpacing="0" GridLines="None"
  AllowSorting="True" AutoGenerateColumns="false"
  >
    <ClientSettings AllowColumnsReorder="True"
      ReorderColumnsOnClient="True"
      ClientEvents-OnGridCreated="GridCreated">
      <Scrolling AllowScroll="True" UseStaticHeaders="True"></Scrolling>
      <DataBinding Location="/api" ResponseType="JSON">
        <DataService TableName="Product" Type="OData"  />
      </DataBinding>
    </ClientSettings>
    <MasterTableView ClientDataKeyNames="Id" DataKeyNames="Id">
      <Columns>
        <telerik:GridBoundColumn DataField="Id" HeaderStyle-Width="0"
          ItemStyle-Width="0"></telerik:GridBoundColumn>
        <telerik:GridBoundColumn DataField="Name" HeaderText="Name"
          HeaderStyle-Width="150" ItemStyle-Width="150">
          </telerik:GridBoundColumn>
        <telerik:GridBoundColumn ItemStyle-CssClass="gridPrice"
          DataField="Price"
          HeaderText="Price" ItemStyle-HorizontalAlign="Right">
          </telerik:GridBoundColumn>
        <telerik:GridBoundColumn DataField="NumInStock"
          ItemStyle-CssClass="numInStock"
          HeaderText="# in Stock"></telerik:GridBoundColumn>
      </Columns>
    </MasterTableView>
  </telerik:RadGrid>

Activation de la grille avec des données en temps réel

La dernière pièce du puzzle est la capacité à afficher les modifications du stock en temps réel au fur et à mesure de l'expédition et de la réception des produits. J'ajoute un hub SignalR pour transmettre les mises à jour et présenter ces nouvelles valeurs dans la grille de recherche. Pour ajouter SignalR à mon projet, je dois émettre les deux commandes NuGet suivantes :

Install-Package -pre Microsoft.AspNet.SignalR.SystemWeb
Install-Package -pre Microsoft.AspNet.SignalR.JS

Ces commandes installeront les composants de serveur ASP.NET en vue de leur hébergement au sein du serveur Web IIS et rendront disponibles les bibliothèques clientes JavaScript pour les Web Forms.

Le composant côté serveur SignalR est qualifié de Hub et je définis le mien en ajoutant une classe nommée StockHub à un dossier nommé Hubs dans le projet Web. La classe StockHub est nécessaire pour hériter de la classe Microsoft.AspNet.SignalR.Hub. Je définis un System.Timers.Timer statique pour permettre à l'application de simuler le changement de stock. Pour cette simulation, je définis de façon aléatoire le stock d'un produit choisi de façon aléatoire toutes les 2 secondes (lorsque le gestionnaire d'événements de minuteur écoulé se déclenche). Une fois le stock de produit défini, j'avertis tous les clients concernés en exécutant une méthode sur le client nommée setNewStockLevel, comme le montre la figure 11.

Figure 11 Le composant côté serveur Hub SignalR

public class StockHub : Hub
{
  public static readonly Timer _Timer = new Timer();
  private static readonly Random _Rdm = new Random();
  static StockHub()
  {
    _Timer.Interval = 2000;
    _Timer.Elapsed += _Timer_Elapsed;
    _Timer.Start();
  }
  static void _Timer_Elapsed(object sender, ElapsedEventArgs e)
  {
    var products = ProductController._Products;
    var p = products.Skip(_Rdm.Next(0, products.Count())).First();
    var newStockLevel = p.NumInStock + 
      _Rdm.Next(-1 * p.NumInStock, 100);
    p.NumInStock = newStockLevel;
    var hub = GlobalHost.ConnectionManager.GetHubContext<StockHub>();
    hub.Clients.All.setNewStockLevel(p.Id, newStockLevel);
  }
}

Pour que les données de ce hub soient accessibles à partir du serveur, j'ajoute une ligne à RouteConfig indiquant la présence du hub. En appelant routes.MapHubs dans la méthode RegisterRoutes de RouteConfig, je termine la configuration côté serveur de SignalR.

La grille doit ensuite écouter ces événements à partir du serveur. Pour cela, j'ajoute des références JavaScript à la bibliothèque cliente SignalR installée à partir de NuGet et le code généré depuis la commande MapHubs. Le service SignalR connecte et expose la méthode setNewStockLevel sur le client à l'aide du code de la figure 12.

Figure 12 Code côté client SignalR pour activer la grille

<script src="/Scripts/jquery.signalR-1.0.0-rc2.min.js"></script>
<script src="/signalr/hubs"></script>
<script type="text/javascript">
  var grid;
  $().ready(function() {
      var stockWatcher = $.connection.stockHub;
      stockWatcher.client.setNewStockLevel = function(id, newValue) {
        var row = GetRow(id);
        var orgColor = row.css("background-color");
        row.find(".numInStock").animate({
          backgroundColor: "#FFEFD5"
        }, 1000, "swing", function () {
          row.find(".numInStock").html(newValue).animate({
            backgroundColor: orgColor
          }, 1000)
        });
      };
      $.connection.hub.start();
  })
</script>

Dans le gestionnaire d'événements ready jQuery, j'établis une référence nommée stockWatcher vers le StockHub à l'aide de la syntaxe $.connection.stockHub. Je définis ensuite la méthode setNewStockLevel sur la propriété cliente de stockWatcher. Cette méthode utilise d'autres méthodes d'assistance JavaScript pour traverser la grille, trouver la ligne avec le produit approprié et modifier le stock avec une animation en couleurs fantaisie fournie par l'interface utilisateur jQuery, comme illustré à la figure 13.

The Search Interface with Grid Generated by Web API and Maintained by SignalRFigure 13 L'interface de recherche avec la grille générée par l'API Web et gérée par SignalR

Pour résumer

Je vous ai montré comment créer un projet ASP.NET MVC et ajouter une disposition Web Forms, des contrôles AJAX tiers et des Web Forms qui vous mènent vers ce projet. J'ai généré une interface utilisateur à l'aide des outils MVC, puis j'ai activé le contenu avec l'API Web et SignalR. Ce projet a eu recours à des fonctionnalités provenant des quatre frameworks ASP.NET de façon à présenter une interface cohérente, en utilisant les meilleures fonctionnalités de chaque composant. Vous pouvez faire de même. Ne choisissez pas un seul framework ASP.NET pour votre prochain projet. Choisissez plutôt de tous les utiliser.

Jeffrey T. Fritz est développer et expert Telerik. Il bénéficie de 15 ans d'expérience dans la création d'applications Web mutualisées à grande échelle dans le modèle SaaS (Software as a Service, logiciel en tant que service). Il est conférencier à l'INETA et tient un blog que vous pouvez consulter à l'adresse csharpfritz.com. Vous pouvez le trouver sur Twitter, à l'adresse twitter.com/csharpfritz, et le contacter à jeff.fritz@telerik.com.

MERCI aux experts techniques suivants d'avoir relu cet article : Scott Hanselman (Microsoft) et Scott Hunter (Microsoft)