Dot.Blog

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

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

[new:31/12/2012]Après avoir présenté la stratégie, sa motivation et ses outils dans la Partie 1, après avoir poser le décor des premiers modules communs dans la Partie 2, il est temps d’aborder la réalisation des modules spécifiques et de voir fonctionner notre applications sur différentes plateformes !

Bref Rappel

 

La stratégie

La Partie 1 expose une stratégie de développement cross-platform et cross form-factor basée sur l’utilisation de C#, .NET, Visual Studio, MonoDevelop, MonoTouch (pour iOS) et MonoDroid (pour Android), le tout avec le framework MVVM cross-platform MVVMCross.
Cet ensemble d’outils permet avec un seul langage de programmation, un seul EDI (pour iOS il faut toutefois utiliser MonoDevelop sous Mac), une seule plateforme (.NET MS et Mono) d’attaquer onze cibles différentes : les smartphones et tablettes Apple, Android, Windows “classique”, Windows 8 (WinRT), Windows Phone...
La stratégie se base aussi sur une séparation particulière du code, l’essentiel de celui-ci n’ayant besoin d’être écrit qu’une seule fois.
On trouve dans la partie immuable à 100% le serveur métier regroupant l’intelligence et le savoir-faire métier. Puis on trouve un projet “noyau” dont la variabilité assumée est très faible puisque tout le code sera simplement lié dans des projets bis visant à produire des binaires spécialisés pour chaque cible.
Dans la partie la plus fluctuante ou la variabilité assumée peut atteindre 100% on trouve bien entendu les différents projets implémentant les Vues de façon spécifique pour chaque cible.
Le maitre mot de la stratégie est “plus le code devient spécifique, moins on écrit de code”. Grâce à cette approche supporter de nombreuses cibles différentes ne coute pas très cher et se limite à la création des Vues sans remise en cause ni des ViewModels ni du code métier.
La concentration du savoir-faire du développeur avec un seul langage, un seul EDI, un seul framework (C#, VS/MonoDevelop, .NET) évite la couteuse mise en place de formations lourdes ou l’embauche de personnel qualifié par cible visée. Tout le monde s’y retrouve, le développeur qui voit son champ d’action élargi donc son travail devenir plus intéressant, le DSI qui ne gère qu’une seule équipe et une formation minimale, l’entreprise qui diminue ses couts de réalisation et de maintenance tout en assurant sa présence sur de multiples médias et cibles.

L’exemple de code

La Partie 2 pose le décor de l’exemple de code utilisé pour illustrer la stratégie proposée.
En repartant d’un exemple datant de 2008 (les codes postaux français) utilisant un service Web et un frontal Silverlight Web nous allons rajouter de nouvelles cibles absolument non prévues à cette époque et ce sans remettre en cause une seule ligne de programmation du serveur métier. Cela démontre au passage l’énorme avantage de cette stratégie qui pérennise le code au travers des années, des modes, et des technologies changeantes.
Après avoir créé la solution VS, y avoir ajouté les librairies MVVMCross, nous créons le ViewModel principal (l’exemple n’aura qu’une page sans navigation pour simplifier).
Ce ViewModel fait partie du projet dit “’noyau”. Il est écrit une fois pour toute et va être utilisé pour créer des projets “bis” ciblant chacun une plateforme spécifique. Il n’y a pas de duplication de code, les projets bis sont créés en ajoutant des liens vers le code du projet noyau original. Seul le fichier projet (csproj) diffère puisque c’est lui qui permet de sélectionner le compilateur à utiliser.
Le projet noyau se voit agrémenté de quelques classes propres au fonctionnement de MVVMCross et des convertisseurs de valeurs qui seront utilisés par les Vues. On utilise ici les mécanismes de MVVMCross qui gomme les nuances entres les OS de façon à pouvoir écrire des convertisseurs qui marcheront aussi bien sous Android que sous WinRT ou Windows Phone sans rien avoir à recompiler ni modifier.

L’étape du jour

Nous possédons le serveur métier (celui des codes postaux), nous possédons le cœur de l’application écrit une seule fois en C# (le noyau). Nous allons écrire maintenant les projets spécifiques à chaque plateforme sélectionnée pour notre exemple.
Se souvenir : Il ne s’agit que d’un exemple simplifié à l’extrême pour ne pas être trop long. Le serveur métier est ici réduit à sa plus simple expression, le noyau ne définit qu’un seul ViewModel, les parties spécifiques ne couvrent que quelques cibles seulement et n’exposent qu’une seule vue. Le lecteur, j’en suis certain, saura transposer cette démonstration simplifiée à des cas plus complexes et plus réels.
A noter: Je ne détaille pas ici tout le fonctionnement de MVVMCross, j’ai prévu d’y revenir dans un article spécialement consacré à cette librairie.

Le Noyau

Je l’ai succinctement décrit dans la Partie 2. Voici son organisation :
image
Il s’agit, pour rappel, d’une librairie de code Android 1.6 afin de se garantir un noyau le plus restrictif possible puisque son code sera ensuite réutilisé partout sans réécriture.
La version d’Android ne compte en réalité pas vraiment puisque le noyau n’utilise pas de spécificités de l’OS cible. C’est la version de .NET Mono qui compte et c’est MonoDroid qui nous fournit le nécessaire (en niveau 3/4 par rapport au framework Microsoft). Nous disposons donc d’un framework puissant gérant, par exemple, Linq et les expressions Lambda...
Le projet déclare un espace de nom CPCross.Core, le projet lui-même portant en suffixe le nom de sa cible (.Android, .NET4 ou .WindowsPhone comme on le voit sur l’image ci-dessus).
On note la présence d’une Référence Web qui point sur le “serveur métier”. Si le code de chaque projet “bis” a été créé par simple lien vers le code du projet noyau, la Référence Web a été ajoutée à chaque projet “manuellement”. Il doit y avoir un moyen d’éviter cette étape mais je n’ai pas eu le temps de le chercher. Cela ne se fait qu’une fois pour chaque projet ce qui n’est pas très contraignant (les mises à jour éventuelles du serveur métier étant prises en compte ensuite par un simple refresh par clic-droit sur la référence).
Comme je l’ai indiqué dans la partie précédente, le seul problème rencontré l’a été avec Windows Phone qui n’accepte pas les Références Web mais qui oblige à utilise une Référence de Service. La classe de service générée ne porte pas le même nom, ce qui a du être pris en compte dans le code du noyau, mais heureusement son fonctionnement reste le même.
Le code du noyau est le suivant :
using System;
using System.Collections.Generic;
#if WINDOWS_PHONE
using CPCross.Core.WindowsPhone.EnaxosCP;
#else
using CPCross.Core.EnaxosCP;
#endif
using Cirrious.MvvmCross.Commands;
using Cirrious.MvvmCross.ViewModels;


namespace CPCross.Core.ViewModels
{
public class MainViewModel : MvxViewModel
{
#if WINDOWS_PHONE
private EnaxosFrenchZipCodesSoapClient service;
#else
private EnaxosFrenchZipCodes service;
#endif
private readonly List<string> errors = new List<string>();
private string searchField;
private bool isSearching;
private bool sortByZip;
private bool searchAnyWhere;

public MainViewModel()
{
// commanding
ClearErrorsCommand = new MvxRelayCommand(clearErrors, () => HasErrors);
GetCitiesByNameCommand = new MvxRelayCommand(getCitiesByName, () => !IsSearching);
GetCitiesByZipCommand = new MvxRelayCommand(getCitiesByZip, () => !IsSearching);


// init service
createService();
}

//add error to error list
private void addError(string errorMessage)
{
errors.Add(errorMessage);
FirePropertyChanged("Errors");
FirePropertyChanged("HasErrors");
ClearErrorsCommand.RaiseCanExecuteChanged();
}

private void clearErrors()
{
errors.Clear();
FirePropertyChanged("Errors");
FirePropertyChanged("HasErrors");
ClearErrorsCommand.RaiseCanExecuteChanged();
}


private void createService()
{
try
{
#if WINDOWS_PHONE
service = new EnaxosFrenchZipCodesSoapClient();
#else
service = new EnaxosFrenchZipCodes();
#endif
service.FullVersionCompleted += service_FullVersionCompleted;
service.FullVersionAsync();
}
catch (Exception ex)
{
addError(ex.Message);
}
}

private void service_FullVersionCompleted(object sender, FullVersionCompletedEventArgs e)
{
if (e.Error == null)
{
Title = e.Result.Title;
Version = e.Result.Version;
Description = e.Result.Description;
Copyrights = e.Result.Copyrights;
WebSite = e.Result.WebSite;
Blog = e.Result.Blog;
MiniInfo = e.Result.Version + " " + e.Result.Copyrights;
FirePropertyChanged("Title");
FirePropertyChanged("Version");
FirePropertyChanged("Description");
FirePropertyChanged("Copyrights");
FirePropertyChanged("WebSite");
FirePropertyChanged("Blog");
FirePropertyChanged("MiniInfo");
return;
}
addError(e.Error.Message);
}


// search by name
private void getCitiesByName()
{
if (!canSearch()) return;
IsSearching = true;
service.GetCitiesByNameCompleted += service_GetCitiesByNameCompleted;
service.GetCitiesByNameAsync(searchField, searchAnyWhere, sortByZip);
}

private void service_GetCitiesByNameCompleted(object sender, GetCitiesByNameCompletedEventArgs e)
{
InvokeOnMainThread(() => IsSearching = false);
service.GetCitiesByNameCompleted -= service_GetCitiesByNameCompleted;
if (e.Error == null)
{
InvokeOnMainThread(() =>
{
Result = e.Result==null ? null : new List<CityObject>(e.Result);
FirePropertyChanged("Result");

});
return;
}
InvokeOnMainThread(() =>
{
addError(e.Error.Message);
Result = new List<CityObject>();
FirePropertyChanged("Result");
});
}


// search by zip
private void getCitiesByZip()
{
if (!canSearch()) return;
IsSearching = true;
service.GetCitiesByZipCompleted += service_GetCitiesByZipCompleted;
service.GetCitiesByZipAsync(searchField, sortByZip);
}

private void service_GetCitiesByZipCompleted(object sender, GetCitiesByZipCompletedEventArgs e)
{
InvokeOnMainThread(() => IsSearching = false);
service.GetCitiesByZipCompleted -= service_GetCitiesByZipCompleted;
if (e.Error == null)
{
InvokeOnMainThread(() =>
{
Result = e.Result==null ? null : new List<CityObject>(e.Result);
FirePropertyChanged("Result");
});
return;
}
InvokeOnMainThread(() =>
{
addError(e.Error.Message);
Result = new List<CityObject>();
FirePropertyChanged("Result");
});
}

// internal test
private bool canSearch()
{
return !IsSearching && !string.IsNullOrWhiteSpace(searchField);
}

// service information
public string Title { get; private set; }
public string Description { get; private set; }
public string Version { get; private set; }
public string Copyrights { get; private set; }
public string WebSite { get; private set; }
public string Blog { get; private set; }

public string MiniInfo { get; private set;}

// errors
public List<string> Errors
{
get
{
return errors;
}
}

public bool HasErrors
{
get
{
return errors.Count > 0;
}
}

// search result
public List<CityObject> Result { get; set; }

// search field
public string SearchField
{
get
{
return searchField;
}
set
{
if (searchField == value) return;
searchField = value;
FirePropertyChanged("SearchField");
}
}

// result sort
public bool SortByZip
{
get
{
return sortByZip;
}
set
{
if (sortByZip == value) return;
sortByZip = value;
FirePropertyChanged("SortByZip");
}
}

// search mode
public bool SearchAnyWhereInField
{
get
{
return searchAnyWhere;
}
set
{
if (searchAnyWhere == value) return;
searchAnyWhere = value;
FirePropertyChanged("SearchAnyWhereInField");
}
}

// search state
public bool IsSearching
{
get { return isSearching; }
private set
{
if (isSearching == value) return;
isSearching = value;
FirePropertyChanged("IsSearching");
GetCitiesByNameCommand.RaiseCanExecuteChanged();
GetCitiesByZipCommand.RaiseCanExecuteChanged();
}
}

// commanding
public MvxRelayCommand ClearErrorsCommand { get; private set; }
public MvxRelayCommand GetCitiesByNameCommand { get; private set; }
public MvxRelayCommand GetCitiesByZipCommand { get; private set; }
}
}
Ce code est réellement très classique pour un ViewModel suivant le pattern MVVM. On y trouve bien entendu des petites spécificités propres à MVVMCross mais quand on pratique de nombreux frameworks MVVM on s’y retrouve facilement, tous ne font que régler les mêmes problèmes de façon finalement assez proche.
Des propriétés et des commandes. Voilà ce qu’est un ViewModel dans la pratique. C’est bien ce que reflète le code ci-dessus.
Lorsque le ViewModel est instancié il fait un premier appel au service pour aller chercher les informations de base retournées par ce dernier (version, copyrights, etc).
Ensuite les recherches sont effectuées à la demande via les commandes exposées.
Le ViewModel expose même une liste des erreurs qui stocke les exceptions éventuelles. Dans notre exemple ne mettant en œuvre qu’une seule Vue sans navigation nous n’utiliserons pas ce mécanisme. Le code source étant publié en fin d’article, à vous de jouer et d’ajouter ce qu’il faut pour afficher les erreurs s’il y en a (il y a une commande ClearErrors qui peut être bindée à un Button pour effacer la liste).

Le serveur métier

Le serveur métier est généralement un service Web (ou un ensemble de services web) le plus standard possible.
Ici j’ai repris le serveur d’une démo écrite en 2008 pour Silverlight. Il marche depuis cette époque sans aucun soucis (sur une base de données Access/Jet ... c’est pour dire à quel point ce n’est pas un code moderne !).
C’est un simple '”asmx” bien loin de sophistications parfois inutiles de WCF. Sa simplicité lui donne l’avantage de l’universalité, et le fait que je puisse aujourd’hui le réutiliser sans changer une ligne de code prouve de façon éclatante que la stratégie proposée permet réellement de pérenniser le code en le protégeant des modes et des technologies clientes changeantes.
le WDSL nous indique ceci :
image
S’agissant d’un exemple, ce “serveur métier” contient peu de “savoir métier” voire aucun... Mais il pourrait contenir des centaines de services sophistiqués, cela ne changerait rien à notre stratégie. Dans un tel cas il serait d’ailleurs certainement segmenté en plusieurs services différents tournant éventuellement sur des machines différentes.
L’exemple multi-plateforme n’utilisera d’ailleurs qu’une partie des services exposés.

Une première cible : Windows “classique” en mode Console

Il est temps d’ajouter une première cible “visuelle”, un client spécifique.
J’ai choisi avec prudence Windows “classique” avec un mode désuet mais très pratique pour m’assurer que tout fonctionne : la console !
Certes cela fait très MS-DOS, mais cela prouve que MVVMCross couvre vraiment tous les cas de figure même les plus étranges et surtout cela m’assurait de pouvoir déboguer le noyau sans aucune complication liée à Android ou Windows Phone ou autre plateforme. Bien entendu cela montre malgré tout une première cible très différente des autres et il faut voir ici la console Win32 comme le symbole de tout ce qui peut être fait sous Windows “classique” comme WPF par exemple (Windows XP, Vista, 7, Windows 8 classic desktop).
Tous les projets d’UI portent le même nom que le projet principal avec le suffixe correspondant à leur cible. Ainsi, ce projet s’appellera CPCross.UI.Console.
Tous ajoutent en référence les librairies spécifiques de MVVMCross (ici Cirrious.MvvmCross.Console), et tous pointent la version du noyau qui correspond à leur cible. Ici CPCross.Core.NET4.
image
L’image ci-dessus nous montre l’arborescence de la solution au niveau du sous-répertoire de solution “UI”. On y trouve les projets d’UI dont le projet console.
Tous les projets ont la même structure. On trouve ainsi un répertoire Views qui contiendra l’unique Vue de notre application. On note aussi la présence de deux classes Program.cs et Setup.cs, propres au mode choisi (la console .NET 4) et à MVVMCross (le setup qui initialise l’application et la navigation vers la vue de départ).
La programmation de la vue en mode Console est très classique : des Console.Writeln(xxx) se trouvant dans une méthode appelée automatiquement par MVVMCross à chaque modification du ViewModel (Binding minimaliste) ce qui permet de rafraichir l’affichage, les saisies se faisant via une méthode spéciale aussi qui transforme les saisies clavier en appel aux commandes du ViewModel ou en modification des propriétés de ce dernier.
Le code cette “vue” un peu spéciale est le suivant :
using System;
using System.Collections.Generic;
using System.Text;
using CPCross.Core.ViewModels;
using Cirrious.MvvmCross.Console.Views;

namespace CPCross.Console.Views
{
class MainView : MvxConsoleView<MainViewModel>
{
protected override void OnViewModelChanged()
{
base.OnViewModelChanged();
ViewModel.PropertyChanged += (sender, args) => refreshDisplay();
refreshDisplay();
}

public override bool HandleInput(string input)
{
switch (input)
{
case "n" :
case "N" :
if (ViewModel.GetCitiesByNameCommand.CanExecute())
ViewModel.GetCitiesByNameCommand.Execute();
return true;
case "z" :
case "Z" :
if (ViewModel.GetCitiesByZipCommand.CanExecute())
ViewModel.GetCitiesByZipCommand.Execute();
return true;
case "c" :
case "C" :
if (ViewModel.Result!=null) ViewModel.Result.Clear();
if (ViewModel.ClearErrorsCommand.CanExecute()) ViewModel.ClearErrorsCommand.Execute();
refreshDisplay();
return true;
case "s" :
case "S" :
ViewModel.SearchAnyWhereInField = false;
refreshDisplay();
return true;
case "a" :
case "A" :
ViewModel.SearchAnyWhereInField = true;
refreshDisplay();
return true;
default :
if (input.Trim().ToUpper().StartsWith("SET ") && input.Length > 3)
{
ViewModel.SearchField = input.Substring(3).Trim();
return true;
}
break;
}
return base.HandleInput(input);
}

private void refreshDisplay()
{
System.Console.BackgroundColor = ConsoleColor.Black;
System.Console.ForegroundColor = ConsoleColor.White;
System.Console.Clear();
System.Console.ForegroundColor = ConsoleColor.Yellow;
System.Console.WriteLine("CP-Cross / .NET 4.0 Console UI");
System.Console.WriteLine();

System.Console.ForegroundColor = ConsoleColor.Gray;
System.Console.WriteLine(ViewModel.Title + " - Version: " + ViewModel.Version);
System.Console.WriteLine(ViewModel.Copyrights);
System.Console.WriteLine(ViewModel.Description);
System.Console.WriteLine();


System.Console.ForegroundColor = ConsoleColor.White;
System.Console.WriteLine("Search by <N>ame, by <Z>ip. <SET> {text} to set search term. <C>lear result."+Environment.NewLine+"<S>tarts with or <A>nywhere");
System.Console.WriteLine();
System.Console.ForegroundColor = ConsoleColor.Cyan;
System.Console.WriteLine("Current Search Term : "+(string.IsNullOrWhiteSpace(ViewModel.SearchField)?"<none>":ViewModel.SearchField) +" - Current option: "+(ViewModel.SearchAnyWhereInField?"Anywhere":"Starts with"));
System.Console.ForegroundColor = ConsoleColor.White;
System.Console.WriteLine();

if (ViewModel.IsSearching)
{
System.Console.ForegroundColor = ConsoleColor.Red;
System.Console.WriteLine("Searching...");
return;
}

if (ViewModel.Result!=null && ViewModel.Result.Count>0)
{
System.Console.ForegroundColor=ConsoleColor.Green;
var limit = Math.Min(10, ViewModel.Result.Count);
for(var i =0;i<limit;i++)
System.Console.WriteLine(ViewModel.Result[i].City+"; "+ViewModel.Result[i].ZipCode+"; "+ViewModel.Result[i].DepartmentName);
}
if (ViewModel.Result!=null && ViewModel.Result.Count>10)
System.Console.WriteLine("... ("+ViewModel.Result.Count+" results. 10 firsts displayed)");
if(ViewModel.Result==null || (ViewModel.Result!=null && ViewModel.Result.Count==0))
{
System.Console.ForegroundColor=ConsoleColor.Blue;
System.Console.WriteLine("<aucun résultat>");
}

if (ViewModel.Errors.Count>0)
{
System.Console.ForegroundColor=ConsoleColor.DarkRed;
foreach (var error in ViewModel.Errors)
{
System.Console.WriteLine("- "+error);
}
}

System.Console.WriteLine();
System.Console.ForegroundColor=ConsoleColor.White;
System.Console.Write("Command ? ");
System.Console.ForegroundColor=ConsoleColor.Yellow;
}
}
}
On voit que ce code est une ‘vraie’ vue qui descend de MxConsoleView, ce qui permet à MVVMCross de gérer la dynamique des changements de propriétés et les saisies clavier de façon transparente. Rappelez-vous, notre ViewModel qui est derrière n’a absolument pas connaissance de cette utilisation qui en est faite...
Le couplage Vue/ViewModel est assuré par MVVMCross de façon automatique (la vue déclare dans son entête de classe qu’elle est liée à MainViewModel). Il reste possible comme avec tous les frameworks MVVM un peu sophistiqués de redéfinir les routes entre Vues et ViewModel, ici nous utilisons le comportement par défaut.

Le film...

Il est intéressant de voir tout cela en action. Quelques captures d’écran feront l’affaire et seront plus rapides à commenter qu’une capture vidéo :
image
Etape 1 : l’application s’affiche. Pendant un cours instant les informations en début de page affichent les valeurs par défaut initialisées par le code (partie supprimée de la copie du code ci-dessus). Puis, une fois que le service a répondu la page se met à jour et affiche les informations du Service à jour. On note le copyright de 2008.
S’agissant d’un mode console j’ai prévu un jeu de commande minimaliste : <N> pour chercher par nom, <Z> par zip (code postal), la commande SET <texte> permet de saisir la valeur à chercher. <C> pour clear (effacer le dernier résultat). Les commandes <A> et <S> permettent d’indiquer si le terme saisi doit être cherché en début de chaine uniquement ou n’importe où dans le nom (en mode Zip seul le début de chaine est contrôlé).
L’application affiche le terme courant (en ce moment <none>, aucun), ainsi que les options sélectionnées (recherche en début de chaine).
Il n’y a aucun résultat (pourquoi c’est en français ? une incohérence de ma part, je fais tout en anglais mais le naturel reviens parfois à la sournoise !).
L’application est en attente de commande. Il va falloir saisir un terme à chercher.
image
En tapant la commande “set xxxx” j’initialise la variable de recherche du ViewModel. Les lettre “orl” sont tapées.
image
Le terme a été validé, on voit dans la ligne turquoise que le “Current Search Term” est bien égal à “orl”. Je tape la commande “n” pour recherche par Nom.
image
La commande “N” vient d’être validée. Le ViewModel expose un booléen “IsSearching” qui est exploité ici pour afficher le texte rouge “Searching...”. Nous exploitons bien toutes les possibilités du ViewModel, même dans sa dynamique...
image
 
Le résultat s’affiche... Nom de la ville, code postal, département. Pour que cela tienne en une page, seuls les 10 premiers résultats sont affichés ce que nous rappelle la dernière ligne verte (en indiquant que l’application à trouvé 12 réponses).
On peut bien entendu faire la même chose avec un code postal partiel ou complet.
Ce qui est intéressant ici est bien entendu le fait d’avoir un frontal .NET 4 pour Windows classique qui symbolise tous les frontaux qu’on peut écrire pour cette cible en natif comme WPF. Rares seront les applications qui utiliseront le mode Console, j’en suis convaincu n’ayez crainte Sourire

Une Seconde Cible : Android

Me voici rassurer sur le fonctionnement du “noyau”, tout se déroule comme prévu. Le serveur est distant (en Angleterre) nous sommes bien dans une configuration “réelle”.
Il est temps de sortir MonoDroid de sa cachette...
C’est très simple, puisqu’il est installé sur ma machine, sous Visual Studio je n’ai qu’à créer un nouveau projet... pour Android. Aussi compliqué que cela !
Voici la structure du projet :
image
 
Je n’entrerai pas dans les détails d’un projet MonoDroid ni même dans les arcanes du développement sous Android, cela s’écarterait bien trop du sujet.
On retrouve ici tout ce qui fait un projet Android de ce type : des Assets, des ressources dont notamment “Layout’ qui contient les fichiers Axml qui définissent les Vues. On trouve aussi, comme sous Windows Phone, la notion de Splash Screen affiché automatiquement pendant le chargement de l’application ainsi que la classe “Setup” qui fait partie du mécanisme de MVVMCross.
Côté références, le projet se comporte comme le précédent : il pointe la librairie MVVMCross adaptée à la cible (Cirrious.MvvmCross.Android) ainsi que le noyau ad hoc (CPCross.Core.Android).
Le répertoire Views fait partie du mécanisme MVVMCross : les vues sont décrites par une classe que le framework peut retrouver facilement, le code ne faisant rien de spécial en général à ce niveau :
using Android.App;
using Cirrious.MvvmCross.Binding.Android.Views;
using CPCross.Core.ViewModels;
using Cirrious.MvvmCross.Converters.Visibility;

namespace CPCross.UI.Android.Views
{
[Activity]
public class MainView : MvxBindingActivityView<MainViewModel>
{
protected override void OnViewModelSet()
{
SetContentView(Resource.Layout.Main);
}
}
}
L’attribut d’activité est propre à l’OS Android (il faut voir cela comme une tâche, un processus). Le code se borne donc à spécifier la vue qu’il faut charger (Resource.Layout.Main).
image
Ci-dessus on voit l’écran SplashScreen en cours de conception via le designer visuel Android qui s’est intégré à Visual Studio.
Ci-dessous l’écran principal avec le même designer visuel, sous MonoDevelop (on ne peut pas voir la différence sur cette capture il faudrait voir les menus pour s’apercevoir que le look est légèrement différent de VS) :
image
On retrouve ici tout ce qui fait une application Android, le format téléphone (on peut choisir bien entendu n’importe quelle résolution), des messages affichés, une case à cocher, des bouton, une zone la liste résultat, un indicateur d’attente, etc.
La liste n’est pas une simple Listbox mais une liste fournie par MVVMCross pour être bindable. Il n’y a pas de Binding sous Android sauf si on ruse un peu...
D’ailleurs pour ceux qui maitrisent déjà Xaml, comprendre Axml (humm le nom est vraiment proche !) n’est pas sorcier, la preuve, voici le code de cette page :
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="fill_parent">
<TextView
android:text="CPCross / Android UI"
android:textAppearance="?android:attr/textAppearanceMedium"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/textView1"
android:textColor="#ff808080" />
<TextView xmlns:local="http://schemas.android.com/apk/res/CPCross.UI.Android"
android:text="Small Text"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/txtInfo"
android:textColor="#ffc0c0c0"
local:MvxBind="{'Text':{'Path':'MiniInfo'}}" />
<LinearLayout
android:orientation="horizontal"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/linearLayout1">
<CheckBox xmlns:local="http://schemas.android.com/apk/res/CPCross.UI.Android"
android:text="Search anywhere"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:id="@+id/cbAnyWhere"
local:MvxBind="{'Checked':{'Path':'SearchAnyWhereInField'}}" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/linearLayout2">
<TextView
android:text="Search term"
android:textAppearance="?android:attr/textAppearanceSmall"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:id="@+id/textView2"
android:gravity="center" />
<EditText xmlns:local="http://schemas.android.com/apk/res/CPCross.UI.Android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="@+id/edSearchTerm"
android:layout_marginLeft="8px"
local:MvxBind="{'Text':{'Path':'SearchField'}}" />
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
android:minWidth="25px"
android:minHeight="25px"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:id="@+id/linearLayout3">
<Button xmlns:local="http://schemas.android.com/apk/res/CPCross.UI.Android"
android:text="Search Name"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:id="@+id/btnSearchName"
local:MvxBind="{'Click':{'Path':'GetCitiesByNameCommand'}}" />
<Button xmlns:local="http://schemas.android.com/apk/res/CPCross.UI.Android"
android:text="Search Zip"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:id="@+id/btnSearchZip"
local:MvxBind="{'Click':{'Path':'GetCitiesByZipCommand'}}" />
<ProgressBar xmlns:local="http://schemas.android.com/apk/res/CPCross.UI.Android"
android:text="Search Zip"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:id="@+id/progressBar1"
android:indeterminate="true"
android:indeterminateBehavior="cycle"
android:indeterminateOnly="true"
android:layout_gravity="center_vertical"
android:layout_marginLeft="30px"
local:MvxBind="{'Visibility':{'Path':'IsSearching', 'Converter':'Visibility'}}" />
</LinearLayout>
<Mvx.MvxBindableListView xmlns:local="http://schemas.android.com/apk/res/CPCross.UI.Android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
local:MvxBind="{'ItemsSource':{'Path':'Result'}}"
local:MvxItemTemplate="@layout/listitem_viewmodel" />
</LinearLayout>
Vous remarquerez la similitude avec Xaml. Et vous noterez l’espace de nom “local” dans une tradition identique à celle qu’on utilise sous Xaml pour référencer du code interne à l’application. Ici cela sert à accéder aux facilités, aux extensions fournies par MVVMCross pour le Binding.
La Syntaxe du Binding MVVMCross tente d’être la plus proche possible de celle de Xaml en utilisant une sérialisation JSon.
Par exemple, la Checkbox est liée ainsi :
local:MvxBind="{'Checked':{'Path':'SearchAnyWhereInField'}}"
C’est à dire que la propriété Checked est liée à la propriété SearchAnyWhereInField, déclarée dans le ViewModel du projet noyau. Le ViewModel est le datacontext implicite de cette vue, MVVMCross s’en charge. Cette notion est exactement la même qu’en Xaml, une fois encore...
Plus subtile est la liaison de l’indicateur busy :
local:MvxBind="{'Visibility':{'Path':'IsSearching', 'Converter':'Visibility'}}"
Ici c’est la propriété Visibility (même nom, décidément !) de la ProgressBar qui est liée à la propriété IsSearching du ViewModel.
Mais rappelez-vous... Cette propriété est un booléen, et autant sous Xaml que sous Android “Visibility” est d’un autre type (différent dans les deux environnements en plus). Comment régler ce problème très courant ?
Si vous avez suivi, vous vous rappelez que le projet noyau déclare un convertisseur qui utilise les facilités de MVVMCross. Et bien c’est ici qu’il est utilisé.
Dans la version Android, notre convertisseur, écrit une seule fois, saura fournir la valeur attendue par visibility, et sous Windows Phone, que nous verrons plus loin, le même binding sur la même propriété du ViewModel utilisera le même convertisseur du noyau qui cette fois-ci retournera l’énumération attendue par Xaml ...
C’est peu de chose au final, très peu même. Mais quelle chose ! Grâce à de petites astuces de ce genre nous avons écrit un seul code pour toutes les plateformes. Et cela n’a pas de prix. Enfin, si, un cout énorme, l’écriture à partir de zéro plusieurs fois de la même application un coup en C#, un coup en Objective-C, l’autre en Java Android, etc... Si vous tentez un léger calcul le gain réalisé par l’approche que je vous propose est démoniaque ! (les bonnes âmes peuvent me verser 10% du montant économisé pour mes bonnes œuvres, je saurai les utiliser à bon escient Sourire).
Est-ce que ça marche ?
image
 
Oui ça marche !
Et bien même.
Ici l’ensemble des résultats est bien entendu accessible, on peut scroller avec le doigt si nécessaire.
La mise en page n’est pas géniale, ce n’est qu’un exemple ne l’oublions pas (parmi plein d’autres).
Les plus curieux se demandent peut-être comment la liste est mise en page ... Comme on sait le faire en Xaml en créant un DataTemplate, ici on créé une Vue spéciale qui ne fait que la mise en page d’un Item, en gros cela marche de la même façon que Xaml donc... Le StackPanel n’existe pas sous ce nom, mais on le trouve sous l’appellation LinearLayout sous Android. Il peut être en orientation verticale ou horizontale, la même chose... Dans ce LinearLayout j’ai placé deux TextBlock, heu non, pardon, deux TextView (très difficile à se rappeler aussi ...) en utilisant la même technique de Binding de MVVMCross.
Dire que cela a été compliqué à faire serait mentir. Il y a bien deux ou trois choses sur lesquelles on bute, mais grâce à Internet on trouve vite les réponses. Et surtout grâce à MonoDroid et MVVMCross qui à eux deux rendent le développement Android presque identique à du Xaml...

Une troisième Cible ?

Allez, pour la route, vous en reprendrez bien une dernière ?
C’est un développement réellement cross-platform, avec un seul code écrit une fois : le serveur métier totalement universel, les ViewModels totalement portables avec MVVMCross.
Rajouter une nouvelle cible me prend juste du temps, mais j’en ai, je suis en vacances ! (même si je vois mal la différence avec d’habitude).
Je vais ainsi ajouter une version Windows Phone. J’aime bien Windows Phone.
D’abord c’est du Silverlight, et en plus c’est vraiment un super OS très différent de Android ou iOS qui se battent en procès en s’accusant d’imitation alors que l’un comme l’autre n’ont fait que reprendre l’idée d’un bureau avec des icônes. Windows Phone au moins ne copie personne. Et pour avoir à la fois un téléphone Android (Sony Xperia Arc S) et un Windows Phone (Nokia Lumia 800), je peut affirmer que j’aime les deux, mais qu’au niveau fluidité et interface j’ai un gros faible pour le Lumia et l’OS Microsoft. Windows Phone n’a pas encore eu le succès qu’il mérite, mais sincèrement je vous invite à le tester vous serez agréablement surpris.
Je vais écourter un peu la démo car ce billet est très long déjà (et avec toutes les captures parfois ça passe mal entre Windows Live Writer et mon serveur où se trouve Dot.Blog).
Ajouter un projet sous VS vous savez le faire. Choisir un projet Windows Phone dans la liste (une fois le SDK installé) n’est pas très compliqué non plus.
image
 
La structure est celle d’un projet Windows Phone, avec son SplashScreen, son App.xaml, sa MainPage.xaml etc.
Pour que le service Web fonctionne j’ai du recopier dans le projet “ServiceReferences.ClientConfig” qui a été généré automatique dans le projet “noyau” Windows Phone après l’ajout du service Web. Bien que référencé, il ne semble pas que le projet Windows Phone trouve ce fichier. La copie est en réalité un lien (comme on le voit à la petite flèche), il n’y a donc toujours pas de duplication de code.
Je passe la mise en page Xaml, le Binding qui cette fois-ci est totalement naturel, la création d’un vrai DataTemplate pour la liste des résultats, etc.
Est-ce que ça marche ?
Aussi !
image
 
C’est pas magique tout ça ?

Conclusion

Voilà... Je vous ai présenté “ma stratégie” de développement cross-platform / cross form-factor.
C’est simple, ça n’utilise qu’un seul langage de programmation, qu’un seul framework, .NET, qu’un seul EDI (VS ou MonoDevelop sa copie), une seul framework MVVM qui en plus efface les petites différences entre les plateformes, et surtout des outils géniaux comme MonoTouch ou MonoDroid.
La stratégie repose aussi sur la pérennisation du code métier dans un ou plusieurs services Web, rendant l’approche encore plus rentable et plus ouverte à toute adaptation.
L’exemple présenté n’est qu’une simple démonstration sans fioriture, mais c’est un vrai projet multi-plateforme, multi OS, multi form factor avec un vrai service distant, des écrans qui font un vrai travail. Il n’y aucune différence avec une application réelle autre que le contenu et la complexité de celui-ci.
L’écriture des 3 parties du billet m’a prise plus de temps que l’écriture de la solution fonctionnelle... 4 jours contre 3. Et si mon expérience de C# et Xaml est reconnue, je ne connais pas encore la même “intimité” avec Android. Et pourtant je l’ai fait (avec plus de soucis en réalité sous Windows Phone que sous Android ce qui est un comble).
Ma stratégie est simple et utilise le moins de “bidules” ou de “trucs” annexes. C’est vraiment le moyen le plus simple de faire du cross-plateforme aujourd’hui pour qui connait C# et Xaml, j’en suis convaincu.
Ca marche, aucun code n’est dupliqué, seule les parties fortement variables sont redéveloppées (les UI) et c’est normal mais peu contraignant puisqu’on travaille en MVVM et que tout est dans les ViewModels... Poser quelques Checkbox ou TextView ou TextBlock n’est franchement pas bien compliqué.
Cette approche minimise les couts, à la fois de développement et de maintenance, mais aussi, les plus chers, ceux du personnel compétent à former pour gérer autant de cibles. Ici cela se résume au strict minimum.
Bien maitriser C#, .NET et MVVM est la base non négociable. Le reste ce n’est que du Lego, normalement un vrai plaisir pour tout ingénieur qui à l’âme d’un ingénieur... C’est à dire pour celui qui aime autant les théories que les rendre tangibles, palpables en réalisant quelque chose qui marche et qui sera utile.
Espérant que ce billet en trois volet aura lui aussi été utile aux lecteurs de Dot.Blog,
A la prochaine (j’en plein de trucs en réserve!) donc...
Stay Tuned !
 
Le code source de la solution (sans MVVMCross), nécessite VS 2010 + derniers patches, le SDK Windows Phone, MonoDroid avec les SDK Android :
Nota: Le web service des codes postaux sert de test, vous pouvez y accéder pour tester la solution mais soyez sympa, n’en abusez pas, si le serveur venait à être gêné par le trafic généré je serai obligé de couper le web service. Merci d’avance !
blog comments powered by Disqus