UIP v2 en pratique : un refactoring du PetShopDNG 2.0 par Sébastien Bouchet (sebastien.bouchet@linkvest.com)

Introduction

Je suis fréquemment surpris par l'apparente discordance entre l'utilité d'un outil, framework ou technologie et la publicité qu'on en fait. La politique éditoriale de MSDN en est un exemple flagrant : loin de moi l'idée de dénigrer cette indispensable et inépuisable mine de contenu qui fait mon pain quotidien, mais n'est-il pas frappant de voir qu'une bonne poignée d'articles sur le thème "What's new in ASP.NET 2.0 ?" nous rabâchant les fonctionnalités du GridView ont eu leur place en première page, alors même que des travaux de fond, notamment ceux menés par l'équipe de Patterns & Practises, ne bénéficient pas de la même médiatisation ?

Entendons-nous bien : nous sommes tous très heureux de constater que les contrôles évoluent, que les master pages sont supportées nativement ou encore que VisualStudio supporte les Smart Tags et que son debugger est extensible grâce aux DebuggerVisualizers (en écrirai-je seulement un un jour ?). Mais je suis de ceux qui aimeraient que les améliorations de surface soient accompagnées de progrès sur certains sujets de fond. Et parmi eux, l'architecture de la couche de présentation.

Ce que l'on peut reprocher à ASP.NET (ou même Windows Forms), ce n'est pas de proposer une mauvaise architecture au niveau de la couche de présentation, mais plutôt de ne pas en favoriser une. L'acronyme ASP.NET est à prendre au pied de la lettre : "Active Server Pages", c'est-à-dire un framework ultra-puissant de génération de page dynamiques. Peu importe la façon dont les développeurs conçoivent les interactions de ces pages entre elles et avec le reste du système.

Cette force (la liberté totale qu'ASP.NET laisse aux développeurs) peut aussi être vue comme une faiblesse : en n'imposant ni ne suggérant d'architecture canonique, ASP.NET prend le risque de laisser l'environnement de développement imposer son propre paradigme. Ce qui dans le cas de Visual Studio .NET prend la forme du "syndrome du code-behind monolithique".

C'est ainsi que je suis surpris de voir que l'application block UIP (User Interface Process), qui tente d'adresser cette problématique fondamentale, n'ait pas fait l'objet de plus de publicité de la part de Microsoft. Cet article tente d'inverser la tendance en présentant les tenants et aboutissants de ce très intéressant framework, tout en le mettant en application dans une application que nous connaissons à peu près tous sur le bout des doigts : le PetShop (bien entendu lorsque je parle de PetShop, entendez le PetShopDNG, le seul, l'unique, dont vous pouvez télécharger les sources ici).

Dans cet article nous allons d'abord disséquer l'architecture du PetShopDNG 2.0 et présenter les principaux mécanismes d'UIP et ainsi proposer un refactoring du PetShop. Nous constaterons un léger problème dans la façon dont UIP s'accommode des pages templatées du PetShopDNG. Ce qui nous forcera à nous plonger plus profondément dans les entrailles d'UIP afin de trouver une solution satisfaisante au problème soulevé.

Remarque
Cette démarche de refactoring n'a pas pour but de produire une nouvelle version "de référence" du PetShop mais de se focaliser sur UIP. D'après moi la prochaine version de référence devrait idéalement réunir ASP.NET 2.0, les contributions de Julien Brunet autour de SOA, un zeste d'AOP et une nouvelle architecture web comme par exemple celle ébauchée dans cette article. Avis aux amateurs !

"Architecture de la couche de présentation" ?

Avant de se pencher sur le cas du PetShop DNG, essayons de recenser quelques bonnes raisons pour lesquelles il est généralement souhaitable de bien penser l'architecture de la couche de présentation :

Par "logique applicative", comprenez non pas la logique métier mais la logique propre à une application considérée. Prenons le cas d'une banque, par exemple. Un élément fondamental de sa logique métier est la fonction "Transférer argent (compte de, compte a)". Nous pouvons imaginer que cette fonctionnalité est exploitée par plusieurs applications :

Dans ce cas précis nous avons deux "logiques applicatives" différentes, correspondant à deux cinématiques distinctes. Pourtant les deux applications se basent sur la même logique métier et consomment sans doute exactement le même service de transfert de fonds.

En d'autres termes la logique applicative est liée à la façon dont la fonctionnalité fondamentale (en l'occurrence, le transfert) est présentée aux utilisateurs. Elle est typiquement soumise à des cycles de vie relativement courts (notamment pour les sites Internet que l'on aime relooker fréquemment - et un relookage ne porte pas forcément que sur le style, il peut s'agir de repenser la navigation et l'allure des formulaires), alors que la logique métier en tant que telle est moins sujette à variation. D'où la nécessité de pouvoir maintenir l'une indépendamment de l'autre.

En matière d'architecture de la couche de présentation, le modèle MVC et ses variantes est un standard de fait, je ne m'attarderai pas sur le sujet tant il est vaste et nous éloignerait des nombreuses choses que nous avons à traiter. Retenons que :

Décortiquons le PetShopDNG

Motivation du PetShopDNG

PetShopDNG = PetShop + DNG. Le but des contributeurs de DNG a été de produire une implémentation du PetShop destinée à servir de référence en matière de bonnes pratiques architecturales .NET. Les différentes versions du PetShopDNG ont certes proposé une architecture sérieuse pour la couche de présentation, mais ont avant tout mis l'accent sur la couche métier et l'abstraction de la couche d'accès aux données.

Dans sa présentation du PetShopDNG 2.0, Thomas Gil expliquait ainsi que "Les amateurs d'ASP.NET, quant à eux, seront probablement déçus : la couche de présentation du PetShopDNG n'a pas évolué depuis la version 1.0. La raison est simple : les choix que nous avions faits à l'époque (janvier 2003) n'ont pas de raison d'être remis en cause pour le moment (même si certains experts ont pu nous faire part de leur opinion concernant le chargement dynamique de contrôles utilisateurs). L'événement qui nous fera revenir sur cette position sera probablement la sortie du framework ASP.NET 2.0.".

Rassure-toi Tom, nous n'avons pas été déçus tant le PSDNG2 est remarquable par ailleurs. Cela dit, il est vrai que la couche de présentation est perfectible dans son architecture, et avant même de la passer à ASP.NET 2.0 (ce qui sera fort intéressant !), essayons de voir comment UIP peut nous aider à améliorer le PetShopDNG 2.0.

Navigation dans la boutique

Le PetShopDNG se prête a priori assez bien à une réflexion sur les "User Interface Process" dans la mesure où il présente un graphe de navigation suffisamment étoffé pour bien mettre à l'épreuve UIP tout en restant raisonnablement simple.

Voici ce graphe, présenté sous la forme d'un diagramme d'états :

Certaines transitions sont communes à toutes les pages du site et ne sont pas représentées ici. Par exemple, la fonctionnalité de recherche est disponible sur toutes les pages.

Remarque : les diagrammes que vous voyez ont été réalisés avec une version d'évaluation de l'outil Enterprise Architect de Sparx Systems, au sujet duquel je pourrais me lancer dans un dithyrambe si ce n'était pas hors sujet ici. Je me bornerai à mentionner qu'une rétro-conception du PSSDNG ne lui a posé aucun souci, qu'il supporte le travail collaboratif au moyen d'un repository de projets, qu'il génère la documentation dans divers formats dont Word et que la version la plus complète coûte à peine plus cher qu'un antivirus... Mais revenons à nos moutons, ou plutôt nos perroquets.

Les pages du PetShop sont toutes conformes au même gabarit :

Une partie du problème architectural posé aux concepteurs du PetShopDNG concernait donc la façon dont on peut centraliser les éléments communs à toutes les pages afin de ne maintenir le gabarit qu'à un seul endroit. Ah, si seulement les master pages avaient été présentes dans ASP.NET 1.1 ...

Architecture du PetShopDNG

L'architecture du PetShopDNG repose sur les éléments suivants :

Les contrôleurs et entités ne sont vus que par leurs interfaces par les couches supérieures, et supportent un chargement "plug-and-play" via une abstract factory. Ce qui donne l'organisation en packages suivante :

Attardons nous sur les principaux éléments de cette architecture.

Commandes

Les commandes du PetShopDNG méritent cette appellation dans la mesure où elles sont proches du pattern Command. Leur implémentation est néanmoins légèrement différente des classiques du genre : en lieu et place d'une méthode Execute(), les commandes DNG effectuent leurs traitements dans le constructeur. Choix dont le principal mérite est de permettre un codage plus rapide des web forms en évitant à chaque fois de devoir instancier une commande puis l'exécuter. L'utilisation du pattern command présente de nombreux avantages : les commandes héritant toutes d'une même classe (AbstractWebCommand), elle rend par exemple aisé l'introduction de fonctions de suivi des utilisateurs, d'enregistrement de navigation, de production de statistiques ou encore de mécanismes "d'undo".

Penchons nous maintenant sur la logique que contiennent ces commandes : celles-ci appellent des contrôleurs DNG afin de manipuler des entités, puis décident de la vue suivante. En d'autres termes la commande DNG me semble proche de ce que l'on appelle contrôleur en terminologie MVC.

Etat fortement typé

Un autre aspect remarquable du PetShopDNG concerne la façon dont l'état courant est manipulé. L'application a besoin de stocker différents types d'objets avec portées différentes. Par exemple, le panier d'achats courant est conservé durant toute la durée de la session utilisateur.

Une implémentation basique manipulerait directement l'état via les objets Context, Session et Application de la manière suivante :

using PetShopDNG.DAL;

//...
const string CURRENT_CART = "CurrentCart";

//...
IShoppingCart cart = (IShoppingCart)Session[Constants.CURRENT_CART];

Cette technique d'implémentation montre rapidement ses limites dès que l'application est un peu ambitieuse, tant il est lourd de devoir caster à chaque fois vers le type cible, en l'occurence IShoppingCart ; enfin qui dit cast systématique dit potentiel d'InvalidCastException. Les développeurs préférant généralement détecter les erreurs à la compilation plutôt qu'à l'exécution, le PetShopDNG introduit un objet d'état, le WebLocalSingleton, qui agit comme une façade masquant les détails de la gestion d'état au moyen de propriétés fortement typées.

using PetShopDNG.DAL;
using PetShopDNG.UserCommands;

//Somewhere in a code-behind
HttpContext ctx = HttpContext.Current;
IShoppingCart cart = WebLocalSingleton.GetInstance(ctx).CurrentShoppingCart;

L'implémentation de ce singleton appelle une petite explication. Quelle que soit la variante choisie (voir par exemple cet article), un singleton au sens traditionnel du terme repose sur une variable statique qui assure l'unicité de l'instance au sein de l'AppDomain courant. Dans le cas présent il faut une instance par contexte d'exécution de requête web afin de garantir entre autres que deux utilisateurs concurrents (dont les requêtes sont potentiellement traitées dans le même AppDomain) ne partagent pas le même état. Le "singleton" n'utilise donc pas une variable statique mais une variable du contexte HTTP, il est donc HttpContext-statique.

Contrôleurs et entités

Les contrôleurs DNG ne sont connus des couches supérieures que par leurs interfaces publiques, et sont chargés dynamiquement par le mécanisme d'Abstract Factory du PetShopDNG. Si l'on regarde justement leurs interfaces publiques, on constate qu'il sont en réalité des objets stateless manipulant des entités sans comportement ou presque (il y a cependant quelques exceptions : la classe ShoppingCart, par exemple, a une propriété Total qui est un comportement au sens strict du terme).

D'autre part si l'on se penche de plus près sur ces contrôleurs afin de déterminer leur responsabilité fondamentale, on constate qu'ils sont en contact direct avec les objets d'accès aux données et qu'ils sont totalement indépendants de la façon dont l'information est présentée à l'utilisateur.

Résumons : à peu de choses près, entités de données sans comportement d'un côté, objets de pur comportement ne maintenant aucun état conversationnel de l'autre, et vocation métier indépendante de la cinématique. Il ne s'agit donc ni plus ni moins de la couche métier de l'application, au sens "services métiers".

Couche d'accès aux données

Il s'agit d'une partie remarquable du PetShopDNG, je ne m'y attarderai pas car le refactoring nécessité par l'introduction d'UIP ne descendra pas jusqu'à cette couche.

Les "MasterPages" du PetShopDNG

Comme je l'ai mentionné plus haut, toutes les pages du PetShop sont conformes au même modèle ; le PetShopDNG utilise donc fort judicieusement un mécanisme de templating. Celui ci repose sur :

Comme le montre l'extrait d'ASCX suivant, ce contrôle référence toutes les vues possibles (une vue = un user control), et ne rend visible que la vue courante.

<dng:Body id="initial" runat="server" Visible="False" />
<dng:Category id="showProductsByCategory" runat="server" Visible="False" />
<dng:Product id="showItemsByProduct" runat="server" Visible="False" />
<dng:Item id="showItem" runat="server" Visible="False" />
<dng:Customer id="showAccount" runat="server" Visible="False" />
<dng:EditCustomer id="editAccount" runat="server" Visible="False" />
<dng:Signon id="showSignOn" runat="server" Visible="False" />
<dng:Signout id="showSignOut" runat="server" Visible="False" />
<dng:ExistingUserSignon id="showExistingUserSignOn" runat="server" Visible="False" />
<dng:NewUserSignon id="showNewUserSignOn" runat="server" Visible="False" />
<dng:Cart id="showShoppingCart" runat="server" Visible="False" />

La vue courante est donnée par la commande courante, propriété d'état du WebLocalSingleton, qui à l'issue de son exécution a déterminé quelle devait être la prochaine vue.

Synthèse

Idées directrices

Ce qu'il est important de retenir, indépendamment des choix de design effectués, ce sont les problèmes que les initiateurs du PetShopDNG 2.0 ont cherché à résoudre :

A nous de voir si UIP fournit un ensemble de réponses à ces problématiques. Pour cela commençons par introduire les grandes lignes d'UIP.

Remarques

J'ose faire quelques remarques de surface sur cette architecture qui historiquement m'a paru être une des premières tentatives véritablement sérieuses (et réussies) d'établir un ensemble de pratiques de référence dans le développement d'applications .NET n-tiers :

UIP sur le papier

Généralités

Pour comprendre les objectifs de design d'UIP, rien de tel qu'une lecture de l'introduction de cet article d'Edward Jezierski :

"It is designed to abstract the control flow and state management out of the user interface layer into a user interface process layer. This enables you to write generic code for the control flow and state management of different types of applications (for example, Web applications and Windows-based applications) and helps you write applications that manage users' tasks in complex scenarios (for example, suspending and resuming stateful tasks). This leads to simpler development and maintenance of complex applications."

En théorie UIP adresse donc les problématiques soulevées par le PetShop, tout en allant plus loin puisque d'une part il propose de rendre les éléments de logique applicative indépendants du type d'application (web ou non), et d'autre part il offre des fonctions avancées comme le transfert de session d'un utilisateur à un autre. Atteint-il ces objectifs ambitieux tout en restant simple ? Le refactoring du PetShop nous le dira ...

Principaux concepts

UIP est une implémentation du modèle MVC

Il n'est pas tout à fait faux de dire qu'UIP est un framework MVC. Ses principaux apports coinsistent en :

Les relations entre ces concepts sont conformes à la vision qu'offre ce Wiki de MVC, à savoir que "Controllers are strategies of the views [...] Views create controllers using a Factory Method", et sont représentées sur le diagramme de classes ci-dessous :

Le fichier de configuration d'UIP permet de nommer les vues et d'associer un nom de vue à un type de vue physique (nom de la classe du Windows Form ou bien URL de la page ASPX) ; c'est aussi dans ce fichier que l'on associe chaque vue à un type de contrôleur. L'association vue-contrôleur repose donc bien sur un mécanisme de type Factory.

Navigation

Le diagramme de classes ci-dessus met en évidence la classe Navigator, sur laquelle s'appuie le contrôleur. La responsabilité du navigateur est d'interpréter les changements de la valeur de la propriété NavigateValue de l'état (changements effectués par le contrôleur) et de déterminer la vue suivante. UIP propose deux types de Navigators :

Pour illustrer la différence entre ces deux mécanismes de navigation, considérons une application minimaliste constituée de trois états/vues (une home page, un formulaire de login et une page "My Account", avec une transition de l'un à l'autre :

Si l'on utilise un GraphNavigator, on trouvera typiquement le code suivant dans le contrôleur :

//ControllerBase subclass
//using a GraphNavigator

public void GoToMyAccount(){
  if(State["CurrentUser"] == null){
    GoToLogon();
    return;
  }

  this.State.NavigateValue = "showmyaccount"; //name of transition to trigger
  this.Navigate();
}

public void GoToLogon(){
  //"logon" is the name of the transition
  this.State.NavigateValue = "logon";
  this.Navigate();
  //this.Navigate("logon");   //Equivalent form
}

Avec ce GraphNavigator, si le contrôleur essaie d'affecter la valeur "aboutus" à NavigateValue puis de naviguer, UIP lancera une exception au motif qu'il ne s'agit pas d'une transition qui a été prévue dans le fichier de configuration.

Avec un OpenNavigator, nul besoin de configuration : le contrôleur précise directement le nom de la vue où l'on doit se rendre :

//ControllerBase subclass
//using an OpenNavigator

public void GoToMyAccount(){
  if(State["CurrentUser"] == null){
    GoToLogon();
    return;
  }

  this.State.NavigateValue = "My_Account"; //name of next view
  this.Navigate();
}

public void GoToLogon(){
  this.Navigate("Sign_on");
}

Gestionnaire de vues (ViewManager)

Le navigateur a pour responsabilité de déterminer la prochaine vue étant donné un changement d'état. Mais ce n'est pas lui qui charge concrètement la vue ; ce rôle est dédié au gestionnaire de vues ou ViewManager qu'utilise le navigateur. C'est par exemple le WebFormViewManager fourni par UIP qui active la prochaine page de l'application par un Response.Redirect().

Tâches

Par tâche, UIP entend une instance d'une navigation. L'état est conservé par tâche et non par utilisateur, donc rien ne s'oppose en théorie à ce que deux navigations se déroulent en parallèle pour une même utilisateur (en théorie seulement, car dans le cas d'applications web, l'ID de la tâche courante est malencontreusement stocké dans une variable globale à la session, comme le montre un examen de la classe WebFormView. Ce qui rend inapplicables en l'état des scénarios de type "centre d'appels" où un utilisateur doit maintenir dans deux fenêtres simultanément ouvertes avec deux instances d'un même graphe de navigation. Il existe néanmoins des moyens de remédier à ce défaut en dupliquant puis retouchant la classe WebFormView de manière par exemple à stocker l'ID de la tâche courante dans un champ HTML caché qui sera véhiculé de page en page dans chaque fenêtre)

Etat

La classe d'état fournie par UIP peut-être personnalisée de deux façons :

Et c'est à peu près tout ce qu'il faut savoir ! UIP propose certes d'autres fonctionnalités (comme par exemple une spécialisation pour créer facilement des wizards - un wizard étant un cas particulier de machine à états consistant en n états successifs reliés l'un à l'autre par deux transitions "Next" et "Previous"), mais il s'agit d'aspects découlant du fonctionnement standard exposé ci-dessus. Dès lors que l'on maîtrise les concepts exposés ici, on peut considérer qu'UIP est acquis.

Mise en oeuvre d'UIP

Mettre en oeuvre UIP est finalement assez direct ; il s'agit de :

  1. décider du type de navigation (ouverte ou graphe)

  2. décider si l'on veut utiliser un état fortement typé, et si oui créer cette classe par sous-classement de State

  3. coder les contrôleurs par héritage de ControllerBase

  4. Faire en sorte que nos vues soient des implémentations de IView. Cela signifie généralement faire en sorte que nos pages ASPX héritent de WebFormView dans le cas d'applications web, et nos formulaires windows de WindowsFormView dans le cas de clients riches. Rien d'insurmontable en somme.

  5. créer le fichier de configuration, tâche qui peut s'avérer relativement ardue tant les messages d'erreur fournis pas UIP en cas de mauvaise configuration sont parfois cryptiques.

La voie étant tracée, il est grand temps de s'occuper du cas du PetShopDNG.

A l'assaut du PetShop

Un essai rapide

Nul besoin de réfléchir très longtemps pour prendre conscience du travail qui nous attend : l'intégralité des commandes va devoir être transformée en contrôleurs UIP. Avant de se lancer dans de telles manoeuvres, il est légitime de vouloir se rassurer en ne touchant qu'une petite partie. Rien de tel pour commencer que la transition entre la page d'index (page d'accueil du PetShopDNG permettant notamment d'initialiser le contenu de la base de données) et la page d'accueil du PetShop.

La première des choses à faire est de compiler UIP, distribué sous forme de solution VS.NET, afin de pouvoir le référencer dans le PetShop. Une fois ceci fait, on fait hériter Template.aspx de Microsoft.ApplicationBlocks.UIProcess.WebFormView. Ainsi la home page du PetShop est une vue UIP et peut donc être la cible d'une transition UIP.

Nous voilà prêts à configurer UIP en éditant le web.config du PetShopDNG. Pour l'instant, il s'agit principalement de déclarer Template.aspx comme vue, et de déclarer un graphe de navigation, en l'occurrence nous modélisons le fait que la home page permet de revenir à elle même. Pour le reste nous conservons les paramètres "par défaut" d'UIP, dûment importés par copier/coller depuis une des nombreuses applications d'exemple fournies avec UIP.

<configSections>
        <section name="uipConfiguration"
		type="Microsoft.ApplicationBlocks.UIProcess.UIPConfigHandler,Microsoft.ApplicationBlocks.UIProcess, Version=1.0.0.0,Culture=neutral,PublicKeyToken=null" />
</configSections>

<uipConfiguration enableStateCache="true" allowBackButton="false"> 
    <objectTypes>        
        <iViewManager 
            name="WebFormViewManager"
            type="Microsoft.ApplicationBlocks.UIProcess.WebFormViewManager, Microsoft.ApplicationBlocks.UIProcess, Version=1.0.0.0,Culture=neutral,PublicKeyToken=null"/>        
        <state 
            name="State" 
            type="Microsoft.ApplicationBlocks.UIProcess.State, Microsoft.ApplicationBlocks.UIProcess, Version=1.0.0.0,Culture=neutral,PublicKeyToken=null"/>            
        <controller 
            name="ShopController" 
            type="PetShopDNG.StoreController, PetShopDNG, Version=1.0.0.0,Culture=neutral,PublicKeyToken=null" />            
        <statePersistenceProvider 
            name="Session"  
            type="Microsoft.ApplicationBlocks.UIProcess.SessionStatePersistence, Microsoft.ApplicationBlocks.UIProcess, Version=1.0.0.0,Culture=neutral,PublicKeyToken=null"/>
    </objectTypes>
    
    <views>
        <!-- La seule transition qui nous préoccupe pour l'instant : de la home page
             du PetShop on peut revenir ... à la home page. Une vue ne peut pas
             être déclarée sans contrôleur, on en créé donc un
        -->
        <view name="home" type="Template.aspx" controller="ShopController" />
    </views>
    
    <navigationGraph 
            iViewManager="WebFormViewManager"
            name="PetShopping" 
            state="State" 
            statePersist="Session"
            startView="home"
            cacheExpirationMode="Absolute"
            cacheExpirationInterval="12:00:00">

        <!-- La seule transition qui nous préoccupe pour l'instant : de la home page
             du PetShop on peut revenir ... à la home page                        -->
        <node view='home'>
            <navigateTo navigateValue="gohome" view="home" />
        </node>

    </navigationGraph>
</uipConfiguration>

Comme vous le voyez, on ne peut pas déclarer de vue UIP sans lui associer un contrôleur. Nous créons donc une classe StoreController héritant de ControllerBase. Ce contrôleur expose une méthode ShowHome provoquant une transition :

public void ShowHome() {
    this.Navigate("gohome");
}

Notez que le nom de la transition passé à Navigate() est le même que celui que nous avons spécifié au niveau du graphe de navigation dans le fichier de configuration. Il ne nous reste plus qu'à appeler cette méthode lorsque l'utilisateur clique sur le "logo au perroquet" en haut à gauche de la page, ce qui se fait en éditant l'event handler du bouton btnLogo dans le contrôle Banner.ascx :

private void btnLogo_Click(object sender, System.Web.UI.ImageClickEventArgs e) {
    ((StoreController)((WebFormView)this.Page).Controller).ShowHome();
}

Notez la multiplication des transtypages, qui deviendra vite insupportable. Il nous faudra trouver un remède à cela.

Tout est maintenant prêt pour tester ce mini-refactoring. Reste cependant une question cruciale : comment démarre un graphe de navigation ? Autrement dit comment se passe l'instanciation du graphe en une tâche ? La réponse est que la classe utilitaire UIPManager permet justement d'instancier un graphe à partir de ce nom. Ce qui signifie que nous devons modifier l'event handler du lien "enter the store" dans la page d'index de la manière suivante :

private void RedirectToMainPage(){
    UIPManager.StartNavigationTask("PetShopping");
}

Un petit test rapide et concluant, et nous voilà mis en confiance et armés de courage pour affronter les modifications sérieuses qui s'imposent dans le code source du PetShopDNG.

Caveats

Pour ceux qui comme moi ont sur leur PC des versions d'ASP.NET postérieures à 1.1, faites bien attention à ce que la version du runtime utilisée par l'application web PetShopDNG soit bien 1.1.4333, car UIP ne semble pas fonctionner avec ASP.NET 2.0 (je n'ai pas creusé le sujet, il y a sans doute moyen d'y arriver). Autre remarque : il est impératif dans la configuration UIP que les noms de types soient complètement qualifiés sous la forme <Type>, <Assembly>, Version=<version>, Culture=<culture>, PublicKeyToken=<token>, et ce même si vos assemblies ne sont pas signés.

Des contrôleurs aux services

Le premier nettoyage auquel je vais me livrer consiste à tordre le cou aux contrôleurs DNG (désolé) en les renommant "services". Ceci pour une raison bien simple : si je ne le fais pas, l'application aura à la fois des contrôleurs UIP (héritant de ControllerBase) et des contrôleurs DNG. Confusion garantie. Il ne s'agit cependant pas d'une mise en conformité de l'architecture du PetShop au modèle SOA telle qu'a pu la proposer Julien Brunet dans cet article, mais d'un bête renommage.

Suite à cela je créé un nouveau projet de type "Class Library" pour héberger mes contrôleurs. Je souhaite en effet faire en sorte que la logique applicative soit réutilisable par différents front-ends (bien qu'une version Windows Forms du PetShop n'ait aucun intérêt ! Mais l'exercice n'en demeure pas moins intéressant). Nommons ce projet PetShopDNG.AppLogic et attelons-nous à exprimer la navigation du PetShop dans le fichier de configuration d'UIP.

Configuration

Il n'y a rien de bien difficile à cela, et il serait plus ennuyeux qu'instructif de lister ici l'ensemble des transitions possibles. Voici un petit extrait de la section "navigationGraph" du fichier de configuration du PetShopDNGUIP :

<node view='home'>
   <navigateTo navigateValue="gohome" view="home" />
</node>
<node view="signon">
   <navigateTo navigateValue="newuser" view="register" />
   <navigateTo navigateValue="existinguser" view="logon" />
</node>
<node view="accountinfo">
   <navigateTo navigateValue="edit" view="editaccount" />
</node>

Ce graphe de navigation n'est pas encore complet car nous n'y avons pas exprimé les transitions globales, valables sur toutes les vues du site (par exemple depuis la vue 'signon' nous pouvons revenir à la page d'accueil). Pour cela nous mettons à profit les "shared transitions" : une shared transition est une transition qui est valable quelle que soit le noeud du graphe. Il s'agit d'un moyen fort pratique de spécifier une fois pour toutes que toutes les pages du site permettent de se rendre à la page de visualisation de contenu du panier d'achats, par exemple.

<sharedTransitions>
  <sharedTransition navigateValue="gohome" navigateTo='home'/>
  <sharedTransition navigateValue="signon" navigateTo='signon' />
  <sharedTransition navigateValue="signout" navigateTo='signout' />
  <sharedTransition navigateValue="showcart" navigateTo='cartcontents' />
  <sharedTransition navigateValue="myaccount" navigateTo='accountinfo' />
  <sharedTransition navigateValue="selectcategory" navigateTo='products' />
  <sharedTransition navigateValue="search" navigateTo='items' />
</sharedTransitions>

Ces transitions partagées correspondent ni plus ni moins aux transitions provoquées par des éléments graphiques communs, c'est-à-dire faisant partie du template de page.

Jusqu'ici nous avons manipulé des vues dans le fichier de configuration sans pour autant les avoir définies. La prochaine étape de notre "uipfication", et pas la plus anodine, consiste justement à définir ces vues.

Gestion des vues templatées

Ce faisant, nous sommes rapidement confrontés au fait que UIP "sorti de la boîte" est pensé pour fonctionner avec des web forms (pages ASPX) et non des user controls (ASCX). Or dans le cas du PetShop, toutes les vues logiques sont physiquement servies par la même page, Template.aspx.

Ce qui ne pose pas de problème particulier du moment que l'on est capable, à l'issue de chaque transition, d'informer template.aspx de quel contrôle utilisateur elle doit afficher.

Plusieurs solutions s'offrent à nous, je vous en propose quatre :

Passage de paramètres par la query string

Qui dit passage de paramètres de page en page dit query string. Au prix d'une légère adaptation de Template.aspx, nous allons pouvoir déclarer nos vues de la manière suivante dans le fichier de configuration d'UIP :

<view name="home" type="Template.aspx" controller="..." />
<view name="accountinfo" type="Template.aspx?view=Customer" controller="..." /> 
<view name="signon" type="Template.aspx?view=Signon" controller="..." /> 

Nous n'aurons qu'à supposer que la valeur du paramètre "view" correspond au nom du contrôle utilisateur à afficher.

Utilisation de la propriété State.CurrentView

Une autre approche nettement plus subtile consiste à exploiter au maximum les possibilités d'UIP, en particulier de la classe State, qui expose le nom de la vue courante au moyen de la propriété CurrentView. Il va donc nous être possible de s'en tenir à un fichier de configuration de la forme suivante :

<view name="Body" type="Template.aspx" controller="..." />
<view name="Customer" type="Template.aspx" controller="..." /> 
<view name="Signon" type="Template.aspx" controller="..." /> 

Template.aspx n'aura qu'à consulter la valeur de Controller.State.CurrentView et faire l'hypothèse qu'à cette valeur correspond un nom de contrôle ASCX pour déterminer quel contrôle afficher.

URL Rewriting

Enfin une technique avancée consisterait à mettre en place un mécanisme de réécriture d'URL (URL Rewriting) pour faire correspondre des chemins virtuels à des pages physiques. En mettant en oeuvre un tel mécanisme, le fichier de configuration aurait typiquement l'apparence suivante :

<view name="home" type="/content/body.aspx" controller="..." />
<view name="accountinfo" type="/content/customer.aspx" controller="..." /> 
<view name="signon" type="/content/signon.aspx" controller="..." /> 

En deux mots l'URL rewriting consisterait à écrire ou réutiliser un module HTTP s'abonnant à l'évènement BeginRequest de l'application web. Ce module extrairait de l'URL (de la forme /content/customer.aspx) la partie variable (ici customer) et récrirait l'URL par un appel à Context.RewritePath("Template.aspx"), ce qui provoquerait l'exécution de template.aspx.
Le module pourrait lui véhiculer le nom du contrôle à charger (en l'occurrence customer) par un élément de contexte (collection HttpContext.Current.Items)

Si le principe est simple, l'implémentation n'est pas pour autant triviale dès lors qu'on souhaite mettre en place un moteur de rewriting avec lequel les postbacks continuent de fonctionner ! Ne rentrons pas plus dans les détails, Scott Mitchell vous dira quasiment tout sur le sujet dans l'article suivant : http://msdn.microsoft.com/asp.net/using/building/web/default.aspx?pull=/library/en-us/dnaspp/html/urlrewriting.asp

Customisation d'UIP

Enfin, il serait également possible de résoudre notre problème en développant un gestionnaire de vues (ViewManager) personnalisé, assorti de vues (IView) personnalisées. Il s'agirait là d'un chantier relativement ambitieux que je me garderai de démarrer ici afin de ne pas s'écarter des objectifs de cet article.

Approche retenue

J'ai choisi la première approche pour ce refactoring, non parce qu'il s'agit de la plus élégante mais car elle est la plus explicite et ainsi la plus facile à comprendre. Encore une fois, ma démarche est plus d'illustrer la mise en oeuvre d'UIP d'un exemple concret que d'établir une nouvelle version de référence du PetShop.

Nous devons donc modifier Template.aspx pour analyser le paramètre passé par la query string et rendre le bon contrôle visible. Profitons de l'occasion pour éliminer PortalContent.ascx et inclure le DynamicPlaceHolder de Denis Bauer dans la page template.aspx :

Je rappelle que l'intérêt de ce contrôle par rapport au placeholder ASP.NET est qu'il conserve sa collection de contrôles entre les postbacks. Il se manipule cependant comme le placeholder "classique", template.aspx prend donc maintenant l'allure suivante :

protected override void OnLoad(System.EventArgs e)
{
    base.OnLoad (e);
    System.Diagnostics.Debug.WriteLine("Current view = " + this.Controller.State.CurrentView);
    if(!IsPostBack)
    {
        string viewName;
        viewName = Request["view"];
        if(viewName == null)
             viewName = "Body";
        viewName = string.Format("UserControls/Views/{0}.ascx",viewName);
        Control c = LoadControl(viewName);
        ctrlContent.Controls.Clear();
        ctrlContent.Controls.Add(c);
        DataBind();
    }
}

Le sort des vues étant réglé et la configuration du graphe de navigation terminée, il ne nous reste plus qu'à nous occuper des contrôleurs et de l'état (modèle).

Contrôleurs et état

Mettre en place un ou plusieurs contrôleurs revient à extraire la logique des commandes du PetShopDNG 2.0 et la reformuler sous forme de méthodes dans des sous-classes de ControllerBase. Ce qui pose une première question : combien de contrôleurs devons nous créer ?
Le chiffre précis dépend de la façon dont on souhaite partitionner les responsabilités fonctionnelles au sein de la couche de logique applicative. Il y a donc plusieurs façons de voir ce découpage, néanmoins je suis sûr d'une chose : nous ne voulons certainement pas avoir qu'un seul contrôleur dans l'application. Celui-ci serait bien trop volumineux et difficile à maintenir.
Soit, nous aurons donc plusieurs contrôleurs. J'ai décidé pour les besoins de la démonstration d'en introduire trois : le UserController qui expose les fonctions liées à la gestion du profil utilisateur (authentification, modification du profil, etc ...), le CartController s'occupant de la logique de gestion du panier d'achats, et le StoreController, responsable de la logique de navigation au sein de la boutique.

En mettant en place plusieurs contrôleurs, nous introduisons un problème auquel nous n'aurions pas été confrontés avec un contrôleur unique : ce problème est lié au fait que les vues du PetShop sont par nature composites, et d'autre part héritent leur composition d'un template commun.

Gestion des vues composites

En effet, considérons un extrait de la configuration maintenant mise en place une fois nos trois contrôleurs créés :

<views>
    <view name="home" type="Template.aspx" controller="ShopController" />
    <view name="editaccount" type="Template.aspx?view=ExistingUserSignon" controller="UserController" /> 
    <view name="cartcontents" type="Template.aspx?view=Cart" controller="CartController" />
</views>

Et tentons d'implémenter le contrôle "Banner.ascx" qui est inclus dans toutes les vues du site. Dans celui-ci, nous retrouvons le "logo au perrroquet", qui lorsque on le sélectionne nous ramène à la page d'accueil :

private void btnLogo_Click(object sender, System.Web.UI.ImageClickEventArgs e) {
    ((???Controller)((WebFormView)this.Page).Controller).ShowHome();
}

Comme vous le voyez, il est bien difficile de savoir vers quel type de contrôleur nous devons caster la propriété Controller ! En effet, tout dépend de l'endroit où l'on se trouve dans le site au moment où l'on clique sur le perroquet ! Si l'on se trouve sur la page de visualisation du panier, alors le contrôleur associé à la page sera un CartController, alors qu'à d'autres endroits il pourra s'agir de ShopController ou de UserController.

Pour résoudre ce petit problème, soit nous ajoutons la méthode ShowHome() dans tous les contrôleurs, ce qui n'est assurément pas la bonne solution, soit nous faisons en sorte de connaître à l'avance le type du contrôleur en imposant aux contrôleurs d'hériter d'une même classe, PetShopControllerBase, approche qui a été retenue ici. Ce PetShopControllerBase contient toutes les fonctions qui, à l'instar du bouton de retour à la page d'accueil, sont présentes sur toutes les vues du site.

En d'autres termes, la réutilisation de modules présentationnel par "héritage" visuel a tout simplement imposé une factorisation des fonctionnalités déclanchées par ces modules par héritage des contrôleurs d'une seule classe de base.

Au passage, nous remarquons que nous sommes confrontés à un problème de détail déjà identifié plus haut : le fait d'utiliser des contrôles utilisateur d'une part, UIP d'autre part conduit à une prolifération des transtypages et donc à des event handlers quasiment illisibles. En introduisant une classe de base pour tous nos contrôles utilisateurs, exposant elle-même une propriété Controller de type PetShopControllerBase, le code devient plus léger. Cette classe de base PetShopControl est définie comme suit :

public class PetShopControl : System.Web.UI.UserControl {
    public PetShopControllerBase Controller
    {
        get
        {
            return ((WebFormView)Page).Controller as PetShopControllerBase;
        }
    }
}

Etat

La plupart des problèmes sont maintenant résolus et nous sommes prêts à implémenter les contrôleurs "au kilomètre" par migration des commandes DNG. Ce faisant nous allons mettre à contribution l'état fourni par UIP via la classe State. Ce qui pose une ultime question : que faire du WebLocalSingleton ?

Réponse rapide : rien du tout ! Les contrôleurs manipulent directement les variables d'état de l'objet state qui leur est associé de la manière suivante :

//Exemple de méthode définie au niveau du CartController
public void AddItemToCart(string itemId)
{
    PetShopDNG.DAL.IShoppingCart cart = this.State["CurrentCart"];
    if (cart != null)
    {
        cart.Add(_factory.SearchService.FindItemById(itemId));
        ShowCart();
    }
    else SignOn();
}

Réponse élégante : on créé une classe d'état fortement typée en héritant de la classe State d'UIP. Cette nouvelle classe, que nous nommerons par exemple PetShopState, possède des propriétés telles que CurrentShoppingCart définie comme suit :

[Serializable]
public class PetShopState : State {
    private IShoppingCart _currentShoppingCart;

    public IShoppingCart CurrentShoppingCart
    {
        get
        {
            return _currentShoppingCart;
        }
        set
        {
           _currentShoppingCart = value; 
           if(null != StateChanged)
              StateChanged(this,new StateChangedEventArgs("CurrentShoppingCart"));
        }
    }
}

Cette classe remplaçant admirablement le WebLocalSingleton, nous terminons le refactoring sans difficulté.

Essai

Et à notre grande déception, l'application ne fonctionne pas ! Le symptôme est le suivant : l'application n'arrive pas à passer d'une vue à l'autre. Une petite session de déboguage nous montre que la propriété CurrentView de l'objet State reste désespérément bloquée sur "home", quelle que soit la transition que nous essayons de provoquer.

Identification du mal

Si nous ne voyons pas de transition de pages dans le PetShop, c'est qu'UIP ne parvient jamais jusqu'au Response.Redirect(). UIP doit probablement prendre une décision qui empêche la transition visuelle. Grâce au débogueur, on se convainc rapidement que c'est au niveau du WebFormViewManager que se situe le problème.

Ce que fait le WebFormViewManager

En effet, un view manager (implémentation de l'interface IViewManager) fournit trois services fondamentaux :

A quoi sert cette dernière méthode ? Elle est tout simplement utilisée par UIP pour déterminer si le bouton "Back" du navigateur a été utilisé ou non. En effet, admettons que d'une page A.aspx, vous cliquiez sur un bouton qui provoque une transition d'UIP vers B.aspx. Au moment où B.aspx s'affiche, la vue courante est B. Si vous utilisez le bouton "Back" du navigateur et recliquez sur le bouton, un postback est provoqué dans A.aspx. UIP reçoit donc une requête HTTP "/A.aspx", et constate que A.aspx correspond à la vue A qui n'est pas égale à la vue courante (B). Ce constat est justement fait par IsRequestCurrentView. UIP est donc en mesure d'affirmer qu'une manipulation de type "Back" a eu lieu.

Dans ce cas, le comportement adopté par UIP dépend du paramètre de configuration "allowBackButton" qui a été renseigné au niveau du noeud <navigationGraph> :

C'est au Navigator qu'est dévolue cette tâche de déterminer si le bouton "Back" a été utilisé ou non, et donc de sélectionner le bon contrôleur pour la vue courante :

private ControllerBase GetControllerForView(IView view)
{
    string viewForController=view.ViewName;                
    
    if (! _viewManager.IsRequestCurrentView(view,_state.CurrentView))    
    {    
        if (UIPConfiguration.Config.AllowBackButton)                                
        {
            viewForController = _viewManager.GetViewNameForCurrentRequest(view);
            if (RunningInNavGraph)
            {
                if (UIPConfiguration.Config.ViewExistsInNavigationGraph(_state.NavigationGraph,viewForController))                        
                    _state.CurrentView = viewForController;                        
            }    
            else
            {
                _state.CurrentView = viewForController;
            }
        }                    
    }

    return ControllerFactory.Create(viewForController,this);            
}

Dans notre cas, nous utilisons le WebFormViewManager, et celui ci implémente IsRequestCurrentView de la manière suivante :

public bool IsRequestCurrentView( IView view, string stateViewName )
{
    //  get state currentview; must all match
    ViewSettings viewSettings = UIPConfiguration.Config.GetViewSettingsFromName(stateViewName);
    if( viewSettings == null )
        throw new UIPException( Resource.ResourceManager.FormatMessage( Resource.Exceptions.RES_ExceptionViewConfigNotFound, stateViewName ) );
    
    string stateViewType = viewSettings.Type;
    
    System.Web.UI.Page page = (System.Web.UI.Page)view;
    string viewType = page.Request.CurrentExecutionFilePath.Replace( page.Request.ApplicationPath + "/", "");            

    return string.Compare(viewType,stateViewType,true,System.Globalization.CultureInfo.InvariantCulture)==0;            
}

Le problème vient du fait qu'UIP utilise la propriété CurrentExecutionFilePath pour déterminer la vue courante. Or cette propriété est totalement indépendante de la query string. Ainsi si le browser demande "/Template.aspxview=Signon", UIP va trouver "Template.aspx" comme CurrentExecutionFilePath. Ainsi quelle que soit la requête, quel que soit l'état courant, UIP croit que nous demandons la home page et IsRequestCurrentView retourne "faux". En bref UIP croit systématiquement que nous utilisons le bouton "Back" !

Qu'à cela ne tienne, affectons la valeur "true" à la propriété de configuration "allowBackButton", et UIP provoquera quand même la transition à la vue cible. Malheureusement cette "solution" n'en est pas une, car GetViewNameForCurrentRequest souffre du même défaut :

public string GetViewNameForCurrentRequest( IView currentView ) {
    System.Web.UI.Page currentPage = (System.Web.UI.Page)currentView;                
    string viewType = currentPage.Request.CurrentExecutionFilePath.Replace( currentPage.Request.ApplicationPath + "/", "");            
    string viewName = UIPConfiguration.Config.GetViewSettingsFromType(viewType).Name;            

    return viewName;
}

Ainsi, même en demandant "/Template.aspx?view=Cart", UIP va décider qu'il s'agit de la home page et ainsi associer à la vue courante le contrôleur de cette home page, qui n'est pas celui que nous souhaitons.

La solution : customiser le gestionnaire de vues

Nous constatons que le problème est lié à de bonnes décisions (celles faites par le Navigator) prises sur la base de fausses informations (fournies par le WebFormnViewManager). Il nous fait donc personnaliser la gestion des vues en développant notre propre IViewManager.

Pour cela nous pourrions bien sûr dupliquer le code source du WebFormViewManager et apporter les quelques corrections qui s'imposent. J'ai préféré procéder par héritage de WebFormViewManager et redéfinitions des méthodes à corriger. Redéfinition ? Ces méthodes ne sont pourtant pas marquées comme virtuelles ! Certes pas, c'est pourquoi j'ai mis à profit l'implémentation explicite des méthodes de l'interface IViewManager pour masquer au Navigator les méthodes définies par le WebFormViewManager.

Pour corriger les deux méthodes fautives, il suffit de remplacer Request.CurrentExecutionFilePath par Request.RawUrl qui contiendra bien les informations de query string. On obtient ainsi un PetShopViewManager flambant neuf :

/// <summary>
/// ViewManager personnalisé pour le PetShop
/// </summary>
public class PetShopViewManager:WebFormViewManager,IViewManager
{
    /// <summary>
    /// La requête HTTP qui a provoqué l'exécution du web form courant
    /// correspond-elle à la vue courante ?
    /// </summary>
    bool IViewManager.IsRequestCurrentView( IView view, string stateViewName ) {
        ViewSettings viewSettings = UIPConfiguration.Config.GetViewSettingsFromName(stateViewName);

        if( viewSettings == null )
            throw new UIPException( "" );//Resource.ResourceManager.FormatMessage( Resource.Exceptions.RES_ExceptionViewConfigNotFound, stateViewName ) );
        
        //Returns something othe the form Template.aspx?view=Signon
        string stateViewType = viewSettings.Type;
        
        System.Web.UI.Page page = (System.Web.UI.Page)view;

        //Nous utilisons la propriété RawUrl plutôt que CurrentExecutionFilePath
        string viewType = page.Request.RawUrl.Replace( page.Request.ApplicationPath + "/", "");
    
        return string.Compare(viewType,stateViewType,true,System.Globalization.CultureInfo.InvariantCulture)==0;                    
    }

    /// <summary>
    /// Détermination du nom de la vue courante pour le web form en
    /// cours d'exécution
    /// </summary>
    string IViewManager.GetViewNameForCurrentRequest( IView currentView ) 
    {
        System.Web.UI.Page currentPage = (System.Web.UI.Page)currentView;                

        //Nous utilisons la propriété RawUrl plutôt que CurrentExecutionFilePath
        string viewType = currentPage.Request.RawUrl.Replace( currentPage.Request.ApplicationPath + "/", "");            
        string viewName = UIPConfiguration.Config.GetViewSettingsFromType(viewType).Name;            

        return viewName;
    }
}

Une fois enregistré dans le fichier de configuration, nous constatons que le PetShopDNGUIP fonctionne enfin !

Conclusion

Nous avons finalement obtenu le résultat suivant : un PetShopDNG 2.0 "UIP" isofonctionnel au PetShopDNG 2.0 (enfin je l'espère, mais je ne garantis pas qu'il soit exempt de bugs !).

J'espère vous avoir convaincu que la mise en place d'UIP dans un projet d'envergure raisonnable est tout sauf transparente pour les concepteurs et développeurs. UIP impose non seulement d'acquérir la connaissance de son modèle de programmation, mais également du détail de son fonctionnement, faute de quoi nous aurions été incapables de résoudre voire même d'identifier précisément certains problèmes posés par l'architecture du PetShopDNG. UIP induit donc sans aucun doute une certaine charge de travail pour sa mise en place.

Moyennant quoi les bénéfices liés à son utilisation sont nombreux : UIP nous apporte beaucoup de fonctionnalités qui étaient absentes de la version originale du PetShopDNG 2.0 (la persistance de l'état dans différents entrepôts, la capacité de transférer une tâche - c'est-à-dire une navigation - d'un utilisateur à un autre, le contrôle d'intégrité du graphe de navigation, la réutilisabilité des contrôleurs avec des écrans Windows Forms ...) tout en ayant contribué à architecturer notre application de manière particulièrement claire, structurée, et facilement maintenable (ce qui dans le cas présent n'est pas un bénéfice évident dans la mesure où l'architecture précédente était déjà propre et réfléchie).

Plus généralement je crois qu'un framework tel que UIP devra pour s'imposer être directement inscrit dans l'outillage de développement. Le modèle de programmation serait alors naturellement supporté par l'IDE et libèrerait le programmeur des turpitudes de la configuration d'UIP. Ce n'est peut-être pas le rôle de VisualStudio d'apporter cette structure architecturale, mais il y aurait au moins de la place pour un add-on UIP.

D'autre part, UIP souffre encore de défauts de jeunesse (dont nous avons fait les frais !) qui ne peuvent être contournés qu'au prix d'astuces techniques que l'on préfèrerait éviter. Nous avons notamment vu dans cet article qu'UIP s'accommodait assez mal des vues composites et de l'URL rewriting en général.

UIP me paraît néanmoins être de très loin la contribution la plus sérieuse sur la thématique "MVC et ASP.NET", et j'ai la faiblesse de croire qu'une prochaine version basée sur .NET 2.0 gommera ces légers défauts. Toutes les conditions seront alors réunies pour faire d'UIP un "first class citizen" de la panoplie des outils de développement .NET.

Auteur : Sébastien Bouchet 

Copyright © Octobre 2004


Qui est Sébastien Bouchet ?

A l'origine de culture J2EE, Sébastien Bouchet est architecte expert .NET chez Linkvest.

Quelques mots sur Linkvest

Linkvest est un intégrateur de systèmes basé à Lausanne et disposant de bureaux à Genève et Paris. Linkvest est depuis ses origines un pionnier des technologies distribuées (CORBA, .NET, J2EE) et fournit avec succès des services d'intégration d'application d'entreprise aux banques, industries et telecoms depuis 1984. Linkvest est l'un des leaders en Suisse dans la mise en oeuvre de solutions .NET innovantes.

 

Téléchargez le code source de l'article

Solution VS.NET : PetShopDNGUIP.zip

Ressources

Le workspace d'UIP sur gotdotnet : http://www.gotdotnet.com/workspaces/workspace.aspx?id=0af2b0ef-b049-401a-a2f2-f55a070c1572

UIP sur MSDN : http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnpag/html/uipab.asp

Le site de Denis Bauer : http://www.denisbauer.com/

Articles sur les PetShops sur DNG :

http://www.dotnetguru.org/articles/PetShop/ReviewsPetShop/ReviewPetShops.htm

http://www.dotnetguru.org/articles/PetShop/mspetshop3/petshop32review.htm

http://www.dotnetguru.org/articles/PetShop/PetShopDNGV2/PetShop_20.htm