Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

Stratégie de développement Cross-Platform–Partie 2

[new:31/12/2012]La Partie 1 de cette série expliquait la stratégie de développement cross-platform et cross-form factor que je vous propose pour faire face à la multiplicité des environnements à prendre en compte aujourd’hui sur le marché. Il est temps de passer à la pratique, je vous invite à suivre le guide pour une démo pas à pas de cette stratégie sur un cas réel.

Rappel sur la stratégie présentée et les outils utilisés

Nous avons vu dans la Partie 1 l’ensemble des contraintes et difficultés auxquelles un développeur ou éditeur de logiciel devait faire face aujourd’hui pour prendre en compte l’explosion des plateformes techniques et des form-factors : PC, Tablettes, Smartphones, le tout avec au minimum 4 OS : iOS, Android, Windows “classique” et Windows 8 (WinRT).

Je vous ai présenté une stratégie que j’ai élaborée après avoir testé de nombreuses solutions, outils, langages et EDI.

Pour résumer, cette stratégie tient en quelques points clés :

  • La centralisation du code métier sur un serveur de type Web Service exploitable par tous les OS
  • L’utilisation de la librairie MVVMCross qui propose un même socle pour les OS à couvrir et permet d’écrire un projet de base compilable pour chaque cible à partir du même code écrit une seule fois. Ce projet à multiple cible regroupe les ViewModels
  • L’écriture de projets spécifiques à chaque cible ne contenant que la définition des Vues et utilisant toutes les possibilités de la plateforme, mais s’appuyant sur MVVMCross pour l’injection de dépendances (ViewModels et autres services) et sachant ajouter le Binding (à la base du pattern MVVM) aux cibles ne le gérant pas (comme Android ou iOS).

Cette stratégie possède de nombreux intérêts :

  • Un seul langage de programmation : C#
  • Un seul framework : .NET
  • Un seul EDI : Visual Studio (ou MonoDevelop qui en est une copie sous Mono)
  • Une seule librairie MVVM cross-platform (MVVMCross)
  • Concentration du savoir-faire, économie des outils et des formations
  • Une grande partie du code est universelle et ne réclame aucune réécriture quelles que soient les cibles
  • Plus le code est spécifique, moins il y en a à produire, la stratégie offrant une maitrise des couts optimale
  • Le cœur du métier est protégé et pérennisé grâce au serveur Web
  • La stratégie supporte des adaptations sans être remise en cause (par exemple ne pas avoir de serveur métier et tout concentrer dans le projet définissant les ViewModels).

Enfin, cette stratégie repose sur deux produits la rendant possible : MonoTouch et MonoDroid de Xamarin, deux plugins Visual Studio (ou MonoDevelop) permettant de développer en C# et .NET de façon native sur les unités mobiles Apple et Android.

Le contexte de l’exemple

Pour concrétiser tout cela je souhaitait bien entendu écrire un exemple complet couvrant plusieurs cibles.

L’exemple devait être concret, faire quelque chose d’utile, mais devait rester assez simple pour être développé en deux ou trois jours maximum.

La preuve de l’intérêt de la stratégie du serveur métier

J’avais développé en 2008 un exemple amusant pour Silverlight, les Codes Postaux Français.

image

A l’époque il s’agissait avant tout de montrer les nouvelles possibilités qu’offraient Silverlight pour le développement Web, principalement le haut niveau de personnalisation de l’UI ainsi que la facilité de dialoguer avec des données distantes.

Le code source des Codes Postaux Français a été publié sur Dot.Blog en 2009.

Ce projet se basait sur un Service Web de type “asmx”, les plus simples et les plus universels, servant des données en XML, tout aussi universelles.

La base de données ainsi que les recherches, et toutes les parties “intelligentes” de cette application se trouvaient donc reportées sur un “serveur métier” même si, s’agissant d’un exemple, ce “métier” et son “savoir” restaient très simples. Le principe étant lui rigoureusement le même que pour une application plus complexe.

Le service Web a été écrit en 2008. Il y a donc 4 ans. Il n’a jamais été modifié depuis et tourne depuis cette date sur mes serveurs, un coup aux USA, un autre en Angleterre, sans que les utilisateurs de l’application Silverlight ne voient la moindre différence (en dehors d’un meilleur Ping avec l’Angleterre).

Je me suis dis que reprendre cet exemple avait plusieurs vertus :

  • Montrer que centraliser le savoir-faire dans un service Web était un gage de pérennité du code
  • Appuyer ma démonstration cross-platform par des accès à des données distantes sur plusieurs OS ajoutait de la crédibilité
  • Accessoirement cela m’évitait de créer le “serveur métier” et me permettait de me concentrer sur la partie cross-platform de l’article

 

A l’heure actuelle, et vous pouvez le tester (suivre les liens plus haut), il existe donc une application Silverlight (cible 1 : le Web, l’Intranet) fonctionnant depuis 4 ans de concert avec un service Web, le serveur métier, qui tourne sans discontinué et qui centralise le “savoir métier” d’une gestion des codes postaux français.

Repartir de ce service Web est une bonne façon de montrer toute l’efficacité de la stratégie proposée. Le code du service n’a jamais été conçu pour des téléphones Android, et pour cause... Pas plus que fonctionner sous WinRT ou un IPhone ni même sous Windows.

Malgré tout, ce service qui justement n’a rien d’exotique (service web XML) reste au fil du temps utilisable sans remise en cause de son code et du “savoir-faire” qu’il contient.

Les cibles que je vais ajouter

S’agissant d’une démonstration je n’allais pas acheter un Mac pour cibler iOS (MonoTouch fonctionne sous Mac, alors que MonoDroid marche très bien sur PC) alors que j’ai eu un mal de chien à me débarrasser d’un G3 il y a quelques années... Un Mac Mini ne coute pas très cher (dans les 500 euros voire moins je crois) et pour ceux qui voudront cibler l’IPhone ou l’IPad c’est un investissement tout à fait justifié et raisonnable.

Il fallait tout de même choisir quelques cibles très différentes et totalement incompatibles entre elles pour montrer tout l’intérêt de la stratégie.

J’ai donc choisi les trois cibles suivantes :

  • Android
  • Windows Phone
  • Windows console

 

Pour ce qui est d’Android j’utilise donc MonoDroid comme plugin Visual Studio. Pour Windows Phone 7.x j’utilise le SDK Microsoft qui se plogue aussi dans VS.

Le choix “Windows console” est là pour ajouter une cible très différente des deux premières et aussi pour servir de base de test à mon ensemble d’applications. MVVMCross propose en effet un mode “console” qui fait très MS-DOS et qui arrive à se connecter aux ViewModels. C’est une cible assez peu intéressante en soi, mais pour les tests c’est facile à mettre en œuvre et pour la démonstration, c’est un OS de plus et une UI qui n’a rien à voir avec les autres. On peut dire que cette cible symbolise toutes les cibles de type Windows “classique”.

Je n’ai pas ajouté de cible WinRT car le développement aurait été très proche de l’exemple WP7 en Silverlight. Mais le lecteur saura transposer ce choix de cibles à toutes autres, au moins en pensée.

L’application

Donc, je vais monter une application dont le but est la consultation et l’interrogation d’une base de données des codes postaux français. La première étape, la constitution d’un “serveur métier” est déjà réalisée (voir plus haut pour le code) ainsi que la première cible (en Silverlight Web).

Je souhaite écrire un seul code C# pour toute la logique de l’application (les ViewModels) et j’accepte bien entendu l’écriture des interfaces spécifiques, que cela soit en Xaml Windows Phone ou en Axml pour Android. C’est la partie variable assumée se résumant aux Vues de l’application.

Pour ne pas tout compliquer, l’application en question fonctionnera sur un seul écran sans navigation. Ce n’est pas un billet sur le développement lui-même d’applications complexes.

Réalisation

Revue de paquetage : J’ai bien un Visual Studio (2010 pour cet exemple) complet, à jour, j’ai bien installé MonoDroid et les SDK Android, j’ai téléchargé et posé dans un coin de mon disque les dernières sources de MVVMCross...

C’est parti !

La solution

Partir d’une page blanche est à la fois excitant et terrifiant... Il suffit donc de créer une nouvelle solution totalement vide. Et le voyage commence.

MVVMCross

Je commence par créer un sous-répertoire “Lib” (un répertoire de solution, pas un répertoire en dur) dans lequel j’ajoute les modules indispensables de MVVMCross (je préfère agir de cette façon plutôt que de référencer les binaires directement, j’ai ainsi l’assurance que le code de la librairie se compile correctement et en cas de problème cela sera plus facile à déboguer).

image

 

Comme on le voit, MVVMCross se présente sous la forme de plusieurs projets, chacun étant spécialisé pour une cible donnée mais exposant exactement le même fonctionnement et les mêmes APIs... C’est là que réside une partie de l’astuce !

Je n’ajoute que les projets correspondant aux cibles que je vais utiliser c’est pour cela que vous ne voyez pas dans l’image ci-dessus ni la version spéciale WinRT pour Windows 8 ni celle pour iOS.

Le projet Noyau

La stratégie exposée repose sur l’écriture d’une application “noyau” (core) qui définie l’ensemble des ViewModels. Il est tout à fait possible de se passer d’un serveur métier dans certains cas, le projet noyau est alors le début de l’histoire au lieu de se placer entre entre le serveur métier et les projets d’UI spécialisés. Certaines applications simples ne dialoguant pas avec des données externes ou ne comportant pas un gros savoir métier peuvent parfaitement se contenter d’implémenter tout le code utile dans le “noyau”.

Dans mon exemple j’ai souhaité mettre en scène la partie “serveur métier” car ce dernier fait partie intégrante de la stratégie que je propose. Mais rappelez-vous que cette stratégie est assez souple pour autoriser des adaptations.

La première question qui va très vite se poser est de savoir en quoi va être développer le noyau ?

On peut prendre n’importe quelle plateforme, l’essentiel est d’utiliser une plateforme qui supporte le minimum vital. Si on utilise un projet WinRT on sera en C#5 incompatible avec le C#4 de Mono par exemple. Si on utilise Silverlight on sera vite amené aux limites du mini framework embarqué par le plugin, ce “profile” (selon la terminologie MS) de .NET n’étant pas forcément représentatif du profile utilisé par Mono.

Bref, le plus simple, c’est de partir d’un projet Librairie de code en Android, et, pourquoi pas, une vieille version pour être tranquille. J’ai donc choisi d’écrire le projet noyau comme une librairie de code Android 1.6.

Le projet noyau s’appelle CPCross.Core.Android, son espace de nom par défaut est CPCross.Core, n’oublions pas que ce projet doit être “neutre” dans le sens où il sera ensuite dupliqué pour toutes les autres cibles. Même si cela ne changerait au fonctionnement, il serait idiot d’avoir à appeler une classe CPCross.Core.Android.MaClasse dans la version Windows Phone par exemple... Le projet est bien un projet Android parce qu’il faut bien choisir une plateforme pour implémenter le noyau, mais ce code doit être totalement transposable. Une bonne raison à cela : il n’y aura qu’un seul code de ce type ! Dans les projets “dupliqués” il n’y a aucune copie, mais des liens vers le code du projet noyau originel. Seul le fichier “.csproj” est différent car il contient les informations de la cible (OS, version de celui-ci) et que c’est grâce à ces informations que le _même_ code sera compilé en _différents_ binaires pour chaque cible.

Le projet noyau référence Cirrious.MvvmCross.Android, puisque c’est une librairie de code Android 1.6. C’est de cette façon que tout le potentiel de MVVMCross sera disponible pour notre propre code.

Dans ce projet j’ajoute les répertoires ViewModels et Converters. Le premier contiendra les ... ViewModels et le second les convertisseurs éventuels (nous y reviendrons).

Le codage de l’application peut alors commencer. Dans le répertoire ViewModels j’ajoute une nouvelle classe, MainViewModel.cs selon une méthode très classique lorsqu’on suit le pattern MVVM.

Ce ViewModel descend de la classe MvxViewModel qui est fournie par MVVMCross.

Afin de ne pas rendre ce billet interminable et de ne surtout pas vous embrouiller, je ne détaillerai pas le fonctionnement de MVVMCross, seules les grandes lignes du montage du code en suivant la stratégie proposée seront évoquées.

Le code complet de la solution, et donc du noyau, est téléchargeable en fin de billet, je vous ferai grâce aussi de la copie complète de chaque unité de code.

Le ViewModel est tout ce qu’il y a de plus classique. Il expose des propriétés, il déclenche des propertyChanged quand cela est nécessaire (via FirePropertyChanged de MVVMCross) et propose des commandes MvxRelayCommand qui seront bindables à l’interface utilisateur. Rien que de très classique somme toute pour qui connait et manipule des librairies MVVM donc.

Le projet noyau va se voir ajouter une Référence Web pour pointer le service Web des codes postaux. Grâce à la compatibilité extraordinaire de MonoDroid avec le framework .NET, tout ce passe comme dans n’importe quel projet .NET... Les noms de classes générées sont rigoureusement les mêmes qu’ils le seraient dans un projet Windows.

J’ai juste rencontré un enquiquinement avec... Windows Phone. Ce dernier n’accepte pas l’ajout de Références Web (Web Reference) dans le mode “avancé” (en réalité des services Web classiques) il faut ajouter un “Service Reference”, ce qui génère un nom de classe mère pour le service légèrement différent. Heureusement le reste du code est identique.

De fait, j’ai été obligé de “ruser” une fois dans le ViewModel :

#if WINDOWS_PHONE
private EnaxosFrenchZipCodesSoapClient service;
#else
private EnaxosFrenchZipCodes service;
#endif

Le nom de classe est différent pour Windows Phone. La variable s’appelle de la même façon et le reste du code n’y voit que du feu...

A noter que cette modification a été faite après avoir créer le projet pour Windows Phone et m’être aperçu de cette différence. Au départ bien entendu je ne l’avais pas prévu.

En cherchant bien on doit pouvoir forcer le nom à être identique, ne serait-ce qu’avec un refactoring et j’aurai pu éviter cette “verrue” qui me dérange. Mais ce n’est qu’une démo, le lecteur me pardonnera j’en suis certain de ne pas avoir atteint la perfection...

Le projet noyau utilise MVVMCross qui, comme de nombreux frameworks MVVM, oblige à suivre une certaine logique pour déclarer les liens entre Vues et ViewModels, pour indiquer les navigations possibles dans l’application, ou l’écran de démarrage, etc. Chaque framework a sa façon de faire, disons que MVVMCross se comporte un peu comme Jounce ou Prism.

Il faut ainsi ajouter au projet une classe StartApplicationObject qui hérite de MvxApplicationObject et qui supporte l’interface IMvxStartNavigation si on veut gérer la navigation en plus (l’application n’a qu’une page mais le gérer “comme une vraie” fait partie de la règle du jeu). Cette classe est très simple car tout est dans la classe mère ou presque :

using CPCross.Core.ViewModels;
using Cirrious.MvvmCross.Interfaces.ViewModels;
using Cirrious.MvvmCross.ViewModels;

namespace CPCross.Core
{
class StartApplicationObject : MvxApplicationObject, IMvxStartNavigation
{
public void Start()
{
RequestNavigate(typeof(MainViewModel));
}

public bool ApplicationCanOpenBookmarks
{
get
{
return true;
}
}
}
}

Cet objet “start” sert en fait à initialiser l’application et notamment ici la navigation vers la première page. MVVMCross ne navigue pas par les Vues, ce qui est généralement la vision classique des choses sous MVVM, mais par les ViewModels. La méthode “Start” réclame ainsi une navigation vers le type “MainViewModel”, notre seul et unique ViewModel. Ce qui déclenchera l’affichage de la page (ainsi que la recherche de la Vue correspondante et son attache dynamique, via le binding, au ViewModel).

L’application peut spécifier de nombreux paramètres qui nous écarteraient des buts de ce billet, mais on voit sur le code ci-dessus par exemple qu’il est possible de gérer ou non des bookmarks. Cela sert à la navigation, mais avec une seule page notre projet n’en tirera aucun avantage...

Reste à définir un second objet, l’objet “application” lui-même, lui encore créé par héritage d’une classe fournie par MVVMCross :

using Cirrious.MvvmCross.Application;
using Cirrious.MvvmCross.ExtensionMethods;
using Cirrious.MvvmCross.Interfaces.ServiceProvider;
using Cirrious.MvvmCross.Interfaces.ViewModels;

namespace CPCross.Core
{
public class App : MvxApplication,IMvxServiceProducer<IMvxStartNavigation>
{
public App()
{
var startApplicationObject = new StartApplicationObject();
this.RegisterServiceInstance<IMvxStartNavigation>(startApplicationObject);
}
}
}

Il y a presque plus de “using” que de code réel...

En réalité l’objet application est plus complexe que l’objet ApplicationStart que nous venons de voir. Mais dans le cas de notre code, la seule chose à faire est de créer dans le constructeur une instance de notre objet “start” puis de l’enregistrer dans le système de navigation.

Tout ce montage permet dans une application réelle de paramétrer finement toute la navigation et certains aspects de l’application. Ici nous n’utilisons que peu des possibilités de MVVMCross.

Et voilà...

L’application “noyau” est faite. Une compilation nous assure que tout est ok. Des tests unitaires seraient, dans la réalité, bienvenus pour s’assurer que le ViewModel fonctionne correctement. Je fais l’impasse sur de tels tests dans cet exemple.

Voici à quoi ressemble le projet noyau sous VS :

image

On retrouve la structure d’une librairie de code classique. On voit que l’icone du projet contient un petit robot Android, VS gère facilement une solution avec des plateformes différentes, c’est un vrai bonheur.

On note la présence de “Web Reference”, ainsi que les classes spécifiques à la mécanique de MVVMCross.

Un dernier mot sur les convertisseurs : Il s’agit ici de regrouper tous les convertisseurs qui seront utilisés dans les bindings ultérieurement, comme dans de nombreux projets Silverlight ou WPF par exemple. Même utilisation, même motivation. Mais l’écriture varie un peu car ici nous allons nous reposer sur MVVMCross.

En effet, prenons l’exemple simple de la visibilité d’un objet. Sous Xaml il s’agit d’une énumération (Vibisility avec Collapsed, Visible), mais sous Android ou iOS c’est autre chose... On retrouve dans ces environnements une propriété qui permet de dire si un objet visuel est visible ou non, ce genre de besoin est universel, mais pas les classes, les énumérations, etc, sur lesquels il repose dans sa mise en œuvre spécifique...

C’est pour cela que MVVMCross est “cross”, il s’occupe de deux choses à la fois : être un framework MVVM tout à fait honorable mais aussi être un médiateur qui efface les différences entre les plateformes...

Dans mon application (dans le ViewModel), lorsque le service Web est interrogé je souhaite afficher un indicateur de type “busy” ou une progress bar en mode “indéterminé”. Je verrais cela au moment de l’écriture des interfaces utilisateurs, mais je sais que j’aurai besoin d’un tel indicateur.

Le code de mon ViewModel expose une propriété booléenne IsSearching qui est à “vrai” lorsque l’application envoie une requête au serveur métier, et qui repasse à “faux” lorsque les données sont arrivées.

Je sais que sous Xaml j’aurai besoin de convertir ce booléen en Visibilty, et que sous Android il faudra faire autrement. Dilemme...

C’est là que MVVMCross va encore m’aider. Regardez le code suivant :

using Cirrious.MvvmCross.Converters.Visibility;

namespace CPCross.Core.Converters
{
public class Converters
{
public readonly MvxVisibilityConverter Visibility = new MvxVisibilityConverter();
}
}

Je ne participe pas au concours du code le plus court, mais vous admettrez que je n’écris pas grand-chose...

Le cas de la visibilité est un tel classique que MVVMCross l’a déjà prévu. Il me suffit donc de créer une classe qui regroupera tous les convertisseurs (afin de les localiser plus facilement quand j’écrirai les UI) et de déclarer une variable convertisseur Visibility qui est une instance de MvxVisibilityConverter.

Sous Xaml (WinRT, Silverlight..) MVVMCross fournira un convertisseur retournant l’énumération correcte, sous Android il fournira la valeur attendue par la propriété Visibility (même nom, mais pas même type...).

Vous l’avez compris, l’écriture du noyau est la partie la plus essentielle de l’application après celle du serveur métier. Ce code est fait pour n’être écrit qu’une seule fois et pour être exécuté par toutes les cibles supportées. MVVMCross nous aide à gommer les différences entre les cibles pour que notre code dans les ViewModels soit universel.

Les noyaux dupliqués

Il est nécessaire maintenant de créer des “projets bis” pour les autres cibles.

En effet, la librairie de code que nous venons d’écrire, pour universelle qu’elle soit, est malgré tout un projet Android qui produira du binaire Android. Totalement inutilisable sous WinRT ou Windows Phone par exemple...

Pourtant nous nous sommes donnés du mal pour écrire un code “portable”, où est l’arnaque ? Rembourseeeeez !

On reste calme...

Aucune arnaque en vue. Ne vous inquiétez pas. Le seul problème que nous avons c’est qu’il nous faut trouver un moyen pour compiler ce code en binaire pour chaque plateforme cible.

Et c’est facile : le chef d’orchestre qui joue les aiguilleurs c’est le fichier “csproj”, le fichier projet. C’est lorsqu’on créé le projet qu’on indique quelle cible on touche.

L’astuce va tout simplement consister à créer de nouveaux projets pour chaque type de cible, projets qu’on videra de tout ce qu’il y a dedans par défaut pour le remplacer par des _liens_ vers le code du “vrai noyau”, le premier.

Je vais ainsi créer un projet CPCross.Core.WindowsPhone en indiquant que je veux créer une librairie pour Windows Phone 7.1, et un projet CPCross.Core.NET4 qui sera un projet librairie de code Windows .NET 4. Ensuite, c’est le côté un peu pénible de la manipulation, je vais ajouter un à un tous les sous-répertoires en le recréant avec le même nom puis je vais ajouter des “items existants” en allant piocher dans le projet noyau original et en demandant à VS de faire un _lien_ et non une _copie_. Pas de code dupliqué. Plusieurs projets mais un seul code à écrire, cela faisait partie des contraintes de la stratégie...

image

Voici le résultat de cette manipulation. Le premier projet est le “vrai noyau” contenant les fichiers de code. Les deux suivants sont les projets “bis” ou “dupliqués” qui ne contiennent que des liens vers le code du premier. Chaque projet porte un nom différent avec en suffixe la cible visée, en revanche, c’est exactement le même code, donc le même espace de nom pour tous (CPCross.Core) sans aucune indication de différence.

Chaque cible doit “croire” que le code a été écrit juste pour elle et que les autres n’existent pas... C’est un peu comme la gestion des petites amies quand on est célibataire Sourire

Conclusion partielle

Nous avons créé une solution Visual Studio dans laquelle nous avons ajouté la librairie MVVMCross. Nous avons ensuite écrit un projet “universel” en choisissant une cible au hasard, ici Android 1.6. Grâce à MVVMCross, MonoDroid et Visual Studio, nous avons pu écrire un premier projet définissant les ViewModels de notre (future) application.

En rusant un petit peu nous avons créé des projets “bis” pour viser les autres cibles, les autres plateformes que nous voulons supporter. En réalité ces projets bis sont des fantômes, ils ne font que contenir des liens vers le code du premier projet, il n’y a pas de code dupliqué.

Chaque projet “noyau” se compile pour produire un binaire adapté à sa cible, mais nous n’avons écrit qu’une seule version de ce code. MVVMCross nous a aider à gommer les petites différences entre les plateformes.

Nous sommes prêts à écrire les projets spécifiques à chaque plateforme ciblée...

C’est là que vous verrez enfin quelques images de l’application en action.

Une petite pause pour votre serviteur et on se retrouve dans la partie 3 !

Je le dis à chaque fois, mais avec un tel suspens je n’ai rien à craindre : Stay Tuned !

 

PS: pour le code, le zip se trouvera en fin de la partie 3

blog comments powered by Disqus