Inclusion de données dynamiques : état des lieux

L'inclusion de données statiques ne pose aucun problème avec Domino et peut se faire aisément avec des champs partagés ou encore des sous-masques (calculés ou non). Toutefois, pour ce qui est de l'inclusion de données dynamiques, et à moins de pouvoir se contenter d'une (et une seule) vue, c'est une toute autre paire de manches, et de toutes les méthodes que j'ai pu tester, utiliser et recenser, aucune ne m'a jamais vraiment satisfaite. En voici la liste et mes commentaires respectifs :

  • Vue : la limite théorique d'une vue embeddée par masque (en pratique et avec quelques bidouilles, on peut en insérer 2 ou 3, mais bon) écarte d'office cette solution pourtant très satisfaisante en terme de performances (puisque les vues sont indexées). Il reste toutefois possible de simuler des inclusions multiples à l'aide d'iframes, mais cela rend le document peu (pour ne pas dire pas du tout) accessible, multiplie le nombre de requêtes http (et donc le temps d'accès pour peu que la connexion lagge), et pose de nombreux soucis en terme de mise en page (ajustement de la taille de l'iframe en fonction de son contenu) et bien vite on sombre dans une usine à gaz en javascript pour que ça tienne a peu près la route (oui, c'est du vécu).
  • Document profil : bien tenté, mais la mise à jour de documents profils ne fonctionne pas bien sur le web (et ce pour de sombres raisons de cache des documents dans la session HTTP). Cela dit, les documents profils sont tout à fait adaptés aux accès en lecture uniquement, et délivrent d'ailleurs à cet effet d'excellentes performances.
  • Agent au WebQueryOpen (ou appel direct d'un agent) : à proscrire, c'est le meilleur moyen de faire tomber son serveur pour peu qu'on ait plus de 20 ou 30 utilisateurs.
  • @Dblookup : c'est mieux, mais ça reste très gourmand. En mettre une poignée peut passer, en mettre 40 devient tout de suite plus problématique. Bien entendu, tout ceci sans évoquer la limite des 64k.
  • @GetDocField : méthode plus satisfaisante du point de vue des performances (d'après certains tests, 25% plus rapide qu'un @Dblookup), mais nécessite de jouer avec les UNID des documents, ce qui peut devenir un casse-tête sans fin pour peu que l'application doive tourner dans plusieurs environnements (dev, recette et prod par exemple). Et la limite des 64k reste toujours présente.
  • Agent qui recalcule les documents (ComputeWithForm) au WebQuerySave : très satisfaisant en terme de performances, mais hélas peu évolutif, chaque nouvelle page / section / rubrique nécessitant une évolution de l'agent pour prendre en compte ladite nouvelle page / section / rubrique. Et pour les masques, ça revient à faire la même chose qu'un @Dblookup ou @GetDocField.
  • XmlHttpRequest : le procédé consiste à générer la page web en javascript (coté client, donc) en récupérant plusieurs documents Domino à l'aide de l'objet XmlHttpRequest. Pour l'avoir essayé, ça marche plutôt pas mal, mais j'ai naturellement tendance à me méfier des solutions reposant sur des paramètres que je ne maîtrise pas (en outre, le navigateur et la configuration du poste client).

Cette liste est probablement loin d'être exhaustive ; c'est tout ce que je connais et/ou qui me vient à l'esprit pour le moment. Si vous connaissez d'autres méthodes, merci d'en faire part en commentaires.
Quoi qu'il en soit, étant clairement insatisfait par cette situation, je suis depuis des mois en quête de la méthode ultime, et il s'avère que mes dernières expérimentations m'ont amené vers une piste qui se montre, pour le moment, plutôt prometteuse. Explications.

Inclusion statique de données dynamiques : la technique ultime ?

L'idée de base est toute bête : puisque aucune méthode d'inclusion dynamique n'est satisfaisante, et puisqu'il existe moult méthodes très au point d'inclusions statiques, essayons de faire de l'inclusion statique de données dynamiques. Techniquement parlant, cela se traduit de la façon suivante : inclure un sous-masque (soit une inclusion tout à fait statique) dont le contenu sera modifié, périodiquement ou sur demande, par traitement : le contenu du sous-masque devient dès lors dynamique.

Imaginez un peu les possibilités offertes par un tel procédé : construire une application web type portail, constituée de "blocs indépendants" au contenu entièrement dynamique, dans laquelle il serait possible de définir la présence ou encore la disposition desdits blocs via une interface d'administration spécifique, devient un véritable jeu d'enfant : cela reviendrait à créer un masque intégrant, selon la configuration choisie, un nombre variable de sous-masques (sous-masques calculés déterminés selon leurs formules de calcul) ; il ne resterait qu'à mettre à jour en cas de besoin (ajout ou modification de données, changement de jour, etc) le contenu des sous-masques en question. Bien évidemment, puisque contenues dans le design de la base, ces données seraient directement et implicitement mises à jour dans tous les documents ouverts avec le masque en question. Et tout ceci, signalons-le, pour une consommation de ressources minimale, les données étant, de par la nature de cette méthode, "cachées" dans le design de la base.

C'est ce que promet cette méthode, pour peu qu'elle se montre stable à l'usage. En effet, modifier la structure d'une base à la volée n'est pas anodin ni sans risque, et avant d'arriver à un premier résultat fonctionnel, un bon nombre de mes tests ont corrompu les éléments de design quand ce n'était pas directement la base, voire même littéralement fait planter le serveur Domino (et c'était d'ailleurs la première fois que je voyais Domino planter à la simple ouverture d'un document). Aussi, je vous mets en garde : il s'agit, pour le moment, d'un concept qui ne demande qu'à être validé. N'envisagez pas de le déployer dans une application critique !

Mise en oeuvre

Je vais tâcher de détailler la mise en oeuvre de cette technique au travers d'un exemple simple : un système de brèves tels que vous pouvez en trouver sur de nombreux sites (Nofrag par exemple).

Tout d'abord, il nous faut :

  • Un serveur de test (relire quelques centimètres au dessus si vous vous demandez pourquoi).
  • Une base vierge (même raison).

Puis, commençons par créer :

  • Un sous-masque, que nous nommerons "breves", qui contiendra le code HTML des brèves.
  • Une vue, que nous appellerons "subforms", qui nous permettra de retrouver, par programmation, le sous-masque des brèves. Elle doit posséder une unique colonne ayant pour formule "$Title".
  • Un masque, qui sera notre point d'accès avec le navigateur et qui inclura donc le sous-masque des brèves (sinon aucun intérêt, hein).

Les éléments de design, comme n'importe quel autre élément de Notes, sont des documents, dans le sens où ils possèdent un UNID, des champs, et peuvent être listés au travers de vues (ce qui est totalement implicite dans le Designer). Par défaut, les vues que l'on crée ne sont configurées que pour lister des documents. Il faut donc les modifier afin de pouvoir lister un ou plusieurs types d'éléments de design.

C'est ce que nous allons faire à l'aide d'un agent LotusScript (que vous aurez la joie d'exécuter après chaque modification de la vue), dont ce sera l'unique tâche : modifier notre vue "subforms" pour qu'elle puisse lister les sous-masques :

Sub Initialize
  Dim session As New NotesSession
  Dim db As NotesDatabase
  Dim view As NotesView
  Dim doc As NotesDocument

  Set db = session.CurrentDatabase
  Set view= db.GetView("subforms")

  Set doc = db.GetDocumentByUNID(view.UniversalID)
  doc.~$FormulaClass = "4"

  doc.Save True, False
End Sub

Toute l'astuce réside donc dans le champ "$FormulaClass", qui spécifie quel(s) type(s) de données liste la vue. Pour ceux qui souhaiteraient plus de détails sur sujet, je vous invite à consulter cet article de DominoPower : Fun with $FormulaClass.

Champ $FormulaClass modifié
On note le champ $FormulaClass modifié..

Vue listant des sous-masques
.. et la vue liste maintenant les sous-masques de la base !

Voilà, notre vue "subforms" liste maintenant les sous-masques (et accessoirement les masques). Le sous-masque "breves" étant créé, il n'y a rien de plus à faire le concernant ; il contiendra le code HTML des brèves, qui sera généré par un autre agent LotusScript (de manière programmée ou à l'ajout/modification de données (WebQuerySave)).

Cet agent va générer le code HTML, récupérer le sous-masque "breve" à l'aide de la vue "subforms" (tout aussi normalement que si l'on récupérait un document depuis une vue), réinitialiser son champ RTF "$Body" (qui représente, à l'instar des mémos, tout le contenu du sous-masque), injecter dedans le code HTML (en HTML relai) préalablement généré, puis enfin enregistrer et signer le sous-masque :

Sub Initialize
  Dim session As New NotesSession
  Dim db As NotesDatabase
  Dim view As NotesView
  Dim doc As NotesDocument
  Dim html As String
  Dim key As String
  Dim i As Integer

  Dim rtitem As Variant
  Dim style As NotesRichTextStyle
  Dim rtnavBegin As NotesRichTextNavigator
  Dim rtnavEnd As NotesRichTextNavigator
  Dim rtrange As NotesRichTextRange

  Set db = session.CurrentDatabase

  ' *************************************
  ' ** GENERATION DU CODE HTML DU BLOC **
  ' *************************************


  html = "<div><h3>Brèves</h3><ul>"
  For i=1 To 5
     html = html + "<li>ma brève "+cstr(i)+"</li>"
  Next
  html = html + "</ul></div>"

  ' *******************************************
  ' ** INJECTION DU CODE DANS LE SOUS MASQUE **
  ' *******************************************


  ' la vue des masques et sous masques
  Set view= db.GetView("subforms")

  ' on récupère le sous-masque des brèves
  key = "breves"
  Set doc = view.GetDocumentByKey(key, True)

  ' On récupère le champ RTF $Body (qui s'avère être le contenu du sous masque)
  Set rtitem = doc.getFirstItem("$Body")
  rtitem.isSigned = False

  ' on supprime tout le contenu du RTF (seule technique qui semble pas tout faire planter..)
  While (Len(rtitem.text) > 0)
     Set rtnavBegin = rtitem.CreateNavigator
     Set rtnavEnd = rtitem.CreateNavigator

     rtnavBegin.FindFirstelement(RTELEM_TYPE_TEXTPARAGRAPH)
     rtnavEnd.FindLastelement(RTELEM_TYPE_TEXTPARAGRAPH)

     Set rtrange = rtitem.CreateRange
     rtrange.SetBegin(rtnavBegin)
     rtrange.SetEnd(rtnavEnd)

     Call rtrange.Remove()
  Wend

  ' On définit un style permettant de passthru-er le code HTML
  Set style = session.CreateRichTextStyle
  style.PassThruHTML = True

  ' On colle le style PUIS le code HTML dans le RTF (désormais vierge)
  rtitem.appendStyle(style)
  rtitem.appendText(html)

  ' Enfin, on enregistre et on signe le sous-masque ! 0wnage !
  Call doc.save(True, False)
  Call db.Sign(DBSIGN_DOC_FORM, False, key, False)
End Sub

Le code HTML injecté dans le sous-masque 'breves'
Le code HTML injecté dans le sous-masque "breves" !

Enfin, il ne reste qu'à créer un masque et à y inclure notre sous-masque "breves", et c'est terminé !

Un masque incluant le sous-masque 'breves'
Un masque, incluant notre fameux sous-masque "breves"

Bien entendu, l'idée n'est pas de générer "en brut" le code HTML dans l'agent, mais de le générer depuis des données stockées dans des documents, renseignés, pourquoi pas, depuis une interface d'administration spécifique. Je n'ai pas considéré toute cette partie dans l'exemple, puisqu'au final se révèle tout à fait dispensable dans la mise en oeuvre de cette technique. Dans cette optique, on aurait pu également envisager de zapper l'étape de la vue "subforms" et de se contenter d'un simple "NotesDatabase.getDocumentByUNID" sur l'UNID du sous-masque "breves", j'ai toutefois préféré la garder, tout d'abord parce que jouer avec les UNID dans le code c'est mal, d'autre part parce que la manipulation permettant de lister des éléments de design dans une vue n'est pas forcément évidente de prime abord et s'avérerait indispensable pour un projet un tant soit peu plus poussé.

Conclusion

Les possibilités offertes par cette technique sont, il me semble, immenses, et je suis persuadé qu'il y a bien des usages que je suis encore très loin de soupçonner. Les quelques essais que j'ai pu faire se sont montrés très concluants, tant du point de vue du fonctionnement que des performances. Rester à espérer qu'elle se montrera viable et stable dans le temps. Mais pour le savoir, il faut bien tester, c'est pourquoi je vais la déployer dans le projet que je réalise en ce moment, j'espère ne pas avoir de mauvaise surprise, mais je reste plutôt confiant ; les opérations réalisées ne relèvent pas tant que ça du bidouillage. Je ne manquerai pas d'en reparler dans un prochain billet.
De votre coté, n'hésitez pas à tester et à me dire ce que vous en pensez, toute suggestion, remarque ou amélioration étant bien évidemment la bienvenue !