Dot.Blog

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

UWP : Personnaliser une Vue selon la famille de devices

[new:15/10/2015]UWP c’est un seul code C# et un seul code XAML pour toutes les familles de devices supportées. Mais dans certains cas il faut pouvoir personnaliser l’UI pour l’adapter plus en profondeur. UWP le permet de façon simple…

Famille de devices

Le lecteur m’excusera d’utiliser “devices” au lieu “d’unités” car hélas ce terme français est trop flou dans le titre d’un article. Device on voit ce que c’est, une machine, une unité mobile ou non. Mais pour les puristes de la langue française disons qu’il s’agit bien de “machines” pour faire simple.

UWP est fait pour tourner sur plein de form factors, mais ces derniers sont regroupés logiquement en familles. Vous n’êtes pas obligé de cibler toutes les machines possibles mais vous ne pouvez pas cibler une seule machine particulière, vous ne pouvez cibler au minimum qu’une famille de machines.

Ces familles sont évidentes : Desktop pour tout ce qui ressemble à un PC, Mobile pour tout ce qui est de l’ordre de la tablette, phablette et smartphone, IOT pour les objets connectés, etc.

Dans un développement “classique” on ciblera soit la famille Mobile, soit la famille Desktop ou plus intelligemment (si cela a un sens) les deux à la fois. Rajouter le support de la Xbox, des Hololens ou des IOT est une autre affaire. Par exemple ces derniers n’ont généralement pas d’UI… Soit aujourd’hui je suis en manque d’imagination soit en effet il y a un truc qui ne collerait pas à avoir une belle app de bureau qui pourrait aussi tourner sur un frigo connecté sans UI … Donc je suppose que lorsqu’il faudra cibler IOT (bracelets connectés, arduino…) le programme ne ciblera que cette famille là.

Pour tous le reste écrire un seul code C# et un seul code XAML pour toutes les familles est le gros intérêt de UWP.

Personnaliser les UI par famille

Même si je n’ai pas encore explicitement fait d’article sur la question j’en ai fait sur le Reactive Design qui explique comment on doit concevoir son code XAML de telle façon à ce qu’il s’adapte aux différents form factors sur lesquels il va tourner. Je reviendrai sur des exemples concrets dans d’autres articles pour parler par exemple du nouveau RelativePanel qui autorise le placement de ses enfants de façon relative (à droite de, en dessous de, au dessus de, etc) ce qui autorise une mise en page automatique qui garde son sens.

Mais malgré toutes les astuces de Design, malgré la parfaite portabilité du jeu de contrôle UWP, malgré l’utilisation de Pixels Effectifs qui tiennent compte de la résolution et de la distance de vision des  objets, malgré tout cela on peut se retrouver dans des cas où une mise en page distincte pour une famille donnée reste la solution la plus propre. Cela évite de créer du XAML spaghetti difficile à maintenir lorsqu’on utilise trop d’astuces au sein d’une même page.

Toucherait-on ici aux limites de UWP ?

Heureusement non !

D’autres mécanismes de personnalisation ont été mis en place dans UWP pour vous permettre de pousser la personnalisation le plus loin possible sans toutefois ne jamais remettre en question le principe “d’un code, toutes les cibles”.

Projet de test

Pour vous montrer tout cela je partirai d’une application UWP de type “blank” (vide) dans laquelle j’ai placé une grille bleue avec un text en 72 centré indiquant qu’on se trouve sur une appli desktop ce que nous considèrerons comme étant le mode “par défaut” de notre application. Ce qui donne un visuel de ce type pour le moment en design :

image

Le XAML de cette page est à rougir de honte tellement il est basique :

<Page
    x:Class="CustomizedViewDemo.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CustomizedViewDemo"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d" Height="664.227" Width="878.864">

    <Grid Background="Blue">
        <TextBlock Foreground="White" Text="App pour Bureau (desktop)" FontSize="72"
                   VerticalAlignment="Center" HorizontalAlignment="Center" />
    </Grid>
</Page>

 

Donc rien de bien exotique pour l’instant mais c’est une bonne base !

Si j’exécute le programme en mode machine locale j’obtiens la sortie suivante :

image

et si j’exécute en mode Windows Phone 10 5” j’obtiens en toute logique l’horreur suivante :

image

On le voit bien, mais c’était à prévoir, ma mise en page pour appli desktop ne passe pas vraiment sur un smartphone… Mon code oui, mais ma mise en page non…

Il va donc falloir remédier à ce petit problème par quelques adaptations automatiques (puisque au final je n’aurai qu’un seul exe rappelons-le !).

Les sous-répertoires Familles

Une fois qu’on a éliminé les astuces XAML permettant de gérer le redimensionnement des éléments, qu’on a épuisé les placements un peu subtiles avec le RelativePanel il faut bien passer à la solution radicale : avoir une UI totalement différentes et adaptée à la cible. Mais toujours en gardant un seul code, un seul projet, pas un pataquès de projets distincts (comme hélas les “universal apps” de Windows 8 l’obligeaient juste pour deux cibles).

La première façon radicale de régler le problème fonctionne selon un principe déjà utilisé part d’autres OS : avoir des noms spéciaux qui permettent à l’OS de charger les bonnes ressources au runtime. Sous Android c’est un foutoire pas possible entre les résolutions, les tailles, les capacités techniques, etc, ça devient très vite un enfer.

Microsoft a choisi de regrouper les machines par familles. Une même famille est présumée faire des choses similaires, ce qui n’interdit donc pas de s’adapter aux petites différences dans une famille données, mais cela marque une différence assez marquée pour justifier de proposer des design un peu différents. Par exemple sur smartphone une appli ne proposera un affichage de PC. Il ne s’agit plus de seulement adapter la taille d’une grille, de jouer le positionnement des objets, non, il faut vraiment un design particulier pour avoir au final une super app PC et une super app smartphone, avec le même code en revanche.

Là où d’autres ont choisi un enfer de règles de nommage et de combinatoires explosives, Microsoft a donc eu l’intelligence de proposer uniquement une variante par famille. Il faut dire que les autres OS fonctionnent de façon tellement préhistoriques que même un bouton doit être prévu dans toutes les tailles et résolutions possibles alors que forcément, avec le vectoriel de XAML ça élimine beaucoup de soucis… Le vectoriel est forcément l’avenir du multi form factors, le collage d’images et autres nine-patches sont une hérésie au XXIeme siècle.

Bref le problème n’est pas la supériorité technologique des produits Microsoft, nous nous le savons, c’est d’en faire un succès auprès du public et des entreprises. Mais c’est un autre débat.

Comme nous l’avons vu plus haut notre projet est bien sympathique mais la sortie sur smartphone est une catastrophe. Nous avons donc décidé de fournir un design totalement spécifique pour les smartphones.

Pour le faire nous allons tout simplement créer un sous’-répertoire dans notre projet que nous allons appeler DeviceFamilly-Mobile. C’est simple à se rappeler. On voudrait un truc spécial pour le bureau, on créerait un DeviceFamilly-Desktop. Idem pour la Xbox, les hololens et le reste.

A l’intérieur de ce répertoire nous allons ajouter un fichier XAML, mais juste un fichier XAML par une page avec code-behind, et nous allons aussi l’appeler de la même façon que la Vue que nous voulons personnaliser.

Pour créer le fichier XAML seul il faut faire ajouter / nouvel item / Visual C# / Xaml View :

image

 

Pour nous aider dans la mise en page n’oubliez pas que Visual Studio propose un échantillon de plusieurs type de surface de Design. Dans le coin haut gauche on trouve un dropdown qui permet de choisir taille et résolution. Cela ne modifie pas votre code XAML mais l’affiche dans le contexte visuel que vous avez sélectionné. C’est hyper pratique ! Ici je vais choisir un affichage pour smartphone 5” :

image

En A on choisit l’orientation (portrait / paysage), en B la taille de la cible (résolution / taille) le résultat est une mise à la taille du code XAML de la page affichée, sans modification, et on peut donc se mettre à travailler en confiance sur ce que verra réellement l’utilisateur (ici un smartphone 5” en mode portrait).

Pour le code XAML je ne fais que recopier celui de la MainPage dont je change les couleurs (fond de page et texte) ainsi bien entendu que la taille de la police qui était bien trop grande pour un si petit écran (et je la place en bold afin d’avoir une présence visuelle plus soutenue qui remplace l’effet de la taille 72 originale).

Si j’exécute mon application en mode “machine locale” (ou “simulation”) j’obtiendrais toujours l’affichage bleu déjà capturé et montré plus haut. Mais si j’exécute en mode mobile 5” j’obtiens bien le résultat suivant :

image

Génial non ? Bien entendu la page de code-behind reste rigoureusement la même (bien que je pourrais aussi la personnaliser de la même façon) et s’il y a un ViewModel je pourrais me binder aux seuls informations et commandes que je souhaite montrer en mode smartphone.

Déconcertant de simplicité et puissant.

Les noms de fichiers par famille

On peut arriver au même résultat en renommant tout simplement les fichier avec la même règle de nommage que celle des sous-répertoires. Par exemple pour cette Vue Xaml personnalisée j’aurais pu, au lieu de créer un sous-répertoire famille et d’y placer un fichier MainPage.Xaml, dupliquer le fichier MainPage.Xaml et le renommer MainPage.DeviceFamily-Mobile.Xaml.

Le résultat serait le même, rigoureusement.

en revanche n’utiliser pas les deux méthodes à la fois car là ça ne collera pas, vous créerez des ressources réellement dupliquées (deux fois MainPage pour le mode Mobile par exemple) et cela n’est pas plus possible qu’avant…

Les ressources aussi ? oui !

Les deux techniques sont utilisables pour les ressources aussi.

Bien entendu ici puisque j’ai deux pages XAML différentes pour la même Vue, je peux mettre une image “A” dans le mode desktop et une image “B” dans le mode smartphone même si elles ont des noms différents.

Mais imaginons que j’arrive à ne conserver qu’une page XAML pour la Vue MainPage mais que dans le cas d’un smartphone j’affiche une icône de de smartphone et dans le cas du mode desktop un petit PC ?

Il me suffira d’avoir une image “desktop” pour mon mode par défaut et de placer une image de même nom mais avec un logo smartphone dans le sous-répertoire famille, ou bien de renommer l’image avec la seconde technique qui ne touche que le nom du fichier. Une page XAML deux images chargées contextuellement…

Les images ne sont qu’un exemple on peut ainsi disposer de tout type de ressources automatiquement adaptées au contexte (vidéo, son, …).

InitializeComponent

Ici il s’agit vraiment d’une feinte… Dès que vous avez créé un sous-répertoire ou une ressource “bis” et après avoir compilé votre code vous découvrirez en cherchant bien que le code généré pour InitializeComponent a légèrement changé…

Il devient ceci :

 partial class MainPage : global::Windows.UI.Xaml.Controls.Page
    {


        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 14.0.0.0")]
        private bool _contentLoaded;

        /// <summary>
        /// InitializeComponent()
        /// </summary>
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 14.0.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
        public void InitializeComponent()
        {
            this.InitializeComponent(null);
        }

        /// <summary>
        /// InitializeComponent()
        /// </summary>
        [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Windows.UI.Xaml.Build.Tasks"," 14.0.0.0")]
        [global::System.Diagnostics.DebuggerNonUserCodeAttribute()]
        public void InitializeComponent(global::System.Uri resourceLocator)
        {
            if (_contentLoaded)
                return;

            _contentLoaded = true;

            if (resourceLocator == null)
            {
                resourceLocator = new global::System.Uri("ms-appx:///MainPage.xaml");
            }
            global::Windows.UI.Xaml.Application.LoadComponent(this, resourceLocator, global::Windows.UI.Xaml.Controls.Primitives.ComponentResourceLocation.Application);
        }

    }

 

Ce qu’on voit c’est qu’il existe une surcharge de la méthode qui désormais accepte une URI en paramètre.

Qu’est-ce à dire ?

Et bien que si vous ne faites rien c’est bien le mécanisme par défaut expliqué plus haut qui va se mettre en place (chargement de l’une ou l’autre version de la page Xaml selon la famille de la machine exécutant le code).

Mais puisque la surcharge existe, cela veut dire que si vous disposez de plusieurs pages alternatives pour une même page, disons PrincipalMainPage et AlternateMainPage, vous pourrez facilement dans le constructeur de MainPage ajouter toute une logique pour décider de charger soit la page principale soit la page alternative (à l’intérieur d’un choix par famille, ce qui commence à faire pas mal de possibilités).

Par exemple :

public MainPage()  
{
    if (AnalyticsInfo.VersionInfo.DeviceFamily </mark> "Windows.Mobile")
    {
        if (usePrimary)
        {
            InitializeComponent(new Uri("ms-appx:///PrincipalMainPage.xaml", UriKind.Absolute));
        }
        else
        {
            InitializeComponent(new Uri("ms-appx:///AlternateMainPage.xaml", UriKind.Absolute));
        }
    }
    else
    {
        InitializeComponent();
    }
}

 

Nous voici maintenant avec une application possédant une version Desktop par défaut parfaitement adaptée aux grands écrans de PC et possédant aussi un visuel adapté parfaitement aux unités mobiles qui peut même prévoir deux affichages différents (mobile de grande taille et mobile très petit par exemple…).

On voit que les procédés à notre disposition sont nombreux, logiques et cohérents sans pour autant rendre tout cela imbuvable. Très franchement les magouilles d’Android sur ce point précis me donnent la nausée presque autant que le trio infernal JS/CSS/Html, c’est pour dire ! Vous vous dites, tiens, depuis le début il n’a pas placé un petit truc sur Apple selon son habitude… Mais que voulez-vous que vous dise ? dans un monde fasciste avec un seul modèle de smartphone à quoi servirait-il de s’adapter ? C’est le client qui s’adapte, pas le produit… Vous voulez vraiment que je vous dise ce que j’en pense ? et bien non, car sur Dot.Blog on est toujours poli Sourire.

Les triggers d’état

Restez ! C’est pas fini ! Il existe encore une autre façon de proposer une UI vraiment personnalisée par famille de machines sans pour autant créer des fichiers supplémentaires !

Oui, il est possible d’arriver au même résultat avec un seul fichier XAML. Parfois c’est rentable, parfois ça fait un gros fichier XAML ce qui n’est pas très maintenable, on a donc le choix des armes selon le contexte sachant que pour une Vue on peut utiliser un mécanisme et que pour une autre un second mécanisme. je préfère les applications où le code est mis en œuvre de façon cohérente donc on choisira une approche et on s’y tiendra, mais rien ne l’oblige.

Voici un bout de code qui démontre la technique :

<Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"> 

		<VisualStateManager.VisualStateGroups> 
 			<VisualStateGroup > 
				<VisualState x:Name="desktop"> 
					<VisualState.StateTriggers> 
 						<triggers:DeviceFamilyStateTrigger DeviceFamily="Desktop" /> 
 					</VisualState.StateTriggers> 
 					<VisualState.Setters> 
 						<Setter Target="greeting.Text" Value="Hello Windows Desktop!" /> 
 					</VisualState.Setters> 
 				</VisualState> 
 				<VisualState x:Name="mobile"> 
 					<VisualState.StateTriggers> 
                         <triggers:DeviceFamilyStateTrigger DeviceFamily="Mobile" /> 
 					</VisualState.StateTriggers> 
 					<VisualState.Setters> 
 						<Setter Target="greeting.Text" Value="Hello Windows Mobile!" /> 
 					</VisualState.Setters> 
 				</VisualState> 
 				<VisualState x:Name="team"> 
 					<VisualState.StateTriggers> 
                         <triggers:DeviceFamilyStateTrigger DeviceFamily="Team" /> 
 					</VisualState.StateTriggers> 
 					<VisualState.Setters> 
 						<Setter Target="greeting.Text" Value="Hello Windows Team!" /> 
 					</VisualState.Setters> 
 				</VisualState> 
 				<VisualState x:Name="iot"> 
 					<VisualState.StateTriggers> 
                         <triggers:DeviceFamilyStateTrigger DeviceFamily="IoT" /> 
 					</VisualState.StateTriggers> 
 					<VisualState.Setters> 
 						<Setter Target="greeting.Text" Value="Hello Windows IoT Core!" /> 
 					</VisualState.Setters> 
 				</VisualState> 
                 <VisualState x:Name="xbox"> 
                     <VisualState.StateTriggers> 
                         <triggers:DeviceFamilyStateTrigger DeviceFamily="Xbox" /> 
                     </VisualState.StateTriggers> 
                     <VisualState.Setters> 
                         <Setter Target="greeting.Text" Value="Hello Xbox!" /> 
                     </VisualState.Setters> 
                 </VisualState> 
             </VisualStateGroup> 
 		</VisualStateManager.VisualStateGroups> 
		<TextBlock x:Name="greeting" Foreground="Black" 
                   Text="Unknown Platform" 
                   HorizontalAlignment="Center" VerticalAlignment="Center" /> 
</Grid> 

 

Ce code est extrait d’une démo postée sur Github, vous pouvez télécharger le projet complet pour le tester : WindowsStateTriggers.

Conclusion

Le concept de Reactive UI n’est qu’un concept. C’est quand il faut passer à la réalité, aux moyens mis à notre disposition qu’on voit si la solution proposée tient la route ou devient un enfer.

Sous Android on peut s’adapter sans problème à plein de choses, ce qui est un minimum dans un environnement qui est supporté par tant de machines différentes. Mais la solution proposée est archaïque, c’est bourrin, fastidieux. Franchement quand j’ai découvert ça au début où j’ai pratiqué Android j’ai été très déçu (les nine-patches m’ont achevé) je m’attendais à aussi moderne que peut l’être le look épuré, jeune et frais qu’on voit de l’extérieur. Et bien non. Je ne parle pas de Cocoa et Objective-C, rien  que d’en parler j’ai des haut-le-cœur.

Encore une fois et quoi qu’on en pense ou dise, que le public adore ou non Windows, tout cela ne changera rien au fait que oui, toujours, Microsoft a 30 ans d’avance sur la concurrence. Ce n’est pas forcément payé par des succès fulgurants en retour, et c’est dommage, mais tout de même, que leurs produits sont fait avec intelligence…

Stay tuned !

Faites des heureux, partagez l'article !
blog comments powered by Disqus