Encapsulation d'une interface IEnumVariant dans un itérateur

Laurent Dardenne

Pour énumérer des collections d'objets COM on peut utiliser l'interface IEnumVaraint dédiée à ce traitement.
À partir de Delphi 2005 il est possible d'utiliser un itérateur pour parcourir une collection d'objets.

Afin de simplifier l'écriture du code d'applications utilisant intensivement les collections d'objets COM, j'ai encapsulé l'usage de l'interface IEnumVariant au sein d'un itérateur.

Vous trouverez dans ce tutoriel l'implémentation de cette solution. ♪

- Titre : Encapsulation d'une interface IEnumVariant dans un itérateur

- Auteur : Laurent Dardenne

- Parution : 12 novembre 2005

- Mise à jour : 12 novembre 2005

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2005 Laurent Dardenne. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Public concerné

- I-A. Les sources

- II. L'interface IEnumVariant

- III. L'interface IDispatch

- IV. Un cas d'école

- IV-A. Exemple d'énumération avec IEnumVariant

- V. Les itérateurs Delphi

- V-A. Implémentation de la classe à énumérer

- V-A-1. Le champ FObjetCollection

- V-A-2. Les propriétés Item et NextItem

- V-A-3. La méthode GetEnumerator

- V-B. Implémentation de la classe énumérateur

- VI. Le résultat

- VII. IEnumVariant sous .NET

I - Public concerné

Testé avec Delphi 2005 sous XP Pro SP1.
Version 1.0

I-A - Les sources

Les fichiers sources des différents exemples :
FTP ;
HTTP.

II - L'interface IEnumVariant

Extrait du SDK

L'automation définit l'interface IEnumVariant afin de fournir une manière standard pour des clients ActiveX d'itérer les objets d'une collection. Chaque objet collection doit exposer une propriété en lecture seule appelée _NewEnum afin d'informer les clients de l'ActiveX que l'objet supporte l'itération. La propriété _NewEnum renvoie un objet énumérateur qui supporte IEnumVariant.

L'interface IEnumVariant fournit une manière d'itérer les éléments contenus dans un objet collection. Cette interface est supportée par un objet énumérateur qui est retourné par la propriété _NewEnum de l'objet collection, comme dans la figure suivante :

L'interface IEnumVariant définit les fonctions membres suivantes :

Nom de méthode

Description

Clone

Crée une copie de l'état courant de l'énumération

Next

Renvoie l'élément suivant dans l'ordre d'énumération

Resets

Réinitialise l'ordre d'énumération au premier élément

Skip

Modifie l'ordre d'énumération, saute les n prochains éléments

Pour le détail des membres, voir le SDK.

L'interface IEnumVariant est déclarée dans l'unité ActiveX.

III - L'interface IDispatch

À travers la méthode IDispatch.Invoke un client peut invoquer un nombre quelconque de méthodes.
Cette interface autorise l'invocation de méthode par leur nom, plutôt que par leur position dans la table des méthodes virtuelle de l'objet. Elle permet donc le « late binding » c'est-à-dire l'appel de méthodes connues lors de l'exécution, mais inconnues lors de la compilation.
Vous trouverez d'autres informations à son sujet dans ce tutoriel ou encore dans cette FAQ.

IV - Un cas d'école

Après ces quelques présentations des interfaces que nous allons utiliser tout au long de ce tutoriel, je vous propose un exemple permettant de récupérer les langages supportés par l'application Microsoft Word.

Mais avant d'aller plus loin, la première chose à savoir ici est 'est-ce que l'objet COM que je souhaite manipuler propose une collection ?'. Par convention Microsoft nomme au pluriel les objets collection, mais dans d'autres contextes, cette règle de nommage peut ne pas exister.
Dans ces cas-là, nous utiliserons le fichier de librairie (xxx_TLB.PAS) fourni ou extrait à l'aide de Delphi.

Recherchons dans le fichier Word2000.pas l'interface Languages :

Languages = interface(IDispatch)
['{0002096E-0000-0000-C000-000000000046}']
function Get__NewEnum: IUnknown; safecall;
...
property _NewEnum: IUnknown read Get__NewEnum;
...
end;

À partir du moment où l'interface hérite de IDispatch on retrouve la dispinterface associée :

LanguagesDisp = dispinterface
['{0002096E-0000-0000-C000-000000000046}']
property _NewEnum: IUnknown readonly dispid -4;
...
end;

Dans la déclaration de l'interface Languages, on retrouve bien la propriété _NewEnum qui nous indique la présence d'une collection IEnumVariant.
On peut donc continuer.

IV-A - Exemple d'énumération avec IEnumVariant

Voici l'exemple récupérant la collection des langages supportés par MS Word :

procedure WordlLangage;
var Word  : Variant;
Element: OleVariant;

IEnum : IEnumVariant;
Langue : Variant;
Nombre : LongWord;
begin
//Crée l'objet Automation Word
Word := CreateOleObject('Word.Application');

// Obtention de la liste des langues
IEnum:=IUnKnown(Word.Languages._NewEnum) as IEnumVariant;

Ici la variable Word est déclarée en tant que Variant, mais la propriété _NewEnum renvoie une interface IUnknown qui est l'ancêtre des interfaces (Elle est en quelque sorte le TObject pour les interfaces).
On doit donc dans un premier temps transtyper le variant en IUnknown pour éviter l'erreur suivante :

E2015 : Opérateur non applicable à ce type d'opérande.

Puis dans un second temps, transtyper l'interface IUnknown en IEnumVariant qui correspond à l'interface que l'on souhaite manipuler.

Voici un extrait de la documentation de Delphi 2005 concernant cette opération :

Interrogation d'interface

Vous pouvez utiliser l'opérateur as afin d'effectuer des transtypages d'interface avec vérification. Ce mécanisme
s'appelle l'interrogation d'interface ; il produit une expression de type interface depuis une référence d'objet
ou une autre référence d'interface à partir du type réel (à l'exécution) de l'objet. Une interrogation
d'interface a la forme suivante :
objet as interface
où object est une expression de type interface, de type variant ou désignant une instance d'une classe qui
implémente une interface, et où interface est une interface déclarée avec un GUID.

L'interrogation d'interface renvoie nil si object vaut nil. Sinon, elle transmet le GUID de interface à la méthode
QueryInterface de object et déclenche une exception sauf si QueryInterface renvoie zéro. Si QueryInterface renvoie
zéro (ce qui indique que la classe de object implémente interface), l'interrogation d'interface renvoie une
référence sur object.

Note : cette opération as sur une interface incrémente le compteur de référence.

L'appel de la méthode Next permet d'itérer sur la collection des langages, ici avec un pas de 1.

// Puis utilisation de l'énumérateur de collection fourni
While IEnum.Next(1,Element,Nombre)=S_OK Do
Begin
// Il est indispensable de transtyper la variable Element
// dans le type de la collection manipulée
Langue := IUnknown(Element) As Language;
// Accés aux informations
Writeln(IntToStr(Langue.ID),' ',Langue.Name,' ',Langue.NameLocal);
End;

Word.Quit;
Word := unassigned;
end;

Cette méthode renvoie en revanche une variable de type OleVariant. On doit donc ici aussi transtyper la variable avec le type d'interface que la collection héberge. On retrouve ce type dans le prototype de la méthode Languages.Item :

Languages = interface(IDispatch)
['{0002096E-0000-0000-C000-000000000046}']
...
function Item(var Index: OleVariant): Language; safecall;
...
end;

Voici un extrait de la documentation de Delphi 2005 concernant la différence entre le type Variant et OleVariant :

Le type OleVariant existe sur les deux plates-formes Windows et Linux. La différence principale entre Variant et
OleVariant est que Variant peut contenir des types de données que seule l'application en cours sait traiter.

OleVariant contient uniquement des types de données compatibles avec OLE Automation, ce qui signifie que ces
types de données peuvent être transférés entre programmes ou sur le réseau sans qu'il soit nécessaire de savoir si
l'autre extrémité saura manipuler les données.

V - Les itérateurs Delphi

Maintenant que nous avons abordé les problématiques de base liées à l'interface IEnumVariant, passons à l'implémentation de l'itérateur.

Vous trouverez ici un tutoriel sur les bases des itérateurs sous Delphi 2005 et supérieure.
Comme indiqué dans le chapitre 3.3 du précédent tutoriel cité, pour créer un itérateur nous devons implémenter deux classes :

- une classe sur la laquelle on souhaite itérer, ici il s'agit de la classeTEnumVariant ;

- une classe « itérateur » qui prend en charge les opérations d'itération sur la collection, ici il s'agit de la classe TEnumerateur.

Note : dans l'exemple la classe TCustomEnumerateurEnumVariant permet de spécialiser les classes dérivées.

type
// Conteneur générique pour une collection IEnumVariant
TCustomEnumerateurEnumVariant = class
private
// Contient la collection
FObjetCollection: IEnumVariant;
// Accés remote possible
FItem:  OleVariant;
public
Constructor Create(Enum : IDispatch);
Function GetNextItem:Boolean;
Function GetCollection(ACollection:IDispatch) :IEnumVariant;

Property Item:OleVariant read FItem;
Property NextItem:Boolean read GetNextItem;
end;

TEnumerateur = class;

// Conteneur spécialisé pour une interface implémentant IEnumVariant
TEnumVariant = class(TCustomEnumerateurEnumVariant)
//Méthode nécessaire pour la prise en charge de l'itération
Function GetEnumerator: TEnumerateur;
end;

// Énumérateur pour la classe conteneur.
// Il doit implémenter une suite de méthodes comme indiqué ci-dessous.
TEnumerateur = class
strict private
FListe : TEnumVariant; //Référence la collection concernée par l'itération
public
constructor Create (AList : TEnumVariant);
//Membre de classe nécessaire pour la prise en charge de l'itération
function GetCurrent: IUnknown;
function MoveNext :Boolean;    // Méthode publique
property Current : IUnknown read GetCurrent; // Propriété en Read Only
end;

Voyons maintenant le détail de l'implémentation.

V-A - Implémentation de la classe à énumérer

La classe TCustomEnumerateurEnumVariant permet d'encapsuler l'interface IEnumVariant.
Nous avons besoin de connaître les informations suivantes :

- la collection IEnumVariant, il s'agit du champ FobjetCollection ;

- l'élément courant, implémenté par la propriété Item ;

- l'élément suivant, implémenté par la propriété NextItem.

V-A-1 - Le champ FObjetCollection

Afin de pouvoir manipuler différents types d'interfaces implémentant IEnumVariant, il nous faut manipuler une interface IDispatch.
Mais du coup ce choix ne permet plus l'accès à la propriété _NewEnum de la manière suivante :

FObjetCollection:=(Enum._NewEnum) as IEnumVariant

car ce code provoque à la compilation l'erreur suivante :

E2003 : Identificateur non déclaré : '_NewEnum'

Comme on ne connaît plus le type de l'interface, via la librairie de type, on appellera dynamiquement la propriété _NewEnum commune aux interfaces implémentant IEnumVariant.

Voici donc le constructeur qui renseignera la collection :

Constructor TCustomEnumerateurEnumVariant.Create(Enum : IDispatch);
begin
Inherited;
FObjetCollection:=GetCollection(Enum);
end;

La méthode GetCollection appelle dynamiquement la propriété _NewEnum et renvoie la collection IEnumVariant attendue :

Function TCustomEnumerateurEnumVariant.GetCollection(ACollection : IDispatch):IEnumVariant;
Var VarResult: OleVariant ; // Résultat de la méthode Invoke
Params: TDispParams;    // Tableau de paramètre, ici 0
lProperty: Widestring;  // Contient le nom de la propriété à appeler
lDispID: Integer;       // Numéro de DispId de la propriété
Resultat: HResult;      // Résultat de l'appel COM
ExcepInfo: TExcepInfo;  // En cas d'erreur d'appel

begin
Result:=Nil;
lProperty:='_NewEnum';
FillChar(Params, SizeOf(DispParams),0);

// Recherche le numéro d'identificateurs de dispatch (dispID) de la propriété contenue dans lProperty
OLECheck(ACollection.GetIDsOfNames(GUID_NULL, @lProperty, 1,LOCALE_USER_DEFAULT, @ldispid)) ;

// Appel de la propriété de l'interface
Resultat:=ACollection.Invoke(ldispid,GUID_NULL,0,DISPATCH_PROPERTYGET,Params,@VarResult, @ExcepInfo,Nil);

// En cas d'erreur on doit lever une exception
if Resultat<> 0
then DispatchInvokeError(Resultat, ExcepInfo);

//Transtype le résultat obtenu en Collection de variant
Result := IUnKnown(VarResult) as IEnumVariant;
end;

Note : si l'interface ACollection n'implémente pas la propriété _NewEnum, l'appel de Invoke déclenchera l'exception EOleSysError : Nom inconnu.

Vous trouverez dans ce tutoriel le détail de l'appel de la méthode IDispatch.Invoke.

V-A-2 - Les propriétés Item et NextItem

Les deux autres informations nécessaires sont gérées dans la méthode GetNextItem :

Function TCustomEnumerateurEnumVariant.GetNextItem:Boolean;
var NombreElement : LongWord;
begin
Result:=(FObjetCollection.Next(1, FItem, NombreElement) = S_OK);
end;

On retrouve donc ici la méthode Next de l'interface IEnumVaraint. Elle récupère, dans la propriété FItem, la prochaine valeur de la collection FObjetCollection.
Cette fonction renvoie False si la fin de la collection est atteinte.

V-A-3 - La méthode GetEnumerator

Il ne nous reste plus qu'à implémenter la méthode GetEnumerator nécessaire pour la prise en charge de l'itération de notre nouvelle classe.

function TEnumVariant.GetEnumerator: TEnumerateur;
begin
Result := TEnumerateur.Create(Self);
end;

Cette méthode est appelée automatiquement par le compilateur et renvoie une instance de la classe itérateur TEnumerateur que nous allons aborder maintenant.

V-B - Implémentation de la classe énumérateur

La classe TEnumerateur à proprement dit ne fait que référencer la collection IEnumVariant et permet de la parcourir.
Le constructeur, appelé par la méthode GetEnumerator de la classe à itérer, renseigne la référence à la collection de la classe à énumérer.

constructor TEnumerateur.Create(AList: TEnumVariant);
begin
inherited;
FListe := AList;
end;

La méthode GetCurrent renvoie la dernière valeur lue dans la collection.

function TEnumerateur.GetCurrent: IUnknown;
begin
Result := FListe.Item;
end;

La méthode MoveNext se positionne sur la prochaine valeur si elle existe.

function TEnumerateur.MoveNext: Boolean;
begin
Result :=FListe.GetNextItem;
end;

Cette classe ne fait tout simplement qu'implémenter le pattern de collection imposé (prescribed collection pattern) en encapsulant la gestion de la collection hébergée dans notre classe à itérer.

On peut dorénavant énumérer cette collection dans une instruction For…in…do begin…end;, où :

- la partie in est liée à l'appel de la méthode TenumVariant.GetEnumerator ;

- la partie For est liée à l'appel de la méthode Tenumerateur.GetCurrent ;

- la partie do begin est liée à l'appel de la méthode Tenumerateur.MoveNext ;

- la partie end appelle le destructeur de la classe TEnumerateur.Destroy, ici TObject.

Sous .NET l'itérateur, ici TEnumerateur, doit implémenter l'interface IDisposable.

VI - Le résultat

Voici le code d'origine modifié :

procedure Erreur;
var Word  : Variant;
Element: IUnKnown;

IEnum :  TEnumVariant;
begin
try
Word := CreateOleObject('Word.Application');

// Obtention de la liste des langues
IEnum:=TEnumVariant.Create(Word.Languages);

// Puis utilisation de l'énumérateur de collection fourni
For Element in IEnum Do
Begin
With (Element As Language) do
Writeln(IntToStr(ID),' ',Name,' ',NameLocal);
End;
Finally
IEnum.Free;
Word.Quit;
Word := unassigned;
end;
end;

Bien évidemment comme dans cet exemple on utilise qu'une seule collection, l'apport de l'itérateur ne semble pas important.
En revanche dans le cas d'itération sur deux ou trois niveaux comme c'est le cas sous WMI, vous n'avez plus qu'à appeler le constructeur puis à itérer sur l'instance renvoyée.

Par exemple :

...
// Affecte un énumérateur pour la collection d'objets SWbemObjectSet
ObjectEnumerator:=TEnumVariant.Create(wmiService.InstancesOf('systemrestore',
wbemFlagReturnWhenComplete+wbemQueryFlagShallow,
nil));

For MonObjetWMI in ObjectEnumerator do
begin
// Affecte un énumérateur pour la collection d'objet SWbemPropertySet
PropertyEnumerator:= TEnumVariant.Create((MonObjetWMI as SWBemObject).Properties_);

For MaProprieteWMI in PropertyEnumerator do
begin
// Retrouve le nom du service via la propriété de classe nommée 'Name'.
With (MaProprieteWMI as SWBemProperty)  do
begin
If Name='CreationTime'
...

VII - IEnumVariant sous .NET

Le chargement du PIA Microsoft.Office.Interop.Word.dll dans l'utilitaire ILDasm nous permet de visualiser le Wrapper COM/Interop du serveur MS Word 2003.

Pour reprendre notre exemple, contrôlons l'interface Languages :

Ici l'interface Languages implémente la méthode GetEnumerator qui renvoie une interface IEnumerator.
IEnumerator est l'interface de base pour tous les énumérateurs et prend en charge une itération simple sur une collection. Elle contient les membres MoveNext et Current.

Sous DotNet pour qu'une classe ou une interface puisse itérer sur une collection, elle doit implémenter l'interface IEnumerable qui expose la méthode GetEnumerator.

Sous dotNet l'interface COM IEnumVariant est mappée sur l'interface System.Collection.IEnumerator.

Ce qui implique que notre gestion de l'itérateur n'est plus nécessaire, car déjà prise en charge par .NET :

procedure Erreur;
// Exemple précédent modifié
var msWord : TWordApplication; // L'application Word
Element: Language;
Empty  : TObject; // Pour les paramètres optionnels

begin
try
msWord := TWordApplication.Create;

// Puis utilisation de la collection native via System.Collection.IEnumerator
For Element in msWord.Languages Do
With Element do
Console.WriteLine('{0} {1} {2}',ID,Name,NameLocal);

Finally
Empty := System.Reflection.Missing.Value;
msWord.Application.Quit(Empty,Empty,Empty);
end;
end;

Pour l'exemple d'itération de classe WMI voici l'adaptation :

Procedure Enumere;
// Enumére les points de restauration système Via WMI
var
WMILocator : ManagementScope;  //TSWbemLocator
WmiService : ConnectionOptions;//SWbemServices;
Path : ManagementPath;
Options : ObjectGetOptions;
EnumOptions: EnumerationOptions;
SRClass : ManagementClass;

SRList: ManagementObjectCollection;
MonObjetWMI: ManagementObject;

begin
try
//préparation des options de connections
WmiService:=ConnectionOptions.Create;
With WmiService do
begin
EnablePrivileges:=False; // valeur par défaut
Timeout:=InfiniteTimeout; // InfiniteTimeout valeur par défaut
end;

// Création du chemin d'accès à l'espace de nom
Path:=ManagementPath.Create('\\.\root\Default:SystemRestore');

// Création d'une connexion avec le chemin préparé
WMILocator:= ManagementScope.Create(Path,WmiService);
WMILocator.Connect;

// Prépare les options
Options:=ObjectGetOptions.Create; // InfiniteTimeout par défaut

SRClass:=ManagementClass.Create(Path.Path, 'SystemRestore', Options);

EnumOptions:=EnumerationOptions.Create;
With EnumOptions do
Begin
ReturnImmediately:=True;
DirectRead:=True;
end;

//Récupére la collection d'instances
SRList:= SRClass.GetInstances(EnumOptions);

For MonObjetWMI in SRList do
Writeln(AdjustLineBreaks(MonObjetWMI.GetText(TextFormat.Mof)));

Except
on E:ManagementException do
begin
Console.WriteLine('ErrorCode {0}',E.ErrorCode);
Writeln('Message : ' + E.Message);
Writeln('Source : ' + E.Source);
if Assigned(ManagementBaseObject(E.ErrorInformation)) //extended error object
then Console.WriteLine('Extended Description : {0}',E.ErrorInformation['Description']);
end;
end;
end;

La classe ManagementObjectCollection implémentant l'interface IEnumerable, sous Delphi la variable SRList peut être utilisée directement dans l'itérateur For..in..do.

En conclusion la classe d'itérateur présenté précédemment n'est valable que pour Delphi Win32.