Dot.Blog

C#, XAML, Xamarin, UWP/Android/iOS

Windows Phone : les bases de données locales

[new:30/05/2014]Depuis la version 7.1 Windows Phone offre une gestion de base de données intégrée manipulable en LINQ. Peu de développeurs semblent en connaitre l’existence et encore moins la façon d’en tirer le meilleur parti alors qu’on la retrouve bien entendu sous Windows Phone 8 et 8.1. Bonne occasion de faire le point sur cette fonctionnalité !

Une vraie base de données

Il est important de bien fixer les choses, la base de données intégrée à Windows Phone est une vraie base de données relationnelle, pas un gadget utilisant une sérialisation XML ou JSon.

Les bases de données sont “locales” au sens de l’Isolated Storage, concept avancé par Silverlight et largement connu aujourd’hui. néanmoins, même si LINQ to SQL sous-entend une certaine “puissance” il faut garder à l’esprit qu’un SGBD-R local sur un smartphone ne peut être utilisé pour les mêmes tâches que sur un PC serveur surdimensionné… La gestion de listes - de type To-do, rendez-vous, recettes de cuisines, liste de références et d’articles – est la cible privilégiée des bases de données embarquées dans les unités mobiles. Les tablettes hybrides comme Surface selon qu’elles sont en pur WinRT ou qu’elles proposent un mode desktop complet (et une puissance supérieure aussi) peuvent ou non entrer dans le cadre d’utilisation de ce type de base de données locales aux capacités restreintes avant tout par la mémoire de masse et la puissance de calcul des machines considérées.

Pour le développeur les applications utilisent LINQ to SQL pour manipuler les données, comme en mode desktop. LINQ est aussi utilisé pour manipuler le schéma des bases puisque Transact-SQL de SQL Server n’est pas disponible dans cette version “de poche” du SGBD Microsoft.

Architecture

Puisqu’on utilise LINQ to SQL pour accéder aux données et aux schémas on se trouve dans un contexte purement objet. Pas de commandes en texte de type Transact-SQL.

Bien que manipulable uniquement au travers d’objets et de commandes orientées objets et bien que stockant des instances d’objets, la base de données elle-même reste bien de type SGBD-R, il ne s’agit pas d’une base de données objet au sens de Caché, Wakanda, db4o ou même de O2.

Comme avec LINQ to SQL en version desktop, on dispose ainsi d’une approche objet pour des données relationnelles se basant sur un modèle et un environnement d’exécution. Tout cela est proche de Entity Framework mais ce n’en est pas. On reste proche de LINQ to SQL qui fut le prédécesseur de E.F. pour les bases SQL Server. Plus simple que E.F. LINQ to SQL est mieux adapté aux petites machines dans lesquelles les exigences en matières de données sont moindre que sur un PC et qui, de toute façon, disposent de moins de puissances pour se permettre d’utiliser des couches trop sophistiquées. La fluidité reste le maitre mot de Windows Phone !

Le modèle objet LINQ to SQL est construit essentiellement sur la base d’un objet DataContext (System.Data.Linq.DataContext). Ce dernier se comporte comme un proxy pour la base de données locale.

Le schéma ci-dessous montre de façon simplifiée les relations entre l’application, le contexte de données (DataContext), l’espace de noms System.Data.Linq d’une part et d’autre part la base de données locale stockée dans l’Isolated Storage, le lien entre les deux mondes s’effectuant grâce au runtime de LINQ to SQL :

Windows Phone DataContext et base de données locale

Le Contexte de données

Le contexte de données (DataContext) est un proxy, un objet qui représente la base de données physique. Un tel contexte contient donc des tables d’objets qui sont le pendant exact des tables d’enregistrements dans la base réelle. Chaque objet de la table du proxy est constitué d’entités, concept qu’on retrouve dans Entity Framework. Dans la base de données une entité est simplement une ligne d’enregistrement.

Les entités manipulées sous Windows Phone sont des POCO – Plain Old CLR Object – c’est à dire des objets C# tout à fait classiques. Toutefois ces objets sont décorés par des attributs dédiés qui permettent de fixer certaines informations essentielles pour un SGBD-R comme la clé primaire.

Les attributs sont aussi utilisés pour fixer les relations entre les entités de tables différentes. Nous sommes bien dans une mode SGBD-R, R pour Relationnelle.

Le mappage des entités sur la base de données est implicite, ainsi un objet possédant les propriétés NomProduit et FamilleArticle donnera naissance à une table physique possédant deux colonnes dont les noms seront identiques.

Le Runtime LINQ to SQL

LINQ to SQL fournit tout un ensemble de services comme le mappage objet vers relationnel évoqué ci-avant. Il offre aussi une déclinaison de LINQ dédié à la manipulation des bases de données, avec ses deux syntaxes – sous forme de méthodes chainables ou de requêtes utilisant la syntaxe LINQ proche de SQL.

Rien ne sert de s’étendre sur LINQ que j’ai présenté à plusieurs reprises et qui n’étant pas une nouveauté est assez bien connu aujourd’hui.

Rappelons juste que les requêtes LINQ to SQL sont traduites par le runtime en requêtes Transact-SQL en s’appuyant sur la définition du modèle, c’est à dire du schéma de la base de données. La requête SQL est ensuite transmise au gestionnaire de la base de données pour exécution, la réponse de ce dernier étant ensuite remontée à l’application (donnée unique, grappe de données, erreur d’exécution).

Les données remontées de la base de données ne sont pas retournées dans un format brut, le runtime LINQ to SQL se chargeant de les transformer en objets. Même si le SGBD-R sous-jacent n’est pas objet, du point de vue du développeur on exploite bien une gestion de données objet avec sauvegarde d’instances et réhydratation automatique de ces dernières.

Sous Windows Phone la partie Transact-SQL reste purement interne au Framework, seul LINQ est utilisable comme je le disais. le DDL notamment, langage de manipulation du schéma, n’est pas accessible.

Différences avec LINQ to SQL desktop

LINQ to SQL pour Windows Phone est très similaire à la version .NET pour desktop. On l’utilise de la même façon pour lire, insérer, supprimer ou modifier des données (opérations CRUD). Les apparences peuvent être trompeuses tellement les comportements semblent justement proches. Mais il ne faut pas oublier qu’un smartphone n’est pas un serveur d’entreprise… La version du moteur de base de données intégrée à WP est donc largement plus modeste qu’un SQL Server tournant sur une grosse machine.

Trois différences essentielles doivent être gardées en mémoire :

  • Une base locale Windows Phone n’est utilisable qu’’au travers de LINQ to SQL, Transact-SQL n’est pas supporté.
  • Une base locale ne peut être exploitée que par l’application qui la définit car elle est stockée dans l’espace privé de cette dernière. Il ne peut donc être question de partager une même base entre plusieurs applications.
  • Une base locale utilisant LINQ to SQL possède un code qui tourne dans le processus de l’application. Il n’y a pas de service SGBD-R distinct qui tournerait en tâche de fond de façon isolée. C’est l’application qui fournit la puissance de calcul en quelque sorte.

Conserver ces contraintes à l’esprit lorsqu’on conçoit une application évite de commettre quelques erreurs d’approche…

Déploiement

Il est extrêmement simple puisqu’il n’y a rien à faire ! En effet le runtime LINQ to SQL est intégré au Framework .NET du SDK Windows Phone et lorsque l’application sera lancée pour la première fois même la création de la base sera automatique (et le fichier sera créé dans l’Isolated Storage de l’application).

Donc la seule complication qu’on puisse avoir à gérer est la situation dans laquelle on désire fournir une base de données pré-remplie. Par exemple une liste de recettes, une base de données des articles vendus par l’entreprise (la mise en jour se faisant ensuite au fur et à mesure), etc…

Je ne détaillerais pas les étapes d’une telle fourniture mais, en toute logique, vous pouvez déduire que d’une part il faudra trouver un moyen de créer la base et la remplir sur votre PC de développement (ce qui peut se faire via l’émulateur en exécutant l’application finale ou bien une application “helper” conçue uniquement pour cela), et d’autre part, qu’il faudra farfouiner sur votre PC pour retrouver le fichier créé puis l’ajouter dans les références de votre application pour fabriquer le package d’installation final (quand je parle de farfouiner c’est avec l’aide de ISETool.exe fourni avec le SDK WP et qui permet de visualiser les Isolated Storage. Outil en ligne de commande assez rustique il n’en reste pas moins un allié efficace ici).

J’aurais peut-être l’occasion de revenir sur ces étapes dans un autre billet où un exemple concret sera développé.

Enfin on notera qu’il est possible de crypter les bases de données Windows Phone ce qui en fait un bon outil pour stocker des données personnelles ou d’entreprise un peu sensibles.

En pratique

Je n’irais pas aujourd’hui jusqu’à la création d’une application de démo car cela allongerait trop le billet. Mais nous allons étudiez les cas de figure les plus courants avec du code dans la partie qui suit car voir du code ça aide toujours à comprendre !

Définir un contexte de données

Nous sommes bien en LINQ to SQL, le prédécesseur de Entity Framework. On retrouve ainsi les principes de ce mode particulier d’accès objectivé aux données, comme la présente d’un contexte et la définition d’un modèle de données.

La création d’une base de données passe donc forcément par une première étape : la définition du contexte de données et des entités (donc le modèle). Les classes qui seront écrites sont des POCO comme je l’ai dit et elles seront décorées d’attributs pour spécifier le rôle de certains champs (clé primaire par exemple).

Les relations entre les tables utilisent le même procédé avec les attributs de mappage de LINQ to SQL.

Ci-dessous un exemple d’écriture d’un contexte de données définissant à la fois l’objet contexte (DataContext) et le modèle (la table ToDoItem) :

public class ToDoDataContext : DataContext
{
    // Specify the connection string as a static, used in main page and app.xaml.
    public static string DBConnectionString = "Data Source=isostore:/ToDo.sdf";
 
    // Pass the connection string to the base class.
    public ToDoDataContext(string connectionString): base(connectionString) { }
 
    // Specify a single table for the to-do items.
    public Table<ToDoItem> ToDoItems;
}
 
// Define the to-do items database table.
[Table]
public class ToDoItem : INotifyPropertyChanged, INotifyPropertyChanging
{
    // Define ID: private field, public property, and database column.
    private int _toDoItemId;
 
    [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "INT NOT NULL Identity",
CanBeNull = false, AutoSync = AutoSync.OnInsert)]
    public int ToDoItemId
    {
        get
        {
            return _toDoItemId;
        }
        set
        {
            if (_toDoItemId != value)
            {
                NotifyPropertyChanging("ToDoItemId");
                _toDoItemId = value;
                NotifyPropertyChanged("ToDoItemId");
            }
        }
    }
         . . .
         . . .

 

Ce code est issu d’exemples fournis par Microsoft.

Pour accéder à l’ensemble des possibilités offertes il ne faut pas oublier de déclarer les espaces de noms suivants :

using System.Data.Linq;
using System.Data.Linq.Mapping;
using Microsoft.Phone.Data.Linq;
using Microsoft.Phone.Data.Linq.Mapping;

 

Les attributs courants

LINQ to SQL offre de nombreux attributs permettant de spécifier l’ensemble des contraintes de la base de données, que cela soit au niveau des champs, des tables ou des relations entre ces dernières. Voici les principaux :

TableAttibute [Table] Indique que la classe décorée définie une table de la base de données
ColumnAttribute [Column(IsPrimaryKey=true)] Effectue la mapping entre une propriété de la classe et un champ de la base de données. IsPrimaryKey permet de spécifier le champ jouant le rôle de clé primaire.
IndexAttribute [Index(Columns=”Colonne1,Colonne2 DESC”, IsUnique=true, Name=”MonIndex”)] Utilisé au niveau de la classe table cet attribut déclare un index nommé.
AssociationAttribute [Association(Storage=”RefNomEntité”, ThisKey=”EntitéID”, OtherKey=”CléAutreTable”)] Indique qu’une propriété servira de liaison dans une association souvent de type clé étrangère vers clé primaire.

 

Création de la base de données

Lorsqu’on dispose d’un contexte de données il est possible d’utiliser la base de données. La première opération consiste bien entendu à créer la base si celle-ci n’est pas fournie avec l’application.

Le code qui suit montre que cette opération est d’une grande simplicité :

// Créer la base de données si elle n’existe pas
using (ToDoDataContext db = new ToDoDataContext("isostore:/ToDo.sdf"))
{
    if (db.DatabaseExists() == false)
    {
        // création de la base.
        db.CreateDatabase();
    }
}

 

Lors de la création de la base, la version de son schéma est créée à la valeur zéro. Pour déterminer la version d’une base, ce qui peut être essentiel pour faire évoluer le schéma avec des updates de l’application, il faut utiliser la class DatabaseSchemaUpdater.

Utilisation de la base de données

Un fois la base créée l’application peut l’utiliser à sa guise pour toutes les opérations CRUD habituelles, via LINQ to SQL.

Insertion

Avant de sélectionner des données il faut pouvoir en créer c’est une lapalissade…

Avec LINQ to SQL il s’agit d’une opération se déroulant en deux étapes : d’abord l’ajout d’un nouvel objet au contexte de données puis la sauvegarde des modifications de ce dernier vers la base de données.

Dans l’exemple de code ci-dessous une instance de ToDoItem est créée et ajoutée à la collection ToDoItems et à la table correspondante dans le contexte de données. Puis seulement ce dernier est sauvegardé :

// Création d’un nouvel item “todo” à partir d’un texte dans un TextBox.
ToDoItem newToDo = new ToDoItem { ItemName = newToDoTextBox.Text };
 
// Ajout de l’item à la collection utilisée par l’application (affichages…)
ToDoItems.Add(newToDo);
 
// Ajout au contexte. Les données seront sauvegardées lors du submit !
toDoDB.ToDoItems.InsertOnSubmit(newToDo); 

 

Mise à jour

Une fois un enregistrement créé il devient possible de le modifier.

La stratégie se déroule en plusieurs étapes : Sélection de l’enregistrement à modifier via une requête LINQ, modification de l’objet puis application des modifications à la base de données via un appel à SubmitChanges du contexte de données.

Bien entendu plusieurs enregistrements peuvent être traités pour un seul appel de sauvegarde final, ce qui est plus rapide (mais plus risqué, donc tout dépend des besoins de l’application et des risques qu’on assume quant à la perte d’anciennes modifications non sauvegardées…).

Si les objets manipulés sont bindés à des éléments de l’UI alors seul le SubmitChanges est nécessaire. Une stratégie souvent utilisée car elle a l’avantage d’éviter de trop longues sessions sans mise à jour : elle consiste à faire l’appel à SubmitChanges en cas de changement de page (navigation). On fera alors attention aux événements de mis en sommeil ou d’arrêt de l’application pour éviter que les dernières modifications ne soient perdues…

protected override void OnNavigatedFrom(System.Windows.Navigation.NavigationEventArgs e)
{ 
    // appel de la méthode de base
    base.OnNavigatedFrom(e);
 
    // application des modifications du contexte de données à la base de données
    toDoDB.SubmitChanges();
}

 

Je conseille fortement d’utiliser un framework MVVM même pour une application smartphone. S’agissant d’exploiter une spécificité de Windows Phone (LINQ to SQL) la portabilité est une question qui ne se pose pas… Un tel logiciel sera fait pour l’univers Microsoft et non portable pour d’autres environnements sans de lourdes modifications. C’est pour cela que ceux qui utilisent Xamarin et MvvmCross préfèrent exploiter SQLite qui a l’avantage d’exister sur les principales plateformes. Bien entendu on perd totalement l’intérêt de LINQ to SQL qui n’existe ni sous iOS ni sous Android…

Donc puisque l’utilisation de LINQ to SQL implique de se limiter à Windows Phone, il n’y a aucun problème à utiliser un framework MVVM non portable vers iOS ou Android. De ce point de vue je conseille MVVM Light que j’ai toujours apprécié et sur lequel j’ai écrit de véritables romans (le lecteur intéressé saura retrouver l’information sur Dot.Blog j’en suis certain !).

Donc on supposera que toute application, même la plus modeste, sera construire à l’aide d’un framework MVVM. Et dans un tel cadre, la stratégie de sauvegarde où l’interception des navigations pourra bien entendu prendre d’autres formes que le code montré plus haut.

Sélection

C’est généralement l’opération la plus fréquente. Avec LINQ to SQL elle est grandement simplifiée et je renvoie le lecteur à mes billets sur cette technologie pour parfaire sa connaissance de LINQ si jamais il en ressent le besoin (je citerais en vrac : mon adaptation des 101 exemples LINQ, le livre All.Dot.Blog sur LINQ et EF, etc).

Il faut se rappeler, malgré le côté un peu rudimentaire de cette version de LINQ to SQL pour Windows Phone, qu’il s’agit bien du même procédé que sur PC en mode desktop : c’est à dire que les requêtes LINQ sont traduites en Transact-SQL et que ce dernier est exécuté par le moteur de base de données. Il ne s’agit pas par exemple d’une astuce montant toutes les données en mémoire et exécutant du LINQ to Object. De fait la sélection d’un enregistrement dans une grande base de données s’avère très efficace.

Même si le SDK ne donne pas accès à Transact-SQL, même si le SGDB s’exécute dans le processus de l’application,

// Définition de la requête de sélection.
var toDoItemsInDB = from ToDoItem todo in toDoDB.ToDoItems
                    select todo;
 
// Exécute la requête et place le résultat dans une liste observable.
ToDoItems = new ObservableCollection<ToDoItem>(toDoItemsInDB);

Le code ci-dessus définit une requête LINQ qui sélectionne l’ensemble de la base données, en général on évitera ce type de sélection dans la réalité. Ensuite une collection est créée à partir de la requête.

Maintenir de nombreux objets en mémoire est couteux surtout sur un smartphone, on tentera au maximum de limiter à la fois la quantité d’enregistrements remontés par une requête et la durée de vie du contexte de données car LINQ to SQL effectue un tracking des modifications qui peut s’avérer très lourd si trop de données sont manipulées dans la même session de vie du contexte.

Suppression

Quatrième opération de base de la séquence CRUD, la suppression fait partie de la vie d’une donnée… Tout comme la modification il s’agit aussi d’une opération en trois phases. La première consiste à obtenir l’enregistrement (donc l’objet) via une requête LINQ puis selon que l’application a un ou plusieurs objets à détruire on appellera DeleteOnSubmit ou DeleteAllOnSubmit. Ces opérations se terminent, comme l’update, par “OnSubmit” pour nous rappeler que leur action n’est pas immédiate. Il s’agit uniquement d’un marquage de l’objet ou des objets en mémoire. L’opération réelle ne sera réalisée sur la base de données qu’une fois le SubmitChanges appelé…

Le code ci-dessous supprime un enregistrement de la liste des tâches à effectuer (application fictive de gestion de to-do list) :

// Obtenir l’item à supprimer
ToDoItem toDoForDelete = (code d’obtention de l’item);
 
// suppression de la liste observable (s’il y en a une)
ToDoItems.Remove(toDoForDelete);
 
// suppression de l’item dans le contexte de données (obligatoire)
toDoDB.ToDoItems.DeleteOnSubmit(toDoForDelete);
 
// application des modifications à la base. L’item est réellement supprimé.
toDoDB.SubmitChanges();

Modifier le schéma de la base de données

Le fonctionnement même de l’application ou bien sa mise à jour peut impliquer d’avoir à modifier le schéma de la base de données. Dans la pratique la mise à jour de l’application sera obligatoire puisqu’un changement de schéma implique un changement du modèle défini dans le contexte de données et que ces définitions sont codées “en dur” dans l’objet DataContext.

La base de données se trouvant déjà dans le smartphone l’application doit adopter une approche précise pour s’assurer qu’elle ne va pas utiliser celle-ci avec un contexte différent ce qui aurait des conséquences désastreuses (au minimum plantage ou pertes de données, etc).

L’espace de noms Microsoft.Phone.Data.Linq déclare un objet évoqué plus haut dans ce billet le DatabaseSchemaUpdater, littéralement l’objet de mise à jour du schéma.

Cet objet permet d’effectuer des opérations qui nécessiteraient l’accès au DDL donc à Transact-SQL ce qui n’existe pas dans LINQ to SQL version Windows Phone.

Le numéro de version du schéma est tenu à jour automatiquement et cela peut grandement aider les opérations de modification en tenant compte justement de cette information. C’est ce que démontre le code exemple ci-dessous :

using (ToDoDataContext db = new ToDoDataContext(("isostore:/ToDo.sdf")))
{
        // création de l’objet updater
        DatabaseSchemaUpdater dbUpdate = db.CreateDatabaseSchemaUpdater();
 
        // obtenir le numéro de version de la base de données
        int dbVersion = dbUpdate.DatabaseSchemaVersion;
 
        // modifier si nécessaire
        if (dbVersion < 5)
        {   // copier les données de l’ancienne vers la nouvelle base (exemple) 
            MigrateDatabaseToLatestVersion();
        }
        else if (dbVersion == 5)
        {   // ajout de colonne à la base actuelle pour matcher le contexte de données
            dbUpdate.AddColumn<ToDoItem>("TaskURL");
            dbUpdate.DatabaseSchemaVersion = 6;
            dbUpdate.Execute();
        }
}
 

Sécuriser les données

La sécurisation des données sur une machine aussi facilement escamotable qu’un smartphone est une obligation dès que l’application gère des informations sensibles ou personnelles.

La base de données Windows Phone prend en charge cet aspect en proposant une gestion d’accès sécurisée par mot de passe et un cryptage des données rendant leur exploitation impossible (ou très difficile) en cas de perte ou de vol.

Dès qu’un mot de passe est posé sur une base cette dernière est automatiquement cryptée. On comprend que de la qualité du mot de passe dépendra la fiabilité du cryptage. L’utilisateur doit être invité à utiliser quelque chose qui ne peut pas se craquer en quelques secondes par une attaque simple de type dictionnaire notamment.

Techniquement le mot de passe est passé par la chaîne de connexion avant l’ouverture du contexte de données. Le cryptage d’une base “après coup” est impossible, cela ne peut se faire qu’au travers d’une copie, ce qui réclame d’écrire le code de cette opération… La méthode de codage retenue dans Windows Phone est un AES-128 et la hashage du mot de passe utilise SHA-256.

Si le mot de passe est bien choisi la résistance à une attaque sera très bonne mais pas parfaite, le codage utilisé pouvant être cracké si les moyens sont mis pour le faire. Il s’agit donc bien ici de sécuriser des données personnelles même très personnelles mais on peut difficilement considérer que le seul codage de la base soit suffisant pour des applications ultra-sensibles.

Création d’une base cryptée :

// création du contexte objet, passage de la chaine de connexion et du mot de passe
ToDoDataContext db = new ToDoDataContext ("Data Source=’isostore:/ToDo.sdf’;Password=’securepassword’");
 
// création d’une base cryptée (si la base n’existe pas déjà)
if (!db.DatabaseExists()) db.CreateDatabase();
 

On notera que le codage de toute la base de données a un prix qui se paye par une certaine lenteur dans les accès aux données. Si on désire protéger uniquement quelques champs qui n’appartiennent à aucun index, alors il préférable d’effectuer le cryptage dans l’application de ces seuls champs et de laisser la base de données non cryptées…

Conclusion

Windows Phone propose un OS particulièrement intéressant. Microsoft devrait, comme pour WinRT sur PC, lâcher l’idée fixe du menu à tuile qui à mon sens est le seul frein à l’expansion de l’OS. Les gens adore personnaliser leur téléphone. Offrir trois façons d’afficher la même tuile ou même de mettre une photo en arrière plan comme sous 8.1 c’est persister dans l’erreur. Pourquoi ne pas offrir un “bureau classique” comme iOS ou Android et ne pas laisser fleurir sur le marché les “themers” ces apps chargées de personnaliser le look des écrans ?

En dehors de ce boulet qu’est le menu à tuile qu’on se traine depuis longtemps (WP7) et qui visiblement n’affole pas les foules, Windows Phone, comme Windows 8 par ailleurs, offre un OS de grande qualité, bien supérieur à la concurrence et une plateforme de développement et un tooling inégalés.

On sera parfois étonné de trouver certaines fonction comme un LINQ to SQL directement disponible dans l’OS d’un smartphone ! C’est vrai que c’est fou quand on pense à la rusticité d’iOS ou Android sur ces points, comme des milliers d’autres d’ailleurs…

Windows Phone est vraiment une réussite technique, comme Windows 8. Ah si seulement l’entêtement à limiter l’UI à ces atroces tuiles pouvait cesser afin que les ventes soient enfin au niveau mérité ! Windows 8 ou Windows Phone 8 sans les tuiles sont les meilleurs OS de l’instant. Pourquoi s’accrocher à un détail de design qui semble troubler les clients potentiels au point de leur faire acheter les produits concurrents ?

Mystère… Les plus grande sociétés finissent pas se prendre les pieds dans le tapis sur des erreurs que le premier crétin venu ne ferait pas. Ca m’a toujours épaté. Hélas Microsoft n’échappe pas à cette règle, c’est dommage, c’est très bien Windows Phone !

Stay Tuned !

blog comments powered by Disqus