CiviKey évolue encore un peu

par antoine.blanchet 27. décembre 2012 20:03

Comme je vous le disais il y a pile un mois (ou un peu plus), une version 2.6.0 de CiviKey est dans les cartons pour la nouvelle année.

Et bien une nouvelle release de la distribution Alpha est disponible (la 2.6.1). Cette release apporte des corrections de bugs dans différents plugins, et surtout de nouvelles fonctionnalités !

Une aide intégrée, facile à utiliser, et bientôt facile à éditer.

 

Un éditeur de clavier beaucoup plus puissant.

Il permet maintenant de créer, modifier, supprimer des touches, des zones, ainsi que leurs layouts etc etc etc. Bref vous pouvez (presque) tout faire avec !

 

 

Niveau stabilité, il reste un peu de chemin à faire afin de corriger les derniers bugs qui peuvent survenir lors de l'utilisation des nouveaux plugins. Cependant le plus dur est fait, il reste maintenant à passer les derniers coups de polish et nous serons près d'ici 2013 pour vous livrer une nouvelle version !

On vous tiens au courant :)

Comme la dernière fois, n'hésitez pas à lire le code sur GitHub ou à nous suivre sur Twitter ou Facebook

Une version 2.6.0 de CiviKey pour 2013

par antoine.blanchet 22. novembre 2012 00:29

Et oui, outre la sortie du nouveau site officiel en aout dernier et un léger problème de mise à jour automatique, CiviKey continu d'évoluer !

Grâce au soutien de la Fondation Garches et d'Alcatel Lucent, CiviKey passera pour la nouvelle de la version 2.5.2 à la version 2.6 !

Une 2.6, pas une 3.0 ?

Et non, pas une 3.0 car aucun changement majeur n'a été fait au sein du noyau depuis la grande bataille de la 2.5. L'objectif de la 2.6 est d'ajouter à CiviKey différents plugins indispensables aux ergothérapeutes et personnes en situations de handicap qui l'utilisent au jour le jour.

Au menu de cette 2.6 les fonctionnalités que tout bon clavier visuel se doit d'avoir : une prédiction de mot, et une solution de défilement des touches du clavier. S'ajoute à cela quatre nouveautés :

  • un clavier permettant grâce au défilement de déplacer et d'effectuer opérations de base de la souris
  • un assistant au premier démarrage
  • un assistant création 
  • et édition de clavier, et enfin une aide contextuelle intégrée

D'ici 2013 ... et avant ?

La sortie finale de la version 2.6 est prévu pour 2013. Vous souhaitez d’ores et déjà essayer nos builds fraichement sortis du four ?

Contactez-nous et nous vous fournirons un installeur spécifique pour la pre-release. Cette alpha version:

  • est déployée parallèlement à tout autre installation existante;
  • se mettra à jour sur notre "canal alpha", un canal de mise à jour différent de celui de la version stable actuelle.

En attendant voici quelques captures des derniers plugins !

La prédiction et défilement fonctionnant sur la skin WPF

Le clavier souris avec le défilement

La première étape de l'assistant création et édition de clavier

 

Evidemment pour ne pas perdre la tradition des posts technique sur l'architecture et l'implémentation de CiviKey de ce blog, deux posts suivront dans les prochaines semaines.

Le premier fera le point sur l'implémentation actuelle du défilement, les concepts sous-jacents ainsi que les évolutions prévues. Et enfin le second plongera dans les tréfonds de la prédiction de mot et explicitera comment cette fonctionnalité majeure est intégrée au sein de notre architecture de plugins. Vaste programme !

En attendant, n'hésitez pas à lire le code sur GitHub ou à nous suivre sur Twitter ou Facebook

La faute de frappe

par jean-loup.kahloun 21. août 2012 11:58

Un module de mise à jour…

CiviKey a été mis en ligne sur son nouveau site www.civikey.fr il y a quelques semaines.
Avant cela, il avait été distribué au sein de l’hôpital de Garches et d’Alcatel-Lucent.
Il comporte depuis longtemps un module d’update,  qui va chercher la dernière mise à jour disponible sur internet, et la met dans un dossier temporaire de la machine utilisateur pour l’exécuter au lancement suivant.

Ce module avait été éprouvé lors de sa distribution limitée, il a fait ses preuves et est fonctionnel.
Avant la mise en ligne sur le site, nous avons fait une petite refonte du produit, et notamment du module d’update, rien de bien méchant, un déplacement de quelques classes, des commentaires en plus.

…Qui ne met pas à jour

La semaine dernière, nous avons décidé de pousser un nouveau setup sur le serveur, pour intégrer le web installer du Framework .NET 4.0 Client Profile à l’installeur CiviKey, ayant eu beaucoup de retours d’utilisateurs ne comprenant pas la marche à suivre pour le mettre en place.

L’équipe de test s’est rendue compte avec un bonheur évident qu’après avoir téléchargé une mise à jour, L’application ne la lançait pas, et pire, proposait à nouveau de télécharger cette mise à jour. « Pourtant, le fichier de mise à jour est bien présent dans le bon dossier. Voilà qui est étrange ! »

Le code qui posait le fichier de mise à jour semblait correct :

« Le fichier Update.exe, qui contient l’installeur de la mise à jour, est bien mis dans Temp/CiviKey/Standard/Updates… »


Avec anxiété, nous sommes allés voir le code qui va lancer cette mise à jour, et là, nous sommes tombés sur :



Trouvé ?

 

Attendez… avec un zoom c’est plus facile.





 

…Updares ? Sérieusement ?

La légère refonte a inséré une jolie faute de frappe au seul endroit absolument crucial de l’application.

A cause d’elle, le module télécharge la mise à jour, mais cette mise à jour n’est pas détectée au lancement suivant. Le module de mise à jour, très simple, détecte à nouveau une mise à jour et propose fatalement de la télécharger à nouveau. On obtient donc un prompt inutile et incompréhensible à chaque lancement de l’application.

Evidemment, on ne peut pas régler le problème en lançant une mise à jour, puisque le bug est précisément dans le module  de mise à jour.

Même les meilleurs font des erreurs

Et qui a fait ça ? Un stagiaire étourdi ? Un dyslexique avéré ?
Non, le patron d’Invenietis lui-même, 25 ans de métier  et deux-trois développements plutôt trapues derrière lui, dont un gestionnaire de portails web dynamique, un moteur de plugins avec gestion des cycles d’interdépendance (intégré à CiviKey d’ailleurs) et bien d’autres outils dont la seule vue du code pourrait scotcher toute personne normalement constituée. Il vous suffit de regarder quelques-uns des autres posts de ce blog pour vous rendre compte qu’on n’a pas affaire ici à un débutant et que le souci du détail ne lui est pas inconnu.

Et où étaient les tests à ce moment-là ? 
« C’est Olivier qui a codé, on ne pensait pas que ça pouvait être buggé. »

Bien joué.

Etat des lieux

Invenietis a pêché par excès de confiance. Résultat : tous les gens ayant téléchargé l’application (et dont nous n’avons pas les coordonnées bien entendu), devront la télécharger à nouveau manuellement pour récupérer une version de CiviKey capable de se mettre à jour automatiquement.

Le projet est encore en béta, et est gratuit, ce n’est donc pas un drame et les utilisateurs savent que des problèmes peuvent encore apparaitre.
Les gens intéressés par le projet suivront son évolution, et les différents hôpitaux et ergothérapeutes s’en arrangeront probablement.
Les développeurs, eux, se sentent tout de même très  mal.

Une leçon à en tirer : Même si Linus Torvald lui-même pose trois lignes de code dans votre application, testez-la.

Une question subsiste : « Qui a mis cette touche ‘r’ à côté du ‘t’ » ?

I just can't (Nu)Get enough!

par Olivier Spinelli 23. avril 2011 14:55

NuGet, pour ceux qui ne connaitrait pas encore est un projet Open Source sponsorisé par Microsoft qui adresse (enfin) le "packaging" de composants logiciels. Tout le monde en a besoin et cela manquait cruellement à l'eco-système .Net.

Ce projet est prometteur sur de nombreux aspects mais il n'adresse qu'une petite partie des enjeux. Ce post est, je l'espère, le premier d'une petite série dont l'objectif est de rendre compte de notre expérience à Invenietis en proposant des évolutions concrètes et/ou des compléments qui nous semblent cruciaux. Nous ne sommes pas les seuls à espérer plus, par exemple Davy Brion qui soulève le problème d'une dépendance à "au moins un de ces packages". Tout ce qui concerne le packaging et la gestion de dépendances n'est pas simple. Il est en fait plus souvent complexe que ce que l'on peut penser car il met en jeu notamment :

  • La stratégie de versionning des composants logiciels. Si Microsoft a donné un cadre général avec l'objet Version, il reste :
    • A décider de ce que l'on met dans les quatres nombres Major.Minor.Build.Revision (dans cet ordre! voir cette question sur stackoverflow), et ce n'est pas le plus simple (voir également cette question et ses réponses)
    • A prendre en compte qu'un assembly possède en réalité deux Versions: celle de l'Assembly proprement dite et celle du fichier (ce qui est expliqué ici). Pour faire simple :
      • La Version est utilisée par le chargeur d'Assembly de .Net. Lorsqu'une référence est spécifiée avec <specificVersion>true</specificVersion> (ce qui est le défaut), il faut que l'assembly soit exactement compatible (à propos de cette spécification de version et ses impacts sur les temps de compilation, ce billet vaut le détour).
        Cette information est stockée dans le manifeste de l'Assembly : c'est donc du "pure .Net".
      • FileVersion est plutôt pour "l'affichage"... certes mais attention : Windows Installer le le prend en compteau moment de mettre à jour (c'est pouquoi j'ai mis des guillemets à "affichage").
        Cette information est stockée dans les ressources du fichier selon le format Portable Executable (PE) : c'est donc une information "Windows".
  • La granularité des composants : le même mécanisme est-il compatible à la fois pour quelques "gros composants" et une multitude de mini-composants ?
  • Les "Saveurs" : Comment fait-on gérer les notions de Debug/Release/CodeContractInside/x32/x64/AnyCpu/SL/.Net4/etc. ?
  • La définition des dépendances, par exemple :
    • "Ceci" dépend de "cela" mais seulement à partir de sa version "x".
    • "Ceci" dépend de "cela" ou "cela" (cf. l'exemple pré-cité).
  • Le "contenu" des packages :
    • certains utilisent NuGet pour distribuer du code source. Les règles applicables à la distribution de binaire (dll, exe) sont-elles toujours valides ?
    • comment choisir les fichiers à packager ? Aujourd'hui NuGet est relativement pauvre sur cet aspect.
  • Le type de dépôt de code source : Subversion offre un Revision Number fiable et pratique car c'est un entier que l'on peut mettre dans le slot Revision de la version, mais Git et d'autres n'ont pas cette notion.
  • La Continuous Integration : car gérer toutes ces versions à la main est impossible en pratique (surtout si une granularité fine est nécessaire).

Je pense avoir fait le tour (si ce n'est pas le cas, dites le moi et je complèterai). A part un aspect : les impacts du temps qui passe... indépendamment de l'augmentation des numéros de version.

La nouvelle version du package ne dépend plus de "X". Le package "Y" a été découpé en deux. "Z" s'appelle maintenant "A". "B" n'existe plus, ses features sont intégrées dans "C". "D" que l'on croyait fiable doit impérativement être mis à jour pour des raisons de sécurité. (...)

Bref, vous voyez l'idée. il y-a-t-il UNE solution ? Peut-être pas, mais il y a certainement de bonnes idées/conventions à trouver et mettre en place pour mieux gérer ces aspects. Peut-être un jour travaillerons-nous avec NuGetMore ?

 

Un peu de Sql : Union d’intervalles

par Olivier Spinelli 16. septembre 2010 18:23

On dispose d’un ensemble de ressources et de leurs disponibilités modélisées par une liste d’intervalles disjoints { BegDate , EndDate }. Concrètement, cet exercice s’applique sur une unique table :

create table dbo.tResourceAvailability
(
	ResourceID int not null,
	BegDate datetime not null,
	EndDate datetime not null,
);

Etant donné un ensemble de ResourceID (typiquement obtenu via d’autres tables et contraintes), il faut construire l’union des intervalles de disponibilité.

Cela ne semble pas excessivement compliqué. Mais ce n’est pas non plus trivial. Bref, c'est une bonne illustration d'un aspect du métier de développeur : résoudre un problème simple, avec une solution qui, si possible, l’est aussi… à condition de la trouver.

La première chose à faire en abordant un problème de ce type est de tout faire pour bien le visualiser mentalement. Un problème bien « vu » est souvent un problème résolu. En l’occurrence, il est judicieux de créer un jeu d’essai sur la base de la table ci-dessus. Un tel jeu d’essai doit être à la fois complet et simple... et parfois on peine. Ici ce n’est pas le cas : on peut se contenter de 5 ressources et de quelques créneaux répartis sur une plage d’une vingtaine « d’instants ».

La table ci-dessous présente le terrain de jeux :

  0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
P0   B   E         B     E              
P1     B     E       B E                
P2   B     E           B   E       B E  
P3     B E                              
P4               B     E         B E    

Et ce que nous cherchons à obtenir est la ligne « d’union » ci-dessous :

U   B       E   B         E     B   E  

Tiens ! Tout se passe comme si l’on ne retenait que certains B ou certains E.

Tilt !... « Retenir » = « Filtrer »… = condition d’une clause where… cela semble compatible avec du Sql…

Il suffit de se poser la question : « Qu’est-ce qui fait que je garde tel B ou tel E ? ».

Prenez une minute pour regarder les deux tableaux ci-dessus…

Réponse : Les seuls B (ou E) à conserver sont ceux qui ne sont pas compris dans un autre intervalle.

On « voit » le problème à résoudre là… et on en entrevoit même la solution, il est temps de passer à l’action :

insert into dbo.tResourceAvailability( ResourceID, BegDate, EndDate )
 values( 0, '2001', '2003' ), ( 0, '2008', '2011' ),
       ( 1, '2002', '2005' ), ( 1, '2009', '2010' ),
	 ( 2, '2001', '2004' ), ( 2, '2010', '2012' ), ( 2, '2016', '2017' ),
	 ( 3, '2002', '2003' ), 
	 ( 4, '2007', '2010' ), ( 4, '2015', '2016' );

J’utilise une syntaxe d’insertion de lignes multiples standard Sql (mais disponible qu’à partir de Sql Server 2008), je ne mets que des années pour simplifier… et je procède tout doucement, étape par étape, en commençant par un select simple :

declare @T datetime = '2008';
select count(*)
	from dbo.tResourceAvailability a
	where BegDate <= @T and @T <= EndDate;

Cela calcule le nombre d’intervalles rencontrés à cette date. L’idée est la suivante (regarder le tableau) : pour un B que l’on doit conserver (par exemple 2007), le calcul donnera un (il se rencontrera lui-même), et pour un B qui ne doit pas être conservé, le calcul donnera 2 ou plus (outre lui-même, d’autres intervalles le contiennent). On progresse non ?

Oui mais… Le calcul donne 2 aussi pour 2001… car deux intervalles débutent pile sur cette date. Il est donc nécessaire d’affiner un peu. Idée : on change légèrement la condition de façon à, lorsque l’on cherche un B, ignorer les B strictement identiques à celui que l’on cherche. On enlève donc au moins un au résultat du calcul précédent, et les B que l’on garde seront ceux pour lequel on aura 0 intersection (et non plus une seule). On procède symétriquement pour E, on a donc deux requêtes légèrement différentes pour les B et les E :

declare @B datetime = '2005';
select count(*)
	from dbo.tResourceAvailability a
	where BegDate < @B and @B <= EndDate
	
declare @E datetime = '2012';
select count(*)
	from dbo.tResourceAvailability a
	where BegDate <= @E and @E < EndDate

J’ai utilisé l’opérateur count pour mieux « voir » mes requêtes, ce que je cherche à filtrer est tous les B et E qui ont un compte de 0, c’est-à-dire ceux qui n’existent pas.

select year(BegDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability f 
    where BegDate < a.BegDate and a.BegDate <= EndDate );
Résultat :  2001  
   2001  ==> Doublon : un distinct le fera disparaitre.
   2007  
   2015  
select year(EndDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate <= a.EndDate and a.EndDate < EndDate );
Résultat :  2005
   2012
   2017

Ce qui est presque parfait puisque nous cherchons à obtenir la liste d’intervalles suivante : 2001 – 2005, 2007 – 2012, 2015 - 2017

Nous avons le résultat final, mais « en deux morceaux ». Comment les réunir ? Avec une « union ». C’est une opération ensembliste on ne peut plus simple :

select year(BegDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate < a.BegDate and a.BegDate <= EndDate )
union
select year(EndDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate <= a.EndDate and a.EndDate < EndDate );
Résultat :  2001
   2005
   2007
   2012
   2015
   2017

On obtient tous les B, puis tous les E. Le doublon a disparu (2001) car c’est le comportement par défaut de l’union (pour ne pas filtrer les doublons, il faut explicitement utiliser union all). Il ne reste plus qu’à ordonner ces éléments en remarquant que l’on a nécessairement une séquence B, E, B, E, etc. Pour la rendre plus lisible, on le précise :

select 'B', year(BegDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate < a.BegDate and a.BegDate <= EndDate )
union
select 'E', year(EndDate)
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate <= a.EndDate and a.EndDate < EndDate )
order by 2;
Résultat :  B  2001
   E  2005
   B  2007
   E  2012
   B  2015
   E  2017

On approche mais ce n’est pas encore idéal car on souhaite une forme de réponse similaire à celle de la donnée source : une liste d’intervalle, donc deux colonnes B et E. Transformer des lignes en colonnes, c’est le rôle de l’opérateur pivot, à condition de s’appuyer une fonction d’agrégation (count, sum, max, avg, etc.) : il ne nous est d’aucune utilité ici (en règle général, n’allez chercher l’opérateur pivot que s’il y a une agrégation possible ou souhaitable). Il nous faut trouver autre chose pour « mélanger » ces deux requêtes :

| 2001 |
  | 2012 |
  | 2001, 2005 |
| 2005 |
 x  | 2015 |
 ==>  | 2007, 2012 |
| 2007 |
  | 2017 |
  | 2015, 2017 |

Problème simple non ? Cherchez un peu…

……………

Le principe est de se servir de B comme « générateur », et de chercher la fin de l’intervalle correspondant :

select B = a.BegDate,
  	 E = << Take the first EndDate greater than a.BegDate >>
  from dbo.tResourceAvailability a
  where not exists( select * 
    from dbo.tResourceAvailability 
    where BegDate < a.BegDate and a.BegDate <= EndDate )

Ce qui revient à prendre la plus petite date supérieure à la date de début, et donne la requête finale suivante :

select distinct 
  B = BegDate,
  E = (select min(EndDate)
         from dbo.tResourceAvailability ae
	   where ae.EndDate > a.BegDate 
		and not exists( select * 
					from dbo.tResourceAvailability 
					where BegDate <= ae.EndDate 
and ae.EndDate < EndDate ))
  from dbo.tResourceAvailability a
  where not exists( select * 
			    from dbo.tResourceAvailability
			    where BegDate < a.BegDate and a.BegDate <= EndDate )

Une remarque sur les tris : Les dates obtenues peuvent être dans l’ordre croissant. C’est un hasard et il ne faut en aucun cas s’y fier  si vous voulez trier, triez explicitement (avec un order by).

 

Enseigner, une passion, un métier... avec des outils si possible.

par Olivier Spinelli 25. août 2010 10:08

Le système de contrôle de connaissance de mes rêves… est peut-être un peu compliqué (c’est, en puissance, un système d’EAO – Enseignement Assisté par Ordinateur – ou encore un LMS – Learning Management System). Mais il me tarabuste depuis des années… Ci-dessous quelques débuts d’ébauches de spécifications informelles de ce que j’aimerai qu’il fasse/supporte/offre. C’est brut, cela aborde de nombreux aspects par le petit bout de la lorgnette et je ne détaille pas le cadre pédagogique dans lequel j’entrevois vivre ces outils (cela demanderait beaucoup de réflexion et de travail - je n’ai pas le temps de m’y consacrer pleinement).

Le contrôle de connaissance passe par la gestion nécessaire d'un stock de Questions. Ce n'est pas très original, mais il faut bien commencer par un bout. Une Question est définit par :

  • Un énoncé multimédia
  • Des réponses qui peuvent être de type
    • QCM (1 parmi N, N parmi M).
    • Ordonnancement des réponses
    • Un fragment de code, du Java, du C#, du SQL, etc.
    • Autres...
  • Une durée normale de réponse (en minute).
  • Des évaluateurs de réponses qui projettent leur évaluation sur un modèle simple :
    • Une valeur flottante [0,1] - Oui, c'est l'horrible "note"... (On peut aussi l'appeler CES - "Coefficient d'Evaluation du Savoir", ou EON - "Estimateur d'Objectifs Normé".)
    • Une liste de chaînes étiquetées chacune par un type { par exemple: Error, Warning, Comment }
  • Certaines évaluations doivent pouvoir être faites par un être humain (cas des textes libres), mais autant que possible, les évaluateurs sont… du code.
  • Les « indices » :
    • une liste optionnelle d’aides constituée d’énoncés multimedia supplémentaires dont les accès sont traqués
    • à l’issue de l’examen, on connait le nombre d’indices consommés pour répondre à la question.

Les relations inter-questions sont multiples (et relativement subtiles). Commençons par la plus simple : une question ne peut avoir de sens qu’au sein d’un groupe de questions. C’est un « scenario » qui enchaîne des questions dépendantes les unes des autres.

Nous sommes devant une alternative classique du design pattern du Composite : doit-on appliquer le « vrai » Composite dans lequel une Question est susceptible de contenir d’autres Questions, ou introduit-on une entité différente qui permet de structurer des paquets de Questions ?

En l’occurrence, il existe une entité du domaine évidente qui regroupe des Questions : l’Exercice.
On introduit donc la notion d’Exercice : un Exercice est composé d’une ou plusieurs Questions. Et pendant que l'on y est, on obtient le schéma de composition plutôt naturel suivant : Un Examen est composé d’un ou plusieurs Exercices, eux-mêmes composés d’une ou plusieurs Questions. Et c'est tout.

La composition d’un Examen se fait donc en manipulant des Exercices : ce sont ces Exercices qui sont les éléments de contenus sélectionnables (les Questions ne sont manipulées directement que dans le cadre d’un Exercice).

Les Exercices peuvent avoir certaines relations entre eux relativement à leur sélection au sein d’un Examen :

  • Si deux exercices sont sélectionnés dans un examen, l’une doit suivre l’autre mais retenir l’un n’entraîne pas la sélection de l’autre. C’est un pré requis d’ordre de présentation uniquement.
  • Un Exercice peut en requérir réellement un ou plusieurs autres.
    • Si l’Exercice est sélectionné, cela entraîne nécessairement la sélection de ses prérequis dans l’Examen. (Note : ce type de prérequis implique celui de l’ordre de présentation.)
    • Une piste à creuser: un pré-requis pourrait-être remplacé par son "résultat" (s'il existe). C'est l'idées des "lemmes" en mathématique (un résultat intermédiaire qui participe à la démonstration d'un théorème plus important).
  • Un Exercice peut requérir que le candidat ait précédemment réussi un ou plusieurs autres Exercices. Ce point soulève la problématique de la mémoire du système… et on voit poindre l’artillerie lourde (bon, c’est un simple serveur).

Le choix d’un tel référentiel (arborescent) est tout à fait discutable, mais je le considère comme un compromis acceptable (suffisamment expressif - correctement opérationnel).

Voilà, cela suffit pour aujourd’hui. Pour continuer il faut introduire la « catégorisation de l’apprenant » (par rapport au référentiel), qui est assimilable aux caractéristiques de son avatar, la fonction d’Evaluation qui fait évoluer les caractéristiques des apprenants, la carte du territoire des compétences à conquérir, etc.

Cela ressemble à un jeu ? Oui, c’est une des idées-forces qui, je pense, devrait être intégrée dans ce type d’outils pédagogiques.

Cela existe déjà ? Tant mieux cela m'intéresse ! Mais ne serait-ce pas une usine à gaz ? Ce ne serait guère étonnant car ce Système dans sa globalité intègre tellement d'aspects (CMS, KM) que je l'imagine bien comme une agglutination de machins divers... Ce qui me motive est de penser le système "from scratch", de modéliser ce qui est nécessaire et uniquement ce qui est nécessaire à la mise en oeuvre de ces idées.

Si d’aventure, cher lecteur, vous êtes intéressé, ou connaissez des systèmes – ou approches – similaires ou approchant(es), quel que soit leur maturité, nous sommes intéressés. Invenietis a les compétences nécessaires au développement/adaptation du système ainsi que le partenaire pédagogique pour l’expérimenter (l’école Intech’Info)… Mais nous ne sommes aucunement spécialistes de ces questions, de l'offre en la matière, des outils existants. N’hésitez pas à me contacter.

Quelle disposition utiliser pour un clavier visuel ?

par antoine.blanchet 13. juillet 2010 17:31

Le développement de CiviKey avançant il est temps de se poser la question de la disposition des touches. Historiquement l’interface a toujours suivi les dispositions des claviers physiques : un clavier Azerty et un clavier Qwerty comportant toutes les touches d’un clavier normal, avec pavé numérique et touches de fonctions.

Pourtant ce n’est pas forcément la solution la plus ergonomique, aussi bien pour un utilisateur valide que pour un utilisateur handicapé. Heureusement de nombreuses personnes se sont posés la question avant nous, comme Maxime Baas, Yohan Guerrier et Christophe Kolski qui ont publié deux articles traitant de la question, intitulés « Etude comparative entre clavier virtuel de type AZERTY et K-Hermès, destinés à des utilisateurs souffrant d’une Infirmité Motrice Cérébrale » et « Système de saisie de texte visant à réduire l’effort des utilisateurs à handicap moteur ». Le premier étant une comparaison entre deux claviers, un AZERTY (Clavicom NG) et le clavier K-Hermès dont les touches possèdent trois lettres et sont disposées en fonction des probabilités avec les lettres suivantes (par exemple dans la langue française le ‘q’ est souvent suivi du ‘u’, donc la touche contenant ‘u’ sera proche de la touche contenant ‘q’). Le second étant une recherche de disposition idéale visant à réduire l’effort aussi bien physique que psychique des utilisateurs du clavier.

Nous avons donc essayé de tirer de ces articles une définition du « clavier idéal », voici les points qu’il doit respecter :

  • Le nombre de touches du clavier doit être minimum.
  • Les touches retour et espaces étant les touches les plus utilisées sur un clavier, elles doivent être très facilement et rapidement accessibles, voire même multiples.
  • La prédiction de mot / de lettre, doit être intégré au clavier nativement et ne doit pas être une surcouche complexe à utiliser. Elle doit faire partie du comportement normal du clavier.
  • La disposition des touches ne doit pas forcement être calculée en fonction des touches les plus probables mais doit plutôt pouvoir être mémorisée très facilement. (Limiter le nombre de touches participe à ce point.)
  • Toujours dans l’optique de réduire le nombre de touches, il faut utiliser toutes les possibilités des périphériques d’entrée afin de supprimer les touches de changement de mode : le clic gauche peut envoyer une lettre en minuscule tandis que le clic droit envoi une majuscule, d’autre modes peuvent être activés par un clic long gauche et droit par exemple.
  • La grille n’est pas la meilleure solution : dans une grille, chaque touche est entourée au maximum de quatre autre touches… un placement en quinconce permet d’entourer chaque touche de six touches au maximum.

Voici donc la nouvelle disposition, disposée en tenant compte des points cités ci-dessus :

Cette disposition est évidemment amenée à évoluer mais elle représente le standard de ce que doit être un clavier visuel qui doit être plus facile à utiliser qu’une reproduction directe d’un clavier physique.

Les opérations sur les modes de clavier

par Olivier Spinelli 11. juin 2010 17:59

Les Keyboard Modes ont chacun (cf. le post précédent), une liste des modes atomiques qui les composent : c’est un ensemble au sens mathématique du terme et les caractéristiques et opérations traditionnelles sur les ensembles sont utiles dans notre cas :

  • L’ensemble vide : Θ est notre mode vide (EmptyMode).
  • L’union : « Ctrl + Shift » U « Ctrl + Frigo » = « Ctrl + Shift + Frigo »
  • L’intersection : « Ctrl + Shift » ∩ « Ctrl + Frigo » = « Ctrl »
  • La soustraction : « Ctrl + Shift » - « Ctrl + Frigo » = « Shift »

Les prédicats suivants sont pratiques :

  • ContainsOne (teste que l’intersection de deux modes est non vide) : X.ContainsOne( Y ) <=> X ∩ Y != Θ
  • ContainsAll (teste qu’un mode est totalement contenu dans un autre) : X.ContainsAll( Y ) <=> X ∩ Y = Y

La liste des modes atomiques est ordonnée (selon la comparaison de chaîne ordinale évoquée précédemment) et cette propriété permet d’implémenter les différentes opérations efficacement. L’idée est d’avancer deux index dans les 2 listes (Gauche contient m éléments et Droite en contient n) à traiter en comparant les éléments : à chaque comparaison, on détecte un élément à gauche uniquement, à droite uniquement ou dans les deux.

Lorsque les deux éléments sont égaux, on peut avancer les deux index simultanément, sinon seul l’un des index avance : au pire des cas il y a donc m+n comparaisons et dans le meilleur des cas min(m,n).

L’implémentation repose sur une unique fonction qui applique l’algorithme ci-dessus sur les deux listes et qui est paramétrée par 3 prédicats (des fonctions booléenes prenant un élément en paramètre) :

static void Process( KeyboardMode left, 
                     IKeyboardMode right, 
                     Predicate<IKeyboardMode> onLeft, 
                     Predicate<IKeyboardMode> onRight,  
                     Predicate<IKeyboardMode> onBoth );

Ces prédicats sont appelés dans l’ordre d’apparition des modes atomiques dans les deux modes left et right selon que l’élément apparait à gauche uniquement, à droite uniquement ou dans les deux. Ces fonctions peuvent retourner false pour arrêter le processus. Process est utilisé pour toutes les opérations sur les modes.

Une seule fonction : pour combien d'opérations différentes ?

Voyons d'abord l'implémentation des deux prédicats.

Les prédicats ContainsOne/ContainsAll

ContainsOne est peut-être la plus simple : dès qu’un élément commun est trouvé, on peut conclure positivement et arrêter le processus. Il suffit donc de fournir une fonction qui arrête le processus et mémorise le succès dans le cas où un élément a été trouvé dans les deux listes.

bool IKeyboardMode.ContainsOne( IKeyboardMode mode )
{
  bool found = false;
  Process( this, mode, 
           null, 
           null,
           delegate( IKeyboardMode m ) { found = true; return false; } );
  return found;
}

ContainsAll est presque identique : c’est le cas « à droite seulement » qui est pisté et l’opposé qui est retourné car si on trouve un mode à droite seulement, c'est que c'est un "alien" qui ne devrait pas être là. (C'est une façon de parler, hein! Les étrangers ont bien le droit d'être là, sinon où serions nous ?)

bool IKeyboardMode.ContainsAll( IKeyboardMode mode )
{
  bool foundAlien = false;
  Process( this, mode,
           null,  // Use lambda syntax below instead of delegate keyword.
           m => { foundAlien = true; return false; }, 
           null );
  return !foundAlien;
}

Un Process, deux prédicats. Il est temps d'aborder les opérations plus complexes.

L’intersection

L’intersection est la plus simple des combinaisons : seuls les éléments qui apparaissent dans les deux listes doivent apparaitre dans le résultat final. On utilise donc une fonction (paramètre onBoth) qui ajoute l’élément à une liste (un collecteur) et continue le processus.

Problème : il nous faut donc appeler la méthode List.Add (qui ne retourne rien) alors que la méthode Process attend un prédicat (une fonction qui retourne un booléen).

Le code est le suivant :

IKeyboardMode IKeyboardMode.Intersect( IKeyboardMode mode )
{
  List m = new List<IKeyboardMode>();
  Process( this, mode, null, null, Adapter.AlwaysTrue<IKeyboardMode>( m.Add ) );
  return Context.ObtainMode( m );
}

L’adaptateur qui suit permet d’envelopper (wrapper) une action dans un prédicat toujours vrai :

public class Adapter 
{
  static public Predicate<T> AlwaysTrue<T>( Action<T> a )
  {
    return delegate( T o ) { a( o ); return true; };
  }
}

L’air de rien, ces quelques lignes mettent en œuvre simultanément les génériques, les fonctions anonymes et la clôture.

Personnellement je trouve ça superbe et c’est - à mon humble avis - un exemple de code qui démontre bien la puissance de ce drôle de langage que devient C#.

J’ai cependant essayé de trouvé plus élégant, l’appel devenant par exemple :

// Perfection (at least for me)… Sadly, it does not work.
var actionAlwaysTrue = m.Add.ToPredicate( true ); 

Malheureusement, cette solution (une méthode d’extension sur le delegate Action) ne permet que d’écrire :

var actionAlwaysTrue = ((Action<IKeyboardMode>)m.Add).ToPredicate( true );

Car la méthode Add doit être explicitement considérée comme un delegate afin que les méthodes d’extension puissent s’appliquer. De même, la forme suivante n’est pas syntaxiquement correcte :

// Not perfect… And does not work.
var actionAlwaysTrue = Adapter.ToPredicate( m.Add, true ); 

Le compilateur exige la définition explicite du type, celui-ci ne pouvant être inférée par le type de l’action, ce qui nous ramène à :

var actionAlwaysTrue = Adapter.ToPredicate<IKeyboardMode>( m.Add, true ); 

Alors, en désespoir de cause, je préfère conserver mes deux petits helpers AlwaysTrue (et AlwaysFalse) :

var actionAlwaysTrue = Adapter.AlwaysTrue<IKeyboardMode>( m.Add );

L’union, la soustraction et le toggle

L’union se définit ainsi : que l’élément apparaisse à droite, à gauche ou dans les deux, il doit apparaitre dans le résultat final. C’est donc la même fonction qui est fournie aux 3 cas et qui ajoute l’élément à la liste (le collecteur).

IKeyboardMode IKeyboardMode.Add( IKeyboardMode mode )
{
  List<IKeyboardMode> m = new List<IKeyboardMode>();
  var add = Adapter.AlwaysTrue<IKeyboardMode>( m.Add );
  Process( this, mode, add, add, add );
  return Context.ObtainMode( m );
}

Ici, j’ai utilisé var pour définir la variable car le contexte est suffisamment explicite et l’algorithme suffisamment simple. En règle générale, je type explicitement mes variables : d’expérience le code est nettement plus lisible et maintenable ainsi.

La soustraction consiste à ne conserver que les modes qui apparaissent à gauche :

IKeyboardMode IKeyboardMode.Remove( IKeyboardMode mode )
{
  List<IKeyboardMode> m = new List<IKeyboardMode>();
  Process( this, mode, Adapter.AlwaysTrue<IKeyboardMode>( m.Add ), null, null );
  return Context.ObtainMode( m );
}

Le petit dernier est le « Toggle ». Intuitivement, il s’agit de l’opération qui « inverse » l’état d’un mode particulier, par exemple :

  • Toggle( « Ctrl + Shift », « Ctrl » ) => « Shift »
  • Toggle( « Shift », « Ctrl » ) => « Ctrl + Shift »

En quelques mots: si le mode est présent je le retire, sinon je l’ajoute. Evidemment, cela doit fonctionner avec plusieurs modes :

  • Toggle( « Ctrl + Shift », « Ctrl + Frigo » ) => « Shift + Frigo »
  • Toggle( « Shift + Frigo », « Ctrl + Frigo » ) => « Ctrl + Shift »

Ce toggle est trivial à implémenter grâce à la fonction Process car il suffit simplement de collecter ceux qui sont à gauche, ceux qui sont à droite, mais pas les éléments communs.

IKeyboardMode IKeyboardMode.Toggle( IKeyboardMode mode )
{
  List<IKeyboardMode> m = new List<IKeyboardMode>();
  var add = Adapter.AlwaysTrue( m.Add );
  Process( this, mode, add, add, null );
  return Context.ObtainMode( m );
}

This is the end. Mais j’ai pourtant eu envie de continuer…

Toutes les opérations possibles ?

Intrigante cette fonction Process qui permet de tout implémenter. A-t-on oublié des opérations intéressantes ?

Mon idée est de me servir de Process pour vérifier que je n’ai rien oublié : le tableau suivant recense toutes les utilisations possibles de la méthode d’ajout à un collecteur (il y a 3 slots, chacun pouvant être null ou add, il y a donc 23 = 8 possibilités).

onLeft    onRight    onBoth    Name & description    Comments
null null null “Empty” Useless.
null null add Intersect (keep commons) Implemented. Kind of symmetric of ‘Toggle’.
null add null “Cleanup” (keep theirs only) Useless since it is the symmetric of ‘Remove’
null add add “Other” (keep theirs and commons, reject mine) Useless. Opposite of ‘This’.
add null null Remove (keep mine only) Implemented. Opposite of ‘Cleanup’
add null add “This” (keep mine and commons and reject theirs) Useless. Opposite of ‘Other’.
add add null Toggle (keep mine, theirs, but reject commons) Implemented.
add add add Add Implemented.

Ces 4 fonctions couvrent bien l’ensemble des opérations sensées. Vous certainement pas, mais moi, ce constat me rassure Smile.

 

PS: Pourquoi le tableau ci-dessus est-il en anglais ? Parceque qand je code, je "suis" en anglais. Et que ce tableau, je l'ai fais dans le code.

Keyboard Modes version 2.5

par Olivier Spinelli 11. juin 2010 12:50

Ce billet est le premier d’une série consacré aux Modes d’un clavier. Il explique la modélisation et l’implémentation des Modes dans CiviKey. D’autres billets traiteront notamment des points suivants :

  • Les Modes se combinent, s’intersectent, se contiennent : quelles sont les opérations de base que doivent supporter les modes et surtout comment les implémenter efficacement ?
  • Le « Fallback d’un Mode », où comment calculer de la meilleure à la pire les combinaisons de modes qui découlent d’un mode combiné. Ci-dessous un exemple :
    1. « Ctrl » + « Frigo » + « Shift »
    2. « Ctrl » + « Frigo »
    3. « Ctrl » + « Shift »
    4. « Frigo » + « Shift »
    5. « Ctrl »
    6. « Frigo »
    7. « Shift »
    8. «»
    Combien y-a-t-il de fallbacks en fonction de du nombre de modes atomiques ? Comment les générer ?
  • La gestion de « AvailableMode » et « CurrentMode »
    Le fait est que sur un clavier, il y a deux collections de modes très différents : les modes possibles et les modes courants. Exemple de modes possibles « Alt+Ctrl+ Frigo+Word » et de modes courants « » (aucun) ou « Ctrl+ Frigo ». Le fait de supprimer un mode possible entraîne nécessairement la suppression de celui-ci des modes courants, ce qui entraîne la suppression des KeyMode et LayoutKeyMode qui étaient définis par ce mode…
  • Le « Fallback » des KeyMode et des LayoutKeyMode : le Mode courant d’un clavier pilote le comportement (KeyMode) et l’affichage (LayoutKey) d’une touche (Key). Quelle implémentation efficace pour la gestion des touches peut-on envisager ?

 

Un cache centrale : un objet, un mode.

Les Modes sont totalement « objectivés ». Les objets IKeyBoardMode ne peuvent être obtenus que via l’objet Context : sa méthode ObtainMode( string m ) se charge de la normalisation et de trouver l’objet-mode correspondant dans un cache interne (de le créer et de l’y ajouter si besoin). Grâce à ce cache, les « objets-modes » sont uniques (dans le cadre d’un Contexte) : il n’y a qu’un et un seul objet « Ctrl + Shift » (il y a également un unique EmptyMode – le mode vide qui correspond à la chaîne vide).

L’empreinte mémoire est diminuée et on peut se servir de ces « objets-modes » pour porter de la donnée potentiellement plus lourde (ce que l’on va exploiter pour stocker la liste des fall backs de chaque mode) et comme clef efficace de hash (l’identité de l’objet garantit celle du mode).

IKeyboardMode m = Context.ObtainMode( "Alpha+Beta" );
Assert.That( Context.ObtainMode( "\r++\t Beta ++ Alpha ++" ) == m, 
                                        "Canonical ordering and trimming." );

Combined & Atomic Modes : les débuts de l’implémentation

Ces deux types de modes doivent rentrer dans un moule commun : le design pattern du Composite s’impose. Tout mode expose donc les modes atomiques qui le composent.

public interface IKeyboardMode : IComparable<IKeyboardMode>
{
    /// <summary>
    /// Gets the <see cref="IContext"/>. 
    /// </summary>
    IContext Context { get; }

    /// <summary>
    /// Gets the atomic modes that this mode contains.
    /// </summary>
    IReadOnlyList<IKeyboardMode> AtomicModes { get; }

    /// <summary>
    /// Gets a value indicating whether this mode contains zero 
    /// (the empty mode is considered as an atomic mode) or only one atomic mode.
    /// </summary>
    bool IsAtomic { get; }

}

Comme le montre le support de l’interface correspondante, les modes sont comparables (en mathématique, on dit que l’on peut définir une relation d’ordre – strict ou total, au choix – sur l’ensemble des modes) ce qui permet de les ordonner (donc de les trier) en s’appuyant sur la chaîne de caractères normalisée, mais avec une astuce :

 public int CompareTo( IKeyboardMode other )
 {
    if( _modes == other ) return 0;
    int cmp = _modes.Count - other.AtomicModes.Count;
    if( cmp == 0 ) cmp = StringComparer.Ordinal.Compare( other.ToString(), _mode );
    return cmp;
}

Tout mode contenant plus de mode atomique qu’un autre est plus grand. Si les deux modes ont autant de modes atomiques l’un que l’autre, alors la chaîne est utilisée. Cela est rendu possible grâce à la normalisation des chaînes de modes (ordonnancement des modes atomiques du plus fort au plus petit séparés par des +), et il faut inverser la comparaison : « Ctrl » est meilleur que « Shift » (alors qu’il est « plus petit » dans l’ordre lexicographique).

Toutes les opérations liées aux modes utilisent la comparaison ordinale : elle est plus rapide et tout aussi solide que celle indépendante de la culture (sous réserve de normaliser les chaînes, au niveau de la représentation interne de l’Unicode, ce qui est fait dans ObtainMode).

Dans l’implémentation, on retrouve les 3 types de modes (vide, atomiques, combinés) via les 3 constructeurs :

class KeyboardMode : IKeyboardMode
{
  Context _context;
  string _mode;
  IReadOnlyList<IKeyboardMode> _modes;

  /// <summary>
  /// Initializes the new empty mode of a Context.
  /// </summary>
  internal KeyboardMode( Context ctx )
  {
    Debug.Assert( ctx.EmptyMode == null, 
                                "There is only one empty mode per context." );
    _context = ctx;
    _mode = String.Empty;
    _modes = ReadOnlyListEmpty<IKeyboardMode>.Empty;
    _fallbacks = new ReadOnlyListMono<IKeyboardMode>( this );
  }

  /// <summary>
  /// Initializes a new atomic mode.
  /// </summary>
  internal KeyboardMode( Context ctx, string atomicMode )
  {
    _context = ctx;
    _mode = atomicMode;
    _modes = new ReadOnlyListMono<IKeyboardMode>( this );
    _fallbacks = ctx.EmptyMode.Fallbacks;
  }

  /// <summary>
  /// Initializes a new combined mode.
  /// </summary>
  internal KeyboardMode( Context ctx, string combinedMode, IReadOnlyList<IKeyboardMode> modes )
  {
     Debug.Assert( combinedMode.IndexOf( '+' ) > 0 && modes.Count > 1, 
                            "There is more than one mode in a Combined Mode." );
     Debug.Assert( modes.All( m => m.IsAtomic ), "Provided modes are all atomic." );
     Debug.Assert( modes.GroupBy( m => m ).Where( g => g.Count() != 1 ).Count() == 0, 
                            "No duplicate in atomic in modes." );
     _context = ctx;
     _mode = combinedMode;
     _modes = modes;
  }
}

Notez l’utilisation des helpers ReadOnlyListEmpty et ReadOnlyListMono. Le premier implémente la liste des modes du mode vide : le mode vide ne contient aucun mode. Le deuxième implémente les modes d’un mode atomique : un mode atomique contient un seul mode atomique : lui-même.

Attention ! Note importante !

Un mode est immuable (immutable in English), exactement comme une chaîne de caractère en C# ou en Java. Ce qui veut dire qu’ajouter un mode à un mode existant en faisant (en utilisant une des opérations qui feront l'ob jet d'un prochain billet) :

m.Add( m2 );

aboutit à… rien de rien de nada. Comme pour les chaînes de caractères, il faut faire

m = m.Add( m2 )

car toutes les opérations sont des fonctions (qui seraient décorées par des const - http://www.parashift.com/c++-faq-lite/const-correctness.html - en C++ mais ce modificateur n’existe malheureusement pas en C#).

ObtainMode : le cœur

La méthode ObtainMode est centrale à la gestion de ces objets. Elle gère le cache qui est un dictionnaire dont la clef est la version normalisée de la chaîne de caractère d’un mode. Sans être complexe, son implémentation effectue le moins possible d’opérations et elle est un peu longue à commenter de façon détaillée ici. Le code source est disponible ici : https://svn.invenietis.com/svn/CK/Core/trunk/CK.Context/Context/KeyboardMode.cs (anonymous/anonymous)

Le billet suivant traite des opérations possibles sur les modes (et de la façon dont elles sont implémentés).

 

Composition des solutions

par antoine.blanchet 27. mai 2010 18:33

Les solutions de CiviKey sont des solutions dites « composées » : chaque solution tire parti d’une ou plusieurs solutions pour ne faire qu’ajouter des fonctionnalités, sans devoir recopier des projets essentiels. La solution Core est la solution de base, elle regroupe les projets du noyau permettant par exemple de lire et sauvegarder les données de CiviKey. Ensuite la solution Certified apporte un host d’exécution du Core, ainsi que des plugins essentiels, elle est donc composée du résultat de la solution Core. Enfin Contrib est une solution « bac à sable » où tout le monde peut venir tester son plugin. Elle est composée par la solution Certified et contient donc tous ses plugins, et en y ajoutant les plugins développés par les développeurs de la communauté Open Source ou par les étudiants d’In’Tech INFO.

Création d’un « Runtime »

Pour que ces solutions puissent être composées par d’autres il a fallu réfléchir à un système assez souple pour ne lier trop fortement deux solutions entre elles. Il faut par exemple que le développement de Core puisse continuer en parallèle sans impacter Certified.

Pour cela nous avons ajouté un dossier « Runtime » à chaque solution (sauf Core car c’est la solution élémentaire) contenant les résultats de la (ou des) Solutions qui la compose : Certified/Runtime contient donc les binaires de Core. Les projets de la Solution composée n’ont qu’à faire référence aux binaires du dossier Runtime pour les utiliser et les développeurs de la Solution composée gèrent leur répertoire Runtime : ce sont eux qui choisissent de prendre en compte on non les nouvelles versions des composants sur lesquels ils s’appuient.

Ce dossier Runtime peut être mis sous contrôle de SubVersion (ou autre gestionnaire de code source), c’est d’ailleurs ce que nous faisons à Invenietis. De cette façon, les Solutions sont autonomes et il est aisé de « remonter dans le temps » du projet : les sources sont en phase avec les versions exactes des composants sur lesquels la Solution est fondée.

Les « Saveurs »

Lorsqu’on compile une solution dans Visual Studio il est possible de choisir dans quel mode se déroulera la compilation. En mode Debug, pour que les « #If Debug », « Debug.* », etc. soient pris en compte, cela permet de résoudre les bugs plus facilement (via des outils de diagnostic). En mode Release, pour que les performances soient optimales.

Mais il n’y a pas que Debug et Release : si ces « modes de compilation » sont les plus courants, il peut s’avérer nécessaire de gérer d’autres « saveurs » : processeur, optimisations plus ou moins agressive, versions gratuites vs. payantes, signé ou non, etc. La notion de « Saveur » généralise le « mode de compilation » : un même composant logiciel est disponible pour le développeur en plusieurs « saveurs » et celui-ci doit pouvoir les choisir.

Le Runtime de chaque Solution contient un sous dossier par « saveur », typiquement Debug et Release, qui contiennent les résultats des compilations correspondantes. Il est donc possible de choisir quelle « saveur » utiliser simplement en changeant les chemins des références.

Notons qu’il est même possible d’intégrer la version dans la « saveur » : rien n’empêche de travailler en même temps sur des composants « ReleaseOld » et « ReleaseNew » (afin de vérifier que le plugin que l’on écrit marche aussi bien avec deux versions différentes du même composant.

Malheureusement, changer toutes les références à chaque changement de configuration est très fastidieux (une solution comme Certified possède, à date, 14 projets, chaque projet possède au moins 3 références au Core, il faut donc changer 42 chemins à chaque fois que l’on veut passer en Debug/Release).

Pour éviter cette opération, le dossier Runtime contient un répertoire Current. Ce dossier est une copie exacte d’une des saveurs disponible dans Runtime. La solution n’a plus qu’à faire référence systématiquement aux binaires de Current : pour changer de configuration, il suffit de remplacer ces binaires par les binaires de la saveur que vous voulez utiliser. Il est ainsi très facile par exemple d’utiliser le Core en Release dans la solution Certified en mode Debug (ou le contraire).

Pour le moment ce dossier Current est à gérer manuellement, il faut le créer, le mettre en « ignore list » dans SubVersion, et faire un export des binaires de Debug / Release dedans afin de pouvoir les utiliser. Nous spécifions actuellement un petit utilitaire pour automatiser et sécuriser cette opération, il devrait permettre aussi de faire un update automatique des « saveurs » disponibles.

Finissons sur une remarque générale : nous proposons ici un moyen simple de gérer un très classique problème du développement logiciel. Dans le bon vieux temps des compilateurs C, des fichiers make permettaient de se lier à des librairies différentes en fonction de valeur de macro (la « saveur » à produire). Visual Studio n’intègre pas cette possibilité (subordonner ses références au « mode de compilation ») car elle a souvent été mal utilisée et source de problème.

Pour notre part, nous estimons avoir besoin de ce petit jeu : compiler facilement ma Solution avec tous les checks intégrés dans la saveur « Debug » du Core pour faciliter la traque des bugs… alors que ma Solution, elle-même, est compilée en « Release ».

Ou le contraire : j’ai confiance dans le Core, je travaille en Debug et utilise le Core en Release. Bref, les différentes combinaisons sont possibles… Mais nous imposons néanmoins UNE contrainte forte : les « saveurs » doivent être interchangeables au sens où elles doivent toutes contenir les mêmes composants.

Exemple pratique : Faire fonctionner Contrib après un checkout

Voici toutes les étapes pour mettre en œuvre la solution Contrib directement après un checkout. Pour information le dépôt de Contrib est à l’adresse suivante : https://civikey.svn.codeplex.com/svn/trunk/Contrib Voici la marche à suivre :

  1. 1 - Aller dans Runtime/, créer le dossier Current/ et l’ajouter en « ignore list » dans votre client SubVersion.
  2. 2 - Exporter les dll d’un des deux dossiers de Runtime/ vers Current/.
  3. 3 - Lancer la solution Contrib.VS2008.sln.
  4. 4 - Compiler une première fois la solution.
  5. 5 - Dans les propriétés du projet Artefact aller dans la section « Déboguer » puis choisir « Output/Debug (ou Release selon votre mode de compilation)/CiviKey.exe » comme programme de démarrage. Attention cette configuration s’enregistre dans un fichier .user, qui ne doit pas être commit car cette configuration est liée à chaque poste.

Une fois cette configuration faite vous n’aurez plus qu’à changer le mode de compilation de Contrib ou de changer les dll de Runtime/Current/ pour compiler avec les saveurs qu’il vous faut.

Powered by BlogEngine.NET