Silverlight+WPF

Blog d'Alexandre Arnaudet et de ses collègues chez CLT-Services autour de WPF, de Silverlight et des RIA

Recent posts

Tags

Categories

Navigation

Pages

    Archive

    Blogroll

    Disclaimer

    The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

    Silverlight - Modifier les styles au runtime

    Lors d'une mission récente, un des pré-requis graphique était d'avoir des couleurs différentes au niveau de la  MainPage et des pages de la NavigationFrame en fonction du menu sélectionné.

    Au travers de ce billet, nous verrons une solution qu'il est possible d'implémenter pour modifier les styles des différents contrôles au Runtime.

    Les styles peuvent être utilisés de deux manières différentes:

    • Les styles que l'on appelle styles implicites. Lors de la déclaration du style, vous ne définissez pas de x:Key mais seulement un TargetType. Tous les contrôles de ce type utiliseront alors ce style
    • Les styles que l'on peut nommer styles explicites. Contrairement au précédent, la déclaration du style contient un x:Key et un TargetType. Afin qu'un contrôle puisse utiliser ce type de style, il doit spécifier le style via la propriété Style du contrôle à l'aide de StaticResource.

    Seulement contrairement à WPF, où il existe les DynamicResources, Silverlight ne propose que StaticResource.  Vous allez me dire, oui mais où se trouve la différence entre ces deux méthodes.

    Globalement la grosse différence réside dans le fait que la résolution d'une StaticResource est réalisée lorsque le xaml de la page est parcouru. Cette opération arrive une fois et vous ne pouvez plus modifier aucune ressource après ça.

    A contrario avec les DynamicResources, la ressource est évaluée au Runtime. Le gros avantage, c'est que vous allez notamment pouvoir mettre à jour la target / contrôle, dès qu'un changement est effectué dans un ResourceDictionary. Ceci est très pratique pour appliquer des thèmes à son application.

    Heureusement depuis Silverlight 3 des améliorations ont été faites sur la gestion des styles et maintenant nous pouvons mettre à un jour le style d'un contrôle en lui assignant un nouveau style. A noté également la gestion des MergedDictionaries qui permet de composer les styles à partir de plusieurs sources.

    Pour mettre en avant ce que nous venons de voir, je vous propose un exemple très simple, dans lequel nous allons simplement modifier la couleur d'un Border et la disposition des contrôles. Le xaml sera composé des éléments suivants:

    <UserControl x:Class="SilverlightDynamicMergeDictionaries.MainPage"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        mc:Ignorable="d"
        d:DesignHeight="300" d:DesignWidth="400">
    
        <Grid x:Name="LayoutRoot" Background="White">
            <StackPanel  VerticalAlignment="Center">
                <Border x:Name="MyBorder" Style="{StaticResource Test}" 
                        Width="100" 
                        Height="100"> 
                </Border>
                <Button Click="Button_Click" 
                        Tag="Red" Width="150" 
                        Height="25"
                        Content="Theme Red"/>
                <Button Click="Button_Click" 
                        Tag="Blue" Width="150"
                        Height="25" 
                        Content="Theme Blue"/>
            </StackPanel>
        </Grid>
    </UserControl>
    
    

    Nous souhaitons définir le style du StackPanel avec un style implicite et le style de l'élément Border avec un style explicite. Comme vous le remarquerez, nous associons le style avec le x:key Test à l'élément Border.

    Ces deux styles, nous allons les définir dans un dictionnaire de ressources. Comme dit plus haut, nous souhaitons pouvoir modifier ces styles au runtime, pour cela nous allons ajouter non pas un, mais deux dictionnaires de ressources: blue.xaml et red.xaml

    Dans le fichier xaml, j'ai également ajouté deux boutons, pour switcher entre les styles des deux dictionnaires de ressources que l'on peut considérer comme des thèmes.

    Partons du principe que le thème par défaut est blue.xaml. Pour l'utiliser nous devons l'ajouter aux ressources de l'application, si l'on souhaite rendre disponible ces styles à l'ensemble des contrôles, quelque soit la page, le contrôle...:

    App.xaml:

     <Application.Resources>
            <ResourceDictionary>
                <ResourceDictionary.MergedDictionaries>
                    <ResourceDictionary 
       Source="/SilverlightDynamicMergeDictionaries;component/blue.xaml"/>
                </ResourceDictionary.MergedDictionaries>
            </ResourceDictionary>
        </Application.Resources>
    

       

    Une fois cette première étape faite, ajoutez un handler commun pour les deux boutons. Dans cet exemple, le thème à charger sera déterminé via le Tag que nous avons ajouté à chacun des boutons.

    Comme nous allons avoir besoin d'ici peu des styles, ajoutons deux variables de type string contenant le chemin vers les ResourcesDictionary.

    private const string blueSource
            = "/SilverlightDynamicMergeDictionaries;component/blue.xaml";
    private const string redSource
            = "/SilverlightDynamicMergeDictionaries;component/red.xaml";

    La nomenclature de nommage est la suivante: /NameSpace;component/Path.xaml

    Maintenant dans le handler, commencez par faire un cast du sender en Button de manière à pouvoir récupérer la valeur correspondant au thème à charger.

    Puis créez un nouveau ResourceDictionary en assignant à sa propriété Source, une Uri qui prend en paramètre soit blueSource, soit redSource. Le ResourceDictionary est créé, il ne nous reste plus qu'à l'ajouter au MergedDictionaries de App.Resources.

    Button btn = sender as Button;
               if (btn != null)
               {
                   ResourceDictionary resources = 
                       App.Current.Resources as ResourceDictionary;
    
                   ResourceDictionary rd = new ResourceDictionary();
                   if (btn.Tag.Equals("Blue"))
                       rd.Source = new Uri(blueSource, UriKind.Relative);
                   else
                       rd.Source = new Uri(redSource, UriKind.Relative);
    
                   resources.MergedDictionaries.Add(rd);

    A ce stade nous avons ajouté les nouveaux styles. Il nous reste une dernière étape, à savoir, faire en sorte que l'UI soit mise à jour.

    Pour cela quelques explications supplémentaires sont nécessaires.

    Si vous avez fait attention, entre le dictionnaire ajouté dans App.xaml et celui que nous venons d'ajouter après le click du bouton, MergedDictionaries contient deux dictionnaires avec exactement les mêmes styles.

    D'après la msdn, voici ce qui se passe si j'essaie de récupérer un style dans ce cas de figure.

    Merged Dictionary Behavior


    Resources in a merged dictionary occupy a location in the resource lookup scope that is just after the scope of the main resource dictionary they are merged into. Although a resource key must be unique within any individual dictionary, a key can exist multiple times in a set of merged dictionaries. In this case, the resource that is returned will come from the last dictionary found sequentially in the MergedDictionaties collection. If the MergedDictionaties collection was defined in XAML, then the order of the merged dictionaries in the collection is the order of the elements as provided in the markup. If a key is defined in the primary dictionary and also in a dictionary that was merged, then the resource that is returned will come from the primary dictionary. These scoping rules apply equally for both static resource references and dynamic resource references.

    Dans notre cas, seule la partie MergedDictionaries nous intéresse. Le Style que nous allons récupérer correspond donc bien au style présent dans le dernier Dictionnaire de ressources ajouté.

    Pour assigner les nouveaux styles, nous allons parcourir l'arborescence XAML et pour chacun des contrôles regarder si un style est présent dans les ressources de l'application.

    Pour parcourir le XAML voici une méthode d'extension.

    public static IEnumerable<DependencyObject>
               Descendents(this DependencyObject root)
           {
               int count = VisualTreeHelper.GetChildrenCount(root);
               for (int i = 0; i < count; i++)
               {
                   var child = VisualTreeHelper.GetChild(root, i);
                   yield return child;
                   foreach (var descendent in Descendents(child))
                       yield return descendent;
               }
           }

    Pour l'utiliser, il nous suffit de choisir un DependencyObject correspondant à la partie de l'UI que nous souhaitons rafraichir et de parcourir les éléments dans une boucle foreach en appelant la méthode récursive Descendents.

    this.Descendents().ToList().ForEach(c => ApplyNewStyle(c));

    Dans l'expression lambda ci-dessus, vous pouvez voir que nous avons également ajouté une méthode ApplyNewStyle avec le contrôle sur lequel on doit l'appliquer. C'est dans cette méthode que nous allons parcourir les dictionnaires, pour trouver l'identifiant correspondant au style et ainsi le récupérer pour l'assigner.

    private void ApplyNewStyle(DependencyObject ctrl)
    {
     FrameworkElement element = ctrl as FrameworkElement;
     if (element != null)
     {
       foreach (ResourceDictionary dictionary 
               in App.Current.Resources.MergedDictionaries)
       {
          foreach (object key in dictionary.Keys)
          {
           if ((element.Style != null && element.Style == dictionary[key]) ||
                  (key.ToString() == element.GetType().FullName))
                element.Style = App.Current.Resources[key] as Style;
          }
       }
     }
    }

    Nous pourrions ne parcourir que le premier dictionnaire car nous savons que dans cet exemple, les deux seuls dictionnaires présents dans MergedDictionaries sont les dictionnaires correspondant à nos styles et que le premier contient les styles de départ. Cependant, dans une application réelle, rien ne vous empêche d'en avoir d'autres.

    Pour joindre le bon style au bon contrôle, deux cas se présentent à nous.

    • Le cas du StackPanel, où nous avions pas ajouté de x:key (style implicite). En réalité une clé est générée. Plus précisément il s'agit du type du contrôle que l'on a mis dans TargetType. Nous devons donc comparer avec le type de notre contrôle.
    • La cas du Border sur lequel nous avons ajouté explicitement un style. Ici il nous suffit donc de comparer le style du contrôle avec le style du dictionnaire possédant la clé courante.

    Dernier point. Comme maintenant les styles appliqués sont ceux du dernier dictionnaire ajouté, nous pouvons supprimer le dictionnaire qui se trouve ici à l'index 0.

    Le même processus sera effectué à chaque fois que l'on clique sur un des deux boutons.

    Comme d'habitude, voici les sources.

    Posted: Jan 12 2011, 23:59 by Alexandre Arnaudet | Comments (0) RSS comment feed |
    • Currently 0/5 Stars.
    • 1
    • 2
    • 3
    • 4
    • 5
    Filed under: Silverlight