| Introduction | |
|---|---|
| Premiers pas avec les contrôles dynamiques dans ASP.NET | |
| Construction du moteur d'interface utilisateur dynamique pour la saisie de données | |
| Conclusion |
Introduction
Lors de la création de sites Web pilotés par des données, l'une des tâches les plus courantes pour un développeur Web est de créer des formulaires de saisie de données. Un formulaire de saisie de données est une page Web qui permet aux utilisateurs du système d'entrer des informations. La création d'un formulaire spécifique commence généralement par une analyse des besoins : déterminer exactement quelles informations doivent être collectées auprès de l'utilisateur. Une fois les besoins identifiés, l'étape suivante consiste à concevoir le formulaire Web, notamment en créant l'interface graphique et en écrivant le code qui met à jour la base de données en fonction des entrées de l'utilisateur.
Lorsque les besoins du formulaire sont connus à l'avance et que ce formulaire est identique pour tous les utilisateurs du système, la création d'un tel formulaire ne présente aucune difficulté. En revanche, si le formulaire de saisie doit être dynamique, la tâche devient beaucoup plus complexe. Par exemple, imaginez une application Web d'entreprise qui collecte des informations sur les produits achetés par les clients : un système d'enregistrement de produits en ligne. Dans une telle application, les questions posées à l'utilisateur peuvent varier selon le produit acheté, ou selon que l'achat a été effectué en magasin ou sur le site Web de l'entreprise.
Comme dans l'exemple ci-dessus, une solution possible pour fournir une interface utilisateur dynamique de saisie de données est d'« imposer » une solution. Vous pourriez créer une page Web distincte pour chaque produit vendu par l'entreprise, chacune contenant les éléments de saisie spécifiques nécessaires. Le problème avec cette approche rudimentaire est que chaque nouveau produit nécessite l'ajout d'une nouvelle page. Bien que la création de ces nouvelles pages ne soit pas forcément difficile, elle est chronophage et sujette à des erreurs si elle n'est pas suffisamment testée et déboguée.
Idéalement, lorsqu'un nouveau produit est lancé, une personne non technique devrait pouvoir spécifier, via une interface Web conviviale, les questions à poser. Un tel système est réalisable avec ASP.NET grâce à sa capacité à charger dynamiquement des contrôles dans une page Web au moment de l'exécution. Avec un petit investissement initial en développement et en tests, vous pouvez créer un moteur d'interface utilisateur dynamique réutilisable. Même des utilisateurs ayant peu de connaissances en informatique peuvent ainsi créer facilement des formulaires de saisie personnalisés. Dans cet article, nous présenterons les bases des contrôles dynamiques dans ASP.NET, puis un système complet et opérationnel de saisie dynamique de données, facilement personnalisable et extensible.
Comme on le sait, une page Web ASP.NET se compose de deux parties :
| • | La partie HTML, qui contient du balisage HTTML statique et des contrôles Web, ajoutés de manière déclarative. |
|---|---|
| • | La partie code, qui peut être implémentée comme un fichier de classe séparé (par exemple avec Visual Studio .NET) ou incluse dans un bloc <script runat="server"> du fichier HTML. |
Les contrôles Web d'une page ASP.NET sont ajoutés au moment de la conception via une syntaxe déclarative, qui spécifie explicitement le contrôle Web à ajouter ainsi que ses valeurs initiales de propriétés, par exemple :
<asp:WebControlName
runat="server"
prop1="Value1"
prop2="Value2"
...
propN="ValueN">
</asp:WebControlName>
Un point important à comprendre est que lorsqu'une page ASP.NET est visitée pour la première fois, ou après que sa partie HTML a été modifiée, le moteur ASP.NET convertit automatiquement le mélange de contenu HTML statique et de syntaxe de contrôle Web en une classe. Cette classe générée automatiquement a pour rôle de créer une hiérarchie de contrôles. Cette hiérarchie est l'ensemble des contrôles qui composent la page – le balisage HTML statique est converti en instances de LiteralControl, et les contrôles Web en instances des types de classes correspondants (par exemple, une instance de la classe TextBox de l'espace de noms System.Web.UI.WebControls).
On parle de hiérarchie de contrôles car il s'agit d'une véritable hiérarchie de contrôles. Chaque contrôle serveur ASP.NET peut avoir un ensemble de contrôles enfants et un contrôle parent. Lorsque la classe générée automatiquement construit la hiérarchie, elle place l'instance de la classe Page représentant la page ASP.NET au sommet de la hiérarchie. Les contrôles enfants de la classe Page sont les contrôles serveur de premier niveau définis dans le HTML de la page (généralement du balisage HTML statique et le contrôle serveur Web Form). (Le Web Form d'une page ASP.NET – c'est-à-dire la balise <form runat="server"> – est implémenté comme une instance de la classe HtmlForm, que l'on trouve dans l'espace de noms System.Web.UI.HtmlControls).
Comme tout autre contrôle serveur, ce Web Form peut contenir des contrôles enfants. Les contrôles enfants du Web Form sont ceux qui se trouvent dans le Web Form lui-même. Même les contrôles du Web Form peuvent avoir leurs propres enfants : le contenu d'un contrôle Panel constitue ses contrôles enfants ; lorsque vous liez des données à un DataGrid, le contenu généré forme son ensemble de contrôles enfants. Comme la classe Page de premier niveau peut avoir des enfants, qui ont eux-mêmes des enfants, et ainsi de suite, l'ensemble des contrôles forme une hiérarchie.
Pour bien comprendre ce concept (essentiel pour utiliser les contrôles dynamiques), imaginez une page ASP.NET dont la partie HTML contient ce qui suit :
<html>
<body>
<h1>Bienvenue sur ma page d'accueil !</h1>
<form runat="server">
Quel est votre nom ?
<asp:TextBox runat="server" ID="txtNom"></asp:TextBox>
<br />Quel est votre sexe ?
<asp:DropDownList runat="server" ID="ddlSexe">
<asp:ListItem Select="True" Value="H">Homme</asp:ListItem>
<asp:ListItem Value="F">Femme</asp:ListItem>
<asp:ListItem Value="A">Non spécifié</asp:ListItem>
</asp:DropDownList>
<br />
<asp:Button runat="server" Text="Envoyer !"></asp:Button>
</form>
</body>
</html>
Lors de la première visite de cette page, une classe est générée automatiquement, contenant le code qui construit par programme la hiérarchie des contrôles. La hiérarchie de cet exemple est illustrée à la Figure 1.
Comme mentionné précédemment, chaque contrôle serveur ASP.NET peut contenir un ensemble de contrôles enfants et un contrôle parent. Les contrôles enfants sont accessibles via la propriété Controls du contrôle serveur, dont le type est ControlCollection. La classe ControlCollection offre les fonctionnalités suivantes :
| • | Utiliser la propriété en lecture seule Count pour déterminer le nombre de contrôles enfants. |
|---|---|
| • | Utiliser la méthode Add() ou AddAt() pour ajouter de nouveaux éléments à la collection. |
| • | Supprimer tous les contrôles enfants via Clear(), ou supprimer un contrôle spécifique avec Remove() ou RemoveAt(). |
Pour ajouter un contrôle en tant qu'enfant d'un contrôle X dans la hiérarchie, il suffit de créer l'instance de la classe correspondante et de l'ajouter à la collection Controls de X. Par exemple, pour ajouter un contrôle Label à la collection Controls de la classe Page, vous pouvez utiliser le code suivant :
' Crée une nouvelle instance de Label
Dim etiquette As New Label
' Ajoute le contrôle à la collection Controls de la page
Page.Controls.Add(etiquette)
' Définit la propriété Text du Label avec la date/heure actuelle
etiquette.Text = DateTime.Now
Ajouter un contrôle à la fin de la collection Controls de la Page le fera apparaître en bas de la page Web. Si vous avez besoin de plus de contrôle sur l'emplacement où apparaissent les contrôles dynamiques, vous pouvez ajouter sur la page un contrôle Web PlaceHolder, qui spécifie un emplacement dans la hiérarchie où vous souhaitez ajouter un ou plusieurs contrôles dynamiques. Pour ajouter des contrôles dynamiques à cet endroit, il suffit de les ajouter à la collection Controls du PlaceHolder. Par exemple, si vous voulez placer un Label à un point précis du Web Form, vous pouvez ajouter un contrôle PlaceHolder comme suit :
<html>
<body>
...
<form runat="server">
...
<asp:PlaceHolder runat="server" id="placeHolderDate"></asp:PlaceHolder>
...
</form>
</body>
</html>
Pour ajouter le Label dynamique dans l'exemple précédent, au lieu d'utiliser Page.Controls.Add(etiquette), vous devez utiliser placeHolderDate.Controls.Add(etiquette), ce qui ajoute le Label à la collection Controls du PlaceHolder, et non à celle de la Page. La Figure 2 illustre la hiérarchie des contrôles avant et après l'ajout du Label dynamique au PlaceHolder.
Lors d'une publication (postback), le chargement de l'état de vue est effectué en sens inverse : chaque contrôle charge l'état de vue de ses enfants en énumérant les informations d'état de vue et en les appliquant à la position spécifiée dans la collection Controls. Si vous insérez un contrôle à une position autre que la fin de la collection Controls avant que l'état de vue ne soit chargé, des problèmes surviennent, car les informations d'état de vue de chaque enfant sont liées à un index spécifique dans la collection Controls.
Pour comprendre pourquoi l'ajout à une position non finale peut causer des problèmes lors du rechargement de l'état de vue, examinez la Figure 3. Cette figure montre un contrôle serveur p qui a trois contrôles enfants : c0, c1 et c2, où le contrôle c1 a un état de vue qui persiste lors des publications. Si, lors d'une publication, vous ajoutez un contrôle dynamique c au début de la collection Controls de p, lorsque l'état de vue sera rechargé, p tentera de recharger l'état de vue de c1 à l'index 1, qui est maintenant occupé par c0.
Accès aux contrôles ajoutés dynamiquement
Lorsque vous ajoutez des contrôles Web statiques à une page ASP.NET, Visual Studio .NET ajoute automatiquement des références à ces contrôles dans la classe code-behind. Ces références permettent un accès fortement typé aux propriétés, méthodes et événements du contrôle. Lorsque vous travaillez avec des contrôles ajoutés dynamiquement, vous pouvez utiliser deux techniques pour accéder à leurs propriétés, méthodes et événements.
Une première méthode consiste à parcourir complètement la hiérarchie des contrôles pour trouver les contrôles dynamiques. Par exemple, le code suivant montre comment parcourir récursivement la hiérarchie des contrôles à partir d'un contrôle racine spécifié. Cela peut être utile si vous avez ajouté un grand nombre de contrôles DropDownList de manière dynamique dans un PlaceHolder. Dans ce cas, vous pouvez appeler ParcourirHierarchie(ControlePlaceHolder) pour énumérer les descendants du PlaceHolder, et vérifier si le type du contrôle courant est DropDownList, puis agir en conséquence.
Private Sub ParcourirHierarchie(ByVal c As Control)
' Faire ce qui est nécessaire avec le contrôle courant c
' Parcourir récursivement les contrôles enfants de c
For Each enfant As Control In c.Controls
ParcourirHierarchie(enfant)
Next
End Sub
Cette approche fonctionne si vous avez un grand nombre de contrôles serveur similaires à traiter collectivement. Mais dans de nombreux cas, vous aurez différents contrôles que vous devez accéder individuellement à différents moments et effectuer des opérations différentes sur chacun. Pour manipuler par programme un contrôle dynamique spécifique, vous pouvez utiliser la méthode FindControl(ID), qui recherche un contrôle par son ID. La méthode FindControl() est définie dans la classe System.Web.UI.Control, donc tous les contrôles serveur, du TextBox au PlaceHolder en passant par le Web Form, en disposent.
L'appel de FindControl() sur un contrôle ne nécessite pas de parcourir tous les descendants de ce contrôle. FindControl() ne recherche que dans le conteneur de noms (naming container) courant. Les contrôles qui implémentent INamingContainer se comportent comme des conteneurs de noms, ce qui signifie qu'ils créent leur propre espace de noms d'ID dans la hiérarchie. Par exemple, un DataGrid est un conteneur de noms. Pour un DataGrid dont l'ID est monDataGrid, les ID de ses contrôles enfants sont préfixés par l'ID du parent, comme monDataGrid:IDenfant. Il est important de comprendre que FindControl() ne parcourt que l'ensemble des contrôles enfants du conteneur de noms, et non tous les descendants du contrôle parent. (De plus, pour rechercher au-delà du premier niveau de contrôles dans le conteneur de noms, vous devez utiliser un ID correctement qualifié.) En résumé, lorsque vous utilisez FindControl() pour localiser un contrôle dynamique, appelez FindControl() à partir du contrôle parent du contrôle dynamique (généralement un contrôle PlaceHolder).
Lorsque vous utilisez FindControl(), vous pouvez attribuer un ID unique au contrôle dynamique lors de son ajout, puis le référencer comme suit :
' Lors de l'ajout du contrôle, définissez la propriété ID
Dim zoneTexte As New TextBox
placeHolderDynamique.Controls.Add(zoneTexte)
zoneTexte.ID = "zoneTexteDynamique"
' Plus tard dans le cycle de vie de la page,
' référencez le TextBox dynamique
Dim zoneTexteRef As TextBox
zoneTexteRef = CType(placeHolderDynamique.FindControl("zoneTexteDynamique"), TextBox)
Comme la méthode FindControl() utilise l'ID du contrôle pour le localiser, il est important d'attribuer une valeur unique et identifiable à la propriété ID de chaque contrôle dynamique. La méthode utilisée peut varier selon les besoins. Comme nous le verrons plus tard dans l'examen du moteur d'interface utilisateur dynamique, chaque question dynamique est représentée par une ligne dans la base de données, avec une clé primaire unique. Cette valeur de clé primaire est utilisée comme ID pour chaque contrôle dynamique ajouté à la page ASP.NET. Si vous n'avez pas besoin de distinguer les contrôles dynamiques, vous pouvez utiliser une autre technique qui leur donne des ID incrémentaux, comme monCtrlDyn1, monCtrlDyn2, etc.
Cycle de vie de la page et contrôles dynamiques
Chaque fois qu'une page Web ASP.NET est visitée (que ce soit une première visite ou une publication), la classe générée automatiquement par le moteur ASP.NET reconstruit la hiérarchie des contrôles à partir de zéro. Non seulement la hiérarchie est reconstruite, mais les événements des contrôles sont reconnectés à leurs gestionnaires d'événements. Par conséquent, lorsque vous ajoutez des contrôles dynamiques à une page ASP.NET, il est important de les ajouter à chaque visite de la page. De nombreux développeurs commencent par utiliser le modèle suivant pour ajouter des contrôles dynamiques :
' Dans le gestionnaire d'événements Page_Load...
If Not Page.IsPostBack Then
' Ajouter des contrôles dynamiques...
End If
Le problème de ce code est qu'il n'ajoute les contrôles dynamiques que lors de la première visite, et pas lors des publications suivantes. Si vous utilisez ce code, vous constaterez que les contrôles dynamiques disparaissent dès la première publication. Vous devez donc vous assurer d'ajouter tous les contrôles dynamiques à chaque visite, en plaçant ce code en dehors de la condition If Not Page.IsPostBack.
Une question importente concernant l'ajout de contrôles dynamiques est le moment du cycle de vie de la page où ils doivent être ajoutés. Comme je l'ai discuté dans Comprendre l'état de vue dans ASP.NET, une page ASP.NET passe par plusieurs étapes à chaque requête. Prenons un moment pour récapituler certaines étapes étroitement liées du cycle de vie de la page. Pour une compréhension plus approfondie, lisez d'abord l'article sur l'état de vue, en vous concentrant sur la section Le cycle de vie de la page ASP.NET.
Rappel du cycle de vie de la page ASP.NET
La première étape du cycle de vie est l'instanciation, au cours de laquelle la classe générée automatiquement construit la hiérarchie des contrôles à partir des contrôles statiques définis dans la partie HTML de la page. Pendant la construction de la hiérarchie, les valeurs spécifiées dans la syntaxe déclarative sont attribuées aux propriétés de chaque contrôle ajouté. Après l'instanciation vient l'étape d'initialisation, où la hiérarchie statique des contrôles est construite, mais l'état de vue n'est pas encore rechargé (en supposant que la requête soit une publication). Si la requête est une publication, après l'initialisation vient l'étape de chargement de l'état de vue. Pendant cette étape, la page extrait les données d'état de vue trouvées dans le champ de formulaire caché VIEWSTATE et, si nécessaire, chaque contrôle de la hiérarchie met à jour son propre état.
Si la requête est une publication, après l'étape de chargement de l'état de vue vient l'étape de chargement des données de publication. Cette étape examine les valeurs des champs de formulaire envoyés et met à jour les propriétés des contrôles concernés. Par exemple, le texte saisi par l'utilisateur dans un contrôle TextBox est renvoyé via le mécanisme POST (signalant le nom du contrôle et la valeur saisie). La page récupère ces valeurs, localise le TextBox approprié dans la hiérarchie et affecte la valeur reçue à sa propriété Text.
L'étape suivante est l'étape de chargement, qui se produit lorsque le gestionnaire d'événements Page_Load est déclenché. Après l'étape de chargement, d'autres étapes suivent, telles que le déclenchement d'événements de publication, la sauvegarde de l'état de vue et le rendu de la page Web, mais celles-ci ne sont pas pertinentes pour le sujet des contrôles dynamiques et ne seront donc pas abordées. La Figure 4 illustre les événements que traverse une page au cours de son cycle de vie.
La question du moment idéal pour ajouter des contrôles dynamiques peut se résumer ainsi : les contrôles dynamiques doivent être ajoutés avant le chargement de l'état de vue et le rechargement des données de publication, afin que tout état de vue ou valeur de publication spécifique aux contrôles dynamiques soit correctement pris en compte. Compte tenu de ces contraintes, le moment normal pour ajouter des contrôles dynamiques est l'étape d'initialisation, car elle se produit avant l'étape de chargement de l'état de vue et l'étape de chargement des données de publication.
Cependant, pendant l'étape d'initialisation, l'état de vue et les données de publication n'ont pas encore été restaurés, il est donc déconseillé d'accéder ou de définir des propriétés de contrôle (qu'ils soient dynamiques ou statiques) qui pourraient être stockées dans l'état de vue ou modifiées par des valeurs de publication, car ces valeurs seront écrasées ultérieurement par l'état de vue et les données de publication lors des étapes suivantes du cycle. Voici le modèle que j'utilise habituellement pour les contrôles dynamiques :
| • | Pendant l'étape d'initialisation, j'ajoute les contrôles dynamiques à la hiérarchie et je définis leur propriété ID. |
|---|---|
| • | Pendant l'étape de chargement, dans un bloc If Not Page.IsPostBack, j'attribue aux contrôles dynamiques les valeurs initiales nécessaires. |
Je dois ajouter les contrôles dynamiques à chaque publication, mais je ne définis leurs propriétés que lors du premier chargement de la page, car ces valeurs seront conservées dans l'état de vue. L'extrait de code suivant illustre ce modèle :
' Dans l'événement Init de la page, ajoutez un TextBox dynamique
Dim zoneTexte As New TextBox
placeHolderDynamique.Controls.Add(zoneTexte)
zoneTexte.ID = "zoneTexteDynamique"
' Dans le gestionnaire d'événements Page_Load, définissez les propriétés du TextBox
If Not Page.IsPostBack Then
Dim zoneTexteRef As TextBox
zoneTexteRef = CType(placeHolderDynamique.FindControl("zoneTexteDynamique"), TextBox)
zoneTexteRef.Text = "Valeur initiale"
zoneTexteRef.BackColor = Color.Red ' Couleur de fond initiale
End If
En plus d'ajouter des contrôles dynamiques pendant l'étape d'initialisation, vous pouvez aussi le faire pendant l'étape de chargement sans conséquences négatives. Lorsque vous ajoutez un contrôle à la collection Controls d'un autre contrôle, le contrôle ajouté est immédiatement synchronisé avec le cycle de vie de son nouveau parent. Par exemple, si le parent est à l'étape d'initialisation, l'événement Init du contrôle ajouté est déclenché, le mettant à niveau avec son parent. Si le parenet est à l'étape de chargement ou à une étape ultérieure, le contrôle enfant ajouté passe immédiatement par l'étape d'initialisation, l'étape de chargement de l'état de vue, l'étape de chargement des données de publication et l'étape de chargement.
Il y a une mise en garde lors de l'ajout de contrôles à l'étape de chargement. Lorsqu'un contrôle termine son étape de chargement de l'état de vue, il commence à suivre les modifications apportées à son état de vue. Cela signifie que toute modification de propriété effectuée après l'étape de chargement de l'état de vue est automatiquement conservée dans l'état de vue du contrôle. Avant qu'un contrôle ne commence à suivre les modifications de son état de vue, les modifications de propriété ne sont pas conservées. Si vous ajoutez un contrôle à l'étape d'initialisation, puis définissez ses propriétés à l'étape de chargement, il n'y a pas de problème, car l'étape de chargement de l'état de vue a eu lieu entre l'initialisation et le chargement, et le drapeau de suivi des modifications d'état de vue du contrôle est activé. En d'autres termes, si vous ajoutez des contrôles dynamiques à l'étape d'initialisation, les affectations de propriétés effectuées à partir de l'étape de chargement seront conservées dans l'état de vue.
Remarque : Le développeur de la page ne peut pas modifier le « drapeau de suivi des modifications de l'état de vue ». System.Web.UI.Control (dont dérivent tous les contrôles serveur ASP.NET) fournit un accès protégé à ce drapeau. Il existe une propriété protégée en lecture seule nommée IsTrackingViewState qui indique si le suivi est actif, et une méthode protégée TrackViewState() qui demande le début du suivi. Tous les contrôles appellent automatiquement cette méthode à la fin de l'étape d'initialisation.
Cependant, si vous n'ajoutez le contrôle dynamique qu'à l'étape de chargement, il est important de noter que ses propriétés ne peuvent être définies qu'après avoir ajouté le contrôle à la hiérarchie. Pour comprendre pourquoi, imaginez ce qui se passe si le code suivant est exécuté à l'étape de chargement :
Dim zoneTexte As New TextBox
If Not Page.IsPostBack Then
zoneTexte.BackColor = Color.Red ' Couleur de fond initiale
End If
placeHolderDynamique.Controls.Add(zoneTexte)
Comme vous le voyez, un TextBox est créé à chaque chargement de page. Ce n'est qu'au premier chargement que la propriété BackColor est définie à Rouge, et lors des chargements suivants, le contrôle est ajouté à la hiérarchie. Bien qu'au premier chargement la couleur de fond du TextBox soit effectivement Rouge, le problème est que lors d'une publication, la couleur de fond du TextBox revient à la valeur par défaut (aucune couleur de fond). Cela est dû au fait que l'affectation de la propriété BackColor n'est pas conservée dans l'état de vue, donc perdue lors de la publication. La raison est que le TextBox, comme tout contrôle serveur, ne commence à suivre l'état de vue qu'après l'étape de chargement de l'état de vue. Or, le TextBox ne passe par cette étape qu'après avoir été ajouté à la hiérarchie, donc l'affectation de BackColor n'est pas conservée. Pour corriger ce problème, assurez-vous d'ajouter le contrôle à la hiérarchie en premier, afin qu'il passe par l'étape de chargement de l'état de vue, puis définissez ses propriétés, comme suit :
Dim zoneTexte As TextBox
placeHolderDynamique.Controls.Add(zoneTexte)
If Not Page.IsPostBack Then
zoneTexte.BackColor = Color.Red ' Couleur de fond initiale
End If
Si vous ajoutez des contrôles dynamiques à l'étape d'initialisation, ces détails ne s'appliquent pas. Pour une discussion plus approfondie, veuillez consulter l'article de mon blog Construction de contrôle et leçon du jour sur l'état de vue.
Événements et contrôles dynamiques
Comme pour les contrôles serveur statiques, les contrôles ajoutés dynamiquement peuvent associer des événements à des gestionnaires d'événements. De même que les contrôles doivent être ajoutés à la hiérarchie à chaque visite de la page, les événements des contrôles dynamiques doivent également être connectés aux gestionnaires d'événements appropriés à chaque visite. La difficulté est de savoir quels gestionnaires d'événements définir dans la classe. Si vos contrôles sont vraiment dynamiques, comment la classe code-behind peut-elle connaître les gestionnaires nécessaires ? D'après mon expérience, la meilleure façon de gérer les événements avec des contrôles dynamiques est d'utiliser des contrôles utilisateur (User Controls) plutôt que des contrôles Web individuels. Avec un contrôle utilisateur, vous pouvez intégrer des gestionnaires d'événements et une logique de programmation spécifiques dans la partie code du contrôle utilisateur. Nous verrons comment ajouter dynamiquement des contrôles utilisateur dans la section suivante.
Si vous devez associer des événements d'un contrôle Web ajouté dynamiquement à des gestionnaires d'événements, assurez-vous de le faire à chaque visite de la page. Le code suivant (inclus dans le téléchargement de cet article) montre comment associer l'événement Click d'un contrôle Button ajouté dynamiquement à un gestionnaire existant. (ph est le nom du contrôle PlaceHolder dans la page. Pour un exemple en C# sur la connexion d'événements à des gestionnaires, et pour plus d'informations sur la gestion des événements dans .NET Framework, consultez l'article de Peter Bromberg Delegates to the Event.)
Private Sub Page_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
Dim bouton As New Button
placeHolderDynamique.Controls.Add(bouton)
If Not Page.IsPostBack Then
bouton.Text = "Cliquez-moi"
End If
AddHandler bouton.Click, New EventHandler(AddressOf Me.GestionnaireClickBouton)
End Sub
Private Sub GestionnaireClickBouton(ByVal sender As Object, ByVal e As EventArgs)
Response.Write("Le bouton a été cliqué !")
End Sub
Au cours des dernières années, de nombreux projets auxquels j'ai participé ont nécessité une interface utilisateur dynamique pour la saisie de données, c'est-à-dire une interface qui dépend d'un ou plusieurs facteurs d'influence de l'utilisateur. L'exigence fondamentale de tous ces projets était que ces interfaces dynamiques puissent être facilement créées, mises à jour et supprimées par des utilisateurs non informaticiens. Grâce à ces projets, j'ai développé un moteur d'interface utilisateur dynamique pour la saisie de données qui permet aux développeurs de créer des blocs de construction d'interface, puis à des non-développeurs de les assembler pour former des interfaces utilisateur spécifiques à des utilisateurs particuliers.
Dans le reste de cet article, je vais vous présenter, étape par étape, une version simplifiée de ce moteur. En particulier, la démonstration de cet article montre comment générer une interface de saisie de données unique pour chaque client en fonction de son type. Par exemple, l'interface affichée pour un client standard est différente de celle d'un client en ligne (online-only) ou d'un client achetant en gros.
Les composants de base du moteur d'interface utilisateur dynamique pour la saisie de données sont :
| • | Blocs de construction d'interface utilisateur : Ce sont des contrôles utilisateur, créés par les développeurs de l'équipe. Ces blocs sont conçus pour être spécifiques uniquement au type d'informations qu'ils collectent, et non aux données demandées. Par exemple, la démonstration inclut un bloc d'interface qui demande à l'utilisateur de saisir une valeur entière. Ce contrôle utilisateur contient un TextBox et un CompareValidator qui garantit que l'utilisateur saisit bien un entier. En associant ce bloc à une question (par exemple « Quel âge avez-vous ? » ou « Combien de kilomètres de votre maison à votre bureau ? »), il peut être intégré dans une interface de saisie dynamique. |
|---|---|
| • | Questions : Les questions sont des blocs personnalisés, créés par des utilisateurs non techniques via une interface Web. Une question associe un texte et un bloc d'interface. |
| • | Variables de différenciation : Chaque interface de saisie dynamique est basée sur une ou plusieurs variables. Par exemple, pour un site d'enregistrement de produits en ligne, l'interface peut dépendre du produit acheté. Pour la saisie des informations d'un employé, l'interface peut varier selon le département. Dans le moteur présenté ici, la variable de différenciation est codée en dur comme étant le type de client. |
| • | Questions dynamiques : Pour une variable de différenciation donnée, un ensemble de questions est spécifié. La combinaison d'une question et d'une variable de différenciation forme les questions dynamiques du système. |
| • | Réponses dynamiques : Lorsque le formulaire de saisie dynamique d'un client est rempli, ses informations doivent être sauvegardées dans la base de données. L'ensemble des réponses d'un client donné constitue les réponses dynamiques du système. |
Dans une application ASP.NET, la partie blocs de construction d'interface utilisateur du moteur est implémentée sous forme de contrôles utilisateur. Le reste est implémenté sous forme d'entités de base de données. La Figure 5 montre le diagramme entité-relation du moteur, décrivant comment chaque partie est représentée dans la base de données.
Les questions dynamiques (l'ensemble des correspondances entre questions et types de clients) sont implémentées via la table dq_DynamicQuestions. Une question y est associée à un type de client et à un ordre de tri, ce dernier indiquant l'ordre dans lequel les questions sont posées pour un type de client donné. Enfin, les réponses dynamiques sont stockées dans la table dq_DynamicAnswers, qui associe chaque question dynamique à un client spécifique. Comme nous ne pouvons pas déterminer à l'avance le type de réponse d'une question donnée (cela peut être une chaîne, un booléen, un entier, etc.), la table dq_DynamicAnswers comporte six colonnes, une pour chaque type de données autorisé par le système. Une question donnée ne peut avoir qu'un seul type ; la colonne correspondante contient la valeur de la réponse, tandis que les autres colonnes sont NULL.
Remarque : Plusieurs points du modèle de données méritent attention. J'ai choisi d'utiliser une clé primaire composite (DynamicQuestionID) dans dq_DynamicQuestions plutôt que de combiner CustomerTypeID et QuestionID comme clé primaire, ce qui permet à un type de client d'avoir des questions en double. Par exemple, une question pourrait être « Autres commentaires » et utiliser un bloc d'interface avec un TextBox multi-lignes. Comme vous pourriez avoir besoin de la question « Autres commentaires » après plusieurs autres questions, j'ai autorisé les doublons. La table dq_Questions a le format le plus simple. Dans les projets précédents, j'ai hésité entre deux approches : garder une table très simple et intégrer les détails dans les blocs d'interface, ou ajouter des champs supplémentaires à cette table pour interagir avec l'interface. Par exemple, une application peut avoir besoin d'indiquer que certaines questions sont obligatoires et d'autres facultatives. Dans un tel système, deux solutions s'offrent à vous. La première est de confier la responsabilité au bloc d'interface. Au lieu de créer un seul bloc (par exemple pour une entrée entière), vous en créez deux – un qui utilise RequiredFieldValidator pour garantir une valeur, et un autre qui n'impose pas cette condition. Lors de l'organisation des questions, l'administrateur choisit le bloc à utiliser selon que la question est obligatoire ou non. Une autre approche consiste à ajouter un champ Requis à la table dq_Questions et à n'utiliser qu'un seul bloc d'interface. Avec cette deuxième méthode, chaque bloc d'interface doit avoir une propriété Requis et être responsable de l'activation/désactivation du contrôle de validation approprié en fonction de cette propriété.
Enfin, la table dq_DynamicAnswers a six champs liés aux réponses et n'accepte que des réponses scalaires provenant d'un seul bloc d'interface. Autrement dit, la réponse d'un bloc d'interface peut être une chaîne, un entier, un double, une date, une devise ou un booléen. Mais que faire si nous avons besoin que le bloc d'interface ait une réponse plus complexe, comme une adresse qui contient plusieurs champs ? Pour de telles réponses complexes, le bloc d'interface devra sérialiser la réponse en un type acceptable lors de son retour, et la désérialiser lors de l'affichage de la réponse. Vous pouvez utiliser les capacités de sérialisation binaire inhérentes à .NET, mais pour cela, vous devrez peut-être ajouter un champ binaire de type binary à cette table.
Règles de conception pour les blocs de construction d'interface utilisateur
Pour faciliter la création d'une véritable interface utilisateur dynamique avec des contrôles utilisateur conçus par des développeurs, il est important que les blocs d'interface utilisés en tant que blocs de construction fournissent un niveau de fonctionnalité de base. L'interface IUIBuildingBlock définit ce niveau de base. Cette interface spécifie trois propriétés que tous les blocs d'interface doivent implémenter :
| • | DataType : Propriété en lecture seule qui retourne le type de données de la réponse fournie par le bloc d'interface. Doit être une valeur de l'énumération DQDataTypes. |
|---|---|
| • | QuestionText : Le texte de la question à afficher dans le bloc d'interface. |
| • | Answer : La réponse de ce bloc d'interface. |
Pour illustrer l'utilisation de ces propriétés, examinons un bloc d'interface simple. Supposons que nous voulions créer un bloc qui invite l'utilisateur à saisir un entier. Nous pourrions implémenter cela en créant un nouveau contrôle utilisateur dont la partie HTML contient ce qui suit :
<asp:Label id="lblQuestion" runat="server" CssClass="DQQuestionText"></asp:Label>:
<asp:TextBox id="txtReponse" runat="server" CssClass="DQAnswer" Columns="4"></asp:TextBox>
<asp:CompareValidator id="ValidateurComparaison" runat="server" CssClass="DQErrorMessage"
ErrorMessage="Vous devez entrer un nombre ici."
ControlToValidate="txtReponse" Type="Integer" Operator="DataTypeCheck"></asp:CompareValidator>
Ce balisage comprend :
| • | Un contrôle Web Label (lblQuestion) qui affiche la propriété QuestionText du bloc d'interface ; |
|---|---|
| • | Un TextBox (txtReponse) dans lequel l'utilisateur saisit la valeur entière ; |
| • | Un CompareValidator qui garantit que la saisie est bien un entier. |
La partie code source de ce contrôle utilisateur est assez simple. Elle fait en sorte que la classe de ce contrôle implémente l'interface IUIBuildingBlock et fournisse la logique pour les trois propriétés obligatoires :
Public Class QuestionEntiere
Inherits System.Web.UI.UserControl
Implements IUIBuildingBlock
...
Public ReadOnly Property DataType() As DQDataTypes Implements IUIBuildingBlock.DataType
Get
Return DQDataTypes.Integer
End Get
End Property
Public Property Answer() As Object Implements IUIBuildingBlock.Answer
Get
If txtReponse.Text.Trim() = String.Empty Then
Return DBNull.Value
Else
Return txtReponse.Text
End If
End Get
Set(ByVal Value As Object)
txtReponse.Text = Value
End Set
End Property
Public Property QuestionText() As String Implements IUIBuildingBlock.QuestionText
Get
Return lblQuestion.Text
End Get
Set(ByVal Value As String)
lblQuestion.Text = Value
End Set
End Property
End Class
La propriété en lecture seule DataType retourne le type de données renvoyé par le contrôle utilisateur – Integer. La propriété QuestionText lit et écrit dans la propriété Text du contrôle Label lblQuestion, tandis que la propriété Answer lit et écrit dans la propriété Text du TextBox txtReponse. Tout cela est contenu dans le bloc d'interface. Pour un bloc simple comme celui-ci, le code et le balisage HTML sont réduits, mais ne vous laissez pas tromper par cet exemple simpliste : la véritable puissance des blocs d'interface réside dans leur capacité à inclure plusieurs contrôles Web avec leurs gestionnaires d'événements, etc. Un bloc d'interface inclus dans le code téléchargeable de cet article illustre comment avoir deux DropDownList dépendants dans un seul bloc.
Remarque : Lors de la création de blocs d'interface, assurez-vous de les placer tous dans le même répertoire. L'emplacement exact n'a pas d'importance. Dans le fichier Web.config, vous trouverez un élément dont la clé est buildingBlockPath. Ce paramètre doit référencer le répertoire des contrôles utilisateur. Dans le code téléchargeable, le chemin par défaut est ~/UserControls/, mais vous pouvez le modifier si vous le souhaitez.
Pour plus d'informations sur les avantages de l'utilisation d'une interface avec des contrôles utilisateur chargés dynamiquement, lisez l'article de Tim Stall Comprendre les interfaces et leur utilité.
Création de questions et association aux types de clients
Pour que la création d'interfaces de saisie dynamique soit une tâche simple même pour les non-développeurs, j'ai créé une interface Web d'administration qui permet de créer des questions et de les associer à des types de clients. Cette interface est disponible dans le code téléchargeable de cet article.
L'interface d'administration comporte deux pages étroitement liées. La première, CreateQuestion.aspx, permet à l'administrateur de créer de nouvelles questions. Rappelons qu'une question est simplement un texte de question associé à un bloc d'interface. Cette page Web est très simple : elle permet à l'utilisateur de saisir le texte de la question et de choisir un contrôle utilisateur dans le répertoire des blocs d'interface (dont le chemin est spécifié dans le fichier Web.config). La Figure 6 montre une capture d'écran de cette page.
Une fois que l'administrateur système a créé les questions et les a associées à des types de clients spécifiques, il est possible de saisir les données du client. La page EnterData.aspx reçoit l'ID du client dans la chaîne de requête et construit l'interface de saisie dynamique correspondant au type de ce client. Cette page comporte trois méthodes importantes :
| • | ConstruireInterfaceDynamique() : Cette méthode est appelée dans le gestionnaire d'événements Page_Init (qui s'exécute pendant l'étape d'initialisation du cycle de vie de la page). Elle construit les contrôles dynamiques correspondant au type de client approprié. Comme nous l'avons vu plus tôt, ConstruireInterfaceDynamique() se contente d'ajouter les contrôles nécessaires à la hiérarchie. |
|---|---|
| • | Page_Load : Le gestionnaire Page_Load attribue des valeurs initiales par défaut aux contrôles Web ajoutés dynamiquement. Par exemple, si l'utilisateur a déjà fourni des valeurs pour un client spécifique, ces valeurs sont chargées dans les contrôles dynamiques appropriés lors de l'accès à la page. Ces propriétés ne sont définies que lors de la première visite ; les publications suivantes ne les modifient pas. |
| • | btnSauvegarder_Click : Cette méthode est liée à l'événement Click du bouton Sauvegarder. Elle énumère les contrôles dynamiques ajoutés et met à jour la base de données. |
Examinons brièvement ces trois méthodes. La méthode ConstruireInterfaceDynamique() est appelée dans le gestionnaire Page_Init. (Ce gestionnaire est automatiquement ajouté par Visual Studio .NET dans la région « Code généré par le concepteur Web Forms ».) Cette méthode récupère l'ID du client à partir de la chaîne de requête, puis remplit un SqlDataReader avec les questions dynamiques correspondant au type de client spécifié. Elle parcourt ensuite le SqlDataReader. Pour chaque enregistrement, le contrôle utilisateur spécifié est chargé et ajouté au PlaceHolder dynamicControls. Chaque contrôle dynamique reçoit un ID sous la forme dqDynamicQuestionID.
Private Sub ConstruireInterfaceDynamique() ' Appelée depuis Page_Init
IDClient = Convert.ToInt32(Request.QueryString("ID"))
...
' Obtenir la liste des contrôles dynamiques pour le client spécifié
lecteur = SqlHelper.ExecuteReader(connectionString, _
CommandType.StoredProcedure, _
"dq_GetDynamicQuestionsForCustomerType", _
New SqlParameter("@CustomerTypeID", TypeClientID))
' Pour chaque question, ajouter le contrôle utilisateur nécessaire
While lecteur.Read
Dim controleUtilisateur As UserControl = _
LoadControl(ResolveUrl(cheminBlocs & _
lecteur("ControlSrc")))
CType(controleUtilisateur, IUIBuildingBlock).QuestionText = lecteur("QuestionText")
controleUtilisateur.ID = String.Concat("dq", lecteur("DynamicQuestionID"))
conteneurDynamique.Controls.Add(controleUtilisateur)
conteneurDynamique.Controls.Add(New LiteralControl(""))
End While
lecteur.Close()
End Sub
Remarque : Dans le code d'exemple inclus dans cet article, j'utilise le Data Access Application Block (DAAB) version 2.0 de Microsoft pour accéder à la base de données. La classe SqlHelper du DAAB fournit un wrapper qui permet d'accéder aux données d'une base de données Microsoft SQL Server en une ligne de code. Pour plus d'informations sur le DAAB, consultez la page officielle Data Access Application Block for .NET et l'article de John Jakovich Examen du Data Access Application Block.
De plus, comme le montre le code, pour charger dynamiquement un contrôle utilisateur, vous devez utiliser la méthode LoadControl(CheminControleUtilisateur) plutôt que de créer une nouvelle instance de la classe du contrôle utilisateur. Pour une discussion approfondie sur les raisons de cette approche et une introduction complète aux contrôles utilisateur, lisez Examen approfondi des contrôles utilisateur.
Ensuite, dans le gestionnaire Page_Load, les réponses actuelles du client pour les contrôles dynamiques sont récupérées depuis la base de données et parcourues. Le contrôle dynamique correspondant est référencé, et sa propriété Answer est définie avec la valeur provenant de la base de données. Cela n'est fait que lors de la première visite, pas lors des publications suivantes, car nous ne voulons pas écraser les valeurs que l'utilisateur a saisies dans les champs du formulaire.
' Obtenir les réponses pour ce client
Dim lecteur As SqlDataReader = _
SqlHelper.ExecuteReader(connectionString, _
CommandType.StoredProcedure, _
"dq_GetDynamicAnswersForCustomer", _
New SqlParameter("@CustomerID", IDClient))
While lecteur.Read
Dim bloc As IUIBuildingBlock = conteneurDynamique.FindControl(String.Concat("dq", lecteur("DynamicQuestionID")))
If Not bloc Is Nothing Then
Select Case bloc.DataType
Case DQDataTypes.String
bloc.Answer = lecteur("StringAnswer").ToString()
Case DQDataTypes.Integer
bloc.Answer = Convert.ToInt32(lecteur("IntegerAnswer"))
Case DQDataTypes.Double
bloc.Answer = Convert.ToSingle(lecteur("DoubleAnswer"))
Case DQDataTypes.Date
bloc.Answer = Convert.ToDateTime(lecteur("DateAnswer"))
Case DQDataTypes.Currency
bloc.Answer = Convert.ToDecimal(lecteur("CurrencyAnswer"))
Case DQDataTypes.Boolean
bloc.Answer = Convert.ToBoolean(lecteur("BooleanAnswer"))
End Select
End If
End While
Enfin, lorsque l'utilisateur clique sur le bouton Sauvegarder, la collection Controls du PlaceHolder dynamicControls est énumérée. Pour chaque contrôle dynamique qui a reçu une réponse, la réponse est réécrite dans la base de données.
' Créer les paramètres nécessaires
Dim parametreString As New SqlParameter("@StringAnswer", SqlDbType.NText)
Dim parametreInteger As New SqlParameter("@IntegerAnswer", SqlDbType.Int)
Dim parametreDouble As New SqlParameter("@DoubleAnswer", SqlDbType.Decimal)
Dim parametreDate As New SqlParameter("@DateAnswer", SqlDbType.DateTime)
Dim parametreCurrency As New SqlParameter("@CurrencyAnswer", SqlDbType.Money)
Dim parametreBoolean As New SqlParameter("@BooleanAnswer", SqlDbType.Bit)
' Énumérer chaque réponse et la sauvegarder dans la base de données
For Each c As Control In conteneurDynamique.Controls
If TypeOf c Is IUIBuildingBlock Then
' Marquer tous les paramètres comme NULL
parametreString.Value = DBNull.Value : parametreInteger.Value = DBNull.Value
parametreDouble.Value = DBNull.Value : parametreDate.Value = DBNull.Value
parametreCurrency.Value = DBNull.Value : parametreBoolean.Value = DBNull.Value
' Déterminer quel paramètre doit être défini
Dim bloc As IUIBuildingBlock = CType(c, IUIBuildingBlock)
Select Case bloc.DataType
Case DQDataTypes.String
parametreString.Value = bloc.Answer
Case DQDataTypes.Integer
parametreInteger.Value = bloc.Answer
Case DQDataTypes.Double
parametreDouble.Value = bloc.Answer
Case DQDataTypes.Date
parametreDate.Value = bloc.Answer
Case DQDataTypes.Currency
parametreCurrency.Value = bloc.Answer
Case DQDataTypes.Boolean
parametreBoolean.Value = bloc.Answer
End Select
Dim questionDynamiqueID As Integer = Convert.ToInt32(c.ID.Substring(2))
SqlHelper.ExecuteReader(connectionString, _
CommandType.StoredProcedure, "dq_AddDynamicAnswer", _
New SqlParameter("@CustomerID", IDClient), _
New SqlParameter("@DynamicQuestionID", questionDynamiqueID), _
parametreString, parametreInteger, parametreDouble, _
parametreDate, parametreCurrency, parametreBoolean)
End If
Next
Conclusion
Ce moteur d'interface utilisateur dynamique pour la saisie de données constitue un bon point de départ pour développer un tel système dans votre application Web, mais il n'est pas conçu pour une intégration transparente avec un système existant. Il a été conçu comme un système de démonstration, pas comme un système complet et opérationnel. Une partie du système qui reste incomplète est l'interface d'administration, qui, bien qu'implémentée, est loin d'être un système complet. Plus précisément, il y a un problème quant à la façon de gérer la suppression d'une question dynamique d'un type de client spécifique. Par exemple, supposons que l'administrateur configure le système de manière à ce que pour les clients en ligne, une question binaire soit posée : « Est-ce votre premier achat chez nous ? »
Maintenant, supposons que de nombreux clients aient répondu à cette question. Si l'administrateur décide de supprimer cette question de l'ensemble des questions pour les clients en ligne, que se passe-t-il ? Faut-il supprimer les réponses correspondantes de la table dq_DynamicAnswers ? Faut-il les conserver pour garder un historique des réponses précédentes ? Pour votre application, vous devez répondre à cette question. Actuellement, lorsque vous supprimez une question dynamique d'un type de client, l'interface d'administration ne fait rien de particulier, ce qui signifie que si un ou plusieurs clients ont répondu à cette question, une exception sera levée et la question ne sera pas supprimée, en raison de l'intégrité référentielle établie dans la base de données.
Dans cet article, nous avons vu comment utiliser les contrôles dynamiques dans ASP.NET pour créer des interfaces utilisateur dynamiques pour la saisie de données. Comme nous l'avons discuté dans la première moitié de l'article, une page ASP.NET est composée d'une hiérarchie de contrôles, qui est normalement constituée uniquement de contrôles définis statiquement. Cependant, nous pouvons manipuler cette hiérarchie à l'exécution en ajoutant des contrôles dynamiques à la collection Controls d'un contrôle existant dans la hiérarchie. Nous avons également appris les techniques pour accéder aux contrôles ajoutés dynamiquement, ainsi que les modèles courants pour les ajouter et interagir avec eux.
La seconde moitié de l'article a présenté une implémentation spécifique pour créer et utiliser des interfaces de saisie dynamiques. Le moteur présenté permet à des utilisateurs non techniques de créer facilement des questions à partir de blocs de construction d'interface utilisateur, qui sont des contrôles utilisateur ASP.NET créés par des développeurs. À partir de ces questions, les administrateurs non techniques peuvent associer un ensemble de questions à un type de client spécifique. Une page Web unique, EnterData.aspx, affiche et enregistre les champs et valeurs de formulaire appropriés en fonction du client qui y accède.
Cet outil est un outil puissant et pratique qui permet de manipuler la hiérarchie des contrôles d'une page ASP.NET au moment de l'exécution, rendant l'application adaptable à de nombreux scénarios courants. En lisant cet article, vous devriez être en mesure d'utiliser avec confiance les contrôles dynamiques dans vos pages ASP.NET.
Bon codage !