Localisation Dynamique

by Nicolas Calvi 17. avril 2013 16:32

Une de mes problématiques en ce moment est liée à la localisation de mes applications. En effet, sous Windows Store App, si l'on veut switcher d'une langue à l'autre, il faut soit changer la langue de son OS et relancer l'application (si celle-ci utilise la localisation à partir des ressources) ou alors créer son système custom et tout ça doit être bien intégré dans votre pattern MVVM, or moi je voudrais faire ça à la volé. Bref un vrai casse-tête.

Personnellement j'ai planché un peu sur le problème et j'ai donc trouvé une première solution, certes ce n'est pas spécialement la meilleur ou la plus performante, mais elle offre je pense un bon compromis entre Resources / Binding / MVVM. Voilà en substance ma solution personnelle :

Le principe est de conserver le système de ressource (ici en rouge avec les fichiers "Resources.resw" et "Resources.lang-fr-FR.resw"), de créer une classe de gestion des ressources (ici en rouge avec la classe "ResourcesSwitcher") et pour faire le lien entre les deux par du Binding, un Converter (ici en rouge avec la classe "LanguageConverter"). Je vous laisse regarder en détail le code source (lien du projet en fin d'article), je vais juste faire un focus sur les éléments importants.

Tout d'abord il faut récupérer le contenu des ressources via la classe du Framework "ResourceManager", cela permet d'avoir l'arbre des différents dictionnaires dans les différentes langues :

// Récupération de la map
ResourceMap map = ResourceManager.Current.MainResourceMap.GetSubtree("Resources");

Ensuite, il faut créer les contextes, cela représente au final les langues que l'ont veut traiter, cela sera utile plus tard quand on voudra récupérer des informations dans la "map" des ressources, en spécifiant la langue le Framework pourra allez chercher la bonne valeur dans la bonne langue.

ResourceContext english = new ResourceContext() { Languages = new List<string>() { "en-Us" } };
ResourceContext french = new ResourceContext() { Languages = new List<string>() { "fr-FR" } };

Pour finir, pour récupérer une valeur dans la "map" des ressources il suffit de faire ceci :

map.GetValue("CleDictionnaire", french).ValueAsString

Ça c'est pour la récupération d'un élément localisé dans les ressources par le code. Vous comprenez donc que pour passer d'une langue à l'autre il suffit de changer le deuxième paramètre de "GetValue()", par exemple on peut remplacer le "ResouceContext" "french" par "english".

Ensuite il faut faire le lien entre le XAML et ce mécanisme. Moi j'ai choisi de créer une classe ("ResourcesSwitcher") qui sera une classe avec propriété notifiée qui exposera la "map". Pour créer un singleton de cette classe, je la déclare dans mon "App.xaml" :

<Application x:Class="BlackBlog.DynamicLocalization.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:conv="using:BlackBlog.DynamicLocalization.Converters"
             xmlns:vcore="using:BlackBlog.DynamicLocalization.Views.Core">

    <Application.Resources>
        <ResourceDictionary>

            <!--  Converteur pour récupérer les conversions  -->

            <conv:LanguageConverter x:Key="LanguageConverter" />

            <!--  Singleton pour le moteur de conversion  -->

            <vcore:ResourcesSwitcher x:Key="ResourcesSwitcher" />

        </ResourceDictionary>
    </Application.Resources>
</Application>

Au passage vous remarquerez que j'ai déclaré aussi un Converter. Celui-ci va servir a extraire une valeur (avec le "GetValue()" de la "map") depuis le XAML en utilisant le "ConverterParameter". En gros ça donne ca si on essaye de binder une ressource sur une propriété "Text" d'un "TextBlock" :

<TextBlock FontSize="16"
                Foreground="White"
                Text="{Binding Source={StaticResource ResourcesSwitcher},
                                      Path=Map,
                                      Converter={StaticResource LanguageConverter},
                                      ConverterParameter='Sentence'}" />

Avec ce système on peut donc binder nos ressources sur nos contrôles. Mais à quoi ça sert tout ça me direz-vous ! Et bien justement, si vous y regardez de plus prês, vous remarquez que nous sommes en binding, qui dit binding dit notification. Comme je l'ai souligné plus haut, dans ma classe "ResourcesSwitcher" j'ai exposé la "Map" via une propriété notifiée, donc il suffit de gérer qu'elle langue est sélectionnée actuellement (pour ma part j'ai mis une propriété dans mon "ResourcesSwitcher"), et au moment du changement de langue il suffit de notifier cette propriété, et là le moteur de binding va réévaluer toutes vos binding et repasser par le converter :

public object Convert(object value, Type targetType, object parameter, string language)
{
    // Récupération de la Map de ressource
    ResourceMap map = value as ResourceMap;

    if (map != null && !string.IsNullOrWhiteSpace((string)parameter))
        return (map.GetValue((string)parameter, ResourcesSwitcher.Instance.CurrentLanguage).ValueAsString);

    // Pas de convertion

    return (value);
}

Et dans le converter, je vais chercher la langue en sélection pour l'évaluation de la valeur. Grâce à ça vous avez un système de localisation dynamique.

Ceci est bien sûr une solution parmi tant d'autre, qui a ses avantages et ses défaut, libre à vous l'améliorer. Vous pouvez télécharger le code par ce lien :

BlackBlog.DynamicLocalization.rar (130,97 kb)

Jouer un son en background (WinRT XAML)

by Nicolas Calvi 22. septembre 2012 19:22

Récemment je me suis confronté à un problème étonnant sur une application WinRT XAML. Je voulais jouer un son mais pas à partir d'une définition dans mon XAML, mais plutôt en code behind. 

J'ai donc cherché sur internet comment faire et force est de constater que la seule solution sans aller à créer un interop DirectSound, c'est de passer par le MediaElement. Cependant, passer par ce contrôle n'a pas été aussi simple qu'il n'y parait.

Déjà la première chose à savoir et qu'il m'a fait perdre un temps infini, c'est que le contrôle MediaElement NE GERE PAS LES WAV. Aussi étonnant que cela puisse paraître, il n'arrive pas gérer des WAVs, ou en tout cas ceux que je lui donnais, je les ai pourtant vérifié en les lisant avec différents lecteurs et le fichier n'avait aucun soucis. Dans notre cas il faudra donc passer par des MP3, donc ne vous faite pas avoir !

Ensuite, pour pouvoir jouer les sons de façon simple et de pouvoir en lancer le même fichier plusieurs fois, j'ai donc créé une classe qui permet de gérer ça de façon transparent. Cette classe n'a besoin que du chemin du fichier dans les ressources de l'application et de l'instance d'un Panel (GridCanvasPanel, peu importe) afin de pouvoir stocker son instance dans un arbre visuel. En effet, le MediaElement a besoin de se trouver dans l'arbre visuel pour fonctionner. Cela n'est pas étonnant car à la base c'est un contrôle XAML visuel.

Le principe de cette classe et quand on lui invoque la fonction Play(), c'est de créer un MediaElement, de lui affecter le fichier, l'ajouter dans l'arbre visuel, jouer le son et quand celui-ci est fini, retirer l'instance de ce MediaElement de l'arbre visuel. Simple mais efficace.

Vous pouvez télécharger cette classe via ce lien : SoundItem.cs (3,73 kb)

Voici le code source de cette classe :

using System;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;

namespace MonApplication
{
    /// <summary>
    /// Représente un son a jouer avec sa gestion
    /// </summary>
    public class SoundItem
    {
        #region Propriétés

        /// <summary>
        /// Obtient la racine de l'arbre visuel que l'on va utiliser pour stocker nos MediaElements
        /// </summary>
        public Panel Root
        {
            get;
            private set;
        }

        /// <summary>
        /// Obtient l'URI associé au son
        /// </summary>
        public Uri Uri
        {
            get;
            private set;
        }

        #endregion


        #region Constructeur

        /// <summary>
        /// Constructeur de la classe
        /// </summary>
        /// <param name="root">Racine de l'arbre visuel que l'on va utiliser pour stocker nos MediaElements</param>
        /// <param name="filename">Fichier son de référence</param>
        public SoundItem(string filename, Panel root)
        {
            this.Root = root;
            this.Uri = new Uri("ms-appx:/" + filename);
        }

        #endregion

        #region Fonctions privées

        /// <summary>
        /// Joue le son
        /// </summary>
        public void Play()
        {
            // Création du MediaElement

            MediaElement sound = new MediaElement();
            sound.AutoPlay = false;
            sound.AudioCategory = AudioCategory.ForegroundOnlyMedia;
            sound.MediaOpened += this.OnMediaOpened;
            sound.MediaFailed += this.OnMediaFailed;
            sound.MediaEnded += this.OnMediaEnded;

            //  Ajour à l'arbre visuel

            this.Root.Children.Add(sound);

            // Chargement du son

            sound.Source = this.Uri;
        }

        #endregion

        #region Fonctions privées

        /// <summary>
        /// Se produit sur la fin de lecture d'un son
        /// </summary>
        /// <param name="sender">Source de l'appel</param>
        /// <param name="e">Argument de l'appel</param>
        private void OnMediaEnded(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            MediaElement sound = (MediaElement)sender;

            // On retire les handlers

            sound.MediaOpened -= this.OnMediaOpened;
            sound.MediaFailed -= this.OnMediaFailed;
            sound.MediaEnded -= this.OnMediaEnded;

            // Suppression de l'arbre visuel

            this.Root.Children.Remove(sound);
        }

        /// <summary>
        /// Se produit sur l'échec d'ouverture du son
        /// </summary>
        /// <param name="sender">Source de l'appel</param>
        /// <param name="e">Argument de l'appel</param>
        private void OnMediaFailed(object sender, Windows.UI.Xaml.ExceptionRoutedEventArgs e)
        {
            MediaElement sound = (MediaElement)sender;

            // On retire les handlers

            sound.MediaOpened -= this.OnMediaOpened;
            sound.MediaFailed -= this.OnMediaFailed;
            sound.MediaEnded -= this.OnMediaEnded;

            // Suppression de l'arbre visuel

            this.Root.Children.Remove(sound);
        }

        /// <summary>
        /// Se produit sur la réussite de lecture du son
        /// </summary>
        /// <param name="sender">Source de l'appel</param>
        /// <param name="e">Argument de l'appel</param>
        private void OnMediaOpened(object sender, Windows.UI.Xaml.RoutedEventArgs e)
        {
            MediaElement sound = (MediaElement)sender;

            // On joue le son

            sound.Play();
        }

        #endregion
    }
}

Si vous avez des questions sur le fonctionnement cette classe ou sur cet article, n'hésitez pas à me demander.