Alain Zanchetta

Conseil et développement logiciel

Windows, Windows Mobile (Smartphone & Pocket PC), .NET, C#, C++, WPF, Silverlight

Développement avec le .NET Compact Framework et Windows Mobile

Dès les premières versions des PDA sous Windows CE (appelés Palm Sized PC ou Handheld PC), Microsoft a proposé une interface de programmation (API) ainsi que des outils de développement proches des versions utilisées par les développeurs Windows :

  •  l'API Windows CE (et ses implémentation dans les systèmes dérivés de Windows CE comme Pocket PC ou maintenant Windows Mobile) est grosso-modo l'ensemble minimal des fonctions Win32 permettant de couvrir les services offerts par Windows CE
  •  les premiers outils de développement pour Windows CE étaient soit des Add-ons à Visual C++ (au temps de VC++5.0) soit des clones de Visual C++ ou Visual Basic.

Avec .NET et Visual Studio .NET 2005, la fusion des deux environnements de développement est maintenant achevée :

  • Visual Studio .NET 2005 incorpore en standard les fonctions de développement pour toutes les plates-formes mobiles de Microsoft (à la fois pour le développement en code managé et pour le développement natif en C++),
  •  le .NET Compact Framework rend disponible sur plate-forme mobile un sous-ensemble important du .NET Framework :
    • les langages VB.Net et C# sont entièrement supportés (y compris avec les classes génériques),
    •  un grand nombre de classes .NET sont disponibles, même si elles n'ont généralement qu'un sous-ensemble de leur équivalent sur desktop ou serveur.

Cet article va illustrer un certain nombre de concepts de programmation sous Windows Mobile à l'aide du .NET Compact Framework 2.0 à l'aide d'un exemple concret : la gestion d'une base de données de livres.

Contexte et architecture

Pourquoi gérer ses livres sur mobile ? Essentiellement pour avoir un sujet de programmation : mais ça peut par exemple se justifier dans le cas d'un amateur de bandes dessinées qui possède de nombreuses séries incomplètes : avoir la liste des livres dans sa poche peut éviter d'acheter des doublons.
Cette application sera communicante :

  • elle va s'appuyer sur des services web pour récupérer les informations sur les livres (auteurs, couvertures, etc),
  • et la base de données sera synchronisée avec une base SQL Server sur un desktop ou un serveur : une application " jumelle " sur desktop permettra aussi l'enrichissement des données.

Architecture Globale

Les projets Windows Mobile sous Visual Studio .NET 2005

Les projets pour Windows Mobile se trouvent regroupés dans une catégorie " Smart Devices " sous le langage choisi. Sous cette catégorie, on trouve ensuite toutes les plates-formes mobiles pour lesquelles un kit de développement a été installé : Visual Studio .NET 2005 vient avec le SDK Pocket PC 2003, les autres kits peuvent être téléchargés et installés indépendamment. Ils complètent l'installation de Visual Studio, apportent des exemples ainsi que des émulateurs permettant de déboguer les applications sans avoir le matériel réel.

Projets Smart Device

Remarque : on voit avec les types de projet notés " (1.0) " qu'il est toujours possible de cibler le .NET Compact 1.0. Dans la mesure où le .NET Compact Framework 2.0 est plus rapide et plus complet, quel est l'intérêt de cibler l'ancienne version ? Eventuellement le déploiement car la plupart des matériels en circulation incorporent le .NET Compact Framework 1.0 en ROM alors que ce n'est pas encore le cas pour le 2.0.

Il faut cependant noter qu'une application peut être par la suite " upgradée " vers le .NET CF 2.0 alors que l'opération inverse n'est pas possible. L'upgrade d'un projet est une opération irréversible et il est donc préférable de sauvegarder une copie des sources lors de cette opération.

En ce qui le concerne, le choix de la plate-forme n'est pas exclusif : un projet peut contenir cibler plusieurs plates-formes et il est aussi possible d'en ajouter par la suite.
Le choix d'une plate-forme détermine quels assemblies pourront être référencés et quel émulateur sera lancé pour le débogage et définit aussi quelques symboles pouvant être utilisés pour une compilation conditionnelle : " PocketPC " ou " Smartphone " .
Upgrade Projet

 

Partage de code entre les différentes plates-formes

Bien qu'un certain nombre de classes soient communes entre le .NET Compact Framework et le .NET Framework " standard ", il est généralement préférable de ne pas chercher à partager des assemblies entre les deux plates-formes : les applications développées pour le .NET Compact Framework fonctionnent sur desktop mais l'inverse n'est pas vrai ; il est donc préférable de créer des assemblies différents pour les deux environnements, mais il est alors possible de partager les sources :

  • créer le premier projet (par exemple " Windows / Class Library ") et y ajouter les fichiers
  • créer le deuxième projet (" Smart Device / Windows Mobile 5.0 Pocket PC / Class Library " et y ajouter les fichiers avec " Add Existing Item " puis " Add as Link " :

Add As Link

L’utilisation de cette dernière option permet d’avoir le même fichier C# partagé entre les projets .NET Framework et .NET Compact Framework.

 A l’intérieur des fichiers ainsi partagés, il est possible d’utiliser les directives de compilations définies par Visual Studio lorsque le code est dépendant de la plate-forme, soit avec la directive #if/#endif soit avec l’attribut Conditional :

private void AfficheNotification(string message) {
#if
Smartphone
  
MessageBox.Show(message);

#else

  
Microsoft.WindowsCE.Forms.Notification notif
     
= new Microsoft.WindowsCE.Forms.Notification();
  
notif.InitialDuration = 2;
  
notif.Visible = true;

#endif

}

Ou par exemple :

[Conditional("PocketPC")]
void
AfficheClavier(bool affiche) {
   
Microsoft.WindowsCE.Forms.InputPanel panel = new
Microsoft.WindowsCE.Forms.InputPanel();
   
panel.Enabled = affiche;
}
private void txtTitre_GotFocus(object sender, EventArgs e) {
   
AfficheClavier(true);
}
private void txtTitre_LostFocus(object sender, EventArgs e) {
    AfficheClavier(false);
}

En ce qui concerne l’interface utilisateur, il faut savoir que depuis Windows Mobile 2003 Second Edition, les Pocket PC ou les Smartphones n’ont plus nécessairement tous la même résolution  et peuvent éventuellement voir leur affichage tourné de 90°.

Pour l’essentiel, le .NET Compact Framework gère ces modifications, le principe d’utilisation est le même que pour les applications Windows Standard : on paramètre le redimensionnement et le replacement des objets graphiques à l’aide de leur propriété Anchor. Cette adaptation fonctionne même lorsque le formulaire est déjà affiché.

L’utilisation des différents émulateurs permet de vérifier le comportement des fenêtres en fonction des différentes résolutions ou avec les rotations.

Il faut noter que les dimensions limitées des écrans vont parfois entraîner l’affichage de barres de défilement (a priori dans une seule direction) :

PortraitPaysage
Paysage avec scroll
Portrait avec scroll

 

Base de données

La base de données de référence sous Windows Mobile est SQL Server. Auparavant, il s'agissait d'une version spécifique mais la version actuelle, appelée SQL Server Compact Edition est aussi disponible sur Desktop où elle se présente sous la forme d'une DLL facilement redistribuable avec une application.

Là encore, SQL Compact partage un certain nombre de concepts et en particulier une bonne partie du langage de programmation (Transact-SQL) avec les autres versions de SQL Server. Comme les classes spécifiques à SQL Compact (SqlCeConnection, SqlCeCommand, etc.) implémentent les mêmes interfaces que les classes SqlConnection ou OleDbConnection, il est possible de partager une bonne partie du code entre l'application mobile et l'application desktop.

L'ouverture de la connexion à la base de données se fait en précisant le chemin de celle-ci :

SqlCeConnection cnx = new SqlCeConnection(
               @"Data Source=\Storage Card\SqlData\Livres.sdf");

Une fois, la connexion établie, il est possible d'accéder de manière uniforme à SQL Server, Access (ou toute autre base accessible via OleDb) ou Sql Server Compact. Voici par exemple, le code d'insertion d'une nouvelle série de livre dans la base de données de test :

protected static void AddParameterWithValue(IDbCommand cmd, DbType type,
                                             string name, object value) {
    IDbDataParameter parameter = cmd.CreateParameter();
    parameter.ParameterName = name;
    parameter.DbType = type;
    if (value == null) {
        parameter.Value = DBNull.Value;
    } else {
        parameter.Value = value;
    }
    cmd.Parameters.Add(parameter);
}
public void Sauver(Serie serie) {
   using (IDbCommand cmd = _cnx.CreateCommand()) {
      cmd.Transaction = _trs;
      cmd.CommandType = CommandType.Text;
      if (serie.New) {
         cmd.CommandText = "insert into SERIE(ID, TITRE) values(@ID,
                           @TITRE)";
      } else {
         cmd.CommandText = "update SERIE set TITRE=@TITRE where ID=@ID";
      }
      AddParameterWithValue(cmd, DbType.Guid, "@ID", serie.Id);
      AddParameterWithValue(cmd, DbType.String, "@TITRE", serie.Titre);
     
int nbLignes = cmd.ExecuteNonQuery();
   }
}

Un des aspects les plus intéressants de SQL Compact est la synchronisation. En effet, beaucoup d'applications mobiles d'entreprises sont basées sur le scénario suivant : l'utilisateur télécharge des données sur son mobile le matin, il effectue ensuite une " tournée " pendant laquelle il enrichit les données récupérées, puis en fin de journée il met à jour la base centrale avec les données saisies.

SQL Mobile offre trois possibilités principales de synchronisation :

  • Réplication fusion (merge replication) avec SQL Server
  • Remote Data Access avec SQL Server
  • Remote Data Access avec une base Access.

Note : Cette dernière possibilité n'est disponible qu'à l'état de CTP actuellement mais est prometteuse pour les utilisateurs particuliers.

La réplication fusion se base sur un des mécanismes de réplication de base de données offert par SQL Server. Le principe de la réplication fusion est le suivant : les bases répliquées sont synchronisées périodiquement, lors de ces synchronisations, les mises à jour effectuées sur chaque base sont transmises et appliquées à l'autre base. En cas de conflit, aucune donnée n'est perdue et les données en conflit sont enregistrées sur la base SQL Server de référence, sur laquelle il est ensuite possible de procéder à une résolution des conflits ; même en cas de conflit, les bases du mobile et du serveur sont identiques après la réplication. Ce type de réplication est bien adapté aux bases connectées épisodiquement.

Le Remote Data Access est plus spécifique aux applications mobiles (alors que la réplication fusion est utilisée dans de nombreux scénarios). Le mobile crée des tables localement à partir de requêtes SQL exécutées sur le serveur ; les modifications effectuées localement sur le mobile peuvent par la suite être rejouées sur le serveur SQL. Il n'y a pas vraiment de notion de synchronisation : on se contente de rejouer sur le serveur les ordres exécutés en mode déconnecté, les modifications effectuées entre temps sur le serveur ne sont pas redescendues sur le mobile lors de la synchronisation.

Dans les deux cas, l'architecture de synchronisation est la suivante : l'agent SQL " client " discute avec un agent SQL " serveur " qui est une DLL ISAPI hébergée dans Internet Information Server. C'est cette DLL et non l'agent " client " qui se connecte au serveur SQL. L'intérêt de cette architecture est évident dans les scénarios de type extranet car ceci évite de devoir exposer le serveur SQL principal sur internet

Architecture de synchronisation

La configuration de cette réplication se fait selon les étapes suivantes :

  1.  Création de la réplication fusion de la base SQL Server
  2.  Configuration du répertoire IIS hébergeant l'agent de réplication SQL Compact.
    Cette configuration peut être faite à l'aide d'un assistant ou manuellement. Le même répertoire peut servir à plusieurs bases de données.

La sécurisation de la réplication supporte toutes les options SQL et IIS :

  • Authentification Windows ou SQL Server au niveau de SQL Server
  • Authentification basique, intégrée, etc au niveau de IIS.

Dans le cadre de l'application exemple, un compte Windows spécifique a été créé (" SqlMobileUser "), IIS effectue cette authentification et prend l'identité correspondante pour effectuer l'accès à SQL Server. Le paramétrage de la réplication dans l'application mobile est le suivant :

SqlCeReplication repl = new SqlCeReplication();
// URL de l'agent de réplication sur IIS
repl.InternetUrl                =
 "http://barney2003R2/sqlmobile/sqlcesa30.dll";
// Informations d'authentification
repl.InternetLogin              = "SqlMobileUser";
repl.InternetPassword           = motDePasse;
// Nom du serveur SQL
repl.Publisher                  = "barneyhpvista";
// Nom de la base répliquée
repl.PublisherDatabase          = "Livres";
// Mode d'authentification auprès de SQL Server
repl.PublisherSecurityMode      = SecurityType.NTAuthentication;
// Nom de la réplication
repl.Publication                = "Livres";
// Nom du client
repl.Subscriber                 = "PocketBD";
// Chaîne de connexion sur le PPC
repl.SubscriberConnectionString = @"Data Source=…";

La base SQL Compact peut être créée lors de la première synchronisation, il suffit pour cela d'ajouter l'option suivante au paramétrage précédent :

repl.AddSubscription(AddOption.CreateDatabase);

Enfin, la réplication peut être déclenchée de manière synchrone ou asynchrone. Dans le cas d'une réplication asynchrone, on peut préciser des fonctions de rappel permettant de suivre l'avancement de la synchronisation.

// Réplication synchrone
repl.Synchronize();

// Réplication asynchrone (AsyncReplicationData est une structure propre

// à l’application)

AsyncReplicationData
data = new AsyncReplicationData(repl, target,
                                                     eventHandler);
repl.BeginSynchronize(base_SyncCompletion, base_StartUpload,
base_StartDownload, base_Synchronisation, data);

Les principaux types de données de SQL Server sont supportés aussi par SQL Mobile (à l'exception notable des documents XML et des objets .NET) et peuvent être répliqués.
Ainsi, la petite application exemple stocke les couvertures des livres enregistrés dans la base dans des champs de type IMAGE qui sont répliqués sans difficulté. Dans la mesure où ces images sont de taille raisonnable, il est assez aisé de les manipuler :

public void SauverImage(string id, byte[] image) {
   
using (IDbCommand cmd = _cnx.CreateCommand()) {
        cmd.Transaction = _trs;
        cmd.CommandType = CommandType.Text;
        cmd.CommandText = "insert into IMAGES(ID, BITMAPLEN, BITMAP)
 values(@ID, @BITMAPLEN, @BITMAP)"
;
        AddParameterWithValue(cmd, DbType.String, "@ID", id);
        AddParameterWithValue(cmd, DbType.Int32, "@BITMAPLEN",
 image.Length);
        IDataParameter paramBlob = cmd.CreateParameter();
        paramBlob.ParameterName = "@BITMAP";
        paramBlob.DbType = DbType.Binary;
        paramBlob.Value = image;
        cmd.Parameters.Add(paramBlob);
        cmd.ExecuteNonQuery();
    }
}

public byte[] ChargerImage(string id) {
    byte[] retour = null;
    using (IDbCommand cmd = _cnx.CreateCommand()) {
        cmd.Transaction = _trs;
        cmd.CommandType = CommandType.Text;
        cmd.CommandText = "select BITMAPLEN, BITMAP from IMAGES where
                          ID=@ID";
        AddParameterWithValue(cmd, DbType.String, "@ID", id);
        using (IDataReader reader = cmd.ExecuteReader()) {
            if (reader.Read()) {
                int totalLength = 0;
                int len = reader.GetInt32(0);
                retour = new byte[len];
                while (totalLength < len) {
                    int lu = (int)reader.GetBytes(1, totalLength, retour,
                                       totalLength, len - totalLength);
                   
totalLength += lu;
                }
            }
        }
    }
    return retour;
}

A noter : dans le cas des images, la structure de la base a été adaptée à une limitation du Transact-SQL supporté par SQL Server Compact Edition. En effet, la fonction datalength qui serait bien pratique pour allouer le tableau d'octets lors de la lecture d'une image n'est pas disponible sous SQL Server Compact Edition : on a donc ajouté la colonne BITMAPLEN qui n'alourdit pas trop la table et permet de simplifier le code C#.

Appels des Services Web

Le ..NET Compact Framework supporte les appels de services web avec SOAP à l'aide des mêmes classes que la version .NET " classique ". Notre application exemple s'appuie sur les services web proposés gratuitement par Amazon (certains services sont payants mais pas l'interrogation simple du catalogue). La génération de proxy se fait de manière habituelle :

Add Web Reference

Les Web Services d'Amazon étant assez riches, on peut encapsuler leurs appels dans des proxys dont le code peut être partagé entre la version desktop et la version mobile de l'application. Voici l'essentiel du code de recherche par titre :

public List<ResumeLivre> ParTitre(string critere, string page, out int nbTotalResults, out int nbPages) {
    try {
        ItemSearch webRequest = new ItemSearch();
        ItemSearchRequest search = new ItemSearchRequest();
        webRequest.SubscriptionId = SubscriptionId;
        search.ResponseGroup = new string[] { "Small" };
        search.SearchIndex = "Books";
        search.Title = critere;
        search.ItemPage = page;
        webRequest.Request = new ItemSearchRequest[] { search };
        ItemSearchResponse webResponse = _service.ItemSearch(webRequest);
        Items response = webResponse.Items[0];
        int nbResults = response.Item.Length;
        nbTotalResults = SafeInt(response.TotalResults);
        nbPages = SafeInt(response.TotalPages);
        List<ResumeLivre> retour = new List<ResumeLivre>();
        for (int i = 0; i < nbResults; i++) {
            Item item = response.Item[i];
           

Ces web services ne récupèrent pas directement les images mais leurs URLs (en plusieurs tailles). Pour récupérer et stocker localement ces images, il est possible d'utiliser les classes habituelles WebRequest et WebResponse et aussi d'effectuer ces appels de manière asynchrone :

// Récupération asynchrone de l'image
WebRequest
request = WebRequest.Create(_details.ImagePath);
request.BeginGetResponse(TelechargerImage, request);

private
void TelechargerImage(IAsyncResult iar) {
   
WebRequest request = iar.AsyncState as WebRequest;
   
using (WebResponse response = request.EndGetResponse(iar)) {
       
using (Stream responseStream = response.GetResponseStream()) {
           
_imageData = new byte[response.ContentLength];
           
int total = 0;
           
while (total < _imageData.Length) {
               
int lus = responseStream.Read(_imageData, total,
                                        _imageData.Length - total);
                total += lus;
            }
           
this.Invoke(new VoidDelegate(AfficherImage));
       
}
    }
}

La méthode TelechargerImage étant appelée de manière asynchrone, elle va s'exécuter sur un thread différent du thread gérant l'interface utilisateur. Sur Windows Mobile comme sur les autres versions de Windows, il est préférable de toujours manipuler une fenêtre et ses composants à l'aide du même thread (certaines opérations peuvent être effectuées par d'autres threads mais ce n'est pas le cas général). L'appel à " this.Invoke" dans l'extrait ci-dessus permet d'appeler AfficherImage sur le thread de l'interface utilisateur.

private void AfficherImage() {
   
if (pictureBox1.Image != null) {
       
pictureBox1.Image.Dispose();
   
}
   
using (MemoryStream ms = new MemoryStream(_imageData)) {
       
pictureBox1.Image = new Bitmap(ms);
   
}
}

Gestion des ressources

Windows Mobile est un environnement puissant mais malgré tout bien plus contraint en termes de ressources (CPU et mémoire) que les versions habituelles de Windows. Il est donc nécessaire de gérer la libération des ressources avec la plus grande rigueur. En particulier, l'utilisation de l'interface IDisposable et de la structure de contrôle using associée doit être systématique :

  • Lorsqu'une classe encapsule une ressource coûteuse (connexion base de données, fichier, objet natif), elle doit implémenter l'interface IDisposable.
  • Lorsqu'on utilise une telle chasse, on doit utiliser using.

Dans notre exemple, l'objet encapsulant les accès aux données conserve à la fois une connexion à SQL Server CE et un objet transaction (certaines opérations de mise à jour sont réparties sur plusieurs tables et nécessitent donc l'utilisation d'une transaction pour garantir l'intégrité de la base). Il implémente donc l'interface IDisposable :

public interface IDataAccess : IDisposable {
  
// Méthodes sur les séries
  
List<Serie> ListeSeries();
  
void Sauver(Serie serie);
  

  
// Validation / Annulation transaction
  
void Commit();
  
void Rollback();

}

Le destructeur et la méthode Dispose annulent la transaction si elle n'a pas été validée puis la libèrent ainsi que la connexion SQL associée :

~DataAccessHelper() {
   
Dispose(false);
}

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

protected void Dispose(bool disposing) {
    if (!_disposed) {
        if ((_trs != null) && (!_committed)) {
           
_trs.Rollback();
        }
        if (disposing) {
            // Libération des objets managés
           
if (_trs != null)  _trs.Dispose();
            if (_cnx != null) _cnx.Dispose();
       
}
        _disposed = true;
    }
}

Si la gestion rigoureuse des objets d'accès aux bases de données est assez classique, les développeurs négligent en revanche assez fréquemment les objets Winform. Or un formulaire Winform encapsule des ressources Windows et implémente pour cette raison l'interface IDisposable. On instanciera donc les formulaires à l'intérieur de structures using :

private void mitParcourir_Click(object sender, EventArgs e) {
    using (FrmLivre dlg = new FrmLivre()) {
       
dlg.Local = true;
       
dlg.Livres = _livres;
       
dlg.ShowDialog();
   
}
}

Une alternative souvent employée sous Windows Mobile est le recyclage des objets graphiques : en effet, le code ci-dessus instancie un nouveau formulaire à chaque fois qu'on veut afficher la fenêtre de parcours des livres d'une série puis libère la mémoire une fois la fenêtre fermée. Il est donc envisageable de conserver le formulaire dans une variable globale et de le réutiliser lors des prochains affichages.

Dans le même ordre d'idée, certaines applications pour Windows mobile définissent un User Control par écran et affichent tous ces contrôle dans un formulaire unique qui reste affiché en permanence.

Un des intérêts de cette approche est de ne faire apparaître qu'une seule entrée correspondant à l'application dans la liste des tâches en cours d'exécution. La copie d'écran ci-jointe montre ce qui se passe quand on empile des fenêtres, même s'il s'agit de boîtes de dialogue modales.

Cette approche est cependant un peu plus délicate à mettre en œuvre au niveau de la gestion de la mémoire et en particulier de la suppression des objets stockés dans le User Controls.
Task Manager2

Il n'y a pas de solution idéale pour l'ensemble des situations et c'est au développeur de faire les bons choix d'implémentation en fonction de la complexité de ses formulaires et de leur nombre : on gagnera à ne pas supprimer un formulaire complexe affiché souvent. En revanche, il faut penser à libérer explicitement les objets " métier " conservés en tant que données membre de tels formulaires et qui servent plus.

Par exemple, le formulaire de recherche sur internet de notre exemple conserve en tant que membre les résultats de la recherche courante afin de pouvoir les passer à la fenêtre de visualisation des détails :

public partial class FrmRecherche : Form {

private
List<ResumeLivre> _resultat;
… 

Si on voulait rendre ce formulaire réutilisable en le masquant au lieu de le détruire, il faudrait alors mettre la variable resultat à null pour permettre la collecte de cette collection inutile lorsque le formulaire n'est pas affiché.

Conclusion

Au travers d'un certain nombre de mécanismes, on a vu que la programmation .NET sur Windows Mobile était assez proche de la programmation Winform " classique " et qu'une bonne partie du code pouvait être réutilisée entre ces différents types d'applications. Windows Mobile associée au .NET Compact Framework est la plate-forme idéale pour développer des solutions mobiles qui peuvent fonctionner manière autonome (avec une éventuelle connexion régulière pour échanger des données avec une base de référence) ou connectées. Le futur .NET Framework 3.5 et Visual Studio .NET 2008 continueront dans cette voie en offrant un .NET Compact Framework 3.5 riche en nouveautés.