vendredi 25 juillet 2008

AgDatagrid et Silverlight Datagrid : comment créer dynamiquement une Datasource ?

English version available HERE

Le code source du projet est disponible ici.

Un exemple live juste en dessous !



Je travaille actuellement sur le composant AgDataGrid, un excellent contrôle Silverlight gratuit et open source de DevExpress (Démonstration complète ici). Bien entendu, mon but est d'implémenter celui-ci comme widget dans PowerShell Dashboard ;-)

J'ai rencontré un petit problème, qui est d'ailleurs valable pour le contrôle DataGrid standard de Silverlight 2, et plus généralement pour la génération dynamique d'objets typés en .NET :

Comment créer dynamiquement la Datagrid ?

AgDatagrid et la datagrid Silverlight 2 utilisent un objet de type IList pour construire la Datasource. Quand on utilise du code managé (C#/VB.NET...), vous devez créer une classe représentant vos données pour pouvoir construire cette IList :

   1: public class Person {   
   2:                 public string Name {   
   3:                      get;   
   4:                      set;   
   5:                 }   
   6:     
   7:                 public string City {   
   8:                      get;   
   9:                      set;   
  10:                 }   
  11:     
  12:                 public string State {   
  13:                      get;   
  14:                      set;   
  15:                 }   
  16:            }   
  17:     
  18: new Person() {   
  19: me = "Michael Jordan", City="Chicago", State="IL" },   
  20: new Person() {   
  21: me = "Kobe Bryant", City="Los Angeles", State="CA" },   
  22: new Person() {   
  23: me = "Shaquille O'Neil", City="Miami", State="FL" },   
  24: new Person() {   
  25: me = "Patrick Ewing", City="New York", State="NY" }    


  26:  



C'est impeccable quand vous connaissez votre source de données, mais comment faire quand vous avez du contenu dynamique (comme par exemple le retour d'un script PowerShell) ?

Je veux en effet utiliser la grid pour afficher mes données venant de PowerShell, je ne connais donc pas par avance le type des objets ni le nombre de colonnes...

Voici le point clé : je ne peux pas utiliser de classe statique pour définir mes données dans le contrôle Silverlight.

Game Over ?


Pas encore!

Nous avons deux stratégies à notre disposition pour arriver à notre résultat :

- Utiliser la programmation par langage dynamique avec Silverlight (Ironpython, IronRuby, JScript managé...) pour construire dynamiquement le contrôle ?

- Créer malgré tout dynamiquement la classe en code managé ?

Utiliser les langages dynamiques serait une solution idéale (et pour moi la meilleure à terme). Malheureusement, nous sommes encore au stade de la Beta côté siverlight et DSL, et il est aujourd'hui encore difficile de créer dynamiquement des contrôles Silverlight 2 Beta 2 dans ce contexte (pour le moment) car tout n'est pas encore figé dans le marbre :), je ne vais donc pas rentrer dans cette explication pour le moment (J'attends avec impatience quelques exemples de la part de l'équipe Silverlight sur ce sujet )

Alors, pouvons nous créer dynamiquement la classe dans notre code managé ?

Ma réponse est : OUI !

Comment générer des objets typés dynamiquement en .NET

Après quelques recherches (INTENSIVES est le mot juste), j'ai trouvé un post excellent de Vladimir Bodurov sous le thème : "How to generated dynamically typed objects in .NET" :

http://www.simple-talk.com/dotnet/.net-framework/dynamically-generating--typed-objects-in-.net/

Bingo ! Vladimir nous donne le code magique pour construire nos objets dynamiquement que nous pouvons simplement implémenter comme Datasource, sans coder de classe statique !

Ok, génial. Voyons maintenant comment j'ai utilisé ceci avec AgDatagrid (Vous pourrez très facilement convertir cet exemple avec le contrôle standard Datagrid de Silverlight 2, mais il n'est pas aussi classe et riche que le super contrôle de Devexpress :-) )

Allons-y messieurs dames, jetons un oeil dans le code !

Mon exemple est l'implémentation la plus basique possible afin de bien vous faire comprendre le principe

mon fichier Page.xaml est plutôt simple :

agdynamic2

J'ai posé 2 Textboxes (Une pour les noms de colonnes, une pour les données), un bouton pour construire la datagrid, et la datagrid elle-même. Nous pourrions récupérer les données de l'extérieur, mais ceci est un autre sujet, je souhaite rester volontairement simpliste.

Voici le code XAML de la page:



   1: <UserControl x:Class="AgDatagridDynamicData.Page"   
   2:     xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"    
   3:     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"    
   4:     xmlns:ag="clr-namespace:DevExpress.Windows.Controls;assembly=DevExpress.AgDataGrid.v8.2"   
   5:     xmlns:local="clr-namespace:AgDatagridDynamicData"   
   6:     Width="800" Height="600">   
   7:     <Grid x:Name="LayoutRoot" Background="black">   
   8:         <Grid>   
   9:             <Grid.RowDefinitions>   
  10:                 <RowDefinition Height="Auto"></RowDefinition>   
  11:                 <RowDefinition Height="Auto"></RowDefinition>   
  12:                 <RowDefinition Height="Auto"></RowDefinition>   
  13:                 <RowDefinition Height="Auto"></RowDefinition>   
  14:             </Grid.RowDefinitions>   
  15:             <Grid.ColumnDefinitions>   
  16:                 <ColumnDefinition Width="Auto"/>   
  17:                 <ColumnDefinition Width="Auto"/>   
  18:             </Grid.ColumnDefinitions>   
  19:             <TextBlock Width="Auto" Grid.Row="0" Grid.Column="0" Text="Columns (Parse with ';'):" />                   
  20:             <TextBox x:Name="ColumnsData" Background="blackSmoke" Width="500" Grid.Row="0" Grid.Column="1" Text="Column1;Column2;Column3"></TextBox>   
  21:             <TextBlock Width="Auto" Grid.Row="1" Grid.Column="0" Text="Data (Parse Row with '*', cells with ';' :" />
  22:             <TextBox x:Name="CellsData" Background="blackSmoke" Width="500" Grid.Row="1" Grid.Column="1" Text="DataA1;DataA2;DataA3*DataB1;DataB2;DataB3"></TextBox>     
  23:             <Button Click="Button_Click" Grid.Row="2" Grid.ColumnSpan="2" Content="Generate AgDataGrid"></Button>   
  24:             <ag:AgDataGrid Width="800" Height="400" Grid.Row="3" Grid.ColumnSpan="2" AutoGenerateColumns="True" x:Name="mygrid">   
  25:                 </ag:AgDataGrid>   
  26:         </Grid>           
  27:     </Grid>   
  28: </UserControl>



Toute la subtilité est dans la page.xaml.cs, regardons la de plus près.

J'utilise seulement les données des textboxes du contrôle Silverlight (Vous verrez dans la version Widget pour PoshBoard comment vous y prendre pour aller chercher les données à l'extérieur)

J'utilise simplement du parsage de string pour construire ma Datagrid :
  • Les colonnes sont splittées avec un ";"
  • Les données sont splittées avec "*" pour chaque ligne, et dans ces lignes les cellules sont splittées avec un ";"
Bien sûr, il serait plus sage de plutôt utiliser XML pour définir ces données, mais restons dans notre optique de simplicité.

Colonne :

Column1;Column2;Column3

Données :

DataA1;DataA2;DataA3*DataB1;DataB2;DataB3*DataC1;DataC2;DataC3

Première chose, Je met directement la classe DataSourceCreator de Vladimir dans le namespace de mon contrôle, sans toucher à rien (C'est ce que j'apelle du code magique)

agdynamic1


Après avoir insérer le code pour appelé les différentes libs nécessaire à la classe de Vladimir(System.Reflection, etc...), définissons le code principal de ma page :

Voyons notre classe principale (UserControl ) :



   1: public partial class Page : UserControl   
   2:     {   
   3:     string Columns;   
   4:     string Data;   
   5:         public Page()   
   6:         {   
   7:             InitializeComponent();               
   8:         }   
   9:     
  10:         private void Button_Click(object sender, RoutedEventArgs e)   
  11:         {   
  12:     
  13:             mygrid.Columns.Clear();   
  14:             // Adding Columns to the AgDatagrid   
  15:             // With Column type added in the Columns string    
  16:             // we could use a Type checker and setting Column Type   
  17:             // from code behind :    
  18:             // mygrid.Columns.Add(new AgDataGridTextColumn() { FieldName = SingleColumn});     
  19:             Columns = ColumnsData.Text;   
  20:             Data = CellsData.Text;   
  21:             foreach (string SingleColumn in Columns.Split(';'))   
  22:             {              
  23:                 mygrid.AddColumn(SingleColumn);
  24:             }
  25:  
  26:             // Here we set the DataSource with a direct call to the function of
  27:             // Vladimir            
  28:             mygrid.DataSource = GenerateData().ToDataSource();        
  29:         }

Que fait-on ici ?

Rien de spécial dans la fonction Public Page() : J'initialise le composant (c'est le code par défaut)

Tout est dans la fonction "Button_Click" :



   1: private void Button_Click(object sender, RoutedEventArgs e)   
   2: {
   3:  
   4:     mygrid.Columns.Clear();
   5:     // Adding Columns to the AgDatagrid
   6:     // With Column type added in the Columns string 
   7:     // we could use a Type checker and setting Column Type
   8:     // from code behind : 
   9:     // mygrid.Columns.Add(new AgDataGridTextColumn() { FieldName = SingleColumn});  
  10:     Columns = ColumnsData.Text;
  11:     Data = CellsData.Text;
  12:     foreach (string SingleColumn in Columns.Split(';'))
  13:     {                
  14:         mygrid.AddColumn(SingleColumn);
  15:     }
  16:  
  17:     // Here we set the DataSource with a direct call to the function of
  18:     // Vladimir            
  19:     mygrid.DataSource = GenerateData().ToDataSource();        
  20: }


Premièrement, je vide la datagrid (sans ça, ma fonction ferait de l'ajout, pas de la création from scratch à chaque click)

Ensuite nous récupérons les données des 2 textbox, puis nous allons créer les colonnes :


   1: foreach (string SingleColumn in Columns.Split(';'))
   2: {
   3:     mygrid.Columns.Add(new AgDataGridTextColumn() { FieldName = SingleColumn });
   4: }


Je génère ici que des colonnes de type "texte". Nous pourrions très facilement définir le type dans la chaine de caractère pour implémenter tout ce que propose AgDatagrid, mais encore une fois je suis resté basique (La faute peut-être à mon pot Chez Octo d'hier) :

Ensuite, je défini la Datasource:


   1: mygrid.DataSource = GenerateData().ToDataSource(); 


Nous faisons appelle à la fonction GenerateDate dans la classe (J'ai juste récupéré l'exemple de Vladimir Que j'ai réadapté) :



   1: public IEnumerable<IDictionary> GenerateData()
   2: {
   3:     string[] dataSplit = Data.Split('*');
   4:     int RowNumber = dataSplit.Count();
   5:     string[] ColumnSplit = Columns.Split(';');
   6:     int ColumnNumber = ColumnSplit.Count();
   7:     for (var i = 0; i < RowNumber; i++)
   8:     {
   9:         string[] cellData = dataSplit[i].Split(';');
  10:         var dict = new Dictionary<string, object>();
  11:         for (var j = 0; j < ColumnNumber; j++)
  12:         {
  13:             dict[ColumnSplit[j]] = cellData[j];
  14:         }
  15:        yield return dict;                
  16:     }             
  17: }



tout ce passe dans ce code précis. comment cela fonctionne-t-il donc ? On déclare un dictionnaire et nous l'alimentons avec des séries Clés/valeurs :

Le clé est le nom de la colonne visée , value est le contenu de la cellule à ajouter. Comme vous le voyez on défini avec cette méthode dynamiquement le nom de colonne visée à partir de la référence de notre string Column :

dict[ColulmnSplit[J]] = ...

c'est ce procédé qui nous évite d'avoir a définir la classe, c'e'st tout simplement excellent.

Et c'est tout !

Comme vous pouvez le constater, il est très facile d'alimenter dynamiquemenet une Datasource avec ce procédé.

Pour aller plus loin, nous pourrions utiliser un format XML pour la récupération des données, choisir un type de données pour les colonnes en fonction du type fourni en entrée.... les possibilité sont infinies !

Un grand merci à Azret et l'équipe support de DevExpress pour leur aide réactive, et merci à Vladimir Bodurov pour son superbe exemple de code que je vous invite à consulter pour comprendre tout le mécanisme !

Des questions ? les commentaires sont à votre disposition !

AMUSEZ VOUS BIEN AVEC SILVERLIGHT ET AGDATAGRID !

1 commentaire:

Anonyme a dit…

Merci pour ce code qui simplifie la compréhension.
Une question tout de meme, comment peut-on alimenter une bdd avec les données modifiées dans la datagrid ?

Merci