|
display_clientdataset_xml - John COLIBRI.
|
- mots clé:affichage du contenu d'un tClientDataset - dbExpress - MyBase
- format .XML - analyseur lexical et syntaxique - encapsulation de
tStringList
- logiciel utilisé: Windows XP, Delphi 6.0
- matériel utilisé: Pentium 1400Mhz, 256 M de mémoire
- champ d'application: Delphi 1 à 6 sur Windows, Kylix
- niveau: utilisateur de tClientDataset
- plan:
1 - Introduction
Nous allons présenter ici un utilitaire qui affiche le contenu d'un
tClientDataset.
Un tClientDataset est un composant qui gère des données sous forme de table en
mémoire. Ces données peuvent provenir:
- soit d'une création par code
- soit d'un Serveur de bases de données (Oracle, Interbase, MySql, Sql
Serveur...)
- soit d'un fichier disque ayant un format particulier
Ce composant permet la "gestion nomade" de tables:
- l'utilisateur récupère des données du Serveur distant le matin sur son
portable
- sauve ces données sur son disque dans un fichier
- chez ses clients, il rallume son portable, recharge les données, effectue
des modification (ajout, effacement, modification) et sauvegarde ses données
sur disque
- le soir, de retour au bureau, il recharge les données et met à jour le
Serveur distant
Le composant permet aussi de gérer des tables indépendantes sans avoir à
utiliser aucun moteur de base de données (le tClientDataset sait sauver et
relire des données dans un fichier disque local).
Ce qui nous intéresse ici est l'affichage du contenu des données d'un
tClientDataset.
2 - Principe du stockage
2.1 - Analyse mémoire et Analyse disque
Le tClientDataset stocke les lignes de la Table dans une structure mémoire.
Le source de tClientDataset n'a pas été publié par Borland. Lorsque Kylix et
Delphi 6 ont été lancés, j'ai beaucoup travaillé sur la toute nouvelle
architecture dbExpress dont le tClientDataset est un des composants
essentiels. J'avais alors analysé le contenu mémoire en utilisant des dumps
directs de la mémoire (les INTERFACE permettent de récupérer l'adresse de
l'objet et donc en analysant la mémoire il est possible de retrouver la
structure). Ces techniques permettaient aussi d'analyser les paquets de données
échangées entre le tClientDataset et le DatasetProvider (envoi du schéma et
des contraintes, envoi des données, envoi des mises à jour, envoi de la liste
des erreurs).
Il existe toutefois une façon plus simple d'analyse des données d'un
tClientDataset: il suffit de sauvegarder ses donnée sur disque et d'analyser
les données sur disque. Le format n'est bien sur pas identique (en mémoire, les
données sont sous forme de colonnes, sur disque, elles sont sous forme de
lignes). Mais puisque le tClientDataset sait relire les données du disque sans
perte de substance, l'analyse des données du fichier fournit une image fidèle
de ce que contient les données en mémoire.
2.2 - Intérêt
Quel intérêt y a-t-il à analyser les données d'un tClientDataset ?
Eh bien, comparé à des Tables relationnelles, ce composant a un comportement
très "riche": il peut annuler entièrement ou partiellement des modifications
effectuées auparavant, utiliser des clones etc. La bonne connaissance du
contenu des données facilite alors grandement l'apprentissage et la
compréhension du fonctionnement du tClientDataset.
2.3 - Les formats disques
Le tClientDataset peut être sauvegardé sur disque sous format binaire ou .XML.
Nous allons analyser le format .XML qui est plus simple à présenter.
3 - Création du fichier .XML
3.1 - Création des données
La création d'une nouvelle table est très simple:
- nous définissons chaque champ en appelant:
FieldDefs.Add
- la table est créée par
CreateDataset
- nous ajoutons des données par
AppendRecord
Par conséquent:
|
créez une nouvelle application et appelez-la "display_cds_xml"
|
|
sélectionnez dans la page "dbExpress" de la Palette le composant
ClientDataset:
et posez-le sur la tForm
|
|
sélectionnez dans la page "Data Access" de la Palette le composant
DataSource:
et:
- posez-le sur la tForm
- sélectionnez sa propriété DataSet et placez-y ClientDataset1
|
|
sélectionnez dans la page "Data Control" de la Palette le composant
dbGrid:
et:
- posez-le sur la tForm
- sélectionnez sa propriété DataSource et placez-y DataSource1
|
|
placez un tButton "create_" sur la Forme et créez sa méthode OnClick.
Placez-y les instructions de création des données de ClientDataset. Dans
notre cas nous créons une table contenant quelques articles de ce site:
procedure TForm1.create_Click(Sender: TObject);
begin
with ClientDataset1 do
begin
Close;
FieldDefs.Clear;
with FieldDefs do
begin
Add('m_id', ftInteger, 0);
Add('m_titre', ftString, 20);
Add('m_date', ftDate, 0);
end; // with FieldDefs
CreateDataSet;
AppendRecord([1, 'Site Editor', '1/9/2001']);
AppendRecord([1, 'Pascal To Html', '1/10/2001']);
AppendRecord([1, 'Blobs Interbase', '2/1/2002']);
AppendRecord([1, 'CGI Form', '1/9/2002']);
AppendRecord([1, 'Find USES', '5/6/2003']);
AppendRecord([1, 'Tutorial Interbase', '5/3/2004']);
AppendRecord([1, 'Dump INTERFACE', '5/6/2004']);
AppendRecord([1, 'Interprète d''expression', '5/9/2004']);
AppendRecord([1, 'ISO CD Extractor', '6/9/2004']);
AppendRecord([1, 'GOF Design Patterns', '3/9/2004']);
AppendRecord([1, 'dbExpress Interbase', '15/10/2004']);
Open;
end; // with ClientDataset1
end; // create_Click
|
|
|
compilez, exécutez, et cliquez le bouton:
|
Pour sauvegarder les données:
Nous pouvons examiner le contenu du fichier .XML en cliquant sur ce fichier
dans un Explorateur Windows: comme .XML est associé à Internet Explorer (ou
tout autre Browser que vous utilisez), nous voyons apparaître les données
contenues dans ce fichier:
3.2 - Chargement
Nous pouvons bien sûr recharger le tClientDataset en utilisant LoadFromFile.
ATTENTION: le format des fichiers utilisés par le tClientDataset est spécial.
Vous ne pouvez pas charger n'importe quel .XML dans un tClientDataset.
Dans notre cas:
|
sélectionnez dans la page "Data Control" de la Palette le composant
FileListBox:
et:
- posez-le sur la tForm
- sélectionnez sa propriété Mask et placez-y la valeur *.XML
- créez son événement OnClick et chargez les données dans
ClientDataset1:
procedure TForm1.FileListBox1Click(Sender: TObject);
var l_file_name: String;
begin
with FileListBox1 do
l_file_name:= Items[ItemIndex];
ClientDataSet1.LoadFromFile(l_file_name);
end; // FileListBox1Click
|
|
|
compilez et exécutez
|
|
l'application présente les fichiers .XML disponibles. Cliquez sur l'un
d'entre eux: le ClientDataset charge les données
|
3.3 - Modifications de données
Pour nous placer dans le cas d'utilisation général d'un tClientDataset, nous
allons modifier les données qu'il contient. Nous pouvons le faire en tapant
dans le dbGrid, mais pour pouvoir répéter les séquences, nous allons utiliser
du code:
|
placez un tButton "delete_" sur la Forme et créez sa méthode OnClick.
Placez-y les instructions d'effacement de la ligne 4 de ClientDataSet1:
procedure TForm1.delete_5_Click(Sender: TObject);
begin
with ClientDataset1 do
begin
First;
Next; Next; Next; Next;
Delete;
end; // with ClientDataset1
end; // delete_4_Click
|
|
|
placez un tButton "append_" sur la Forme et créez sa méthode OnClick.
Placez-y les instructions d'ajout d'une ligne à ClientDataSet1:
procedure TForm1.append_Click(Sender: TObject);
begin
with ClientDataset1 do
begin
AppendRecord([112, 'display ClientDataset .XML', '16/10/2004']);
end; // with ClientDataset1
end; // append_Click
|
|
|
placez un tButton "modify_interbase_" sur la Forme et créez sa méthode
OnClick. Placez-y les instructions qui vont modifier le mois de l'article
"Interbase Tutorial":
procedure TForm1.modify_interbase_Click(Sender: TObject);
var l_year, l_month, l_day: Word;
begin
with ClientDataset1 do
begin
if Locate('m_titre', 'Tutorial Interbase', [])
then begin
DecodeDate(FieldByName('m_date').AsDateTime, l_year, l_month, l_day);
Inc(l_month);
if l_month= 13
then begin
l_month:= 1;
Inc(l_year);
end;
Edit;
FieldByName('m_date').AsDateTime:= EncodeDate(l_year, l_month, l_day);
Post;
end;
end; // with ClientDataset1
end; // modify_interbase_Click
|
|
|
compilez et exécutez
|
Nous allons effectuer les trois types de modifications sur notre
tClientDataset:
3.4 - Le contenu du fichier
En comparant les deux fichiers .XML, il est aisé de voir que:
- le fichier comporte une partie méta-donnée et une partie lignes
- les méta données comportent
- la définition de chaque champ
- le journal des modifications de chaque ligne sous forme de triplets
contenant:
- un numéro de ligne (débutant à 1 et, dans notre cas jusqu'à 15)
- le numéro de la version précédente de cette ligne en cas de
modification
- un code de changement:
- 4 pour un ajout
- 8 pour une modification
- les lignes sont codées ainsi:
- le nom du champ et sa valeur
- optionnellement, une valeur RowState
Dans notre cas, comme toutes les lignes ont été créée, RowState est
toujours présente. Mais si nous avions importé des données du Serveur
Interbase, cette valeur RowState n'est pas présente pour les lignes
importées (cf l'article Interbase dbExpress
)
Nous allons à présent construire un utilitaire qui relit ce fichier .XML pour
nous présenter les lignes et leur histoire.
4 - Analyze du fichier .XML
4.1 - Principe
Nous allons procéder en deux temps:
- une première unité va analyser le fichier .XML et afficher son contenu en
reformatant les lignes
- une seconde unité va contenir une classe qui contient les données de notre
tClientDataset: la liste des lignes et pour chaque ligne, la liste des
valeurs
4.2 - Analyse .XML
Le fichier est composé essentiellement de balises "< xxx >" et de retours
chariot. Les balises sont de plusieurs type:
- les balises "<xxx>" et "</xxx>"
- les balises "<xxx yyyy zzz/>
Et dans chaque balise se trouve
- la clé, comme METADATA FIELD ROW etc
- éventuellement des paramètres qui sont au format "nom="valeur""
La classe c_cds_analyze_xml:
- charge le fichier dans un tampon mémoire
- analyse chaque élément du fichier en isolant les balises et les retours
chariot
- pour les balises:
- si elles contiennent des paramètres, ceux-ci sont extraits
- si la balise est une balise double, nous récursons pour retrouver les
balises encapsulées
Voici quelques extraits
- la fonction qui extrait les symboles:
function f_get_symbol_recursive: String;
// .. handle_opening_bracket and handle_return
begin // f_get_symbol_recursive
Result:= '';
if m_buffer_index>= m_buffer_size
then exit;
repeat
case m_pt_buffer[m_buffer_index] of
'<' : handle_opening_bracket(l_exit);
chr(13) : handle_return;
else handle_other;
end; // case
until (m_buffer_index>= m_buffer_size) or (Result<> '');
end; // f_get_symbol_recursive
|
- la procédure récursive qui analyse les balises:
procedure handle_opening_bracket(var pv_exit: Boolean);
// -- ... here analyze_tag etc
begin // handle_opening_bracket
pv_exit:= False;
// -- skip <
inc(m_buffer_index);
if m_pt_buffer[m_buffer_index]= '/'
then begin
l_slash:= '/';
inc(m_buffer_index);
end
else l_slash:= '';
l_start_index:= m_buffer_index;
repeat
inc(m_buffer_index);
until (m_buffer_index>= m_buffer_size)
or (m_pt_buffer[m_buffer_index] in k_tag_end);
l_length:= m_buffer_index- l_start_index;
if l_length> 0
then begin
l_tag:= f_extract_string_start_end(l_start_index, m_buffer_index- 1);
l_tag:= l_slash+ UpperCase(l_tag);
Result:= l_tag;
// -- get command parameters
if m_pt_buffer[m_buffer_index]<> '>'
then begin
l_parameters:= '';
l_start_index:= m_buffer_index;
repeat
if m_pt_buffer[m_buffer_index]= '"'
then skip_apostroph
else inc(m_buffer_index);
until (m_buffer_index>= m_buffer_size)
or (m_pt_buffer[m_buffer_index] in ['>']);
l_parameters:= f_extract_string_start_end(l_start_index, m_buffer_index- 1);
if l_tag= 'PARAMS'
then analyze_change_log_params(l_parameters);
end; // parameters
// -- skip >
if m_pt_buffer[m_buffer_index]= '>'
then begin
inc(m_buffer_index);
if l_slash= '/'
then Result:= l_tag
else analyze_tag(l_tag, l_parameters);
end
else display('*** could not find >');
end
else begin
l_tag:= '';
display('*** tag is void');
end;
end; // handle_opening_bracket
|
- lorsque la procédure précédente arrive au caractère ">" elle appelle la
procédure qui examine le contenu d'une balise:
procedure analyze_tag(p_tag, p_parameters: String);
var l_row_string, l_log_list_string: String;
l_row_index: Integer;
begin
add_line;
if p_tag= 'ROW'
then begin
l_row_string:= f_spaces(l_indentation);
l_row_string:= l_row_string+ IntToStr(l_row_number+ 1);
// -- find if a CHANGE_LOG entry has this number
l_row_index:= l_c_merge_log_line_number_list.IndexOf(IntToStr(l_row_number+ 1));
If l_row_index<> -1
then l_log_list_string:= l_c_merge_log_list[l_row_index]
else l_log_list_string:= '';
l_row_string:= l_row_string+ ' ('+ l_log_list_string+ ') :';
l_row_string:= l_row_string+ ' <'+ p_tag+ p_parameters+ '>';
add(l_row_string);
// -- also add to the c_table structure
if p_c_table<> Nil
then p_c_table.f_c_add_line(l_row_number, l_log_list_string, p_parameters);
Inc(l_row_number);
end
else add(f_spaces(l_indentation)+ '<'+ p_tag+ p_parameters+ '>');
add_line;
if (p_tag<> 'ROW') and (p_tag<> 'FIELD')
then Inc(l_indentation, 2);
add(f_spaces(l_indentation));
// -- recurse
Result:= f_get_symbol_recursive;
if Result<> '/'+ p_tag
then begin
// -- backup
pv_exit:= True;
if (p_tag<> 'ROW') and (p_tag<> 'FIELD')
then Dec(l_indentation, 2);
end
else begin
add_line;
Dec(l_indentation, 2);
// -- ?? assume that the </xxx> has never any params
add(f_spaces(l_indentation)+ '<'+ Result+ '>');
add_line;
add(f_spaces(l_indentation));
// -- look for the next symbol or tag
Result:= f_get_symbol_recursive;
end;
end; // analyze_tag
|
Les procédures add_line et add_text ajoute le texte dans une tStringList
appelée m_c_result_list, que nous pouvons réafficher dans le programme
appelant.
De plus si nous recontrons la balise PARAMS CHANGE_LOG, nous décortiquons
les triplets pour pouvoir les placer au début de leur ligne respective.
Incorporons donc ce traitement dans notre programme:
|
placez un tButton "analyze_xml_" sur la Forme et créez sa méthode
OnClick. Placez-y les instructions qui appelle l'analyse du fichier:
procedure TForm1.analyze_xml_Click(Sender: TObject);
begin
with c_cds_analyze_xml.create_cds_analyze_xml('xml', 'resu.xml') do
begin
analyze_xml;
display_line;
display_strings(m_c_result_list);
Free;
end; // with c_analyze_xml
end; // analyze_xml_Click
|
|
|
compilez et exécutez. Cliquez "analyze_xml_"
|
|
le programme affiche:
Vous constatez que nous avons bien replacé les triplets en début de chacune
des lignes.
|
4.3 - Stockage de la table dans une structure
Nous avons aussi créé une structure qui stocke les données de la table dans une
structure de lignes / colonnes.
La structure est composée d'une tStringList de lignes, chaque ligne contenant
une tStringList de colonnes, chaque cellule étant un objet (ne contenant dans
la version actuelle que le texte contenu dans la cellule).
La définition des 3 classes est la suivante:
type
c_column= Class(c_basic_object)
// -- m_name: the content of the cell
Constructor create_column(p_name: String);
Destructor Destroy; Override;
end; // c_column
c_table= Class; // forward
c_line= // one "line"
Class(c_basic_object)
// -- m_name: the parameters of "<ROW ... >"
m_c_parent_table: c_table;
m_c_column_list: tStringList;
m_line_number, m_previous_line_number, m_update_code: Integer;
Constructor create_line(p_name: String;
p_c_parent_table: c_table;
p_line_number, p_previous_line_number, p_update_code: Integer);
function f_column_count: Integer;
function f_c_column(p_column_index: Integer): c_column;
function f_index_of(p_column_name: String): Integer;
function f_c_find_by_column(p_column_name: String): c_column;
procedure add_column(p_column_name: String; p_c_element: c_column);
function f_c_add_column(p_column_name: String): c_column;
procedure display_column_list;
Destructor Destroy; Override;
end; // c_line
c_table= // "line" list
Class(c_basic_object)
m_c_line_list: tStringList;
// -- the header of each column
m_c_column_name_list: tStringList;
Constructor create_table(p_name: String);
function f_line_count: Integer;
function f_c_line(p_line_index: Integer): c_line;
function f_index_of(p_line_name: String): Integer;
function f_c_find_by_line(p_line_name: String): c_line;
procedure add_line(p_line_name: String; p_c_line: c_line);
function f_c_add_line(p_row_number: Integer; p_log_list_string, p_parameters: String): c_line;
procedure display_formatted_line_list;
Destructor Destroy; Override;
end; // c_table
|
Il s'agit donc d'une encapsulation à 2 niveaux d'une tStringList (cf.
le squelette d'une tStringlist ).
Nous utilisons cette structure de la façon suivante:
- le programme principale crée un objet de type c_table
- cet objet est passé comme paramètre à c_cds_analyze_xml.analyze_xml
- chaque fois que c_cds_analyze_xml.analyze_tag a récupéré une balise, la
procédure c_table.f_c_add_line est appelée en fournissant le numéro de la
ligne, le triplet, les paramètres
- c_table.f_c_add_line crée alors une nouvelle ligne, puis décompose les
paramètres pour en extraire le titre de la colonne et la valeur de la
colonne
- c_table peut alors réaliser un affichage formaté:
- les titres des colonnes sont affichés une fois uniquement
- la taille maximale du texte de chaque colonne est calculé
- pour chaque ligne, nous affichons
- son numéro
- les codes de modifications et RowState
- la liste des valeurs
Par conséquent
|
placez un tButton "display_table_" sur la Forme et créez sa méthode
OnClick. Placez-y les instructions qui créent une c_table, appelle
l'analyse du fichier, et affiche le résultat:
procedure TForm1.display_table_Click(Sender: TObject);
var l_c_table: c_table;
begin
with c_cds_analyze_xml.create_cds_analyze_xml('xml', 'resu.xml') do
begin
l_c_table:= c_table.create_table('table');
analyze_xml(l_c_table);
display_line;
l_c_table.display_formatted_line_list;
l_c_table.Free;
Free;
end; // with c_analyze_xml
end; // display_table_Click
|
|
|
compilez et exécutez. Cliquez "display_table_"
|
|
le programme affiche:

|
Le dernier affichage montre que:
- la ligne 5 comporte bien une signature RowState d'effacement
- rien ne distingue l'ajout de la ligne 12 des 11 lignes précédentes chargées
depuis le fichier .XML
- les lignes modifiées sont chaînées:
- la ligne 13 désigne la ligne 6
- la ligne 14 désigne la ligne 13
- la ligne 15 désigne la ligne 14
De plus les valeurs de RowState sont différentes pour la ligne originale
(6), les lignes intermédiaires (13 et 14) et la dernière valeur connue (15)
5 - Améliorations
L'analyze du text .XML date de nos premières explorations de dbExpress /
Midas sous Linux. Cela se voit: la récursion semble mal emboîtée. Si je devais
réécrire le programme, je partirais d'une grammaire .XML correspondant aux
fichiers Borland, et je laisserais le Con-Compilateur mettre en place
l'analyseur syntaxique.
Les classes de stockage sont aussi un peu trop grosses. Actuellement nous
pourrions nous passer de la classe c_column. De même, nous avons utilisé des
tStringList, alors que des tList auraient suffi. Encore que nous pourrions
utiliser les chaînes pour trier les lignes par exemple (chaque enregistrement
et ses versions modifiées pourraient être regroupés).
En attendant le programme marche, et il avait surtout été sorti de la
naphtaline pour afficher le contenu d'un tClientDataset pour l'article
Interbase dbExpress.
Le module qui analysait directement le contenu mémoire d'un tClientDataset et
qui nous servait aussi à surveiller les paquets échangés entre un
tDatasetProvider et un tClientDataset n'a pas été publié pour le moment.
6 - Télécharger le source
Nous avons placé le projet dans un .ZIP qui comprend:
- le .DPR, la forme principale, les formes annexes éventuelles
- les fichiers de paramètres (le schéma et le batch de création)
- toutes les librairies (Unit) nécessaires (le .ZIP est autonaume)
Ce .ZIP contient des chemins RELATIFS. Par conséquent:
- créez un répertoire n'importe où sur votre machine
- placez le .ZIP dans ce répertoire
- dézippez et les sous-répertoires nécessaires seront créés
- compilez et exécutez
Au niveau sécurité:
- le .ZIP:
- ne modifie pas votre PC (pas de changement de la Base de Registre,
d'écrasement de DLL ou autre .BAT). Aucune modification de répertoire ou
de contenu de répertoire ailleurs que dans celui où vous dézippez
- ne contient aucun programme qui s'exécuterait à la décompression (.EXE,
.BAT, .SCR ou autre .VXD) ou qui seraient lancés plus tard (reboot)
- passez-le à l'antivirus avant décompression si vous êtes inquiets.
- le programme ne change pas la base de registre et ne modifie aucun autre
répertoire de votre machine
- pour supprimer le projet, effacez simplement le répertoire.
Voici le .ZIP:
Avec les mentions d'usage:
- j'apprécie tous les commentaires, remarques, critiques ou améliorations
- signalez-moi les bugs ou les inexactitudes que vous trouverez.
7 - L'auteur
John COLIBRI est surtout intéressé par la programmation en Pascal, Delphi et
Kylix. Son site contient des articles, la descriptions de ses
livres, le programme des stages qu'il anime tous les mois.
|