Bienvenue à Blogs CodeS-SourceS Identification | Inscription | Aide

Aurélien GALTIER

The dark world of .Net

Coding is like Read

Aujourd'hui j'ai envie de sensibiliser les développeurs sur comment coder. C'est ambitieux et je sais que tout le monde ne sera pas d'accord avec moi sur la vision que j'ai du code. Ce que je vais dire par la suite, est pour certain du bon sens. Mais faite-moi confiance que beaucoup de société ne respecte pas toujours ce bon sens.

Pour cela on va partir d'un petit projet. Le code est disponible sur https://github.com/swoog/CodeCoding:

static void Main(string[] args)
{
   var j = new Jedi
   {
      Name = "Skywallker",
      FirstName = "Luke",
      Saber = new Saber(Color.Blue),
   };

   var dv = new Lord
   {
      Saber = new Saber(Color.Red),
   };

   var e = new Emperor();

   j.Saber.Severing(dv);

   e.Lightning(j, dv);

   var a = dv.To(firstName: "Anakin", name: "Skywalker");

   a.Kill(e);

   TE();
}

Ok on voie dans ce code que cela parle de Jedi, de Luke, Anakin, … Bref on est dans star wars mais on ne comprend rien.

Bien choisir ces noms de classes

Le choix du nom des classes est souvent fait rapidement. Ici on a une classe qui est nommé "Saber". Mais quel type de sabre ? Un sabre laser ?

Si vous nommé vos classe avec une convention claire on comprendra déjà plus ce qu'elles font. Dans mon cas je nommerai la classe LightSaber. Le jour ou voudrez mutualiser du code entre deux classe "Saber" vous pourrez appeler votre classe abstraite Saber (Ou AbstractSaber).

Pour ma part je respecte les conventions suivantes pour l'héritage :

Classe mère Service si c'est une classe abstraite AbstractService et pour la classe fille ProductService si c'est un service qui gère des produits.

Evité les abréviations. Les classes du style PrdSrv, ce n'est pas très claire. ProdServer ? ProductServer ? … Il sera plus facile de comprendre ce que fait une classe si l'on peut lire le nom sans être obligé de connaitre les abréviations. 

Les noms des propriétés

Maintenant que j'ai changé le nom de la classe. Je renommerais la propriété Saber en LightSaber. Effectivement la propriété est maintenant du type LightSaber.

Mais la deuxième propriété que je n'aime pas c'est "Name". Donc sur la classe on en déduit que Name est juste le nom de famille. Alors pourquoi pas le préciser. On renomme la propriété Name en LastName.

J'ai aussi sur la classe Jedi une propriété "Full". Au vue du code elle donne le nom complet du Jedi. Je propose de la nommer FullName.

public string Full
{
   get
   {
      return string.Format("{0} {1}", this.FirstName, this.Name);
   }

Les abréviations sont aussi à éviter pour les propriétés. J'ai déjà vue des cas d'abréviation qui ne veulent pas dire la même chose et qui sont différent à une seule lettre prêt (PrdhId et PrdId). Par contre effectivement on peut se fixer comme convention que Id est l'abréviation de Identifier. Mais n'abusez pas des abréviations pour tous. Car si je met les noms suivants :

PrdId
SrvId
UsrLog
Pwd
TxValue

C'est quand même plus lisible de mettre :

ProductId
ServiceId
UserLogin
Password
TaxValue

Nommer ces variables

Le nom des variables ce n'est pas important !!! Erreur

Certain aurais nommé la propriété "FullName" en "JediFullName".

Effectivement on obtiendrait :

j.JediFullName

C'est vrai que l'on comprend ce que cela renvois. Mais on ne sait pas trop ce qu'est "j"

Je pense que ce n'est pas la peine de répéter le nom de l'objet dans le nom des propriétés. On doit nommer correctement ces variables et le problème sera résolu. Si je fais :

var jedi = new Jedi{ FirstName = "Luke", LastName = "Skywalker" }
Console.WriteLine(jedi.FullName);
On comprend déjà mieux ce que l'on fait.

Mais on peut aller plus loin.

var luke = new Jedi{ FirstName = "Luke", LastName = "Skywalker" }
Console.WriteLine(luke.FullName);


Effectivement on manipule une instance de la classe Jedi et c'est même Luke

Je ne rentrerais pas dans le débat du mot clef var. Est-ce que c'est bien de mettre var ou pas ? Moi je n'ai pas d'avis tranché sur la question. Je mets naturellement le mot clef var dans mes applications. Maintenant je pense que si le code est propre et lisible il n'y a pas de problème à comprendre le type de la variable.

Le nom des méthodes

Pour le nom des méthodes aussi il faut faire attention.

Ma convention c'est chaque mot avec la première lettre en majuscule. Évitez les abréviations. Utilisez des conventions pour tous ce qui est CRUD. Tel que :

GetProduct, InsertProduct, UpdateProduct, DeleteProduct.

Si votre méthode renvois plusieurs produits alors utilisez GetProducts.

Vous pouvez aussi utiliser les mots clefs : From,To, Of, At, …

Dans notre exemple on peut faire luke.LightSaber.SeveringOf(lord) Et on peut même préciser quoi luke.LightSaber.SeveringHandOf(lord)

Dans cette exemple on comprend bien ce qui ce passes.

De la même manière je ferais

emperor.UseLightningForceOn(luke, lord)

Dans certain cas cela fait un nom de méthode très grand. Mais je trouve c'est plus lisible. Si j'utilise des abréviations encore une foi j'obtiens quelque chose comme :

emperor.UseLghtForceOn(luke, lord)

Lght ? Light Force ?

Pour décider du nom d'une méthode comme pour le nom d'une classe, vous pouvez faire appel à votre équipe. Il est toujours bon quand vous avez à fixer le nom d'une classe ou méthode importante de faire un vote avec propositions. Chacun propose un ou plusieurs noms et vous votez. Avantage vous aurez peut-être pas eu l'idée du nom et ce nouveau nom sera connus de tout le monde. Par contre il ne faut pas non plus que cela prennent l'après-midi pour décider du nom.

TheEnd

Pour finir la méthode TE à la fin du programme peut être renommé en TheEnd() Ce qui donne le code suivant:

static void Main(string[] args)
{
    var luke = new Jedi
    {
        LastName = "Skywallker",
        FirstName = "Luke",
        LightSaber = new LightSaber(Color.Blue),
    };

    var darkVador = new Lord
    {
        LightSaber = new LightSaber(Color.Red),
    };

    var emperorSith = new Emperor();

    luke.LightSaber.SeveringHandOf(darkVador);

   
emperorSith.UseForceLightningTo(luke, darkVador);

   
var anakin = darkVador.To(firstName: "Anakin", lastName: "Skywalker");

   
anakin.Kill(emperorSith);

   
TheEnd();
}

On comprend mieux ce que fait le code il raconte le duel de Luke et dark vador contre l'empereur Sith. Pour le fun l'exécution du programme donne comme résultat:

Luke Skywallker severing hand of Dark Vador
Sith emperor use force lightning on Luke Skywallker and Dark Vador
Anakin Skywalker reemerges
Anakin Skywalker kill Sith Emperor

Un dernier point, est le choix des noms français ou anglais ? Moi je penche pour l'anglais. Si je prends le français, avec une méthode qui renvois des produits on obtient "GetProduits" normalement il serait plus correcte de dire "ObtientProduits". Pour ma part je préfère écrire "GetProducts". Mais je pense que c'est un choix important dans la nomenclature du code. Évité les mélange français/anglais.

J'ai fait ce code exprès sans commentaire et on peut voir que même sans commentaire il est compréhensible. D'une manière générale il faut faire du code qui pourra être lus par un nouveau développeur sur le projet. Ainsi le code sera auto documenté.

Kinect + Speech Recognition + Eedomus = Dommy

Dommy c'est quoi ?

Il y a quelque temps j'ai voulus faire un peu de domotique. Contrôler la lumière, le chauffage, et bien d'autre chose à distance.

J'ai donc acheté une box Eedomus (http://www.eedomus.com/fr/). C'est une box de domotique française et très bien pensée. Cette box utilise le protocole Zwave sans fil pour transmettre ces ordres à différents composants dans la maison. Ces composants peuvent être un micro module derrière un interrupteur, un capteur de présence, de température, …

Mais voilà utiliser son smartphone pour allumer la lumière ce n’était pas suffisant. Pourquoi ne pas brancher la Kinect et faire de la reconnaissance vocale ?

J'ai donc développé Dommy une application qui permet de faire de la reconnaissance vocale et exécuter des scenarios. Le nom Dommy est une référence à la B.D. Travis dans laquelle une intelligence artificielle (Dommy) contrôle toute la maison.

Et c'est là que cela devient intéressant. D'un petit programme j'ai maintenant une solution complexe. http://dommy.codeplex.com/

 

Comment cela marche ?

En trois grandes parties :

- Listeners : Qui écoute sur différentes sources (Audio, REST, UIRT, Kinect Skeleton, …)

- Un moteur de scénarios : Qui permet de décrire des scénarios plus ou moins complexe (Utilisation de Roslyn)

- Des actions : Déclenchements d'actions (API Eedomus, API Samsung, …)

Et pour lier tous cela j'utilise Ninject.

Le cœur de mon application c'est la class Engine dans la méthode "Init" elle va :

- Lire tous les scripts.

- Initialiser tous les listeners.

- Construire les scénarios lus dans les scripts.

- Initialiser les scénarios.

- Démarrer tous les listeners.

 

Pourquoi Kinect ?

Parceque c'est fun. Non ce n’est pas la bonne réponse. Kinect parce que c'est un Microphone Array http://en.wikipedia.org/wiki/Microphone_array

En pratique il permet d'isoler les voix et de supprimer les bruits parasitaires. De plus le SDK Speech Recognition associé est l'une des dernières versions de chez Microsoft que l'on peut utiliser dans nos applications. (A pars Bing Speech qui ne marche que dans les applications WP8 et W8)

Et plus tard je compte rajouter la détection de mouvement, QRCode, …

 

Et maintenant

D'un côté on a Kinect de l'autre une box domotique avec une API la plus simple possible. Une requête HTTP pour exécuter une action. (Exemple allumer la lumière)

La première version de l'application était très simple. Mes scénarios étaient codés en dure dans le code. Mais j'ai voulue abstraire un peu tous cela.

Contraintes que je me suis fixé :

- Mes scénarios sont fortement typés

- Je peux les créées sans recompiler l'application

- Le déploiement des scenarios se fais par copier/coller.

Et c'est là que j'ai eu l'idée d'utiliser Roslyn.

Avec Roslyn rien de plus facile pour exécuter un fichier de code.

   1: var engine = new Roslyn.Scripting.CSharp.ScriptEngine();
   2: engine.AddReference("System.Core");
   3: var session = engine.CreateSession(this);
   4: session.SetReferenceSearchPaths(Environment.CurrentDirectory);
   5: session.ExecuteFile(file);

Jusque l'à il y a rien de bien compliqué. Sauf que : Comment je vais décrire des scénarios ?

La syntaxe des scénarios

Souvent quand je ne sais pas comment démarrer le développement je me dis : qu'est-ce que je veux écrire comme code à la fin ?

Dans un premier temps il faut créer le scenario. Facile :

   1: Scenario.Create()

Ensuite un scenario sa doit démarrer sur un déclencheur :

   1: Scenario.Create()
   2:  .SpeechTrigger("redemarre")

Mon scenario va se déclencher sur un SpeechTrigger. Lorsque l'application entendra "redemarre" alors j'exécute le code. Mais quel code ?

C'est là que j'ai quelque actions toutes faite dont une : .Say()

   1: Scenario.Create()
   2:  .SpeechTrigger("redemarre")
   3:  .Say("Redémarage", "Je redémare", "a dans 2 secondes")

Ce qui aura pour effet de prendre aléatoirement une des phrases en paramètre et de la lire vocalement grâce au TextToSpeech.

Et pour terminer le code de mon scénario est :

   1: Scenario.Create()
   2:  .SpeechTrigger("redemarre")
   3:  .Say("Redémarage", "Je redémare", "a dans 2 secondes")
   4:  .Action(() =>
   5:  {
   6:     Application.Restart();     
   7:     Environment.Exit(0);
   8:      return true;
   9:  })
  10:  .Start();

Le start c'est pour indiquer à l'environnement d'exécution d'ajouter le scenario dans la liste des scenarios à exécuter.

Maintenant quand mon application entend "Dommy redemarre" alors l'application redémarre.

 

Et après ?

Maintenant que j'ai un moteur je vais pouvoir ajouter des scénarios. Et un scénario que j'utilise régulièrement c'est :

   1: Scenario.Create("Je pars")
   2: .SpeechTrigger("je pars", "je m'envais")
   3: .UsbUirtTrigger("261236020F0")
   4: .Say("Je ferme la maison", "A plus", "A bientôt")
   5: .EedomusOnOff("902331", false) // Eteint la lumière
   6: .EedomusOnOff("834522", false) // Ferme le volet
   7: .TVCommand(TVCommand.PowerOff)
   8: .UsbUirt(irOffVentilo)
   9: .Start();

A la final quand je pars de chez moi je dis "dommy je pars" et à ce moment-là Dommy :

- Me dis "a plus"

- Eteint la lumière du salon (bientôt tous l'appartement)

- Ferme le volet de la salle

- Eteint la télévision

- Eteint le ventilateur en simulant la commande infra rouge.

Et si je ne veux pas parler je peux prendre une vielle télécommande infra rouge qui me permet de déclencher le même scénario grâce au UsbUirtTrigger. Qui s’inscrit sur le listener infra rouge.

 

Mais finalement à quoi ça sert ?

Ce projet n'a pas d'autre but que d'être un projet gadget ! Je rajoute d’autres fonctionnalités au fur et à mesure de mes idées et envies :

- Ajout de la Kinect 2.0

- Multi langues

- Je développe une interface web de contrôle et de suivis de Dommy qui je l'espère un jour ressemblera à cela :

image

- …

 

En résumer beaucoup de boulot pour éteindre la lumière.

Dispose, Finalize et Optimisation de code égal bug ?

Dernièrement l'équipe de recette ou je travaille, test la nouvelle application et nous a remonté des bugs sur l'impression des documents XPS. Hors lorsque je teste sur mon poste cela marche correctement.

Le bug : Impression de 3 ou 4 pages puis une erreur sur le faite que le flux est fermé. Quand je fais F5 et que je teste j'imprime facilement les 20 pages qui compose mon XPS.

Alors d’où viens le problème ?

Après avoir tester en debug en release, avec la version de la recette… Le problème ne se reproduit que lorsque l'application est compilé en release et quel est lancé sans faire F5 (Sans debugger attaché)

Alors si le problème se reproduit seulement en release avec exactement les mêmes données. C'est que cela vient d'une option différente entre DEBUG et RELEASE.

Et l'option qui fait tous planter c'est : "Activer les optimisations" Pour en être sur je désactive l'option sur l'assembly ou se fais l'impression. Je compile exécute l'application et tous fonctionne.

Attention : La désactivation de cette options n'est pas recommandé dans un environnement de production.

Pourquoi mon code optimisé plante ?

Diverse raison peuvent entrainer un problème lorsque le code est optimisé. Ces bugs sont assez rare. Dans mon cas cela vient d'un objet qui implémente le paterne Disposable et Finalize.

J'ai fais une petite application qui reproduit le bug.

Je créé une classe DisposableObject :

    public sealed class DisposableObject : IDisposable
    {
        public MemoryStream MyStream { get; private set; }

        public DisposableObject(byte[] file)
        {
            this.MyStream = new MemoryStream(file);
        }

        public void Dispose()
        {
            this.Dispose(true);
            GC.SuppressFinalize(this);
        }

        public void Dispose(bool disposing)
        {
            if (!disposing)
            {
                this.MyStream.Dispose();
            }
        }

        ~DisposableObject()
        {
            this.Dispose(false);
        }
    }

Cette classe créé un MemoryStream a partir du tableau de byte passé au constructeur. Elle implémente Idisposable et donc dispose mon MemoryStream quand elle est disposé ou détruite. D'une manière générale évité d'exposer des objets Disposable (MemoryStream) en propriété public.

Et le corps de mon application :

            try
            {
                byte[] b = new byte[int.MaxValue / 100];

                var disposableObject = new DisposableObject(b);
                var stream = disposableObject.MyStream;

                ReadStream(stream);

                Console.WriteLine("End");
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex);
            }

            Console.ReadLine();

Je créé un tableau de byte assez gros (Pour que l'application mette un peut de temps a s'exécuté)

J'utilise mon objet pour récupéré mon MemoryStream et le lire dans la méthode ReadStream.

La seul erreur dans ce code c'est que je n'ai pas utilisé de using pour mon DisposableObject. Dans un bout de code aussi simple on ce dis que le using si il n'est pas exécuté c'est pas trop grave. De toutes façons le garbage est la pour nous détruire notre objet comme un grand.

Je sélectionne le mode Release, appuis sur F5. L'application lis le MemoryStream, met un peut de temps a se terminer. Mais tous fonctionne correctement.

Je vais dans le répertoire de Release et double clique sur l'exécutable (Je n'ai pas modifié le code, ni recompilé). Et la on obtient une erreur :

System.ObjectDisposedException: Impossible d'accéder à un Stream fermé.

Ce qui se passe :

· L'application créé un MemoryStream

· Elle commence a lire le MemoryStream dans la méthode ReadStream

· Le garbage collector décide de passer pour libéré la mémoire.

· Il détecte que disposableObject n'est plus jamais utilisé et décide de le détruire.

· Notre destructeur appelle dispose qui lui dispose le MemoryStream.

Pourquoi en faisant F5 je ne reproduit pas le problème ? Simplement car vous êtes dans un environnement de développement. Pour que le debugger puisse agir correctement, il y a beaucoup d'optimisation qui sont automatiquement désactivé.

La solution

La solution est toutes simple. Et je l'ai déjà donné au dessus. Lorsque vous utiliser un objet qui implémenter le paterne Disposable, utilisé le mot clef using.

Evité aussi d'utilisé le destructeur. En générale votre classe vas être garbagé. Et si vous avez utilisé le mot clef using, vous aurez alors libéré les ressources au moment ou cela vous convient. Dans mon cas c’est-à-dire après le ReadStream.

Source

Ninject : un framework de Ninja

Ninject est une framework d'injections de dépendances, d'IOC, … Lorsque l'on commence avec Ninject on en découvre un petit framework rapide à prendre en main et très limité. Mais il est loin d'être limité.

Ninja framework
Tous les exemples et tests unitaires de Ninject sont faits sur le thème des ninjas, voilà pourquoi Ninject  a un logo Ninja. http://www.ninject.org/
La version 3.0 vient de sortir il y a peu de temps. C'est un framework Open source et disponible sur Nuget. 
Beaucoup de versions disponibles :
.NET 3.5/4/4.5
SL 2/3/4/5
WP 7/7.1
Mono 2/4
Compact Framework 3.5
Et prochainement une version pour Windows 8 Metro apps.

Des intégrations dans différents type de projets WCF, MVC, WF, ...

Un kernel pour les gouverner tous
Au contraire de beaucoup de frameworks de ce type, vous n'aurez pas besoin de créer un fichier XML pour utiliser Ninject qui marche par une API fluent.

Dans Ninject la notion principale est le Kernel. Ce kernel va permettre de définir l'ensemble des éléments à intégrer dans le contexte. En général un StandarKernel suffira. Dans certains cas très spécifiques on pourra créer sa propre class Kernel. Mais vous verrez en l'utilisant qu'un StandardKernel répond déjà à beaucoup de choses.

// Création d'un kernel.
var kernel = new StandardKernel();
// Ajout de la class ProductDao dans notre contexte.
kernel.Bind<ProductDao>().ToSelf();
// on récupère une nouvelle instance de ProductDao
var productDaoInstance = kernel.Get<ProductDao>();
foreach (var product in productDaoInstance.GetProducts())
{
    Console.WriteLine(product.Title);
}

La méthode Bind permet d'indiquer quel type on va demander via la méthode Get. Puis on appelle ToSelf pour dire que l'on renverra un objet du même type. La méthode bind non générique prenant un Type en paramètre existe, ainsi on peut aussi faire des généralités.
Pour plus de souplesse je souhaiterais faire la même chose mais ne passer qu'une interface au Get. Il me suffit de faire un Bind sur une interface et de faire autre chose qu'un ToSelf. Par exemple :

// Ajout de la class ProductDao dans notre contexte.
kernel.Bind<IProductDao>().To<ProductDao>();
//On récupère une nouvelle instance de ProductDao
var productDaoInstance = kernel.Get<IProductDao>();

Maintenant on vient de créer une liaison entre l'interface IProductDao et la classe ProductDao. Cette liaison est créé à l'exécution et peut être changée dans différents cas entre autre dans les tests unitaires.
Maintenant Get<IProductDao>() me renvoit une instance de type ProductDao.

Une autre façon de faire aurait pu être d'utilisée ".ToMethod(a => new ProductDao())" ainsi vous pouvez faire ce que vous voulez au moment de la création de l'objet.

Convention
Ce que j'aime bien dans les applications c'est le développement par convention. C'est-à dire respecter des conventions d'écriture de nommage pour que l'application fonctionne correctement. Vous gagnez sur 2 points :
    - Le premier point, vous obligez l'ensemble des développeurs à faire du code le plus similaire possible.
    - Le deuxième point est la généralisation de certains fonctionnements de votre application.

On imagine bien que dans un gros projet faire un bind de chaque interface avec leurs classes pour les DAO peut être long et contraignant. Avec d'autres frameworks on fait cela dans un fichier XML mais cela est aussi très contraignant. Pour ma part il m'arrive souvent dans ces cas là d'oublier de le référencer dans le fichier XML.

Ninject est un framework très extensible il y a des packages qui existent déjà tel que Ninject.Extensions.Conventions. Utilisez Nuget pour l'installer. Dans le StandardKernel par défaut il y a un chargement automatique des modules contenus dans des assemblies Ninject.Extensions.*.dll ainsi l'extension Conventions est automatiquement opérationnelle.

Maintenant on peut écrire cela :

using Ninject.Extensions.Conventions;
            // Ajout des class *Dao dans notre contexte.
            kernel.Bind(c => c
                .FromThisAssembly()
                .SelectAllClasses()
                .EndingWith("Dao")
                .BindDefaultInterface());

Ne pas oublier de rajouter le namespace Ninject.Extentions.Conventions car si non cela ne compile pas. Ce namespace nous rajoute une méthode d'extension Bind pour les conventions.
Cette méthode Bind accepte une lambda qui passe en paramètre un objet implémentant IFromSyntax. Cette interface permet de décrire des conventions de binding dans le kernel.

.FromThisAssembly() va indiquer que l'on va travailler dans l'assembly courante. D'autres méthodes vous permettent  d'indiquer d'autres assembly.
.SelectAllClasses() on récupère toutes les classes de l'assembly courante.
.EndingWith("Dao") les noms des classes doivent terminer par le mot Dao.
.BindDefaultInterface() va binder les interfaces I*Dao avec les classes *Dao.

Il existe différentes méthodes de binding en convention. 
    - BindAllInterfaces : Qui bind toutes les interfaces de la classe avec la classe.
Equivalent :

kernel.Bind<IInterface1>().To<Class1>())
kernel.Bind<IInterface2>().To<Class1>())
kernel.Bind<IInterface3>().To<Class1>())

Si Class1 implémente les 3 interfaces.

    - BindDefaultInterface : Qui bind l'interface portant un nom similaire avec la classe.
Equivalent :

Kernel.Bind<IClass1>().To<Class1>()
Kernel.Bind<IClass2>().To<Class2>()

    - BindSingleInterface : Qui bind la seule interface existant sur le type avec la classe existante.
    - … Et bien d'autres manières où vous pourrez même créer vos propres mécaniques de Binding avec la méthode Bindwith<>

Dans mon cas l'écriture du binding est un peu plus complexe qu'avant. Mais il faut voir que demain il me suffira d'ajouter une interface et une classe qui porte un nom similaire et qui termine par Dao pour que automatiquement mon application ajoute ce DAO dans le contexte.

Conclusion
Cette article n'est qu'un début dans un prochain article on ira un peu plus loin dans l'utilisation de Ninject avec de l'injection, AOP, et des modules. Au premier abord le framework est très simple. Mais il est aussi très puissant. J'ai poussé le framework dans plein de cas, à chaque fois j'ai trouvé une solution. Au contraire de la première impression, ce framework est très flexible et je n'ai pas encore trouvé de limitation.

kernel.Bind<EndUtility>().ToSelf();
kernel.Get<EndUtility>().Bye();
Source
VS11 : Metro ne rime pas avec moche

Avant de commencer, je tiens à signaler que je suis un fervent défenseur de Visual Studio  depuis plusieurs années. J'ai trouvé que VS s'était amélioré, autant sur le plan visuel que sur le plan technique. Mais malgré beaucoup de choses intéressantes dans cette nouvelle version, je trouve qu'il y a beaucoup de choses qui ne vont pas.

VS11 est une beta, peut-être que nombre de critiques que je fais seront corrigées dans la version finale. De plus je sais que beaucoup de problèmes peuvent se régler par des options, plugin, … Mais je souhaite juste faire un retour sur l'interface par défaut proposée par Microsoft.

Metro comme dans le Métro Parisien ?
Microsoft nous sort des nouvelles "guides lines" sous le nom Metro. Je dirais que Metro c'est 2 choses :
    - Simplification visuelle
    - Ergonomie
   
Dans Metro on enlève une bonne partie des fioritures. Les dégradés, les arrondis, etc, disparaissent pour une simplification. De plus on affiche les données essentielles à l'application, on repense l'utilisation de nos applications. On cherche à guider l'utilisateur. Des icones claires nettes et précises.

Metro oui mais non
Metro ne rime pas avec moche au contraire. Quand je vois VS11 j'ai plusieurs questions :
Pourquoi Gris ?
Pourquoi avoir enlevé des icones ?
Pourquoi les majuscules ?

Avec VS11 on reprend le principe monochrome gris/noir et une couleur d'accent bleu.
On va commencer par les icones. En regardant la version Lumière et la version Sombre on se rend compte que les deux versions n'ont  pas le même impacte visuel.
En regardant de près on se rend compte que les deux icones sont identiques.
Image1

Je ne pense pas que cela soit judicieux d'utiliser les mêmes icones pour  les 2 versions. Le problème c'est que du coup les icones de la version Lumière sont moins nettes. Elles ont une espèce de lueur autour.
Je trouve que le séparateur d'icones est assez moche :
Image2

Qu'est ce que c'est que ce séparateur ? Pourquoi un effet 3D ? Pourquoi 2 couleurs ? On nous démontre que la simplicité de l'interface permet d'aller à l'essentiel et voilà que l'on se retrouve avec un séparateur à 2 couleurs. Dans visual studio 2010 on avait qu'une seule couleur.

Explorateur du Metro
Image3L'explorateur de solution revu et corrigé. Importé de ce qui se trouvait dans le Productivity Power Tools. J'adore il est rapide, pratique, … De plus dans VS11 on a les aperçus du fichier qui sont plutôt pratiques. Le seul truc  à dire si je veux être critique :
    - Les cadenas bleu alors que tout le reste est gris c'est normal ? Peut être que c'est seulement pour la beta.
    - Les flèches sur la gauche de chaque fichier c'est un peu lourd. Sur VS2010 les flèches étaient plus légères. Ensuite on aurait pu trouver une solution de flèches qui ne se voit qu’au survole de la ligne.

Pour aller plus loin, une solution le Metro
Sur les crollbares j'ai deux remarques :
Image5
    - La première remarque c'est pourquoi elles n'ont pas été customisées au style Metro ? Je pense qu'il faut aller jusqu'au bout du concepte.
    - La deuxième remarque c'est pourquoi les indicateurs sur la scrollbar fournis dans Productivity Power Tools ne sont pas intégrés directement dans VS11 ?
   
En customisant proprement la scrollbar, je pense que l'on pouvait faire une intégration des indicateurs plutôt jolie. Et je tiens à signaler que cette fonctionnalité je l'utilise tous les jours dans VS2010. Il est tellement pratique d'avoir une indication visuelle sur  tout le fichier quand vous sélectionnez un mot en haut. 

Exécuter son application
Image6
J'aime beaucoup le nouveau bouton débugger qui devient contextuel. En plus fini les 2 triangles en mode debug. Ce bouton contextualisé en fonction du type de projet est une très bonne idée. Par contre je critique encore, mais pourquoi le bouton arrêter se trouve après la liste des configurations (que je ne peux pas changer a l’exécution) le bouton rechercher (qui s'est perdu), les options de barres et une barre de séparation. Pourquoi repenser des fonctionnalités de cette manière là ?
J'aurais préféré : La liste des configurations, le bouton exécuter, les boutons pause/arrêter/redémarrer, puis le reste.
Je dis cela car le petit carré en guise de bouton je le cherche sans arrêt.

Juste un autre petit truc, c'est quoi cette icone :
Image7
Il ne me semble pas que cela soit une icone indispensable. De plus le rouge indique une erreur, un problème, ….

Test unitaire
Image8
Je n'ai pas testé un autre framework de test unitaire. Mais la nouvelle interface est une merveille. Le résumé des tests passés est pratique pour voir la progression globale quand on a beaucoup de tests qui ne passaient plus. Les tests affichés à  gauche, ouvrent le fichier qui fait l'erreur sur la droite. On a une impression d'explorateur de tests. On a la liste des tests passés et ceux qui ne sont pas passés. On peut choisir la liste des tests qui ne passent pas et les traités un par un, ainsi ils disparaissent de la liste et la progression globale augmente. Cette interface est parfaite si l'on souhaite faire TDD. Un peu comme si on avait des tâches à résoudre.

Metro dans TFS
Image9Encore une fois je trouve cela super. L'interface est repensé, pratique. Des nouvelles et existantes fonctionnalités sont bien mises en avant. Réviser du code, suspendre, démarrer une taches.  J'ai juste une remarque, de temps en temps je suis un peu perdu entre l'affichage "mon travail" et l'affichage "Modifications en attentes". Du coup je cherche un peu comment revenir en arrière. Mais ce n'est qu'une habitude je pense.

Et après
Bon j'ai été très critique sur le visuel, mais je trouve que tout n'est pas à jeter dans VS11. Loin de là…
Beaucoup d'efforts sur des composants utilisés à plus ou moins grande échelle. Un concept Metro intégré dans Visual Studio, une très bonne chose, espérons que Microsoft fasse encore quelques efforts d'ici la sortie. Le gris n'est peut-être pas la meilleure couleur, mais je me faisais la remarque en utilisant Photoshop, je n'ai pas le problème. De plus la couleur d'accent fait ressortir un peu le côté triste.
VS11 sera encore un très bon cru.

Windows Phone 7 NeoBD

Ma troisième application publiée sur le market de Windows Phone. Les deux premières, une lampe torche et un sabre laser développés en très peu de temps. Et la troisième, une application qui me tient plus à cœur. Cette application c'est NeoBD.

But de l'application, gérer sa BD thèque. Quand on a beaucoup de B.D. on peut avoir des soucis pour savoir quels sont les nouveaux tomes de certaine séries ou les tomes que l'on ne possède pas dans une série.

Dans ce poste je vais faire un retour sur les grandes étapes de mon développement. Je ne vais pas parler technique mais plus conceptuel ainsi que des outils utilisés pour développer une application WP7.

J'ai commencé le développement de cette application avant la sortie des premiers Windows Phone. Temps de développement bout à bout je dirais environ 1 mois.

Le développement comprend :

  • Maquettage WP7 grâce au PSD fournis par Microsoft
  • Apprentissage des preview de "Entify Framework Code First" (A chaque preview quelques changements importants)
  • Service de récupération des nouvelles B.D. via l'API Amazon.
  • Service de récupération de flux RSS et association avec les séries.
  • Génération des images de couverture
  • Création et optimisation de services WCF.
  • Création de l'application Windows Phone.

Ce qui est important dans tout cela c'est que je n'avais pas connaissance de tous les produits.

Maquettage :

Pour le maquettage de l'application WP7 Microsoft met à disposition des PSD pour vous aider avec tous les contrôles de base de WP7. http://go.microsoft.com/fwlink/?LinkId=196225 Lorsque l'on connait un peu photoshop on peut arriver à faire une petite maquette correcte.

Une version Noire :

image

Et une version blanche :

clip_image001

Code First Entity

Comme à son habitude les Preview de Microsoft sont à prendre avec des gants. Quelques changements de noms de classes. Quelques changements de manière de configurer nos Entités. Et le regret de deux fonctionnalités retirées à la version finale :

Génération des images de couvertures

Je génère les images à la demande, j'utilise la librairie de .Net. Graphics et Bitmap. Pour les performances l'utilisation de la méthode LockBits ce qui permet d'optimiser un minimum. Par contre une fois l'image générée, elle est renvoyée directement. Un peu de réécriture d'URL et voilà le résultat : http://www.neobd.fr/phone/images/livre/120x170/2-radar-le-robot/352.jpg

Création et optimisation de services WCF.

L'optimisation du service WCF est importante elle passe par le choix du protocole mais aussi par la mesure des informations renvoyées. Lorsque vous utilisez un ORM tel que Entity Framework, il peut charger plus d'informations que ce que l'on souhaite. Hors ces éléments ne sont pas toujours intéressants et surchargent la connexion. D'autres facteurs comme EmitDefaultValue a false, … permettent de gagner quelques Ko.

Création de l'application Windows Phone.

Et vient la création de l'application en elle-même. Le plus dur dans la création de l'application c'est de trouver une navigation, entre les pages, correcte. Mais aussi d'avoir une impression de rapidité. Il faut aussi trouver une certaine ergonomie pour saisir une B.D. dans son catalogue.

Voilà un petit aperçu des différents modules composant mon application NeoBD. Malgré tous les efforts il reste encore des optimisations sur les différents points :

  • Optimisation des requêtes Entity
  • Optimisation WCF (Mode Semi hors ligne)
  • Simuler des chargements en 2 temps en récupérant l'objet à la page précédente.
  • Améliorer la saisie des livres que l'on possède
  • ...

Et bien d'autres idées...

clip_image001[9]

Télécharger NeoBD : http://windowsphone.com/s?appid=1c303447-2688-e011-986b-78e7d1fa76f8

TabControl, Surface et look Metro

Avec l'apparition du SDK Surface 2, je me suis demandé ce que je pouvais réaliser. Parallèlement j'ai un projet personnel NeoBD qui me permet de gérer ma collection de B.D. Pour donner le contexte, ce projet utilise une base centralisée exposée par des services WCF.

Après le téléchargement du SDK je me suis posé la question : Qu'est ce que je peux faire ? Alors j'ai créé une petite maquette rapide et voilà le résultat :

image

La maquette n'est pas complète et ne représente pas le fonctionnement entier de l'application. Mais j'ai décidé de faire un menu de taille fixe sur la gauche et un espace libre qui prend le reste de l'écran. Le but, pouvoir glisser plusieurs fiches de Séries. Je vais parler dans ce poste du menu de gauche où j'utilise un TabControl pour gérer les modules "mes B.D.", "manquantes", "menu".

Comment faire pour customiser le TabControl pour qu'il ressemble au menu de gauche.

Son utilisation :

  1:         <TabControl x:Name="tabcontrol">
  2:             <TabItem Header="mes B.D.">
  3:             </TabItem>
  4:             <TabItem Header="manquantes">
  5:             </TabItem>
  6:             <TabItem Header="menu">
  7:             </TabItem>
  8:         </TabControl>

On vas changer le template du TabControl :

  1:         <Style TargetType="TabControl">
  2:             <Setter Property="Template">
  3:                 <Setter.Value>
  4:                     <ControlTemplate TargetType="TabControl">
  5:                         <Grid KeyboardNavigation.TabNavigation="Local">
  6:                             <mesbdControl:MenuTabPanel  
  7:                             Panel.ZIndex="1"
  8:                             Margin="0,0,4,-1"
  9:                             IsItemsHost="True"
 10:                             KeyboardNavigation.TabIndex="1"
 11:                             Background="Transparent" />
 12:                             <Border Margin="70,75,0,0" Panel.ZIndex="100">
 13:                                 <ContentPresenter
 14:                             x:Name="PART_SelectedContentHost"
 15:                               Margin="4"
 16:                               ContentSource="SelectedContent" />
 17:                             </Border>
 18:                         </Grid>
 19:                     </ControlTemplate>
 20:                 </Setter.Value>
 21:             </Setter>
 22:         </Style>
 23: 

Dans ce template j'ai ajouté un mesbdControl:MenuTabPanel qui est un contrôle héritant du MenuTabPanel de WPF. Et j'ai ajouté un ContentPresenter pour le contenu du TabItem.

Que fait le contrôle MenuTabPanel ? Il place les onglets. Ce contrôle ressemble à un contrôle Panel, j'ai donc override les méthodes "MeasureOverride" et "ArrangeOverride"

  1:         protected override Size MeasureOverride(Size constraint)
  2:         {
  3:             double height = 50;
  4:             foreach (UIElement item in this.Children)
  5:             {
  6:                 item.Measure(constraint);
  7:  
  8:                 if (item is TabItem)
  9:                 {
 10:                     var tabItem = item as TabItem;
 11:                     if (!tabItem.IsSelected)
 12:                     {
 13:                         height += tabItem.DesiredSize.Width;
 14:                     }
 15:                 }
 16:             }
 17:             constraint.Height = height;
 18:             return constraint;
 19:         }

MeasureOverride : Je calcul la hauteur nécessaire a l'affichage correcte des éléments.

  1:         protected override System.Windows.Size ArrangeOverride(System.Windows.Size arrangeSize)
  2:         {
  3:             double height = 75;
  4:  
  5:             foreach (var item in this.Children)
  6:             {
  7:                 if (item is TabItem)
  8:                 {
  9:                     var tabItem = item as TabItem;
 10:                     if (tabItem.IsSelected)
 11:                     {
 12:                         tabItem.Arrange(new Rect(new Size(this.DesiredSize.Width, 50)));
 13:                         Animate(tabItem, 50, 0, 0);
 14:                     }
 15:                     else
 16:                     {
 17:                         height += tabItem.DesiredSize.Width;
 18:                         tabItem.Arrange(new Rect(new Size(tabItem.DesiredSize.Width, 50)));
 19:                         Animate(tabItem, 0, height, -90);
 20:                     }
 21:                 }
 22:             }
 23:  
 24:             return arrangeSize;
 25:         }

ArrangeOverride : Je place et modifie l'angle des TabItem correctement. Pour cela lorsque le TabItem est sélectionné je mets un angle de 0 (Ce qui correspond à un affichage horizontal) et je mets la position sur une valeur fixe x:50 et y:0.

Pour tous les autres TabItem je mets un angle de -90 (Ce qui correspond à un affichage vertical) et je mets la position sur une valeur variable en hauteur calculée sur la somme des largeurs des TabItem non sélectionnées.

La méthode Animate va ajouter une petite animation des TabItems ce qui va donner un effet de changement.

  1:         private void Animate(TabItem item, double x, double y, double angle)
  2:         {
  3:             DoubleAnimation animAngle = new DoubleAnimation(angle, new Duration(TimeSpan.FromMilliseconds(500)));
  4:             DoubleAnimation animX = new DoubleAnimation(x, new Duration(TimeSpan.FromMilliseconds(500)));
  5:             DoubleAnimation animY = new DoubleAnimation(y, new Duration(TimeSpan.FromMilliseconds(500)));
  6:  
  7:             TransformGroup tc = null;
  8:             RotateTransform rt = null;
  9:             TranslateTransform tt = null;
 10:  
 11:             if (item.RenderTransform is TransformGroup)
 12:             {
 13:                 tc = item.RenderTransform as TransformGroup;
 14:                 rt = tc.Children[0] as RotateTransform;
 15:                 tt = tc.Children[1] as TranslateTransform;
 16:             }
 17:             else
 18:  
 19:             {
 20:                 tc = new TransformGroup();
 21:                 rt = new RotateTransform();
 22:                 tt = new TranslateTransform();
 23:                 tc.Children.Add(rt);
 24:                 tc.Children.Add(tt);
 25:                 item.RenderTransform = tc;
 26:             }
 27:  
 28:             tt.BeginAnimation(TranslateTransform.XProperty, animX);
 29:             tt.BeginAnimation(TranslateTransform.YProperty, animY);
 30:             rt.BeginAnimation(RotateTransform.AngleProperty, animAngle);
 31:         }

En une demie seconde je vais faire un déplacement et une rotation de mon TabItem. Attention si les transformations existent déjà je les réutilise, si non votre control ne s'animera pas de son emplacement précédent.

Il reste a modifier le template du TabItem :

  1:         <Style TargetType="TabItem">
  2:             <Setter Property="Template">
  3:                 <Setter.Value>
  4:                     <ControlTemplate TargetType="TabItem">
  5:                         <Grid>
  6:                             <VisualStateManager.VisualStateGroups>
  7:                                 <VisualStateGroup x:Name="SelectionStates">
  8:                                     <VisualState x:Name="Unselected">
  9:                                     </VisualState>
 10:                                     <VisualState x:Name="Selected">
 11:                                     </VisualState>
 12:                                 </VisualStateGroup>
 13:                                 <VisualStateGroup x:Name="CommonStates">
 14:                                     <VisualState x:Name="Normal" />
 15:                                     <VisualState x:Name="MouseOver" />
 16:                                     <VisualState x:Name="Disabled">
 17:                                     </VisualState>
 18:                                 </VisualStateGroup>
 19:                             </VisualStateManager.VisualStateGroups>
 20:                             <TextBlock FontSize="40" FontFamily="Segoe WP"
 21:                                 VerticalAlignment="Top"
 22:                                 HorizontalAlignment="Left" Foreground="White">
 23:                             <ContentPresenter x:Name="ContentSite"
 24:                                               ContentSource="Header"
 25:                                               Margin="20,0,0,0"
 26:                                 RecognizesAccessKey="True" />
 27:                             </TextBlock>
 28:                         </Grid>
 29:                         <ControlTemplate.Triggers>
 30:                             <Trigger Property="IsSelected" Value="True">
 31:                                 <Setter Property="Panel.ZIndex" Value="100" />
 32:                             </Trigger>
 33:                         </ControlTemplate.Triggers>
 34:                     </ControlTemplate>
 35:                 </Setter.Value>
 36:             </Setter>
 37:         </Style>
 38: 

Et pour le moment j'obtiens quelque chose qui ressemble à cela :

image
Windows Phone 7 et Login page

Comment bien structurer son application WP7 lorsqu’une page de login est requise. Prenons un exemple d’une application.

Cas de base

LoginView.xaml ----Clique sur Connecter-----> MainView.xaml

Problèmes :

  • Aucun login automatique
  • Le bouton back renvoie sur la page LoginView
  • Le temps d’ouverture complet est long (Login + Main)

Cas de base avec autologin

Même navigation que dans le cas précédent. On utilise le login automatique à l’ouverture de la page si on a enregistré les informations dans l’isolatedstorage.

LoginView.xaml ----automatique-----> MainView.xaml

Problèmes :

  • Le bouton back renvoie sur la page LoginView et il est impossible de quitter l’application car on est reconnecté à chaque fois.
  • Le temps d’ouverture complet est long (Login + Main)

Cas avec GoBack

On reprend le même mode que dans le cas de base et on va maintenant utiliser RootFrame.GoBack(). Cela nous demande de restructurer la navigation de nos pages.

MainView.xaml ----Clique sur se connecter----->LoginView.xaml-----Clique sur connecter(GoBack)----->MainView.xaml

A la sortie d'une page lorsque l'on a vérifié les informations de login on fait GoBack() ce qui évite d'ajouter des pages dans la pile des pages précédentes.

Problème :

  • Il vous faut prévoir une page principale en mode non connecté.
  • Pas d'autologin
  • Le temps d’ouverture complet est toujours long

Ajout d'autologin

Dans tous les cas vous êtes obligé de garder un token qui identifie votre utilisateur. Ce token peut être un cookie ou un identifiant unique généré. J'ai choisi un token que le serveur renvoie pendant la phase d'authentification. Je stocke le token dans l'isolatedstorage afin d'authentifier automatiquement l'utilisateur. J'envoie le token dans le header de chaque requête WCF.

Coté client :

  1: using(new OperationContextScope(base.InnerChannel))
  2: {
  3:     OperationContext.Current.OutgoingMessageHeaders.Add(
  4:         MessageHeader.CreateHeader(
  5:             "UserToken",
  6:             "http://www.neobd.fr/",
  7:             WcfService.UserToken));
  8: 
  9:     return Channel.BeginGetNewsPosts3(serieId, possede, firstRow, countRows, callback, asyncState);
 10: }

Pour plus de sécurité il faudrait sécuriser les connections. Mais je ne rentrerai pas dans ces détails.

Je récupère le token côté serveur et j'authentifie l'utilisateur pour tout le contexte d'exécution de cette requête WCF.

Côté serveur :

  1: var incomeMessageHeaders = OperationContext.Current.IncomingMessageHeaders;
  2: int indexToken = incomeMessageHeaders.FindHeader("UserToken", "http://www.neobd.fr/");
  3: var monToken = incomeMessageHeaders.GetHeader<string>(indexToken)

A ce moment là si le token n'est pas correcte alors je renvoie une exception avec le message "security". J 'ai un helper côté Windows Phone qui intercepte les exceptions avec le message "security" et qui renvoie vers la page de login.

  1: private bool hasSecurityException = false;
  2: 
  3: protected void ExecuteConnection(Action action) 
  4: { 
  5:     if (NetworkInterface.GetIsNetworkAvailable()) 
  6:     { 
  7:         try 
  8:         { 
  9:             action(); 
 10:         } 
 11:         catch (CommunicationException ex) 
 12:         { 
 13:             if (ex.Message == "security") 
 14:             { 
 15:                 if (!hasSecurityException) 
 16:                 { 
 17:                     hasSecurityException = true; 
 18:                     ApplicationMvvm.Navigate("/LoginView.xaml?auto=true"); 
 19:                 } 
 20:             } 
 21:             else 
 22:             { 
 23:                 Application_ErrorConnection(ex); 
 24:             } 
 25:         } 
 26:     } 
 27:     else 
 28:     { 
 29:         Application_ErrorConnection(null); 
 30:     } 
 31: }

Au passage lorsqu'il y a une erreur ou que la connexion n'est pas disponible j'affiche un message d'erreur dans la méthode "Application_ErrorConnection". Attention on est en asynchrone, il faut appeler cette méthode sur la méthode "End" dans le callback pour intercepter l’exception.

Ainsi dans le cas d’un fonctionnement normal. Ouverture de MainView.xaml avec chargement des données. Aucune phase d'authentification n'est effectuée. On récupère le token de la dernière authentification.

En cas d'erreur d'authentification (le token a expiré, mot de passe expiré, compte bloqué, …)

MainView.xaml---navigation automatique--->LoginView.xaml

La page LoginView fait une tentative automatique pour renouveler le token. Si celui ci échoue l'utilisateur reçoit une information concernant le problème d'authentification.

Avantage, le temps d’ouverture complet sera réduit dans la majorité des cas.

En résumé

Le mode de développement par page a son importance. Il permet d'avoir une application structurée et intuitive. Sans en abuser il faut se souvenir que l'on peut faire un GoBack().

Ses avantages :

  • Dépile une page peu importante (Page de login, page d'inscription, page de sélection, …)
  • Permet de quitter une page sans connaitre sa destination. Ce qui est pratique dans un page de login qui peut être appelée à différents moments.

Je n’ai pas expliqué dans l’article mais il peut y avoir plusieurs niveau à dépiler. La page principale qui appelle la page de login puis la page de création de compte. De la même manière les pages sont dépilé pour retrouver la page principale.

L’utilisation d’un token est requise pour le fonctionnement global de l’application. Pourquoi ne pas le stocker pour réduire la phase de login ce qui évite d’authentifier à chaque ouverture de l’application.

CSS Hack et ASP.Net

Pour mon premier article je vous propose une astuce que je viens de trouver pour gérer les différents navigateurs en ASP.Net.

Je cherchais un Hack pour IE9 pour corriger un bug CSS. Et je suis tombé sur cet article http://davidbcalhoun.com/tag/ie-hack

L’astuce utilisée permet de spécifier la balise body en fonction du navigateur directement dans le HTML : 

   1: <!--[if lt IE 7 ]> <body class="ie6"> <![endif]-->
   2: <!--[if IE 7 ]>    <body class="ie7"> <![endif]-->
   3: <!--[if IE 8 ]>    <body class="ie8"> <![endif]-->
   4: <!--[if IE 9 ]>    <body class="ie9"> <![endif]-->
   5: <!--[if gt IE 9]>  <body>             <![endif]-->
   6: <!--[if !IE]><!--> <body>         <!--<![endif]-->

Mais avec ASP.Net on peut faire mieux :

   1: body.Attributes.Add("class", Request.Browser.Browser.ToLower() + " " + Request.Browser.Browser.ToLower() + Request.Browser.MajorVersion);

Ajoutez cette ligne dans le OnLoad de votre MasterPage. Ensuite dans vos styles CSS il ne reste plus qu’à cibler ce qui vous intéresse.

Vous pouvez cibler un navigateur :

   1: .myClass{ /* default */ }
   2: .ie .myClass{ /* pour ie */ }
   3: .firefox .myClass{ /* pour firefox */ }

 

Ou vous pouvez cibler une version d’un navigateur :

   1: .myClass{ /* default */ }
   2: .ie6 .myClass{ /* pour ie 6 */ }
   3: .firefox4 .myClass{ /* pour firefox 4 */ }

 

Plus besoin de hack avec des fichiers CSS compliqués. De plus, si vous utilisez JQuery vous pouvez adapter votre JQuery de la même manière.

Posted: jeudi 7 avril 2011 21:04 par agaltier | 4 commentaire(s)
Classé sous :
Aurélien GALTIER

Hello,

J’ai environ 5 ans d’expérience dans les technologies Microsoft. Je rejoins la communauté des blogs de Codes-Sources pour communiquer sur les technologies .Net que j'ai rencontré et que je rencontrerai (C#, F#, ASP.Net, Sharepoint, Windows Phone 7, SQL Server,..) Mais aussi des concepts d’architectures AOP, SOA ,… Et bien d’autres choses…



Les 10 derniers blogs postés

- Technofolies, votre évènement numérique de l'année par Le Blog (Vert) d'Arnaud JUND le 09-26-2014, 18:40

- Xamarin : From Zero to Hero par Fathi Bellahcene le 09-24-2014, 17:35

- Conférences d’Automne 2014 par Le blog de Patrick [MVP SharePoint] le 09-24-2014, 14:53

- [TFS] Supprimer un projet de Visual Studio Online par Blog de Jérémy Jeanson le 09-22-2014, 20:42

- Nouveau blog en anglais / New blog in english ! par Le blog de Patrick [MVP SharePoint] le 09-18-2014, 18:42

- [ #Yammer ] From Mailbox to Yammer and back / De votre messagerie vers Yammer et retour ! par Le blog de Patrick [MVP SharePoint] le 09-15-2014, 11:31

- [ #Office 365 ] New service settings panel / Nouveau panneau de paramétrage des services par Le blog de Patrick [MVP SharePoint] le 09-11-2014, 08:50

- Problème de déploiement pour une démo SharePoint/TFS? par Blog de Jérémy Jeanson le 09-10-2014, 21:52

- [ #Office365 ] Delve first impressions / Premières impressions sur Delve par Le blog de Patrick [MVP SharePoint] le 09-09-2014, 16:57

- [ #Office365 ] How to change Administration console language ? / Comment changer la langue de la console d’administration ? par Le blog de Patrick [MVP SharePoint] le 09-09-2014, 08:25