Développer un client Silverlight pour Sharepoint 2010 en appliquant le pattern MVVM (2/2)

Voici la seconde partie de l’article. Vous trouverez la première partie ici : Développer un client Silverlight pour Sharepoint 2010 en appliquant le pattern MVVM (1/2) 

Ainsi que les sources complètes:

Nous avons créé une ListBox classique Silverlight, mis en place tous les éléments de l’architecture MVVM ainsi que l’accès aux données dans Sharepoint. Malgré tout, l’application ne fonctionne pas encore tout à fait correctement.

Dans cette seconde partie, nous verrons:
    les opérations à effectuer sur les classes pour que le binding fonctionne correctement
    l’écueil classique lors d’un développement Silverlight/WPF dans un contexte multithread
    comment passer d’un rendu habituel de liste à quelque chose de plus…original :) grâce à Silverlight et xaml

Sommaire de cet article

  1. Implémentation de INotifyPropertyChanged par le ViewModel
  2. Qui dit asynchrone dit contexte multi-thread
  3. Modification de l’aspect visuel de la liste
  4. Téléchargez les sources

 

Implémentation de INotifyPropertyChanged par le ViewModel

Pour que le binding fonctionne, la classe cible du binding – la couche ViewModel en MVVM - doit implémenter un mécanisme de notification à savoir l’interface INotifyPropertyChanged. Cette interface contient un événement qui doit être levé chaque fois que le binding d’une propriété de la classe doit être réévalué. Notre classe WinesListVM va donc implémenter INotifyPropertyChanged et l’événement sera levé chaque fois que l’on affectera les propriétés SelectedItem et WineItems de cette classe. Voilà comment cela se présente:

Dans le fichier WinesListVM.cs, ajoutez la référence au namespace suivante :

 using System.ComponentModel;

Implémentez INotifyPropertyChanged pour la classe WinesListVM:

 public class WinesListVM : INotifyPropertyChanged

Cliquez droit sur INotifyPropertyChanged et sélectionnez “Implement Interface”. Visual Studio ajoute pour vous les membres de l’interface à implémenter, à savoir ici un événement PropertyChanged.

 public event PropertyChangedEventHandler PropertyChanged;

Ajoutez la méthode suivante qui permet de déclencher l’événement dont le paramètre est le nom de la propriété modifiée (dans notre cas ce sera “SelectedItem” ou “WineItems”). Le moteur de binding actualisera tous les éléments de l’UI qui sont bindés à cette propriété.

 protected void OnPropertyChanged(string name)
 {
     PropertyChangedEventHandler handler = PropertyChanged;
     if (handler != null)
     {
         handler(this, new PropertyChangedEventArgs(name));
     }
 }

Il reste ensuite à lever l’événement, chaque fois que l’on affecte la propriéte “SelectedItem” et “WineItems”:

 IEnumerable<Wine> _wineItems;
 public IEnumerable<Wine> WineItems
 {
     get
     {
         return _wineItems;
     }
     set
     {
         if (_wineItems != value)
         {
             _wineItems = value;
             OnPropertyChanged("WineItems");
         }
     }
 }
  
 Wine _selectedItem;
 public Wine SelectedItem
 {
     get
     {
         return _selectedItem;
     }
     set
     {
         if (_selectedItem != value)
         {
             _selectedItem = value;
             OnPropertyChanged("SelectedItem");
         }
     }
 }

Voici le code complet de la classe WinesListVM :

 using System;
 using Wines.SL.DAL;
 using System.Collections.Generic;
 using Wines.SL.Model;
 using System.ComponentModel;
 using System.Threading;
  
 namespace Wines.SL.ViewModel
 {
     public class WinesListVM : INotifyPropertyChanged
     {
         
         public WinesListVM(IWinesDAL winesDal)
         {
             winesDal.WineItemsUpdated += ((s, e) => WineItems = e.WineItems);
         }
  
         IEnumerable<Wine> _wineItems;
         public IEnumerable<Wine> WineItems
         {
             get
             {
                 return _wineItems;
             }
             set
             {
                 if (_wineItems != value)
                 {
                     _wineItems = value;
                     OnPropertyChanged("WineItems");
                 }
             }
         }
  
         Wine _selectedItem;
         public Wine SelectedItem
         {
             get
             {
                 return _selectedItem;
             }
             set
             {
                 if (_selectedItem != value)
                 {
                     _selectedItem = value;
                     OnPropertyChanged("SelectedItem");
                 }
             }
         }
  
         public event PropertyChangedEventHandler PropertyChanged;
  
         protected void OnPropertyChanged(string name)
         {
             PropertyChangedEventHandler handler = PropertyChanged;
             if (handler != null)
             {
                 handler(this, new PropertyChangedEventArgs(name));
             }
         }
  
     }
 }

A présent, chaque fois que l’on modifie la propriété “SelectedItem” du ViewModel, l’élément sélectionné dans la vue sera synchronisé avec cette propriété. De la même manière, chaque fois que l’on rechargera la liste des vins et que l’on réaffectera la propriété WineItems, la contenu de liste xaml sera rafraichie à l’écran avec les nouvelles données.

Relançons l’application : F5….boum une exception UnauthorizedAccessException : Accès inter-threads non valide : mais courage c’est la dernière étape….

image

Qui dit asynchrone dit contexte multi-thread

Souvenez-vous, dans la DAL, nous accédons aux données de manière asynchrone. Les données provenant de Sharepoint arrivent donc dans une callback par un thread dédié. Que ce soit en Winform, en WPF, en Silverlight…en C# ou même en VB, seul le thread qui a créé le contrôle graphique peut en modifier ses propriétés. Or dans notre cas, si l’on déroule le chemin d’exécution c’est le thread qui exécute le code de la callback dans la DAL qui au final va modifier la propriété ItemsSource du contrôle List de l’UI Silverlight.

Il faut donc passer la main au thread principal à un moment ou un autre dans la chaine pour que ce soit lui qui exécute le code de réévaluation du binding. Puisque c’est la DAL qui effectue un appel asynchrone, c’est elle qui va prendre la responsabilité de se remettre dans le thread principal. Et voici comment procéder en utilisant le SynchronizationContext (la même opération peut être effectuée avec Dispatcher.BeginInvoke):

Déclarons un objet de type SynchronizationContext dans la classe WinesFromSP, que l’on initialize de la manière suivante (sans oublier de référencer le namespace System.Threading):

 SynchronizationContext _syncCtxt = SynchronizationContext.Current;

Puis on encapsule la levée de l’évenement par la méthode Post du SynchronizationContext. Le code situé dans le corps de la clause Post sera exécuté dans le thread principal dès que celui-ci en aura la possibilité, ce qui règle notre problème.

 _syncCtxt.Post(unused => WineItemsUpdated(this, new WineItemsUpdatedEventArgs(WineItems)), null); 

voici le code complété:

 using Wines.SL.Model;               // Pour utilisation de la couche Model
 using System;
 using Microsoft.SharePoint.Client;  // Référence au ClientObjectModel
 using System.Collections.Generic;   // Pour les Generics
 using System.Linq;
 using System.Threading;                  // Pour Linq
  
 namespace Wines.SL.DAL
 {
     public class WinesFromSP : IWinesDAL
     {
         // Contexte de connexion au serveur
         ClientContext _ctx = new ClientContext(@"https://stephe:2828/MySite");
         // La liste des vins résultante
         ListItemCollection _wines;
         // Pour notifier l'update des éléments
         public event EventHandler<WineItemsUpdatedEventArgs> WineItemsUpdated;
         SynchronizationContext _syncCtxt = SynchronizationContext.Current;
  
  
         public void RequestFromServer()
         {
             // Récupère la liste "Wines"
             var wineList = _ctx.Web.Lists.GetByTitle("Wines");
             // Récupère les éléments de la liste
             _wines = wineList.GetItems(new CamlQuery());
             // Charge les éléments de la liste
             _ctx.Load(_wines);
             // Exécute la requête asynchrone
             _ctx.ExecuteQueryAsync(onWineElemsLoaded, onWineElemsError);
         }
  
         public IEnumerable<Wine> WineItems { get; set; }
  
         private void onWineElemsLoaded(object sender, ClientRequestSucceededEventArgs args)
         {
             // Succes
             // _wines.AsEnumerable() pour repasser en Linq To Object [using System.Linq]
             WineItems = _wines.AsEnumerable().Select(w => new Wine()
             {
                 Name = w["Name"].ToString(),
                 Count = Convert.ToUInt16(w["Count"])
             });
  
             if (WineItemsUpdated != null)
             { 
                 _syncCtxt.Post(unused => WineItemsUpdated(this, new WineItemsUpdatedEventArgs(WineItems)), null); 
             }
             
         }
  
         private void onWineElemsError(object sender, ClientRequestFailedEventArgs args)
         {
             // Traitement sur erreur
         }
  
     }
 }

On relance l’application et cette fois-ci on peut voir apparaître notre liste de vins.

image

Remarquez que grâce au binding du SelectedItem, si vous mettez un point d’arrêt sur le setter de la propriété SelectedItem du ViewModel, on s’y arrête chaque fois que l’on change d’élément sélectionné dans la liste.

Modification de l’aspect visuel de la liste

En Silverlight comme en WPF, le choix d’un contrôle s’effectue par-rapport au rôle fonctionnel que l’on souhaite lui attribuer. Le rôle fonctionnel d’une ListBox est de contenir un ensemble d’éléments ainsi qu’un élément courant sélectionné.

Ainsi nous allons déclarer un contrôle de type “ListBox” dans notre vue, non pas parce qu’elle afficherait une liste au sens où les éléments sont placés les uns en dessous des autres, mais parce que le ViewModel qui lui est associé comprend les 2 fonctionnalités “ensemble d’éléments” (WineItems) et “élément sélectionné” (SelectedItem).

Pour avoir le rendu ci-dessous, il faudra effectuer les opérations suivantes:

    modifier la position d’affichage des éléments de la liste : plutôt que de les placer les uns au-dessus des autres, on les positionne de manière aléatoire à l’écran

    modifier l’aspect visuel d’un élément de la liste : au lieu du titre de l’élément, on affiche un cercle de diamètre = à la quantité de bouteilles disponibles + son titre

Le résultat attendu peut être codé de différentes manières en xaml (utilisation de styles, de templates, …). Pour cet exemple j’ai préféré utiliser un code qui fonctionne aussi bien en Silverlight qu’en WPF.

image

Modifier la position d’affichage des éléments de la liste

Par défaut, les éléments d’une ListBox sont affichés dans un conteneur (ListBox.ItemsPanel) de type StackPanel ce qui est parfait si l’on veut disposer les éléments horizontalement ou verticalement les uns par-rapport aux autres. Dans notre cas, on souhaite positionner chaque élément à des coordonnées qui lui sont propres. C’est ce que permet le conteneur de type Canvas.

Dans le fichier WinesListView.xaml, complétez le code de la ListBox de la manière suivante :

 <Grid x:Name="LayoutRoot">
     <ListBox ItemsSource="{Binding WineItems}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
         <ListBox.ItemsPanel>
             <ItemsPanelTemplate>
                 <Canvas Background="DarkSeaGreen" >
                 </Canvas>
             </ItemsPanelTemplate>
         </ListBox.ItemsPanel>
     </ListBox>
 </Grid>

Relancez l’application (F5) : vous verrez à présent tous les éléments se positionner sur le bord gauche du Canvas. En effet, aucune directive de placement n’a été effectuée pour l’instant:

image 
Redéfinissons à présent l’ItemTemplate de la ListBox pour pouvoir modifier les coordonnées et le visuel d’un élément de la liste. Pour l’instant, nous y mettons simplement une zone de texte.

 <ListBox ItemsSource="{Binding WineItems}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
             <ListBox.ItemTemplate>
                 <DataTemplate>
                     <TextBlock  Text="{Binding}" 
                                 FontSize="9" FontWeight="Bold" FontStyle="Italic" 
                                 Foreground="Brown" 
                                 HorizontalAlignment="Center" VerticalAlignment="Center" 
                                 Opacity="0.6">
                         <TextBlock.RenderTransform>
                             <TranslateTransform X="{Binding Converter={StaticResource ElemToPositionConverter}}" Y="{Binding Converter={StaticResource ElemToPositionConverter}}" />
                         </TextBlock.RenderTransform>
                     </TextBlock>
                 </DataTemplate>
             </ListBox.ItemTemplate>
             <!-- Modification du type de conteneur de l'ensemble des éléments de la ListBox -->
             <ListBox.ItemsPanel>
                 <ItemsPanelTemplate>
                     <Canvas Background="DarkSeaGreen" >
                     </Canvas>
                 </ItemsPanelTemplate>
             </ListBox.ItemsPanel>
         </ListBox>

Nous effectuerons une translation du TextBlock dans le Canvas en bindant x (Canvas.Left) et y (Canvas.Top) avec l’élément de la liste en appliquant un converter. Le converter nous permet d’effectuer les ajustements entre ce que nous propose le ViewModel et le rendu que l’on souhaite obtenir dans la View. Le converter sera appelé chaque fois que le binding de l’élément concerné sera réévalué (ici 1 seule fois lors du chargement). Dans notre cas, nous allons récupérer un élément de type Wine en entrée du converter et nous souhaitons renvoyer un nombre aléatoire qui représentera un nombre de pixels. Le paramètre d’entrée du converter ne nous sert à rien. Mais on aurait très bien pu décider de positionner les éléments en fonction de la nom ou de leur quantité.

Créez un nouveau répertoire “Converters” dans le répertoire “View” du projet. Créez-y une nouvelle classe “ElemToPositionConverter".cs”. Copiez-y le code suivant:

 using System;
 using System.Windows.Data;
  
 namespace Wines.SL.View.Converters
 {
     public class ElemToPositionConverter : IValueConverter
     {
         static Random _rand = new Random();
  
         public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
         {
             return _rand.Next(30, 250);
         }
  
         public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
         {
             throw new NotImplementedException();
         }
     }
 }

Un converter doit implémenter l’interface IValueConverter et dans notre cas, seul le sens de conversion Source vers UI sera à définir. Nous renvoyons simplement un nombre aléatoire entre 30 et 250 qui correspondra au positionnement x et y en pixels de chacun de nos éléments sur l’écran. Dans du code xaml, l’utilisation d’un converter s’effectue en l’instanciant dans les resources de la View. On peut ensuite l’utiliser dans les contrôles en le référençant comme une StaticResource.

Voici le résultat, remarquez qu’à chaque exécution, les éléments se placent différemment.

image

 

 

Ajoutons à présent les cercles dont le diamètre représente la quantité de bouteilles. Chaque élément de la liste ne sera plus uniquement représenté par un TextBlock, mais par un TextBlock surmonté d’une Ellipse. On a vu tout à l’heure, que le contrôle qui permet contenir des éléments les uns en dessous des autres est un StackPanel. Nous allons donc placer le TextBlock existant ainsi qu’une Ellipse dans un contrôle StackPanel:

 <navigation:Page x:Class="Wines.SL.View.WinesListView" 
            xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
            xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml" 
            xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
            xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
            mc:Ignorable="d"
            xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation"
            xmlns:cnv="clr-namespace:Wines.SL.View.Converters"
            d:DesignWidth="640" d:DesignHeight="480"                 
            Title="MainView Page">
     
     <navigation:Page.Resources>
         <cnv:ElemToPositionConverter x:Key="ElemToPositionConverter" />
     </navigation:Page.Resources>
  
     <Grid x:Name="LayoutRoot">
         <ListBox ItemsSource="{Binding WineItems}" SelectedItem="{Binding SelectedItem, Mode=TwoWay}">
             <ListBox.ItemTemplate>
                 <DataTemplate>
                     <StackPanel>
                         <StackPanel.RenderTransform>
                             <TranslateTransform X="{Binding Converter={StaticResource ElemToPositionConverter}}" Y="{Binding Converter={StaticResource ElemToPositionConverter}}" />
                         </StackPanel.RenderTransform>
                         <Ellipse  x:Name="Circle"  
                                     Width="{Binding Count}" 
                                     Height="{Binding Count}">
                             <Ellipse.Fill>
                                 <SolidColorBrush 
                                     Color="Azure" 
                                     Opacity="0.6"/>
                             </Ellipse.Fill>
                         </Ellipse>
                         <TextBlock 
                                 Text="{Binding}" 
                                 FontSize="9" FontWeight="Bold" FontStyle="Italic" 
                                 Foreground="Brown" 
                                 HorizontalAlignment="Center" VerticalAlignment="Center" 
                                 Opacity="0.6"/>
                     </StackPanel>
                 </DataTemplate>
             </ListBox.ItemTemplate>
             <!-- Modification du type de conteneur de l'ensemble des éléments de la ListBox -->
             <ListBox.ItemsPanel>
                 <ItemsPanelTemplate>
                     <Canvas  Background="DarkSeaGreen" >
                     </Canvas>
                 </ItemsPanelTemplate>
             </ListBox.ItemsPanel>
         </ListBox>
     </Grid>   
 </navigation:Page>

Remarquez que la translation Canvas.Left et Canvas.Top a été déplacé dans le StackPanel pour que l’ellipse ainsi que le texte profitent du placement aléatoire.

 

Nous avons terminé !! Cet article est assez long car très détaillé, mais si vous téléchargez les sources de la solution, vous constaterez qu’elle contient finalement très peu de lignes de code.

image

 

 

Téléchargez les sources :