Renaud Harduin
ADOMD.Net : Accès aux bases multidimensionnelles (et présentation avec DataGridView)
L'objet de cet article est de présenter comment se fait l'accès aux données de cube via ADOMD.Net qui est aux bases multidimensionnelles ce qu'est ADO
Par Renaud Harduin publié le 17/04/2006 à 22:44, lu 7286 fois, 5 pages
 4 | III - Présentation hiérarchique du Cellset dans le DataGridView
Téléchargez le code source - 111 Kb
III - Présentation hiérarchique du Cellset dans le DataGridView

Nous avons avancé dans la présentation du CellSet. On conviendra néanmoins que la présentation des données reste encore un peu plate. On préfèrera une présentation du type :



Ici, nous affichons les membres de dimensions/hiérarchie la première fois où nous les voyons mais nous ne les répétons pas de ligne en ligne (idem en colonne). Nous obtenons une impression d'arborescence (équivalent aux regroupements d'Excel).
Même si fondamentalement ce que je vais présenter n'a pas trait directement à ADOMD.Net, il me parait intéressant de détailler un algorithme qui exploite ces API pour obtenir ce type de résultat.
Dans ce qui suit je vous demande de vous laisser guider dans un premier temps avant que toutes les briques ne se mettent en place à la fin (c'est toujours comme ça)

3.1 – Header & Headers
Il s'agit donc de manipuler des header. Autant les définir en tant que classes :



Un Header est finalement une classe avec une Caption et une propriété IsNew qui nous informe si ce header est un nouveau Header auquel cas on l'affichera.
Headers est une liste de Header, donc « contient finalement » la liste des Caption.
Il s'agit donc d'identifier pour un axe donné la liste des lignes (colonnes) en terme de position pour les quel nous avons une nouvelle rupture en terme de Header, donc un nouveau Headers.

3.2 – Recherche de nouveaux Headers
Je propose la méthode :
private Dictionary<int, Headers> analyseHeaders(OlapInfoAxis  axisInfo, Axis  axis)

Cette méthode nous renvoie un dictionnary dont la clef sera un numéro de position pour lequel nous aurons identifié un nouveau Headers (donc une nouvelle séquence de Header)
Afin de bien comprendre le fonctionnement je vais raisonner implicitement sur l'axe ligne. Cette méthode fonctionne de même sur un axe colonne.

private Dictionary<int, Headers> analyseHeaders(OlapInfoAxis  axisInfo, Axis  axis)
{
    Dictionary<int, Headers>  dict = new Dictionary<int,Headers>();
    bool isNew = false;

    Headers previousNewHeaders = new Headers();
    
    for (int i = 0; i <  axisInfo.Hierarchies.Count; i++)
    {
        Header hdr = new Header();
        hdr.Caption = null;
        hdr.IsNew = true;

        previousNewHeaders.Add(hdr);
    }

Commentaire :
Nous créons ici un objet previousNewHeaders que l'on comparera de ligne en ligne avec le Headers en cours. Tous ses header sont null en terme de hdr.Caption et nouveaux (hdr.IsNew = true)

for (int position = 0; position <  axis.Positions.Count; position++)
{
    // Step 1
    isNew = false;
    Headers tempHeaders = new Headers();
    for (int hierCount = 0; hierCount <  axisInfo.Hierarchies.Count-1; hierCount++)
    {
        Header hdr = new Header();
        hdr.Caption =  axis.Positions[position].Members[hierCount].Caption;
        if ( !hdr.Caption.Equals(previousNewHeaders[hierCount].Caption))
        {
            isNew = true;
            hdr.IsNew = true;
        }
        tempHeaders.Add(hdr);
    }

Commentaire :
Nous parcourons chaque position (ligne) de l'axe en créant un Headers temporaire.
Pour une position (ligne) donnée nous examinons les métadonnée (itération dans axisInfo.hiérarchies). Pour chaque Hierarchy (compteur hierCount) nous créons un header (hdr). Nous comparons la Caption de ce header à la Caption du header de même ordre (hierCount) dans le previousNewHeaders. Si ils sont différents, il s'agit alors d'un nouveau header (hdr.IsNew) et nous mettons en plus un « autre » booléen IsNew = true au vol.

        // Step 2
        Header lastHeader = new Header();
        lastHeader.Caption =  axis.Positions[position].Members[ axisInfo.Hierarchies.Count-1].Caption;
        lastHeader.IsNew = true;
        tempHeaders.Add(lastHeader);

        // Step 3
        if (isNew)
        {
             dict.Add(position, tempHeaders);
            previousNewHeaders = tempHeaders;
        }
    }


    return  dict;
}
#endregion

Commentaire :
Pour comprendre la logique du code je vous demande de sauter au step 3. A cette étape, si la variable isNew a été affectée à true (lors d'un test sur l'un quelquonque des header), on a une nouvelle rupture.
En conséquence, on ajoute un nouvelle élément au dict dont la clef est la position en cours et la valeur le Headers que nous avons construit. En sortie de cette méthode nous aurons une « liste » de positions pour lesquels nous identifions de nouveaux Headers (et les headers associés).
J'en reviens au step 2. Peut être aurez vous remarqué que la boucle sur les hiérarchies se faisait sur hierCount < axisInfo.Hierarchies.Count-1 et non pas sur ; hierCount < axisInfo.Hierarchies.Count.
Si je laissais courir la boucle jusqu'à axisInfo.Hierarchies.Count, l'algorithme aurait identifié toutes les positions (lignes) comme étant nouvelle puisque par définition le dernier membre change à chaque fois (c'est le niveau le plus fin).
Ceci dit, j'ai tout de même besoin de la Caption du dernier membre et c'est pour cela que je crée un lastHeader (qui est IsNew à chaque fois) que j'ajoute à mon tempHeaders.

La routine suivante vous permet de déplier le dictionnary :

StringBuilder sb = new StringBuilder();
foreach (int key in rowDict.Keys)
{
    sb = new StringBuilder();
    sb.Append(" Key ");
    sb.Append(key);

    Headers tempHeaders = rowDict[key];

    foreach (Header hdr in tempHeaders)
    {
        sb.Append(" ");
        sb.Append(hdr.Caption);
        if (hdr.IsNew) { sb.Append(" Is New "); }
        else { sb.Append(" is not new "); }
    }
    Debug.WriteLine(sb.ToString());
}

Le résultat sur l'axe ligne :
Key 0 No Discount Is New  Specialty Bike Shop Is New  Accessories Is New 
Key 3 No Discount is not new  Value Added Reseller Is New  Accessories Is New 
Key 6 No Discount is not new  Warehouse Is New  Accessories Is New 
Key 9 Reseller Is New  Specialty Bike Shop Is New  Accessories Is New 
Key 12 Reseller is not new  Value Added Reseller Is New  Accessories Is New 
Key 15 Reseller is not new  Warehouse Is New  Accessories Is New

3.3 – Présentation dans le DataGridView
La méthode ressemblera beaucoup à la précédente :

private void displayCellSetHierarchy(CellSet  cs)
{
    int columnPosition;
    int rowPosition;

    OlapInfo olapInfo =  cs.OlapInfo;
    OlapInfoAxis columnAxisInfo = olapInfo.AxesInfo.Axes[0];
    OlapInfoAxis rowAxisInfo = olapInfo.AxesInfo.Axes[1];

    Axis columnAxis =  cs.Axes[0];
    Axis rowAxis =  cs.Axes[1];

    int colOffset = rowAxisInfo.Hierarchies.Count;
    int rowOffset = columnAxisInfo.Hierarchies.Count;
    

    int numberOfColumns = rowAxisInfo.Hierarchies.Count +  cs.Axes[0].Positions.Count;
    int numberOfRows = columnAxisInfo.Hierarchies.Count +  cs.Axes[1].Positions.Count;

    // 1 - DGV Setup
    DgvCube.ColumnCount = numberOfColumns;
    DgvCube.RowCount = numberOfRows;
    DgvCube.ColumnHeadersVisible = false;
    DgvCube.RowHeadersVisible = false;

    DgvCube.Columns[colOffset-1].Frozen = true;
    DgvCube.Rows[rowOffset- 1].Frozen = true;

Commentaire:
Petit raffinement, on peut figer une ligne et/ou une colonne à la manière d'Excel avec .Frozen.
  // 2 - Information Analysis
  Dictionary<int, Headers> rowDict = analyseHeaders(rowAxisInfo, rowAxis);
  Dictionary<int, Headers> colDict = analyseHeaders(columnAxisInfo, columnAxis);


  StringBuilder sb = new StringBuilder();
  foreach (int key in rowDict.Keys)
  {
      sb = new StringBuilder();
      sb.Append(" Key ");
      sb.Append(key);

      Headers tempHeaders = rowDict[key];

      foreach (Header hdr in tempHeaders)
      {
          sb.Append(" ");
          sb.Append(hdr.Caption);
          if (hdr.IsNew) { sb.Append(" Is New "); }
          else { sb.Append(" is not new "); }
      }
      Debug.WriteLine(sb.ToString());
  }

  // 3 - Column Header Presentation
  int columnHierNumber = columnAxisInfo.Hierarchies.Count;
  for (columnPosition = 0; columnPosition < columnAxis.Positions.Count; columnPosition++)
  {
      for (int hierCount = 0; hierCount < columnHierNumber - 1; hierCount++)
      {
          if (colDict.ContainsKey(columnPosition))
          {
              Header header = colDict[columnPosition][hierCount];
              if (header.IsNew)
              {
                  DgvCube[columnPosition + colOffset, hierCount].Value = header.Caption;
              }
          }
          DgvCube[colOffset + columnPosition, hierCount].Style.BackColor = Color.SteelBlue;
          DgvCube[colOffset + columnPosition, hierCount].Style.ForeColor = Color.White;
      }

      DgvCube[colOffset + columnPosition, columnHierNumber-1].Style.BackColor = Color.SteelBlue;
      DgvCube[colOffset + columnPosition, columnHierNumber-1].Style.ForeColor = Color.White;
      DgvCube[colOffset + columnPosition, columnHierNumber-1].Value = columnAxis.Positions[columnPosition].
          Members[columnHierNumber - 1].Caption;
  }


Commentaire :
On analyse donc les lignes et les colonnes pour obtenir 2 Dictionnary.
Si je m'intéresse à la restitution des entêtes de colonne ; on itère dans les position de colonnes (for columnPosition ...).
Pour chaque position, on itère dans les hiérarchies (hierCount). (step 3.1)
A chaque fois , on vérifie si le rowDict contient la clef columnPosition (step 3.2). Si c'est le cas, on a une nouvelle rupture qu'il faut restituer. Bien que l'on soit une nouvelle rupture, on affichera la valeur d'un Header que si et seulement si il est considéré comme header.IsNew. (Step 3.3)
Dernier détail, comme je l'indiquais, on restitue systématiquement les membres les plus fins des hiérarchies (cf les 3 dernières lignes). (step 3.4)

// 4 - Row Header and Data loading
int rowHierNumber = rowAxisInfo.Hierarchies.Count;

for (rowPosition = 0; rowPosition < rowAxis.Positions.Count; rowPosition++)
{
    // 4.1 - Header drawing if necessary
    for (int hierCount = 0; hierCount < rowHierNumber - 1; hierCount++)
    {
        if (rowDict.ContainsKey(rowPosition))
        {
            if ( rowDict[rowPosition][hierCount].IsNew)
            {
                DgvCube[hierCount, rowPosition + rowOffset].Value = rowAxis.Positions[rowPosition].
                    Members[hierCount].Caption;
            }
        }
        DgvCube[hierCount, rowPosition + rowOffset].Style.BackColor = Color.SteelBlue;
        DgvCube[hierCount, rowPosition + rowOffset].Style.ForeColor = Color.White;
    }

    // 4.2 - Last hiearchy member is systematically  written
    DgvCube[rowHierNumber - 1, rowPosition + rowOffset].Style.BackColor = Color.SteelBlue;
    DgvCube[rowHierNumber - 1, rowPosition + rowOffset].Style.ForeColor = Color.White;
    DgvCube[rowHierNumber-1, rowPosition + rowOffset].Value = rowAxis.Positions[rowPosition].
        Members[rowHierNumber-1].Caption;


    // 4.3 - Data loading
    for (columnPosition = 0; columnPosition < columnAxis.Positions.Count; columnPosition++)
    {
        DgvCube[columnPosition + colOffset, rowPosition + rowOffset].Value =  cs.Cells[columnPosition, rowPosition].
            FormattedValue;
    }
 }

 DgvCube.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.AllCells;

}

Commentaire :
On effectue exactement le même traitement que précédemment, mais cette fois ci sur l'axe des lignes, « mixé » avec le chargement des données.
3.4 – Fermeture de la connexion
Même si cela parait trivial, il faut refermer la connexion avec une méthode dans la Façades :
public void Close()
{
    _mdConn.Close();
}

 
» Démarrer une discussion