La documentation officielle a un peu tendance à tourner en rond avec des liens en références circulaires, le portail Azure est vaste et pas super user-friendly (on peut passer beaucoup de temps à chercher une option qu’on est pourtant certain d’avoir aperçue cinq minutes plus tôt…).
J’ai donc rédigé un guide rapide à l’attention des développeurs de l’équipe, je vous le livre tel quel, ce n’est donc pas nécessairement très bien rédigé, mais vous aurez toutes les informations pour débuter, depuis la création du Directory B2C sur Azure jusqu’à l’authentification et la récupération des données de l’utilisateur dans l’application Xamarin.Forms.
]]>La documentation officielle a un peu tendance à tourner en rond avec des liens en références circulaires, le portail Azure est vaste et pas super user-friendly (on peut passer beaucoup de temps à chercher une option qu’on est pourtant certain d’avoir aperçue cinq minutes plus tôt…).
J’ai donc rédigé un guide rapide à l’attention des développeurs de l’équipe, je vous le livre tel quel, ce n’est donc pas nécessairement très bien rédigé, mais vous aurez toutes les informations pour débuter, depuis la création du Directory B2C sur Azure jusqu’à l’authentification et la récupération des données de l’utilisateur dans l’application Xamarin.Forms.
https://aka.ms/azfree-docs-mobileapps
Pré-requis :
Je ne détaille pas, il suffit de suivre les étapes. Sachez simplement que vous disposez d’un crédit de 170€ pour débuter, de quoi faire des tests et de l’autoformation.
Très important, peut-être un bug temporaire, mais si vous conservez le portail en français, la création de AD B2C échouera avec un message abscons vous indiquant que la valeur de l’emplacement du groupe de ressource est invalide alors qu’on la sélectionne dans une liste (et peu importe la valeur sélectionnée) !
C’est simplement que la liste est traduite et que visiblement quelque part dans le code, Azure attend une valeur en dur tirée de la liste en anglais. Franchement, j’ai honte pour Microsoft !
Donc évitons-nous bien des soucis et passons immédiatement le portail en anglais.
Aucun tuto ne vous le dira, et c’est peut-être un bug temporaire, mais si vous ne le faites pas maintenant, vous obtiendrez cette erreur à la toute fin du processus et vous devrez tout recommencer. :-)
Cliquez sur Subscriptions
Dans la liste, sélectionnez votre subscription courante.
Si vous venez de créer un compte gratuit, il n’y en aura qu’une :
Dans le panneau de gauche, cliquez sur Resource providers
Cherchez Azure, sélectionnez Microsoft.AzureActiveDirectory et cliquez sur Register
Patientez… patientez… patientez… patientez… Cliquez de temps en temps sur Refresh au cas où. Si ça ne fonctionne pas, recommencez (sélection, register), au bout d’un moment ça finit par passer. :-)
C’est bon, on y est, on peut commencer !
Cliquez sur Create a resource
Cherchez et sélectionnez Active Directory B2C
Cliquez sur Create
Create a New Azure AD B2C tenant
Renseignez les informations du tenant
Créer un Groupe de ressources avec le lien Create New s’il n’en existe pas déjà un.
Cliquez sur Review + Create pour valider les informations
Cliquez sur Create pour en finir !
Patientez. Tant que ça gigote là-dedans c’est que ça bosse :
Cliquez sur le lien, bienvenue à Black Mesa !
Dans la toolbar en haut à droite, trouvez le bouton qui ressemble à un carnet avec un entonnoir :
Mais en principe vous devriez déjà être dessus si vous avez cliqué sur le lien à la fin de l’étape précédente.
Si vous vous retrouvez sur une page d’accueil comme ça et que vous ne retrouvez pas votre tenant AD B2C…
Saisissez simplement B2C dans la barre de recherche en haut et dans la rubrique Services, sélectionnez Azure AD B2C.
Sélectionnez App registrations…
Puis New registration
Renseignez les informations
Nom et type de compte supporté :
Redirect URI :
Cela dépend du type d’application (application mobile, web app…).
Dans notre cas, il s’agira d’une application mobile Xamarin.Forms.
Attention, l’uri répond à un schéma bien particulier :
msal[redirect_uri]://auth
.
Cet URi sera à renseigner dans les manifestes Android et iOS.
Validez le tout en cliquant sur le bouton Register. Bravo ! Votre application est enregistrée :
Si vous vous en tenez là et suivez le tuto Microsoft et l’application Sample fournie, cela ne fonctionnera pas : les URI utilisés comme point d’accès dans l’application ne sont pas autorisés par défaut !
Rendez-vous dans la rubrique Authentication et cochez les cases :
Pensez bien à sauvegarder. Le bouton en haut, je vous laisse le trouver tout seul.
Créer un user flow revient à autoriser les utilisateurs à interagir avec leur compte AD : se connecter, réinitialiser son mot de passe, modifier son profile…
NB : les flows sont partageables entre les applications.
Au minima, les utilisateurs auront besoin de se connecter. Dans la page de votre tenant AD B2C, allez sur User flows dans la rubrique Policies et cliquez sur New user flow.
Puis sélectionnez Sign In et créez le flow dans sa version recommandée.
Renseignez les informations en fonction du mode de connexion et de l’authentification multiple souhaités.
Notez bien le nom, vous en aurez besoin côté client :
La dernière rubrique permet de sélectionner les données qui seront récupérées côté client lors de la connexion utilisateur. Cliquez sur Show more… pour obtenir la liste complète.
Finalisez en cliquant sur le bouton Create.
La procédure est similaire pour les autres types de flows. Créez ceux dont vous avez besoin.
Sur la page de votre tenant Azure AD B2C, cliquez sur Users dans la rubrique Manage.
Ensuite, cela dépend du mode de connexion envisagé. Il sera peut-être nécessaire de d’abord configurer un ou plusieurs fournisseurs d’identité (compte Microsoft, compte local, Github, Facebook…), dans la même rubrique que précédemment, lien Identity providers.
A priori, tout est en place côté serveur, reste à implémenter la couche MSAL côté client.
Direction le nuget package manager. Installez Microsoft.Identity.Client sur le projet commun et les projets des plateformes :
Pour commencer, il est nécessaire de définir quelques constantes.
tenantName
: le nom de domaine que vous avez défini pour votre tenant Directory B2C
tenantId
: le même suffixé par .onmicrosoft.com
clientId
: Application (client) Id, vous trouverez cette information dans l’overview de votre application sur le portail Azure :
redirect_uri
: l’uri enregistré côté azure
policySignin
et policyPassword
: le nom des polices telles que vous les avez définies précédemment
iosKeychainSecurityGroup
: identifiant du Bundle iOS que vous trouvez dans Info.plist
.
public static class Constants
{
const string tenantName = "[your_tenant_name]";
const string tenantId = "[your_tenant_name].onmicrosoft.com";
const string clientId = "[your_application_client_id]";
const string redirectUri = "[redirect_uri]";
const string policySignin = "B2C_1_signin";
const string policyPassword = "B2C_1_resetpassword";
const string iosKeychainSecurityGroup = "[application iOS Bundle identifier]";
static readonly string[] scopes = { "" };
static readonly string authorityBase = $"https://{tenantName}.b2clogin.com/tfp/{tenantId}/";
public static string RedirectMsalScheme => $"msal{redirectUri}://auth";
public static string AuthorityBase => authorityBase;
public static string[] Scopes => scopes;
public static string PolicySignin => policySignin;
public static string PolicyPassword => policyPassword;
public static string IosKeychainSecurityGroups => iosKeychainSecurityGroup;
public static string ClientId => clientId;
public static string AuthoritySignin => $"{authorityBase}{policySignin}";
public static string AuthorityPasswordReset => $"{authorityBase}{policyPassword}";
}
AuthenticationClient
Dans le fichier App.xaml.cs
, définir les propriétés AppUi
(nécessaire pour l’implémentation dans Android) et AuthenticationClient
:
using Microsoft.Identity.Client;
[...]
public partial class App : Application
{
public static IPublicClientApplication AuthenticationClient { get; private set; }
public static object UIParent { get; set; } = null;
[...]
}
Le service d’authentification implémente IPublicClientApplication
, pour l’instancier, on utilise le builder PublicClientApplicationBuilder
:
public App()
{
InitializeComponent();
AuthenticationClient = PublicClientApplicationBuilder.Create(Constants.ClientId)
.WithIosKeychainSecurityGroup(Constants.IosKeychainSecurityGroups)
.WithB2CAuthority(Constants.AuthoritySignin)
.WithRedirectUri(Constants.RedirectMsalScheme)
.Build();
[...]
}
Souvenez-vous côté serveur, vous avez configuré un Redirect Uri sous la forme msal[redirect_uri]://auth
Il s’agit ici de renseigner cette valeur dans le fichier Info.plist du projet iOS. Cela se passe dans l’onglet Advanced, rubrique URL Types :
Il est ensuite nécessaire d’enregistrer un Keychain dans Entitlements.plist :
Si cela ne s’est pas fait automatiquement, vérifiez bien que le fichier Entitlements.plist est sélectionné comme Custom Entitlements dans l’onglet iOS Bundle Signing du projet iOS.
Faîtes-le bien pour chacune de vos configurations de Build, sinon vous aurez une exception à l’exécution concernant le Keychain !
using Microsoft.Identity.Client;
[...]
[Register("AppDelegate")]
public partial class AppDelegate : global::Xamarin.Forms.Platform.iOS.FormsApplicationDelegate
{
[...]
public override bool OpenUrl(UIApplication app, NSUrl url, NSDictionary options)
{
AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(url);
return base.OpenUrl(app, url, options);
}
}
Comme pour iOS, le Redirect Uri doit être déclaré dans le manifeste Android.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" android:versionCode="1" android:versionName="1.0" package="com.companyname.adb2capp">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
<application android:label="ADB2CApp.Android" android:theme="@style/MainTheme">
<!-- MSAL REGISTRATION START -->
<activity android:name="microsoft.identity.client.BrowserTabActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="msal[redirect_uri]" android:host="auth" />
</intent-filter>
</activity>
<!-- MSAL REGISTRATION END -->
</application>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
L’authentification MSAL nécessite :
using Microsoft.Identity.Client;
[...]
public class MainActivity : FormsAppCompatActivity
{
protected override void OnCreate(Bundle bundle)
{
TabLayoutResource = Resource.Layout.Tabbar;
ToolbarResource = Resource.Layout.Toolbar;
base.OnCreate(bundle);
Xamarin.Essentials.Platform.Init(this, savedInstanceState);
global::Xamarin.Forms.Forms.Init(this, savedInstanceState);
LoadApplication(new App());
// ADDED
App.UIParent = this;
}
// OVERRIDED
protected override void OnActivityResult(int requestCode, Result resultCode, Intent data)
{
base.OnActivityResult(requestCode, resultCode, data);
AuthenticationContinuationHelper.SetAuthenticationContinuationEventArgs(requestCode, resultCode, data);
}
[...]
}
On va faire super simple, créez une page LoginPage.xaml
, ajoutez-y un bouton Login
et abonnez-le à l’événement Clicked
.
<ContentPage.Content>
<Button x:Name="LoginButton"
Text="LOGIN"
Clicked="LoginButton_Clicked"
VerticalOptions="Center"
HorizontalOptions="Center"
/>
</ContentPage.Content>
Visuellement, difficile de faire plus basique :
A l’apparition de la page, appelez AcquireTokenSilentAsync
pour rafraîchir le token d’authentification, au cas où un utilisateur soit déjà authentifié.
public partial class LoginPage : ContentPage
{
[...]
protected override async void OnAppearing()
{
try
{
// Look for existing account
IEnumerable<IAccount> accounts = await App.AuthenticationClient.GetAccountsAsync();
AuthenticationResult result = await App.AuthenticationClient
.AcquireTokenSilent(Constants.Scopes, accounts.FirstOrDefault())
.ExecuteAsync();
await Navigation.PushAsync(new LogoutPage(result));
}
catch
{
// Do nothing - the user isn't logged in
}
base.OnAppearing();
}
[...]
}
Au clic sur le bouton, AcquireTokenInteractive
est utilisé pour ouvrir le navigateur de l’appareil et afficher la page de Signin.
LogoutPage
.private async void LoginButton_Clicked(object sender, EventArgs e)
{
AuthenticationResult result;
try
{
result = await App.AuthenticationClient
.AcquireTokenInteractive(Constants.Scopes)
.WithPrompt(Prompt.SelectAccount)
.WithParentActivityOrWindow(App.UIParent)
.ExecuteAsync();
await Navigation.PushAsync(new LogoutPage(result));
}
catch (MsalException ex)
{
if (ex.Message != null && ex.Message.Contains("AADB2C90118"))
{
result = await OnForgotPassword();
await Navigation.PushAsync(new LogoutPage(result));
}
else if (ex.ErrorCode != "authentication_canceled")
{
await DisplayAlert("An error has occurred", "Exception message: " + ex.Message, "Dismiss");
}
}
}
Et voici le code pour la réinitialisation du mot de passe :
private async Task<AuthenticationResult> OnForgotPassword()
{
try
{
return await App.AuthenticationClient
.AcquireTokenInteractive(Constants.Scopes)
.WithPrompt(Prompt.SelectAccount)
.WithParentActivityOrWindow(App.UIParent)
.WithB2CAuthority(Constants.AuthorityPasswordReset)
.ExecuteAsync();
}
catch (MsalException)
{
// Do nothing - ErrorCode will be displayed in OnLoginButtonClicked
return null;
}
}
Une fois la page créée, remplacez MainPage
dans App
par une NavigationPage
: MainPage = new NavigationPage(new LoginPage());
de façon à naviguer directement sur la LoginPage
à l’ouverture de l’application.
La page LogoutPage
est très similaire à la page de login à ceci près :
Label
affiche les informations de base de l’utilisateurLogin
est remplacé par un bouton Logout
<ContentPage.Content>
<StackLayout VerticalOptions="Center" HorizontalOptions="Center">
<Label x:Name="UserInfoLabel"
FontSize="Large"
HorizontalTextAlignment="Center"
/>
<Button x:Name="LogoutButton"
Text="LOGOUT"
Clicked="LogoutButton_Clicked"
HorizontalOptions="Center"
VerticalOptions="Center"
/>
</StackLayout>
</ContentPage.Content>
Commençons par récupérer l’AuthenticationResult
dans le constructeur de la page :
AuthenticationResult authenticationResult;
public LogoutPage(AuthenticationResult result)
{
InitializeComponent();
authenticationResult = result;
}
Les informations concernant l’utilisateur, celles que vous avez sélectionnées parmi les Claims
lors de la création de votre User Flow Signin
sont codées dans la propriété authenticationResult.IdToken
sous la forme d’un token jwt.
Pour le décoder, vous aurez besoin d’installer le package nuget System.IdentityModel.Tokens.Jwt
.
Et voici le snippet pour le décoder :
var stream = authenticationResult.IdToken;
var handler = new JwtSecurityTokenHandler();
JwtSecurityToken decodedToken = (JwtSecurityToken)handler.ReadToken(stream);
Vous retrouverez toutes les informations dans une collection d’objet Claim
, notamment au travers les propriétés Type
et Value
:
Exemple, pour afficher le nom et la ville de l’utilisateur :
string name = decodedToken.Claims.FirstOrDefault(c => c.Type == "name")?.Value ?? "Noname";
string city = decodedToken.Claims.FirstOrDefault(c => c.Type == "city")?.Value ?? "Unknown city";
UserInfoLabel.Text = $"Welcome {name}, from {city}";
Le code sera appelé de préférence dans le OnAppearing
de la page plutôt que dans son constructeur.
Et voilà le résultat :
La déconnexion consiste principalement à retirer un compte de AuthenticationClient
:
private async void LogoutButton_Clicked(object sender, EventArgs e)
{
IEnumerable<IAccount> accounts = await App.AuthenticationClient.GetAccountsAsync();
while (accounts.Any())
{
await App.AuthenticationClient.RemoveAsync(accounts.First());
accounts = await App.AuthenticationClient.GetAccountsAsync();
}
await Navigation.PopAsync();
}
Fruit de la collaboration entre l'équipe Xamarin et la communauté Open Source, le Xamarin Community Toolkit est à Xamarin.Forms ce que le sabre laser est au Jedi, c'est-à-dire son indispensable complément.
Si j'en parle aujourd'hui, c'est parce que le toolkit est désormais disponible dans sa version stable mais aussi parce que c'est lui qui recevra toutes les nouveautés concernant Xamarin.Forms, cette plateforme n'étant plus vouée à évoluer après la version 5.0 et jusqu'à son absorption par dotNet MAUI. Mais voyons cela plus en détails.
]]>Fruit de la collaboration entre l'équipe Xamarin et la communauté Open Source, le Xamarin Community Toolkit est à Xamarin.Forms ce que le sabre laser est au Jedi, c'est-à-dire son indispensable complément.
Si j'en parle aujourd'hui, c'est parce que le toolkit est désormais disponible dans sa version stable mais aussi parce que c'est lui qui recevra toutes les nouveautés concernant Xamarin.Forms, cette plateforme n'étant plus vouée à évoluer après la version 5.0 et jusqu'à son absorption par dotNet MAUI. Mais voyons cela plus en détails.
Xamarin.Forms est depuis peu disponible dans sa dernière version, et quand je dis dernière, c'est réellement la dernière. Ensuite, il n'y en aura plus ! Mais n'ayez crainte, il ne s'agit pas d'une disparition mais d'une mutation vers Microsoft MAUI.
Toujours est-il que dans un soucis de qualité, l'équipe Xamarin a décidé que cette dernière mouture serait avant tout axée sur la stabilité et l'amélioration des performances. Le choix a donc été fait de déplacer dans le Community Toolkit les fonctionnalités de Xamarin.Forms qui étaient restée au stade expérimental (comme les contrôles Expander ou MediaElement par exemple) ou dont le développement n'était pas terminé.
C'est donc au Community Toolkit que revient la charge de toutes les nouveautés de Xamarin.Forms !
Vous comprenez donc bien que pour bénéficier des prochaines évolutions de Xamarin.Forms, il faudra vous tourner vers le toolkit !
Mais ce fameux toolkit, que contient-il donc ?
Je ne vais pas vous donner une liste exhaustive du contenu du toolkit, d'abord parce que tout est disponible dans sa documentation et surtout il va continuer d'évoluer après l'écriture de cet article. Mais voici ce que vous y trouverez dans les grandes lignes.
Fonctionnalité peu connue (ou en tout cas trop peu utilisée) de Xamarin.Forms, les Behaviors
sont des bouts de code réutilisables qui ajoutent des comportements à vos composants XAML. Leur puissance est d'être modulable et de permettre d'ajouter ou de modifier des comportements à des contrôles XAML sans en modifier le code ni en créer de nouveaux.
Par exemple, vous trouverez dans le toolkit un EmailValidationBehavior
qui, vous l'aurez compris, ajoute un comportement de validation d'email à vos champs de saisie.
Dans un autre registre, il y a le toujours très utile EventToCommandBehavior
qui permet d'exécuter une commande de votre ViewModel à partir du déclenchement d'un événement du contrôle.
Bref, les Behaviors
sont puissants et très diversifiés, vous en trouverez une grosse douzaine dans le toolkit !
Les Converters
deviennent vite indispensables quand on souhaite suivre proprement l'architecture MVVM. Ils évitent bien des bidouilles inutiles dans vos ViewModels (genre créer artificiellement une propriété qui inverse la valeur booléenne d'une autre, vous voyez ce que je veux dire, hein, inutile de nier !).
Le plus connu de tous est sans doute le Converter
d'inversion de booléen : vous avez une propriété IsBusy
dans votre ViewModel et vous voulez rendre un contrôle visible quand sa valeur est false
. Vous devez donc lier la propriété IsVisible
avec l'inverse de IsBusy
! Vous avez le choix entre faire le cradingue et ajouter une nouvelle propriété IsNotBusy
dans votre code (non mais quelle horreur) ou faire cela proprement avec un Converter.
C'est une vingtaine de Converters
très utiles qui vous attendent dans le toolkit.
AvatarView, BadgeView, CameraView, Expander, MediaElement... une douzaine de contrôles XAML prêts à l'emploi qui vous éviteront d'installer de multiples librairies tierces pas toujours maintenus dans le temps et finissent souvent par poser des problèmes de compatibilité. Ici, pas de soucis, c'est maintenu main dans la main par l'équipe Xamarin et la communauté.
Des helpers MVVM, des Effects
XAML (cousin des Behaviors
mais plus light), des extensions, pour en savoir plus, je vous invite à consulter la documentation et les quelques ressources que je donne juste après, là, maintenant.
J'ai déjà donné le lien plusieurs fois mais si vous l'avez manqué, dirigez-vous vers la documentation officielle du Xamarin Community Toolkit.
Le dépôt GitHub : https://github.com/xamarin/XamarinCommunityToolkit
Le dépôt de l'application d'exemples : https://github.com/xamarin/XamarinCommunityToolkit/tree/main/samples
L'article de blog de Gerald Versluis : Xamarin Community Toolkit: A Must-Have Xamarin Library
Les vidéos de présentation par deux stars du monde Xamarin (Gerald Versluis et Javier Suarez)
]]>
Chercher des ressources concernant Xamarin.Forms est parfois un parcours du combattant. C'est tout éparpillé, il y a du bon, du moins bon, des choses pas franchement à jour.
Et s'il existait un pays merveilleux dans lequel les meilleurs blogs étaient réunis ?
Ce pays, ou plutôt cette planète, existe et s'appelle Planet Xamarin !
Il s'agit d'un agrégateur de blog, s'y abonner, c'est s'abonner au gratin international des blogs Xamarin !
Bon, c'est vrai, j'en fais des caisses, mais c'est aussi parce que depuis peu je fais partie de cette joyeuse communauté. Mon blog est désormais "Featured by Planet Xamarin".
]]>Il est possible de longue date d'utiliser des bibliothèques tierces telles que Skiasharp pour afficher des images vectorielles dans Xamarin.Forms. Mais quand on est soucieux de la légèreté et des performances de son application, il est souvent préférable de se limiter aux fonctionnalités proposées par défaut par notre plateforme de développement.
Depuis l'apparition des Path
avec la version 4.8, Xamarin.Forms est en mesure d'utiliser d'afficher des images vectorielles sans l'aide d'aucun autre package !
Je vous montre comment ça se path... euh... passe ?
Les fonctionnalités utilisées dans cet article nécessitent Xamarin.Forms 5.0 ou au minimum Xamarin.Forms 4.8 avec le flag expérimental : Device.SetFlags(new string[] { "Shapes_Experimental" });
dans le constructeur de la classe App
Alors oui, je vous ai un tout petit peu menti. On ne peut toujours pas incorporer un fichier svg et l'utiliser comme ressource. Mais un fichier svg n'étant que du texte balisé en XML, il est très facile d'en extraire le contenu et d'en faire une forme dans Xamarin.Form.
Nous prendrons deux exemples : un cas simple où l'image est constituée d'un seul chemin et un cas plus complexe où l'image est composée de plusieurs chemins.
Nous verrons ensuite comment rendre une image vectorielle réutilisable facilement dans le code sans duplication à l'aide des Styles
.
Nous allons partir de ce svg très simple représentant un avion :
Si vous ouvrez le fichier avec un éditeur de texte, vous y trouverez ceci :
<?xml version="1.0" encoding="UTF-8"?>
<svg enable-background="new 0 0 64 64" version="1.1" viewBox="0 0 64 64" xmlns="http://www.w3.org/2000/svg">
<path d="M15,6.8182L15,8.5l-6.5-1 l-0.3182,4.7727L11,14v1l-3.5-0.6818L4,15v-1l2.8182-1.7273L6.5,7.5L0,8.5V6.8182L6.5,4.5v-3c0,0,0-1.5,1-1.5s1,1.5,1,1.5v2.8182 L15,6.8182z"/>
</svg>
Ce qui nous intéresse ici, c'est la balise path
et en particulier son attribut d
contenant le tracé du dessin. Nous allons simplement copier le contenu de d
dans la propriété Data
d'un Path
Xamarin.Forms. Comme ceci :
<Path Aspect="Uniform"
Data="M15,6.8182L15,8.5l-6.5-1 l-0.3182,4.7727L11,14v1l-3.5-0.6818L4,15v-1l2.8182-1.7273L6.5,7.5L0,8.5V6.8182L6.5,4.5v-3c0,0,0-1.5,1-1.5s1,1.5,1,1.5v2.8182 L15,6.8182z"
Fill="Black"
HeightRequest="64" />
La propriété Aspect
avec la valeur Uniform
conserve les proportions de l'image. La propriété Fill
indique la couleur de remplissage de la forme.
Sans entrer trop loin dans les détails, il est très facile de manipuler l'image, par exemple en modifier le contour, le remplissage ou lui appliquer des transformations.
La même image vectorielle agrandie x2, pivotée de 45° avec un fond vert et un contour pointillé noir.
<Path Aspect="Uniform"
Data="M15,6.8182L15,8.5l-6.5-1 l-0.3182,4.7727L11,14v1l-3.5-0.6818L4,15v-1l2.8182-1.7273L6.5,7.5L0,8.5V6.8182L6.5,4.5v-3c0,0,0-1.5,1-1.5s1,1.5,1,1.5v2.8182 L15,6.8182z"
Fill="Green"
HeightRequest="64"
Rotation="45"
Stroke="Black"
Scale="2"
StrokeDashArray="2 1"
StrokeThickness="2" />
Le résultat sous Android :
Malheureusement, les images vectorielles ne sont pas toujours aussi simples et sont parfois composées de plusieurs chemins. Par exemple celle-ci :
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<path d="M104,320a24,24,0,1,0,24,24A24.028,24.028,0,0,0,104,320Zm0,32a8,8,0,1,1,8-8A8.009,8.009,0,0,1,104,352Z"/>
<path d="M408,320a24,24,0,1,0,24,24A24.028,24.028,0,0,0,408,320Zm0,32a8,8,0,1,1,8-8A8.009,8.009,0,0,1,408,352Z"/>
<path d="M488,304V256a48.051,48.051,0,0,0-48-48H373.54l-22-58.68A43.052,43.052,0,0,0,310.7,120H160a43.044,43.044,0,0,0-40.72,28.97L90.96,208H72a48.051,48.051,0,0,0-48,48v48A16.021,16.021,0,0,0,8,320v16a16.021,16.021,0,0,0,16,16H48.58a55.994,55.994,0,0,0,110.84,0H352.58a55.994,55.994,0,0,0,110.84,0H488a16.021,16.021,0,0,0,16-16V320A16.021,16.021,0,0,0,488,304ZM356.46,208H256V160h82.46ZM40,264H56v8H40Zm8.58,72H24V320H53.41A55.5,55.5,0,0,0,48.58,336ZM104,384a40,40,0,1,1,40-40A40.04,40.04,0,0,1,104,384Zm248.58-48H159.42a55.5,55.5,0,0,0-4.83-16H357.41A55.5,55.5,0,0,0,352.58,336ZM408,384a40,40,0,1,1,40-40A40.04,40.04,0,0,1,408,384Zm40-80a7.337,7.337,0,0,0-.81.05,55.871,55.871,0,0,0-78.37-.01A7.383,7.383,0,0,0,368,304H144a7.383,7.383,0,0,0-.82.04,55.871,55.871,0,0,0-78.37.01A7.337,7.337,0,0,0,64,304H40V288H64a8,8,0,0,0,8-8V256a8,8,0,0,0-8-8H41.01A32.058,32.058,0,0,1,72,224H96a8,8,0,0,0,7.21-4.54l30.71-64a7.5,7.5,0,0,0,.37-.93A27.079,27.079,0,0,1,160,136H310.7a27.027,27.027,0,0,1,19.22,8H160a8,8,0,0,0-8,8v64a8,8,0,0,0,8,8H440a32.058,32.058,0,0,1,30.99,24H448a8,8,0,0,0-8,8v24a8,8,0,0,0,8,8h24v16ZM240,160v48H168V160ZM472,264v8H456v-8Zm16,72H463.42a55.5,55.5,0,0,0-4.83-16H488Z"/>
<path d="M256,248h40a8,8,0,0,0,0-16H256a8,8,0,0,0,0,16Z"/>
</svg>
Malheur ! l'image contient plusieurs chemins mais notre objet Path
n'a qu'une seule propriété Data
! Comment faire ?
Heureusement, la propriété Data
de notre Path
peut contenir bien plus qu'une simple chaîne de caractère, notamment un groupe de géométrie. Nous allons donc créer un GeometryGroup
contenant des PathGeometry
dont nous renseigneront la propriété Figures
.
Chaque path de notre svg alimente ainsi les propriétés Figures
de nos PathGeometry
:
<Path Aspect="Uniform"
Fill="Black"
HeightRequest="64">
<Path.Data>
<GeometryGroup>
<PathGeometry Figures="M104,320a24,24,0,1,0,24,24A24.028,24.028,0,0,0,104,320Zm0,32a8,8,0,1,1,8-8A8.009,8.009,0,0,1,104,352Z" />
<PathGeometry Figures="M408,320a24,24,0,1,0,24,24A24.028,24.028,0,0,0,408,320Zm0,32a8,8,0,1,1,8-8A8.009,8.009,0,0,1,408,352Z" />
<PathGeometry Figures="M488,304V256a48.051,48.051,0,0,0-48-48H373.54l-22-58.68A43.052,43.052,0,0,0,310.7,120H160a43.044,43.044,0,0,0-40.72,28.97L90.96,208H72a48.051,48.051,0,0,0-48,48v48A16.021,16.021,0,0,0,8,320v16a16.021,16.021,0,0,0,16,16H48.58a55.994,55.994,0,0,0,110.84,0H352.58a55.994,55.994,0,0,0,110.84,0H488a16.021,16.021,0,0,0,16-16V320A16.021,16.021,0,0,0,488,304ZM356.46,208H256V160h82.46ZM40,264H56v8H40Zm8.58,72H24V320H53.41A55.5,55.5,0,0,0,48.58,336ZM104,384a40,40,0,1,1,40-40A40.04,40.04,0,0,1,104,384Zm248.58-48H159.42a55.5,55.5,0,0,0-4.83-16H357.41A55.5,55.5,0,0,0,352.58,336ZM408,384a40,40,0,1,1,40-40A40.04,40.04,0,0,1,408,384Zm40-80a7.337,7.337,0,0,0-.81.05,55.871,55.871,0,0,0-78.37-.01A7.383,7.383,0,0,0,368,304H144a7.383,7.383,0,0,0-.82.04,55.871,55.871,0,0,0-78.37.01A7.337,7.337,0,0,0,64,304H40V288H64a8,8,0,0,0,8-8V256a8,8,0,0,0-8-8H41.01A32.058,32.058,0,0,1,72,224H96a8,8,0,0,0,7.21-4.54l30.71-64a7.5,7.5,0,0,0,.37-.93A27.079,27.079,0,0,1,160,136H310.7a27.027,27.027,0,0,1,19.22,8H160a8,8,0,0,0-8,8v64a8,8,0,0,0,8,8H440a32.058,32.058,0,0,1,30.99,24H448a8,8,0,0,0-8,8v24a8,8,0,0,0,8,8h24v16ZM240,160v48H168V160ZM472,264v8H456v-8Zm16,72H463.42a55.5,55.5,0,0,0-4.83-16H488Z" />
<PathGeometry Figures="M256,248h40a8,8,0,0,0,0-16H256a8,8,0,0,0,0,16Z" />
</GeometryGroup>
</Path.Data>
</Path>
Et hop :
Nous voilà capable d'afficher des images vectorielles simples ou complexes directement dans Xamarin.Forms. Mais pour l'instant, cela reste fastidieux : pour utiliser une même image à plusieurs endroits, il reste nécessaire de copier-coller tout le contenu du Path
à chaque fois.
Voyons comment résoudre ce problème avec Style !
Nous allons créer un ResourceDictionary
pour y placer des Styles
définissant le tracé de chaque image vectorielle.
Toutes nos images SVG auront des propriétés communes, nous allons donc commencer par créer un style de base :
<Style x:Key="BaseSVGStyle" TargetType="Path">
<Setter Property="Aspect" Value="Uniform" />
<Setter Property="Fill" Value="Black" />
</Style>
Nous allons ensuite créer un style par image héritant de BaseSVGStyle
à l'aide de la propriété BasedOn
. Voici un exemple :
<Style x:Key="PlaneSVG"
BasedOn="{StaticResource BaseSVGStyle}"
TargetType="Path">
<Setter Property="Data" Value="m430.37 48.711c29.15-29.153-11.17-67.267-39.96-38.479l-106.3 106.3-243.52-64.372-40.59 40.595 200.71 105.71-81.18 81.17-63.104-7.75-32.106 32.11 71.595 37.64 37.645 71.6 32.1-32.11-7.38-62.74 81.18-81.17 103.43 199.39 40.6-40.6-61.63-238.58 108.51-108.71z" />
<Setter Property="HeightRequest" Value="64" />
</Style>
<Style x:Key="CarSVG"
BasedOn="{StaticResource BaseSVGStyle}"
TargetType="Path">
<Setter Property="HeightRequest" Value="32" />
<Setter Property="Data">
<Setter.Value>
<GeometryGroup>
<PathGeometry Figures="M104,320a24,24,0,1,0,24,24A24.028,24.028,0,0,0,104,320Zm0,32a8,8,0,1,1,8-8A8.009,8.009,0,0,1,104,352Z" />
<PathGeometry Figures="M408,320a24,24,0,1,0,24,24A24.028,24.028,0,0,0,408,320Zm0,32a8,8,0,1,1,8-8A8.009,8.009,0,0,1,408,352Z" />
<PathGeometry Figures="M488,304V256a48.051,48.051,0,0,0-48-48H373.54l-22-58.68A43.052,43.052,0,0,0,310.7,120H160a43.044,43.044,0,0,0-40.72,28.97L90.96,208H72a48.051,48.051,0,0,0-48,48v48A16.021,16.021,0,0,0,8,320v16a16.021,16.021,0,0,0,16,16H48.58a55.994,55.994,0,0,0,110.84,0H352.58a55.994,55.994,0,0,0,110.84,0H488a16.021,16.021,0,0,0,16-16V320A16.021,16.021,0,0,0,488,304ZM356.46,208H256V160h82.46ZM40,264H56v8H40Zm8.58,72H24V320H53.41A55.5,55.5,0,0,0,48.58,336ZM104,384a40,40,0,1,1,40-40A40.04,40.04,0,0,1,104,384Zm248.58-48H159.42a55.5,55.5,0,0,0-4.83-16H357.41A55.5,55.5,0,0,0,352.58,336ZM408,384a40,40,0,1,1,40-40A40.04,40.04,0,0,1,408,384Zm40-80a7.337,7.337,0,0,0-.81.05,55.871,55.871,0,0,0-78.37-.01A7.383,7.383,0,0,0,368,304H144a7.383,7.383,0,0,0-.82.04,55.871,55.871,0,0,0-78.37.01A7.337,7.337,0,0,0,64,304H40V288H64a8,8,0,0,0,8-8V256a8,8,0,0,0-8-8H41.01A32.058,32.058,0,0,1,72,224H96a8,8,0,0,0,7.21-4.54l30.71-64a7.5,7.5,0,0,0,.37-.93A27.079,27.079,0,0,1,160,136H310.7a27.027,27.027,0,0,1,19.22,8H160a8,8,0,0,0-8,8v64a8,8,0,0,0,8,8H440a32.058,32.058,0,0,1,30.99,24H448a8,8,0,0,0-8,8v24a8,8,0,0,0,8,8h24v16ZM240,160v48H168V160ZM472,264v8H456v-8Zm16,72H463.42a55.5,55.5,0,0,0-4.83-16H488Z" />
<PathGeometry Figures="M256,248h40a8,8,0,0,0,0-16H256a8,8,0,0,0,0,16Z" />
</GeometryGroup>
</Setter.Value>
</Setter>
</Style>
Les styles consistent donc principalement à définir la valeur de la propriété Data
de nos Path
.
Pour les utiliser dans nos Views
, nous devons d'abord déclarer le dictionnaire de ressource. Soit dans les ressources de la page, soit dans App.xaml
si l'on souhaite accéder aux images partout dans le projet (PathesStyles est le nom de mon dictionnaire de ressource).
<ContentPage.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<pathes:PathesStyles />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</ContentPage.Resources>
Ensuite, dans la contenu de la page, c'est simple comme Bonjour , on applique le style souhaité à notre path :
<Path Style="{StaticResource CarSVG}" />
Il est toujours possible, bien entendu, de surcharger les valeurs proposées par le style. Pour afficher l'image en rouge par exemple :
<Path Style="{StaticResource CarSVG}" Fill="Red" />
Pour la démo, les valeurs sont en dur dans le code, mais bien entendu toutes les propriétés sont Bindables
pour en modifier les valeurs à l'exécution. Votre créativité fera le reste !
Retrouvez la démonstration de l'utilisation des Path Xamarin.Forms pour afficher des images vectorielles dans mon dépôt GitHub.
Pensez bien à liker et partager pour que ce soit utile au plus grand nombre !
]]>
Vous apprendrez notamment à créer des propriétés bindables (BindableProperty
) et à utiliser les Converters
.
Si ce n'est pas déjà fait, je vous invite à d'abord consulter la première partie de cette article dans laquelle nous avons construit les bases d'une image circulaires avec quelques options (placeholder, bordure...).
Commençons par un petit rappel, voici une démonstration animée de l'image circulaire en situation.
Comme je suis un gars vraiment sympa, pour le même prix je vous ajoute une copie d'écran iOS. La base de code est 100% commune, un seul code pour générer deux applications natives. C'est la beauté de Xamarin.Forms.
On y voit :
La mise-en-page est très simple, un titre, un sous-titre indiquant le nombre de favoris et une liste de contacts.
Par contre, hors de question de reprendre tel quel le XAML de l'image circulaire créée dans l'article précédent et de le coller comme ça, l'air de rien. Hé ! Oh ! On est des vrais professionnels n'est-ce pas ? Nous allons donc en faire un contrôle découplé du reste du code et réutilisable au besoin partout dans le projet.
Souvenez-vous, dans la première partie de l'article nous avions créé une image circulaire à l'aide de la propriété Clip
et d'une EllipseGemotry
. Puis nous l'avions agrémentée de quelques options en y superposant d'autres contrôles dans une Grid
.
Nous avons donc une base avec une image circulaire simple :
<Image Source="monimage.png" WidthRequest="128" HeightRequest="128">
<Image.Clip>
<EllipseGeometry RadiusX="64"
RadiusY="64"
Center="64,64" />
</Image.Clip>
</Image>
Et une image circulaire avancée, composée à partir de la précédente :
<Grid>
<Image Source="monPlaceholder.png" WidthRequest="128" HeightRequest="128">
<Image.Clip>
<EllipseGeometry RadiusX="64"
RadiusY="64"
Center="64,64" />
</Image.Clip>
</Image>
<Image Source="monimage.png" WidthRequest="128" HeightRequest="128">
<Image.Clip>
<EllipseGeometry RadiusX="64"
RadiusY="64"
Center="64,64" />
</Image.Clip>
</Image>
<Ellipse Margin="0"
HorizontalOptions="Center"
VerticalOptions="Center"
Stroke="Yellow"
StrokeThickness="2"
HeightRequest="128"
WidthRequest="128"
/>
<ActivityIndicator IsRunning="{Binding Source={x:Reference monImage}, Path=IsLoading}"
VerticalOptions="Center"
HorizontalOptions="Center"
/>
</Grid>
Nous avons le squelette de notre contrôle mais quelque chose me chiffonne. Ce n'est tout de même pas bien beau cette duplication de code. Et si, au lieu de copier-coller le code de l'image "clippée" avec l'ellipse, on en faisait un contrôle d'image circulaire tout simple mais réutilisable ?
Notre image étant inscrite dans un cercle, cela me gêne de définir HeightRequest
et WidthRequest
. Les deux propriétés auront toujours la même valeur, autant définir une nouvelle propriété ImageSize
.
Quelque chose comme ça :
<Grid>
<local:CircleImage x:Name="monPlaceholder"
Source="monPlaceholder.png"
ImageSize="128" />
<local:CircleImage x:Name="monImage"
Source="monImage.png"
ImageSize="128" />
[...] Le reste du contrôle avec la bordure et l'activityIndicator
</Grid>
Nous allons donc commencer par créer un contrôle CircleImage
qui hérite du contrôle Image de base, en y ajoutant une propriété ImageSize
et un "clipping" circulaire.
Et comme affecter des valeurs en dur ça n'a pas beaucoup d'intérêt dans un vrai projet, nous rendrons la propriété ImageSize
bindable de façon à pouvoir écrire quelque chose comme :
<local:CircleImage x:Name="monPlaceholder"
Source="{Binding MonImage}"
ImageSize="{Binding MyImageSize}" />
La propriété Source
héritant du contrôle Image de base, elle est déjà bindable.
Si ceci vous parait obscur, relisez mon article sur l'architecture MVVM !
Pour faire les choses proprement, créez un dossier CircleImage à la base de votre projet Xamarin.Forms et ajoutez-y un ContentView
nommé CircleImage. Il n'y a pas de mal à être pragmatique sur le nommage. 😁
Par défaut, votre ContentView
est de type... ContentView
. Ça va de soi mais je le précise pour les deux qui dorment au fond vers le radiateur.
Nous pourrions ajouter notre Image
comme contenu du ContentView
, mais pourquoi alourdir le code et l'affichage natif en imbriquant des contrôles alors que nous avons uniquement besoin d'une image ? Nous allons donc remplacer le type ContentView
par le type Image
.
Attention ! Pensez bien à le faire à la fois dans le XAML...
<!-- Remplacer -->
<ContentView x:Class="CircleImageDemo.CircleImage.CircleImage"
<-- Par -->
<Image x:Class="CircleImageDemo.CircleImage.CircleImage"
...et dans le code-behind C# !
// Remplacer
public partial class CircleImage : ContentView { }
// Par
public partial class CircleImage : Image { }
Nous avons presque tout ce qu'il nous faut, il s'agit juste de créer le découpage circulaire dans le XAML et d'ajouter la propriété bindable ImageSize
. Commençons par ceci.
Je suis en train de créer un contrôle visuel et je souhaite exposer une propriété qui soit bindable depuis et/ou vers l'extérieur. Il s'agit principalement de définir une propriété qui encapsule une BindableProperty
au lieu d'un champs privé comme cela se fait habituellement.
Je vous vois froncer les sourcils, alors un exemple :
#region ImageSize Bindable property
// Bindable property
public static readonly BindableProperty ImageSizeProperty =
BindableProperty.Create(
propertyName: nameof(ImageSize),
declaringType: typeof(CircleImage),
returnType: typeof(double),
defaultValue: 0.0,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: (bindable, oldValue, newValue) =>
{ });
// Gets or sets value of this BindableProperty
public double ImageSize
{
get => (double)GetValue(ImageSizeProperty);
set => SetValue(ImageSizeProperty, value);
}
#endregion
Nous avons la propriété ImageSize
qui sera exposée et sur laquelle on viendra "binder" dans le XAML.
La gestion du binding proprement dit est laissée à la BindableProperty
ImageSizeProperty
instanciée par la méthode statique BindableProperty.Create
avec les arguments suivants :
Func
Seuls les trois premiers arguments sont obligatoires.
Tout ceci se place bien entendu dans le code-behind de la View
.
Nous avons désormais une propriété ImageSize
mais comment allons-nous l'utiliser en interne dans notre CircleImage
?
Il y a plusieurs façons de voir les choses. Dans des cas simples qui touchent directement l'aspect visuel, on peut le faire directement dans le XAML. Parfois, c'est plus pertinent d'agir via les callbacks propertyChanged
ou propertyChanging
de la BindableProperty qui offrent plus de souplesse et de liberté que XAML.
Je vais vous présenter les deux mais, bien entendu, dans le projet GitHub je n'ai pu en laisser qu'un !
Partons de notre CircleImage
de base.
<?xml version="1.0" encoding="UTF-8" ?>
<Image x:Class="CircleImageDemo.CircleImage.CircleImage"
xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
HeightRequest="128"
WidthRequest="128">
<Image.Clip>
<EllipseGeometry RadiusX="64"
RadiusY="64"
Center="64,64"
/>
</Image.Clip>
</Image>
L'enjeu est de réussir à lier la propriété ImageSize
aux propriétés HeightRequest
et WidthRequest
de l'image, ainsi qu'aux RadiusX
, RadiusY
et Center
de l'ellipse dont les valeurs sont la moitié de ImageSize
.
Pour cela, nous allons utiliser un Binding. Cela peut paraître un peu confus, il faut suivre un peu : on va binder en interne une valeur qui provient d'un Binding externe. Une fois qu'on a compris ça, on a tout compris.
Sauf que là, comme ça, ça ne fonctionnera pas. Il est nécessaire de préciser à notre View
que son contexte de binding est... elle-même !
Une méthode simple est de donner un nom à notre contrôle, "this" par exemple pour mimer le mot clé "this" du C# et de donner "this" comme source du contexte de binding :
<Image x:Name="this" BindingContext="{x:Reference this}" ...>
On peut désormais binder ImageSize
aux propriétés de l'Image
:
<?xml version="1.0" encoding="UTF-8" ?>
<Image x:Name="this"
[...]
BindingContext="{x:Reference this}"
HeightRequest="{Binding ImageSize}"
WidthRequest="{Binding ImageSize}"
>
[...]
</Image>
Ça y est, nous avons une image carrée de côté ImageSize
.
Vous la voulez circulaire ? Fastoche, il suffirait de binder (ImageSize / 2) sur les propriétés de l'ellipse... si seulement c'était possible !
Il nous faut trouver un moyen de modifier la valeur du binding à la volée... Ça tombe bien, Xamarin.Forms nous propose un outil pour cela : le Converter
.
Je n'entre pas dans les détails car j'ai un article en cours de rédaction sur le sujet, sachez simplement que le principe est simple : il s'agit d'une classe implémentant l'interface IValueConverter
, dont la principale méthode reçoit une valeur en entrée et en retourne une autre en sortie. Exactement ce qu'il nous faut. Nous allons créer un DividerConverter
!
Pour rendre le converter plus versatile, celui-ci expose une propriété Divider
définissant la quantité par laquelle nous souhaitons diviser. Nous aurions pu simplement diviser par deux, en dur dans le code, mais ça limiterait la réutilisation du converter.
La méthode Convert()
divise la valeur reçue de l'extérieur, la méthode ConvertBack()
fait l'inverse dans le cas d'un binding à double sens (si on modifie la valeur au niveau de l'interface utilisateur, elle sera répercutée dans le code, mais bien entendue avec l'opération arithmétique inverse !)
public class DoubleDividerConverter : IValueConverter
{
public int Divider { get; set; } = 1;
// Divise la valeur reçue par Divider
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double doubleToDivide)
{
return doubleToDivide / Divider;
}
throw new ArgumentException($"{nameof(value)} should be of type double");
}
// Multiplie la valeur renvoyée par Divider
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is double doubleToMultiply)
{
return doubleToMultiply * Divider;
}
throw new ArgumentException($"{nameof(value)} should be of type double");
}
}
Reste à utiliser le Converter dans le XAML :
View
EllipseGeometry
son contexte de Binding.<?xml version="1.0" encoding="UTF-8" ?>
<Image x:Name="this"
[...]
>
<Image.Resources>
<ResourceDictionary>
<local:DoubleDividerConverter
x:Key="DoubleDividerConverter"
Divider="2" />
</ResourceDictionary>
</Image.Resources>
<Image.Clip>
<EllipseGeometry BindingContext="{x:Reference this}"
RadiusX="{Binding ImageSize, Converter={StaticResource DoubleDividerConverter}}"
RadiusY="{Binding ImageSize, Converter={StaticResource DoubleDividerConverter}}"
Center="??? Center attend un type Point, je vous laisse réfléchir quelques instants"
/>
</Image.Clip>
</Image>
C'est bon pour la forme de l'ellipse, nous venons d'en faire un cercle de rayon (Imagesize / 2).
Reste à la positionner via la propriété Center
. Celle-ci attend une valeur de type Point
. Nous allons là encore utiliser un converter. Il recevra la taille de l'image en entrée et retournera un point dont les coordonnées sont au centre de l'image.
Vous trouverez tout le code dans le dépôt GitHub.
Si la beauté du XAML vous laisse de marbre, il est possible d'effectuer la même chose directement dans le code-behind de la View.
Il suffit de placer notre code dans l'événement propertyChanged
de la BindableProperty
: nous y définirons le rayon du cercle, la taille de l'image et nous appliquerons l'ellipse à l'image après avoir créé celle-ci.
Et c'est tout !
public static readonly BindableProperty ImageSizeProperty =
BindableProperty.Create(
propertyName: nameof(ImageSize),
declaringType: typeof(CircleImage),
returnType: typeof(double),
defaultValue: 0.0,
defaultBindingMode: BindingMode.OneWay,
propertyChanged: (bindable, oldValue, newValue) =>
{
if (bindable is CircleImage circleImage && newValue is double imageSize)
{
double radius = imageSize / 2;
EllipseGeometry ellipseGeometry = new EllipseGeometry()
{
Center = new Point(radius, radius),
RadiusX = radius,
RadiusY = radius
};
circleImage.HeightRequest = imageSize;
circleImage.WidthRequest = imageSize;
circleImage.Clip = ellipseGeometry;
}
});
Vous allez me dire, ça valait bien la peine de s'embêter en XAML avec des converters ! Ce n'est pas complètement faux. Mais pas totalement vrai.
La qualité d'un code ne se limite pas à sa simplicité mais aussi à sa clarté. Ici, un développeur ne connaissant pas le code devra chercher à l'aveuglette dans le code-behind pour comprendre comment l'image est dimensionnée et devient circulaire. Tant que ça reste simple, ça passe. Dans un code plus touffu, cela peut vite devenir un véritable casse-tête !
Il y a également un risque accru de monstre spaghetti et de crotte de nez dans le code. Un développeur débutant ou peu consciencieux, aura vite fait de mélanger tout et n'importe quoi dans propertyChanged
. Au minimum, il aurait fallu que j'extrais le code dans une méthode à part. Et pour être parfaitement propre, séparer le dimensionnement de l'image du découpage circulaire dans deux méthodes distinctes. Et tant qu'on y est, placer la conversion entre la taille de l'image et l'ellipse dans une classe à part pour que ce soit réutilisable. Une sorte de Converter
qui cacherait son nom, en quelque sorte... Bref, le code-behind est plus simple surtout parce qu'on peut librement y coder comme un sale.
L'avantage de la méthode "XAML", c'est que 100% du code concernant l'aspect visuel est au même endroit, directement là où on irait le chercher spontanément. Tout y est explicite, on sait d'emblée où chercher l'information. Et ça, en débogage, ça n'a pas de prix !
Nous avons désormais une image circulaire simple et réutilisable. Nous allons de même créer une image circulaire encapsulant la précédente pour lui ajouter quelques options.
Pour commencer, nous allons créer comme précédemment un ContentView
que nous nommerons AdvancedCircleImage
et qui héritera de Grid
puisque c'est le Layout
de base de notre contrôle.
Je vous laisse faire, c'est exactement comme tout à l'heure, avec une Grid
à la place de l'Image
.
A l'intérieur de notre Grid, nous aurons donc :
CircleImage
, l'une pour l'image, l'autre pour l'image de substitutionEllipse
pour la bordureActivityIndicator
pour l'indicateur de chargement
Dans le code-behind, nous définirons quelques BindablePropertie
:
Nous allons appliquer la même recette que précédemment, voici une version simplifiée du XAML de laquelle j'ai retiré ce qui n'est pas nécessaire à la compréhension :
<Grid x:Name="this">
<Grid.Resources>
<DoubleDividerConverter x:Key="DoubleDividerConverter" Divider="2" />
<ImageSizeToCenterConverter x:Key="ImageSizeToCenterConverter" />
<ColorToBrushConverter x:Key="ColorToBrushConverter" />
</Grid.Resources>
<local:CircleImage BindingContext="{x:Reference this}"
ImageSize="{Binding ImageSize}"
Source="{Binding PlaceholderImageSource}"/>
<local:CircleImage x:Name="Image"
BindingContext="{x:Reference this}"
ImageSize="{Binding ImageSize}"
Source="{Binding MainImageSource}" />
<Ellipse BindingContext="{x:Reference this}"
Stroke="{Binding BorderColor, Converter={StaticResource ColorToBrushConverter}}"
StrokeThickness="{Binding BorderThickness}"
HeightRequest="{Binding ImageSize}"
WidthRequest="{Binding ImageSize}"
IsVisible="{Binding IsBorderVisible}"/>
<ActivityIndicator IsRunning="{Binding Source={x:Reference Image}, Path=IsLoading}"
BindingContext="{x:Reference this}"
IsVisible="{Binding IsLoaderEnabled}"
Color="{Binding BorderColor}"/>
</Grid>
Vous noterez juste l'apparition d'un nouveau Converter
: notre propriété BorderColor
transmet un type Color
alors que la propriété Stroke
de l'Ellipse attend une Brush
.
Pour le reste, ce n'est que du Binding
vers les BindableProperty
et des astuces que nous avons déjà vu dans la première partie de l'article.
Nous avons désormais une image circulaire avancée parfaitement réutilisable dans notre projet ! Chouette !
Nous en avons même deux : CircleImage
et AdvancedCircleImage
!
En réalité, vous savez déjà comment utiliser votre contrôle personnalisé puisque vous l'avez déjà fait en incorporant CircleImage
dans le XAML de AdvancedCircleImage
.
Il suffit de :
ContentPage
Voici un exemple très simplifié :
<ContentPage x:Class="CircleImageDemo.MainPage"
xmlns:ci="clr-namespace:CircleImageDemo.CircleImage"
[...]>
<ci:AdvancedCircleImage ImageSize="64"
MainImageSource="{Binding PhotoUrl}"
PlaceholderImageSource="{Binding PlaceholderImage}"
BorderColor="Yellow"
BorderThickness="4"
IsBorderVisible="{Binding IsBookmarked}"
IsLoaderEnabled="True" />
</ContentPage>
Dans le projet de démonstration, j'utilise l'AdvancedCircleImage
dans une liste (en fait un BindableLayout mais ce n'est pas le sujet).
Je vous redonne le lien vers le dépôt GitHub du projet : https://github.com/SylvainMoingeon/CircleImageDemo ainsi que la petite animation :
Celle-ci simule une page de contacts qui existe avant tout pour illustrer le contrôle personnalisé que nous venons de créer.
L'AdvancedCircleImage
est utilisée pour afficher les photos de contacts. Si l'image n'est pas renseignée ou indisponible (url invalide par exemple), le contrôle affiche une image de substitution par défaut à la place.
Au clic sur une fiche contact, celui-ci bascule de l'état "mis en favori" à l'état "pas mis en favori", ce qui affiche ou non la bordure.
L'indicateur de chargement est fugace mais visible dans les deux dernières fiches puisqu'une image est définie dans chacune d'elle mais inaccessible (erreur 404 et url non joignable).
C'est tout en ce qui concerne l'image circulaire, mais il y a deux ou trois petites choses dans le code qui pourraient vous intéresser si vous n'êtes pas familier avec XAML et MVVM. Et si vous êtes arrivé jusqu'à la fin de cet article, c'est sans doute le cas !
Pour illustrer l'usage de MVVM, j'ai ajouté un Label
qui indique le nombre de contacts en favori. Celui-ci utilise StringFormat
pour afficher une phrase complète intégrant la valeur obtenue via le Binding
. StringFormat
est un outil très puissant et malheureusement souvent négligé par les développeurs. Il vous évitera pas mal de mauvaises bidouilles et de crottes dans le code.
Il y a quelques cas à la marge mais suffisamment fréquents où malheureusement un code 100% commun n'est plus suffisant. Cela tient surtout à des différences fonctionnelles entre les plateformes.
Par exemple, l'encoche et le bouton virtuel apparus sur iOS à partir de l'iPhone X (il me semble). Lorsque vous définissez votre mise-en-page, vous risquez fort d'obtenir quelque chose comme ceci :
Il existe quelques fonctionnalités spécifiques aux plateformes directement accessibles dans le code commun, notamment pour iOS et Android. Ici, nous allons nous intéresser au SafeArea
d'iOS.
Dans le XAML :
<ContentPage
xmlns:ios="clr-namespace:Xamarin.Forms.PlatformConfiguration.iOSSpecific;assembly=Xamarin.Forms.Core"
ios:Page.UseSafeArea="True"
[...]>
[...]
</ContentPage>
Ce qui corrigera le problème en protégeant les zones de l'encoche et du bouton virtuel pour éviter ces affreux chevauchements :
Fouillez dans la documentation, il y en a d'autres qui vous éviterons prises de tête et tentatives de corriger ces choses là vous même en codant dans les projets natifs.
Si vous utilisez le Previewer Xamarin.Forms, vous constaterez que vos contrôles personnalisés n'apparaissent pas dedans, ce qui peut être gênant lorsque vous êtes en train de dessiner vos interfaces.
La plupart du temps, ça ne tient qu'à une ligne de code ! Un simple attribut à ajouter à votre classe : [DesignTimeVisible(true)]
Comme son nom l'indique de façon assez explicite, cet attribut indique si la classe doit être traitée ou non par le previewer.
[XamlCompilation(XamlCompilationOptions.Compile)]
[DesignTimeVisible(true)]
public partial class AdvancedCircleImage : Grid
{
[...]
}
Pensez bien à l'ajouter à chacun de vos contrôles personnalisés ! Ici au CircleImage et à l'AdvancedCircleImage. Voici le résultat, une fois l'attribut ajouté à chacune des classes :
Pour allez plus loin, il faudrait totalement sortir le contrôle personnalisé du projet et pourquoi pas en faire un package nugget. Peut-être une idée pour un prochain article !
Si vous avez des questions, des astuces ou même des remontrances concernant le sujet, je vous invite à laisser des commentaires. Juste là dessous. ⬇
]]>Xamarin.Forms 5.0 pointe bientôt le bout de son nez et apporte son lot de nouveautés. Alors c'est vrai, pour la plupart il s'agit du passage en version stable de fonctionnalités qui étaient déjà disponibles en version expérimentale, mais c'est l'occasion d'en faire une récap et de prendre conscience des avancées qu'à fait Xamarin.Forms cette dernière année.
Pas d'article fleuve pour cette fois, je me contente de vous présenter quelques nouveautés et de transmettre quelques liens vers les ressources qui vous donneront toutes les informations.
La plupart des informations proviennent de la vidéo que vous trouverez en fin d'article et du post Xamarin.Forms 5 preview de David Ortinau.
L'un des derniers point faible de Xamarin.Forms vient de sauter !
Créer des formes vectorielles, des dégradés ou des chemins complexes ne nécessitent plus aucune bibliothèque tierce. Les éléments visuels peuvent être découpés selon n'importe quelle géométrie ouvrant la porte à des design modernes et bien léchés. Tout cela grâce aux Brush
, Shape
et Path
.
Jusqu'à maintenant, on pouvait créer de nouveaux contrôles soit par composition de contrôles Xamarin.Forms existant soit en créant un nouveau contrôle de toute pièce de façon native, c'est-à-dire en codant à partir de zéro une version par plateforme ciblée.
La nouvelle version du RadioButton
va plus loin et s'approche de ce que WPF propose puisqu'on peut désormais en modifier complètement le contenu à l'aide du templating.
Ça n'a l'air de rien dit comme ça, mais quand ce sera généralisé à tous les contrôles, ceux-ci pourront prendre n'importe quelle apparence sans toucher au code natif, sans recours à des custom renderers et donc sans besoin de créer de nouveaux contrôles. Cela représente donc une énorme partie du code spécifique aux plateformes qui disparaîtra au profit du code commun. Autant de temps de développement, de maintenance et de correction de bug en moins ! C'est un premier (grand) pas vers le Xamarin.Forms 100% cross-plateforme et donc vers MAUI.
D'abord intégré en tant que type de Page puis transformé en Layout expérimental, le CarouselView
atteint enfin l'âge de la maturité et dispose désormais de tout le nécessaire pour en faire un vrai contrôle utilisable en production : support du VisualStateManager
, IndicatorView
(par exemple, les petits points qui indiquent sur quel item on est dans la liste), PeekAreaInsets
pour faire empiéter les items précédents et suivants sur l'item courant...
Je vous renvois à la documentation sur le CarouselView.
Aussi incroyable que cela puisse paraître, Xamarin.Forms ne proposait encore aucune solution pour intégrer un mouvement de Swipe
sur les items d'une liste. On trouvait bien ici ou là quelques implémentations de ce comportement, mais rien d'officiel ou de bien stable. Au point que j'avais fini par créer mon propre composant SwipableView pour Xamarin.Forms !
C'est désormais de l'histoire ancienne, bienvenue au SwipeView
.
Vous trouverez dans Xamarin.Forms 5.0 un nouveau gesture Drag and Drop, principalement dédié au surface duo avec ses deux écrans. Mais bien entendu, rien ne vous empêche de l'utiliser au sein de vos applications.
Plusieurs contrôles ont migré dans le Xamarin Community Toolkit, tels que MediaElement
et Expander
. Ceux-ci n'étaient pas encore prêts à passer en version finale dans Xamarin.Forms mais l'équipe souhaitait les faire sortir du statut expérimental. Ils sont donc disponibles dans le toolkit.
Un changement de version majeure s'accompagne souvent de "breaking changes", ici notamment :
Flyout
.UIWebView
qui était déprécié a été complètement retiréeLe changelog complet de Xamarin.Forms 5.0.
Votre récompense pour être allé au bout de l'article :
Dans cet article "from scratch" en deux parties, je vous présente une manière simple de créer une image circulaire avancée et réutilisable à partir de zéro, sans custom renderer, ni bibliothèque tierce.
Partie 1 :
Clip
et des Geometry
récemment apparues dans Xamarin.FormsGrid
Shape
, autre nouveauté de Xamarin.FormsPartie 2 :
BindableProperty
)Converters
Les présentations sont faites, place au concret.
Certaines fonctionnalités comme les Shapes et leurs Geometries nécessitent Xamarin.Forms 4.8 ou supérieure, pensez bien à mettre-à-jour le package après avoir créé votre solution !
Pas de suspens, je vous livre d'emblée un visuel pour que vous sachiez où je vous amène. Celui-ci simule une page de contact :
Niveau contenu, la page affiche une liste de contacts dont chaque photo provient d'une source différente :
Les trois derniers cas présentent une image de substitution par défaut car aucune photo valide ne peut être affichée.
Les contacts enregistrés comme favoris sont signalés par une bordure jaune autour de la photo.
Pour finir, si vous observez bien l'animation vous remarquerez un bref indicateur de chargement sur les deux dernières photos, le temps que Xamarin.Forms échoue à charger les images.
Mais je vous vois frétiller d'impatience, vous êtes venu ici pour voir du code, alors allons-y
Plutôt que de vous livrer de gros blocs de code illisibles sur une page web, je vais dans cet article aller directement à l'essentiel, vous trouverez le code complet sur mon compte GitHub, comme d'habitude.
Pour commencer, comment afficher une image circulaire ?
Nous allons utiliser deux fonctionnalités apparues dernièrement dans Xamarin.Forms, la propriété Clip (découper en anglais) et les Geometries.
Clip
est une propriété des VisualElements, de type Geometry
et qui définit le contour du contenu d'un élément visuel.
Ce n'est pas clair ? Un exemple, celui qui nous concerne : j'ai une image carrée et je souhaite afficher uniquement son contenu inscrit dans un cercle. Je vais donc définir une Geometry
circulaire et l'appliquer à Clip
pour découper l'image selon cette forme.
Attention les yeux, voici le code :
<Image Source="monimage.png" HeightRequest="200" WidthRequest="200">
<Image.Clip>
<EllipseGeometry RadiusX="100"
RadiusY="100"
Center="100,100" />
</Image.Clip>
</Image>
La géométrie circulaire n'existant pas, nous nous baserons sur une ellipse. Celle-ci se définit à partir d'un centre et de deux rayons, horizontal (RadiusX
) et vertical (RadiusY
). Pour obtenir un cercle, il suffit que RadiusX
et RadiusY
aient la même valeur. Center
permet de positionner l'ellipse par rapport à son contenant.
Pour simplifier le calcul et le positionnement, autant partir d'une image carrée.
Pour obtenir un cercle centré et inscrit dans le carré, les rayons seront moitié plus petits que le côté de l'image et le centre du cercle sera au centre de l'image.
Voilà, obtenir une image circulaire c'est devenu aussi simple que ça avec Xamarin.Forms. Beaucoup de texte et d'explications pour quelque chose devenu finalement assez trivial !
Comment faire pour afficher une image par défaut quand il n'y a pas d'image à afficher ?
On va utiliser une technique ancienne et bien connue mais toujours très utile : l'empilement dans une Grid
. Les éléments contenus dans une même cellule d'une Grid se superposent les uns sur les autres, le dernier déclaré dans le fichier Xaml étant au-dessus, la lecture de celui-ci étant séquentielle.
Nous allons donc définir une Grid à une seule cellule et y ajouter deux Images
: la première est destinée à afficher l'image de substitution, la seconde à afficher l'image proprement dite.
Pour que les deux images se superposent sans décalage, il est nécessaire que les deux composants Image
et leurs ellipses aient les mêmes dimensions.
Nous renseignerons également les propriétés HorizontalOptions
et VerticalOptions
de la Grid avec des valeurs qui forceront celle-ci à adopter les mêmes dimensions que son contenu.
DRY - Je vais avoir besoin de créer deux fois une image circulaire simple, et il est probable que ce besoin se répète dans d'autres contextes. Dans la seconde partie de l'article, nous verrons comment créer un contrôle CircleImage
réutilisable encapsulant le code précédent.
<Grid HorizontalOptions="Center" VerticalOptions="Center">
<local:CircleImage Source="monPlaceholder.png" ImageSize="128" />
<local:CircleImage Source="monImage.png" ImageSize="128" />
</Grid>
De cette manière, l'image vient cacher le placeholder si elle est disponible, sinon on voit ce dernier à la place.
Aujourd'hui c'est atelier empilage, nous allons encore superposer : ajoutons un ActivityIndicator
à notre Grid.
L'ActivityIndicator
a un visuel légèrement différent sous Android et iOS mais le principe reste le même : c'est un bidule qui tourne de façon hypnotique pour faire oublier le temps qui passe à l'utilisateur. Pour le voir tourner, il suffit d'affecter True
à sa propriété IsRunning
.
<Grid>
<local:CircleImage x:Name="monPlaceholder" ImageSize="128" />
<local:CircleImage x:Name="monImage" ImageSize="128" />
<ActivityIndicator IsRunning="True"
VerticalOptions="Center"
HorizontalOptions="Center"
/>
</Grid>
Nous avons donc un indicateur qui tourne au centre de notre image. C'est bien, mais il tourne en permanence et nous aurions besoin de l'activer uniquement pendant le chargement de l'image.
Cela tombe bien, le contrôle Image
expose une propriété IsLoading
qui vaut True
pendant le chargement de l'image et False
une fois le chargement terminé, peu importe que l'opération ce soit bien déroulée ou non.
Reste une question : comment lier la propriété IsRunning
de l'indicateur à la propriété IsLoading
de l'image ? Par un Binding
bien entendu ! Mais comment binder une propriété d'un contrôle à une propriété d'un autre contrôle ? En précisant la source du Binding !
Le plus simple ici est de donner un nom explicite à notre contrôle Image (monImage par exemple) et de se référer à ce nom comme source du Binding. La propriété visée sera définie comme Path
du Binding : "{Binding Source={x:Reference monImage}, Path=IsLoading}"
Avec un peu plus de contexte, cela donne :
<Grid>
<local:CircleImage x:Name="monPlaceholder" ImageSize="128" />
<local:CircleImage x:Name="monImage" ImageSize="128" />
<ActivityIndicator IsRunning="{Binding Source={x:Reference monImage}, Path=IsLoading}"
VerticalOptions="Center"
HorizontalOptions="Center"
/>
</Grid>
De cette façon, nous verrons un indicateur de chargement tournoyer au centre de l'image durant le chargement de celle-ci.
Pour afficher une bordure autour de l'image, nous allons tirer parti des Shapes
encore sous forme expérimentale sous Xamarin.Forms 4.8.
Si la version de Xamarin.Forms que vous utilisez n'intègre pas encore les Shapes
de façon officielle, vous devrez déclarer la fonctionnalité expérimentale dans le constructeur de la classe App.xaml.cs
sous peine de lever une InvalidOperationException
au lancement de l'application :
public App()
{
Device.SetFlags(new string[] { "Shapes_Experimental" });
InitializeComponent();
MainPage = new MainPage();
}
Les Shapes
comme leur nom l'indique sont des formes que l'on peut ajouter en tant que VisualElement
sur la page. La forme qui nous intéresse est là encore une Ellipse
.
Attention à ne pas confondre l'EllipseGeometry
qui est la description d'un objet géométrique, avec l'Ellipse
qui est un contrôle Visuel !
Stroke
et StrokeThickness
sont les deux propriétés exposées par l'Ellipse qui définissent la couleur et l'épaisseur de son contour. Nous n'aurons pas besoin des autres propriétés, mais nous pourrions affiner en définissant le type de contour (pointillé...) ou la couleur de remplissage de l'ellipse.
<Ellipse Stroke="Yellow" StrokeThickness="2" />
Ensuite, c'est juste une histoire d'empilage dans la Grid
, vous commencez à avoir l'habitude.
<Grid>
<local:CircleImage x:Name="monPlaceholder" ImageSize="128" />
<local:CircleImage x:Name="monImage" ImageSize="128" />
<Ellipse Margin="0"
HorizontalOptions="Center"
VerticalOptions="Center"
Stroke="Yellow"
StrokeThickness="2"
HeightRequest="128"
WidthRequest="128"
/>
<ActivityIndicator [...] />
</Grid>
Ce qui donne :
L'article s'avère bien plus détaillé que je ne l'avais prévu au départ et ça commence à être bien consistant. Je vous laisse donc digérer tout ça et vous donne rendez-vous pour la seconde partie dans laquelle nous verrons comment assembler tout ceci pour en faire un contrôle réutilisable avec des propriétés Bindable pour le rendre "MVVM friendly".
]]>Ah, l'inversion de dépendance, voilà une pratique qui est parfois difficile à faire passer dans les équipes. En particulier quand il s'agit d'applications mobiles. Les arguments sont toujours les mêmes :
- Tu peux m'expliquer l'inversion de dépendance ?
- Fastoche, tu ouvres le gestionnaire de package nuget et tu installes AutoFac
Et si on trouvait un moyen simple, efficace et sans framework d'inverser les dépendances ?
]]>Ah, l'inversion de dépendance, voilà une pratique qui est parfois difficile à faire passer dans les équipes. En particulier quand il s'agit d'applications mobiles. Les arguments sont toujours les mêmes :
- Tu peux m'expliquer l'inversion de dépendance ?
- Fastoche, tu ouvres le gestionnaire de package nuget et tu installes AutoFac
Et si on trouvait un moyen simple, efficace et sans framework d'inverser les dépendances ?
Le but ici n'est pas de proposer un cours complet sur les principes d'inversion et d'injection de dépendances mais juste de présenter une astuce rapide, facile et pas chère pour effectuer une inversion de dépendance sans avoir recours à aucun framework.
Si vous ne connaissez pas ces principes, je vous invite à vous rendre sur votre moteur de recherche préféré et d'y saisir les mots clés suivants (pas tous en même temps, hein !) : inversion de dépendance / injection de dépendance / dependency inversion / dependency injection / constructor injection / ioc di.
Vous verrez qu'assez souvent cela vous ramène à l'un ou l'autre des frameworks à la mode. Et dans les cas les plus complexes, c'est pertinent de ne pas réinventer la roue. Mais dans une application mobile, on a souvent besoin de quelque chose de beaucoup plus simple sans s'encombrer d'un framework ou d'une bibliothèque supplémentaire.
Sans m'étendre sur le sujet, le but principal de l'inversion de contrôle est de ne pas laisser une classe dépendre d'autres classes. Elle ne doit dépendre que de leurs abstractions.
L'application de ce principe a des conséquences assez variées mais qui vont toutes dans le sens d'un code plus propre et mieux structuré :
Pour l'astuce que je vais vous présenter très bientôt (je vous tiens en haleine !) nous allons passer les dépendances via le constructeur de la classe.
L'intérêt est double :
Ici, les puristes auront sans doute les poils qui se dressent car ce que je vais vous montrer n'est pas au sens strict de l'injection de dépendance, mais pour les cas simples, ça en présente tous les avantages.
Ce que nous allons chercher à faire c'est avoir pour une classe :
Partons d'une classe ne suivant aucun pattern particulier, la dépendance est directement instanciée dans le corps de la classe. Beurk !
public class MaClasse
{
private readonly MaDepdendance _maDependance;
MaClasse()
{
_maDependance = new MaDependance();
}
}
Imaginez juste un instant que la classe MaDependance
dépende elle-même d'une autre classe, bienvenue au code spaghetti et aux crottes de nez dans le code !
Dans un premier temps, nous allons simplement chercher à passer la dépendance depuis l'extérieur. De cette façon, notre classe n'aura plus la responsabilité de l'instancier. Rien de bien compliqué.
public class MaClasse
{
private readonly MaDepdendance _maDependance;
// dépendance injectée via la constructeur
MaClasse(MaDependance maDependance)
{
_maDependance = maDependance ?? throw new ArgumentNullException("Un message bien senti !");
}
}
Mieux mais pas terrible. Nous n'avons en réalité fait que déplacer le problème. Pour l'instant, toujours pas de découplage ou de modularité dans le code.
Pour faire mieux, nous allons appliquer stricto-sensu le principe d'inversion de contrôle : dépendre d'abstraction au lieu d'implémentation.
public class MaClasse
{
private readonly IMaDependance _maDependance;
// On injecte une abstraction (interface) au lieu d'une implémentation concrète
MaClasse(IMaDependance maDependance)
{
_maDependance = maDependance ?? throw new ArgumentNullException("Un message bien senti !");
}
}
Vous avez vu ? Le "I" pour interface
?
Ce n'est bien entendu qu'une convention de nommage, mais en réalité il s'est passé ceci :
// interface décrivant les membres à implémenter
public interface IMaDependance
{
MaMethode();
}
// MaDependance implémente désormais l'interface IMaDependance
public class MaDependance : IMaDependance
{
public void MaMethode()
{
// implémentation de l'interface
}
}
Au lieu de passer directement l'implémentation concrète de la dépendance, nous avons passé son abstraction. Chouette !
Reste un souci. Sans l'aide d'un framework qui ferait ça dynamiquement, il reste nécessaire de passer explicitement les dépendances au constructeur de notre classe lors de son instanciation.
Double problème :
Pour ajouter une implémentation pour défaut, nous allons simplement tirer parti du mot clé this
appliqué au constructeur de la classe. this
permet, en effet, d'appeler un constructeur à partir d'un autre constructeur.
public class MaClasse
{
private readonly IMaDependance _maDependance;
// Constructeur acceptant un argument
MaClasse(IMaDependance maDependance)
{
_maDependance = maDependance ?? throw new ArgumentNullException("Un message bien senti !");
}
// Constructeur vide, qui par appelle l'autre constucteur avec 'this'
MaClasse() : this(new MaDepdendance()){}
}
De cette manière, le constructeur vide appellera systématiquement le constructeur ayant la dépendance en argument en lui passant une implémentation concrète.
Nous obtenons donc deux manières d'instancier notre classe :
Nous avons donc atteint notre objectif premier avec un simple mot clé this
du langage C#
C'est bien, mais il subsiste un problème : l'implémentation par défaut est instanciée en dur dans le code :
MaDependance
.Trouvons donc un moyen de globaliser la correspondance entre une abstraction et son implémentation par défaut. C'est-à-dire, qu'en pratique, pour une interface donnée on obtienne systématiquement la même implémentation.
Avec Xamarin.Forms nul besoin de chercher très loin, le DependencyService
jouera ce rôle à merveille.
A strictement parler, le DependencyService
sert surtout à la résolution d'implémentations natives (dans les projets iOS, Android...) pour les utiliser dans le projet commun Xamarin.Forms.
En pratique, cela fonctionne très bien pour résoudre n'importe quel type.
Le DependencyService
expose principalement trois méthodes : Register
, Get
et Resolve
.
Register
ne souffre pas d’ambiguïté, c'est ici que nous enregistrerons notre implémentation par défaut.
Pour la simplicité de la démonstration, et parce qu'en pratique ce sera souvent le cas, je prends la classe App.xaml.cs
comme point d'entrée pour enregistrer mes dépendances.
public partial class App : Application
{
public App()
{
InitializeComponent();
// On enregistre le type MaDependance pour l'interface IMaDependance
DependencyService.Register<IMaDependance, MaDependance>();
MainPage = new MainPage();
}
//[...]
}
Faites comme vous voulez, ce qui est important c'est de bien enregistrer les types avant toute utilisation du DependencyService
dans le code, cela va de soi.
Get
et Resolve
semblent fonctionner de manière identique, cependant la documentation affichée dans Visual Studio nuance quelque peu leur usage :
Resolve : The method to use to resolve dependencies by type
Get : Returns the platform-specific implementation of type T
Resolve
semble mieux correspondre à notre besoin.
public class MaClasse
{
private readonly IMaDependance _maDependance;
MaClasse(IMaDependance maDependance)
{
_maDependance = maDependance ?? throw new ArgumentNullException("Un message bien senti !");
}
// On résoud le type avec le DependencyService
MaClasse() : this(DependencyService.Resolve<IMaDependance>()){}
}
Désormais, notre classe ne dépend plus d'aucune implémentation concrète, elle n'est plus qu'abstraction !
Imaginons que de nombreuses classes dépendent de IMaDependance
et qu'on ait besoin de changer l'implémentation de la dépendance partout dans le code, il suffira de remplacer l'implémentation enregistrée dans DependencyService.Register
.
Pour le reste, ça n'a pas changé : pour injecter une implémentation différente il est nécessaire d'instancier la classe en passant explicitement la dépendance dans le constructeur. Ce sera notamment le cas pour les simulacres créés à fin de tests unitaires.
Si l'inversion de contrôle est souvent confondue avec le framework qui la met en oeuvre, il s'agit en réalité d'un principe à l'énoncé plutôt simple : une classe doit dépendre d'abstractions et non d'implémentations.
Et dans les cas simples, on peut l'appliquer sans framework et profiter de ses avantages à moindre frais : découplage, modularité, testabilité.
Je vous en ai présenté ici une façon fort simple à base d'interface
, d'appel à un constructeur par défaut avec le mot clé this
et du DependencyService
de Xamarin.Forms.
Pour un projet mobile, c'est souvent largement suffisant !
Comme toujours, un petit projet d'exemple sur mon GitHub. Celui-ci est minimaliste : un service, un ViewModel et un test unitaire. Son seul intérêt est de démontrer le fonctionnement de tout cela de façon un peu moins théorique.
Et chez vous l'inversion de contrôle, ça se passe comment ?
]]>Je m'en souviens encore, mon premier projet WPF où avec l'habitude des WinForms j'ai commencé à coder la logique métier dans le code-behind de la fenêtre. J'ai alors senti un regard lourd dans mon dos, un ricanement et, tel le Denis Brogniart du code, mon collègue qui me lâche son irrévocable sentence :
Avec WPF, tu dois faire du MVVM. D'ailleurs, à partir d'aujourd'hui, on va tous faire du MVVM.
Et donc, on s'y est tous mis. De façon purement dogmatique, sans rien y comprendre. Ni le pourquoi, ni le comment. Sans connaître ses véritables raisons d'être, sans en avoir étudié les avantages et contraintes. Sans même avoir conscience qu'il s'agissait d'une architecture logicielle et donc de la fondation de notre applicatif.
Alors donc, ce fût un grand moment de n'importe quoi où chacun cherchait par tous les moyens à contourner les problèmes posés par MVVM. Et quand tu commences à considérer ton architecture comme un problème, c'est qu'il y en a un sérieux dans l'équipe de développement. Bien entendu, les ressources sur le sujet étaient ténues à l'époque et consistaient principalement à la résolution des problèmes apportés par MVVM. Oui, on était tous dans le même bateau.
Heureusement, une dizaine d'années plus tard tout cela a bien changé et... hein ? Quoi ? C'est toujours comme ça ? Des équipes qui font du MVVM sans réfléchir, sans le comprendre, juste parce qu'on leur a dit c'est comme ça.
Il est peut-être temps de prendre un peu de recul et de comprendre à quoi ça sert.
]]>Je m'en souviens encore, mon premier projet WPF où avec l'habitude des WinForms j'ai commencé à coder la logique métier dans le code-behind de la fenêtre. J'ai alors senti un regard lourd dans mon dos, un ricanement et, tel le Denis Brogniart du code, mon collègue qui me lâche son irrévocable sentence :
Avec WPF, tu dois faire du MVVM. D'ailleurs, à partir d'aujourd'hui, on va tous faire du MVVM.
Et donc, on s'y est tous mis. De façon purement dogmatique, sans rien y comprendre. Ni le pourquoi, ni le comment. Sans connaître ses véritables raisons d'être, sans en avoir étudié les avantages et contraintes. Sans même avoir conscience qu'il s'agissait d'une architecture logicielle et donc de la fondation de notre applicatif.
Alors donc, ce fût un grand moment de n'importe quoi où chacun cherchait par tous les moyens à contourner les problèmes posés par MVVM. Et quand tu commences à considérer ton architecture comme un problème, c'est qu'il y en a un sérieux dans l'équipe de développement. Bien entendu, les ressources sur le sujet étaient ténues à l'époque et consistaient principalement à la résolution des problèmes apportés par MVVM. Oui, on était tous dans le même bateau.
Heureusement, une dizaine d'années plus tard tout cela a bien changé et... hein ? Quoi ? C'est toujours comme ça ? Des équipes qui font du MVVM sans réfléchir, sans le comprendre, juste parce qu'on leur a dit c'est comme ça.
Il est peut-être temps de prendre un peu de recul et de comprendre à quoi ça sert.
Sans refaire un historique complet des technologies Microsoft, sachez qu'avec .NET 3.0 (sorti en 2007 il me semble) est apparu un nouveau paradigme de programmation, en particulier au niveau graphique. Exit les vieilles fenêtres grises figées sur lesquelles on n'avait aucune liberté visuelle et dans lesquelles on mélangeait allègrement l'interface utilisateur et le code métier. Bienvenue à la conception graphique via la technologie XAML. Le développeur s'est alors retrouvé devant deux langages distincts pour traiter l'affichage ou le code :
Voici un exemple de page Xamarin.Forms décrite en XAML :
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.MainPage">
<StackLayout>
<!-- Place new controls here -->
<Label Text="Welcome to Xamarin Forms!"
VerticalOptions="Center"
HorizontalOptions="Center" />
</StackLayout>
</ContentPage>
Bien entendu, il a fallu d'une façon ou d'une autre faire communiquer code et affichage. C'est pourquoi le concept de DataBinding est devenu central avec XAML : les différentes propriétés des contrôles visuels (texte, couleur, visibilité, switch...) sont liées à des propriétés dans le code et reflètent leurs valeurs et changement de valeurs de façon autonome et automatique.
Par exemple, une liste sera affichée en étant "bindée" à une collection dans le code. Une case à cocher sera "bindée" à une propriété booléenne...
Ici, même exemple que précédemment mais le texte du Label
est "bindé" à la propriété RandomQuote
d'une classe. On peut imaginer, par exemple, que RandomQuote
est une citation célèbre extraite aléatoirement d'une liste.
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:XamlSamples"
x:Class="XamlSamples.MainPage">
<StackLayout>
<!-- Place new controls here -->
<Label Text="{Binding RandomQuote}"
VerticalOptions="Center"
HorizontalOptions="Center" />
</StackLayout>
</ContentPage>
Mais qui est chargé d'exposer la propriété RandomQuote
? Comment les changements de valeurs de cette propriété seront propagés jusqu'au XAML ? Et inversement si la valeur est modifiée par l'utilisateur au niveau de l'interface, comment l'information est remontée dans la classe d'origine ?
C'est pour répondre à ces questions et architecturer tout ceci que Microsoft a proposé un nouveau modèle (repris depuis un peu partout) : MVVM, c'est-à-dire Model-View-ViewModel.
Ce qu’il faut en retenir c’est que cette architecture a pour but de découpler les données (Model), la logique (ViewModel) et l’interface utilisateur (View).
La View est décrite en XAML de façon purement déclarative. Elle dispose tout de même d'un code-behind dans lequel on peut coder en C#, mais cela doit se limiter à du code lié à l'affichage (déclencher une animation par exemple).
Le Model est la représentation d'une donnée, au sens métier du terme.
Le ViewModel est la couche intermédiaire qui prépare les données et les expose à la View. C'est généralement là qu'on trouvera la logique de validation des données, l'exécution des calculs, l'appel aux différents services...
Il y a donc un découpage assez net et précis entre ce qui concerne l'affichage, ce qui va stocker les données et ce qui va préparer les données.
C'est là tout l'enjeu de MVVM, et pour que ça ne parte pas en cacahuètes, il y a bien entendu un certain nombre de règles à respecter.
MVVM, c'est comme un mogwai, ça a l'air tout mignon tant qu'on suit scrupuleusement les règles.
Ok, je vous parle depuis tout à l'heure de View, de Model et de ViewModel, mais tout cela reste bien abstrait, voyons donc ça plus en détails.
La View est un élément visuel, une page par exemple, qui définit la mise-en-page et l'apparence, mais également l'interaction utilisateur (boite de dialogue...).
La View référence un ViewModel via sa propriété BindingContext
. Les contrôles sur la View héritent du BindingContext de la View et sont liés à des propriétés et des commandes exposée par le ViewModel via le système de DataBinding
.
Un DataBinding peut être personnalisé de diverses manières (validation, converters, changement de BindingContext...) mais nous y reviendrons en détail dans d'autres articles.
Toute la logique d'affichage qui ne peut pas être directement traitée en XAML est codée dans le code-behind de la page.
La View définit le comportement visuel et réagit aux changements d'états du ViewModel.
Le ViewModel est une classe non visuelle et qui ne référence en aucune façon ni la View ni aucun élément relatif à l'affichage. Il encapsule la logique de présentation mais pas son affichage. J'insiste car c'est une règle trop souvent bafouée et qui mène irrémédiablement au monstre spaghetti.
Le ViewModel expose des propriétés et des commandes auxquelles la View se lie par DataBinding : il notifie la View de ses changements d'états à travers l'interface INotifyPropertyChanged
.
En pratique, on crée souvent une classe de base qui implémente INotifyPropertyChanged
et dont tous les ViewModels vont hériter. C'est plus simple comme ça.
Les listes d'objets sont généralement exposées sous forme d'ObservableCollections
qui implémentent INotifyCollectionChanged
pour informer la View de la suppression ou de l'ajout d'éléments dans la liste.
Le cas échéant, c'est le ViewModel qui contient la logique propre à l'application (mais pas la logique métier, en principe).
Le ViewModel se charge de préparer et de mettre en forme les données pour la View.
Le Model encapsule les données, la logique métier et la validation, même si c'est sujet à discussion. En pratique, la validation est parfois traitée directement par le ViewModel.
Les Models ne référencent jamais ni la View, ni le ViewModel. Ce sont des entités indépendantes.
Ils implémentent la plupart du temps INotifyPropertyChanged
.
En fait, il y a deux écoles :
INotifyPropertyChanged
INotifyPropertyChanged
Qui a raison, qui a tort ? J'ai envie de dire, ça dépend du projet et du modèle de données. A titre personnel, je préfère encapsuler les propriétés dans le ViewModel, c'est bien plus propre : la View ne voit que ce dont elle a besoin.
Pour résumer, le ViewModel prépare et expose des données pour que la View les affiche à l’écran. Mais le ViewModel ne sait pas ce qu’en fait la View, c’est à la View de se débrouiller avec ce que le ViewModel lui fournit.
Les Models sont indépendants à la fois de la View et du ViewModel. Ce sont eux qui contiennent la logique métier, bien que ce sujet soit ouvert à discussion (la doc Microsoft n'est pas toujours en accord avec elle-même à ce propos !).
Le but de MVVM est de découpler au maximum l'affichage de la logique et la logique des données. Mais pourquoi me direz-vous ?
Pour résumer, MVVM c'est une place pour chaque chose et chaque chose à sa place.
L'idéal est que chaque Model n'ait aucune dépendance vis-à-vis des ViewModels et des Views et que chaque ViewModels n'ait aucune dépendance vis-à-vis des Views.
Alors, MVVM, c'est tout beau tout rose ?
Evidemment, raconté comme ça, ça a l'air d'être la panacée. En réalité, MVVM est une architecture logicielle et vient donc avec un certain nombre de règles et de contraintes.
C'est comme le code de la route. Il est là pour garantir la sécurité de tous et éviter les accidents, mais cela ne vient pas sans quelques restrictions à nos libertés (s'arrêter au rouge, ne pas rouler à contresens...).
La principale question qui se pose quand on se lance pour la première fois dans un projet architecturé autour de MVVM, c'est comment gérer les interactions utilisateurs dans le ViewModel.
Je vous donne un exemple, sans doute le premier auquel vous serez confronté : je clique sur un bouton SUPPRIMER
, une boite de dialogue me demande de confirmer avant suppression. Simple n'est-ce pas ? Sauf que le code de suppression est dans le ViewModel, que la commande liée au bouton s'y exécute directement et donc, comment fait-on pour afficher une boite de dialogue quand on est dans un ViewModel qui ne doit rien savoir de l'affichage ?
Eh bien, ce n'est pas simple !
Et là, deux écoles s'affrontent :
Un exemple très simple, rencontré personnellement sur un projet :
Une page de l'application freezait de façon inexplicable. Je n'ai pas mis longtemps à lever le lièvre : la View était injectée dans le ViewModel !
Cette violation directe de MVVM a conduit à un effet extrêmement pervers : un deadlock. En fait, le ViewModel exécutait une méthode déclarée dans le code-behind de la View qui elle-même exécutait une méthode du ViewModel qui attendait un résultat de la View ! Et patatra ! Les pieds dans le tapis !
Cela a été corrigé en redécoupant proprement le code. Certes, cela a demandé plus de réflexion et mis plus de temps que le code départ qui était plus simple et plus rapide à mettre en place. Mais maintenant ça fonctionne et surtout ça continue de fonctionner malgré les évolutions de l'application ! Au final, prendre son temps fait gagner du temps.
En fait, le véritable défi avec MVVM est d'assurer la communication entre les différentes couches tout en prenant soin qu'elles ne se référencent pas les unes et les autres.
Il y a encore beaucoup à dire sur MVVM mais cet article est déjà bien trop long et je souhaitais rester à un certain niveau d'abstraction, n'aborder que les concepts de base.
J'espère que vous comprenez mieux les enjeux de MVVM, ses raisons d'être.
Si vous avez des questions ou si vous n'êtes pas d'accord (vous avez le droit !) laissez-moi un commentaire !
Je voudrais juste terminer en précisant que MVVM est un choix d'architecture, pas quelque chose qu'on vous impose et qu'il faut contourner !
Si MVVM ne vous convient pas, il existe des alternatives. Plutôt que massacrer MVVM, trouvez l'architecture qui convient à votre projet !
Récemment, j’ai entendu un développeur tenir les propos suivants :
Non mais ça ne sert à rien de faire du bon code, tout ce que le client demande c’est que ça fonctionne.
Et malheureusement, sur la deuxième partie de l’assertion il n’a pas tort. Mais c’est parce que le client ne connait pas le métier et n’a aucune idée des répercussions sur le moyen/long terme d’un code de mauvaise qualité. Alors il faut que ça cesse !
Et si au lieu de profiter de la situation pour justifier du travail de sagouin, on en profitait plutôt pour prendre ses responsabilités ?
]]>Récemment, j’ai entendu un développeur tenir les propos suivants :
Non mais ça ne sert à rien de faire du bon code, tout ce que le client demande c’est que ça fonctionne.
Et malheureusement, sur la deuxième partie de l’assertion il n’a pas tort. Mais c’est parce que le client ne connait pas le métier et n’a aucune idée des répercussions sur le moyen/long terme d’un code de mauvaise qualité. Alors il faut que ça cesse !
Et si au lieu de profiter de la situation pour justifier du travail de sagouin, on en profitait plutôt pour prendre ses responsabilités ?
Laissez-moi vous raconter une histoire :
Vous vous rendez à la boulangerie, ça sent bon le pain chaud, la baguette est bien dorée, croustillante à l’extérieur, moelleuse à l’intérieur, une véritable baguette 100% fonctionnelle. Vous en salivez déjà.
Et maintenant, si je vous raconte que j’ai surpris le boulanger la main dans le froc en train de se gratter l’anus pendant qu’il pétrissait la pâte. Toujours envie de vous en faire des tartines ?
Livrer une baguette seulement fonctionnelle est-ce vraiment tout ce qu’on demande à un artisan boulanger ? Non, on exige de lui qu’il prépare son pain dans les règles de l’art, en respectant les bonnes pratiques de son métier et en suivant scrupuleusement les règles sanitaires.
Si je vous raconte qu’en cette période de crise covid19, votre boulanger travaille sans porter de masque et en éternuant allégrement sur la pâte des croissants qu’il est en train de préparer. Vous voyez la scène ? Un gros ATCHOUM accompagné d’un feu d’artifice de postillons et de sécrétions nasales qui viennent se déposer gaiement un peu partout dans la pâte. Mmmm, un délice. Vous en pensez quoi ? Sérieusement ? C’est professionnel comme comportement ?
Ça vous dit un croissant totalement fonctionnel aromatisé à la crotte de nez ?
Bien entendu, vous avez parfaitement le droit de répondre oui et de trouver que c’est normal de travailler comme ça tout en prétendant être un professionnel du métier. Mais je n’irai pas manger chez vous ni vous recommander auprès de mes clients !
Parce qu’un produit fonctionnel, c’est juste le B-A-BA, c’est le niveau zéro du métier. C’est ce qu’on demande de produire à un étudiant pour son projet de première année. Juste un truc qui marche. Mais être professionnel, c’est un peu plus que ça non ? Non !?
Bien entendu, je ne prétends pas que tout est toujours parfait, peut-être le boulanger va-t-il y laisser un poil de bras ou une goutte de sueur. On ne peut pas tout prévoir, on n’est pas toujours au top. Parfois aussi on se trompe, c’est humain. Je vous le dis discrètement, ça reste entre nous, mais on n’a pas non plus la science infuse. Parfois on ne sait pas bien faire et on aurait pu faire mieux.
Alors quoi ?
Alors, on prend soin de respecter au mieux les bonnes pratiques, on se forme, on s’exerce, on ne se cache pas derrière un de toute façon le client attend juste que ça fonctionne. On se comporte comme un véritable artisan du code.
C’est-à-dire que s’il y a des bugs, c’est accidentel. Si le code lève une exception, c’est parce qu’on a rencontré un cas exceptionnel (le terme d’exception n’a pas été choisi par hasard).
Un bug issu de l’erreur humaine est acceptable. Un bug issu de non mais ce n’est pas la peine de coder proprement il suffit juste que ça fonctionne est juste inacceptable. Vous saisissez la nuance ?
Autrement dit, on évite autant que possible d’éternuer sur le clavier et de laisser des crottes de nez partout dans le code.
Je pourrais écrire une thèse sur le sujet mais, que vous soyez développeur ou que vous ayez un projet de développement, je vous recommande plutôt la lecture du fantastique The Pragmatic Programmer, from journeyman to master de Andrew Hunt et David Thomas.
Ce livre qui date de plus d’une vingtaine d’années reste complètement d’actualité tant les thèmes qu’il aborde sont tranverses au développement informatique et se basent avant tout sur les bonnes pratiques et surtout les risques de ne pas les suivre.
Vous y apprendrez par exemple pourquoi en vertu de l’hypothèse de la vitre brisée, si un développeur laisse traîner quelques crottes de nez par-ci par-là, le code finira irrémédiablement recouvert de sécrétions nasales.
Et en informatique, une crotte de nez peut avoir des répercussions graves.
En fait, une crotte de nez dans le code, c’est ce truc-là, qu’on ne voit pas de l’extérieur, qui ne semble pas influer sur le fonctionnel du point de vue de l’utilisateur mais qui, dans certains cas, laisse un arrière-goût plutôt désagréable.
Comme oublier de crypter des mots de passe. Je veux dire, au niveau du code, ce n’est pas grand-chose, hein (ah si ?). On oublie juste de passer le mot de passe dans la moulinette, rien de très grave en soit, personne ne s’en rend compte, tout est parfaitement fonctionnel. Et puis, un beau jour, c’est le scandale…
J’ai une autre anecdote sympa, c’est cocasse vous allez voir. J’ai assisté à une scène surréaliste ou un développeur a voulu montrer à un opérateur comment supprimer des données dans la base clients. Jusque-là, rien de dramatique, on manipule des données, on les insère, on les modifie, on les supprime, c’est la raison d’être des bases de données.
Sauf qu’il a, par mégarde, sélectionné l’ensemble des données d’une table. Et que cette table avait une relation directe ou indirecte avec l’ensemble de toutes les autres tables de la base de données.
Vous voyez le coup venir ? Une crise d’éternuement en approche !
Dans le meilleur des mondes, il aurait dû y avoir une boite de dialogue de confirmation de suppression au niveau applicatif. Il aurait dû y avoir un contrôle au niveau de la base de données, en particulier sur une table critique comme celle-ci.
Mais voilà, la boite de dialogue est restée engluée dans une flaque de morve et la base de donnée a supprimé toutes les données en cascade sans broncher.
Quand le développeur a demandé, l’espoir dans l’âme, Ce n’est rien, nous allons restaurer les données, vous avez bien une sauvegarde récente ?, l’opérateur a constaté que les scripts de sauvegarde ne tournaient plus depuis des semaines, sans envoyer les messages d’avertissement prévus dans ce cas. La faute à une crotte de nez. Encore.
Être professionnel, ce n’est pas seulement produire du code fonctionnel, c’est aussi garantir une qualité, une pérennité, une évolutivité du code. C’est-à-dire un code qui fonctionne aujourd’hui et continuera de fonctionner demain, même après modification.
Je pourrais également parler de stabilité (ça fonctionne mais c’est un peu bancal), de fiabilité (ça fonctionne très bien… quand ça fonctionne), ou de performance (ça fonctionne mais il faut être patient, très patient).
Ou encore, insister sur la lisibilité d’un code que d’autres développeurs n’auront pas de mal à lire et à comprendre. D’un code qui coule de source (ah ah ah) dans lequel les autres n’auront pas peur de mettre les mains. J’ai failli dire les doigts… dans le nez, bien entendu.
Code toujours comme si la personne qui va maintenir ton code est un violent psychopathe qui sait où tu habites.
John F. Woods
Alors évidemment, comme je l’ai dit, on ne pas toujours être au top, on va parfois laisser quelques gouttes de sueurs perler dans la farine, le pain sera un peu moins cuit ou la pâte moins croustillante que d’habitude. C’est comme ça, c’est la vie.
Mais s’il vous plait, cessez de vous gratter le cul ou de vous curer le nez en bossant sous prétexte que ça ne va pas se voir !
]]>