FAQ DCOM/OLE/ATLConsultez toutes les FAQ

Nombre d'auteurs : 2, nombre de questions : 91, dernière mise à jour : 2 septembre 2018 

 
OuvrirSommaireATL

Comme vous avez pu le constater, DCOM est une technologie extrêmement complexe à utiliser, et encore plus à mettre en oeuvre. En réalité, c'est tout simplement inutilisable tel quel, brut de fonderie. C'est pour cela que Microsoft a dû développer des outils complémentaires pour permettre l'utilisation et la réalisation facile de composants DCOM.

L'un des principaux outils est bien entendu Visual Basic, qui permet non seulement d'utiliser facilement les composants DCOM pourvu qu'ils disposent d'une type library, mais aussi de créer d'autres composants avec une facilité déconcertante au regard de ce qui se passe en interne. C'est réellement dans ce genre d'environnement que l'on se rend compte de la puissance de cette technologie, car la simplicité d'utilisation de Visual Basic provient réellement de la richesse de fonctionnalités fournies par DCOM.

En C++ en revanche, tout n'est pas si facile. La réalisation d'un composant est un véritable cauchemar, même si on est absolument maître de tout. Microsoft a développé une bibliothèque de classes template spécifiquement pour réaliser des composants DCOM : j'ai nommé les Active Template Library (ATL en abrégé).

Ces bibliothèques de classes soulagent réellement le programmeur, en réalisant la plupart du code de gestion de la durée de vie des composants. Les frais de développement sont donc d'autant plus réduits. Cependant, les ATL sont insuffisantes en soi, il faut leur ajouter un certain nombre de Wizards, fournis par l'environnement de développement Visual C++, qui permettent de générer le code C++ pour la plupart des opérations des composants. En particulier, les points de connexions ne sont pas totalement pris en charge par les ATL, du code spécifique doit être généré par l'intermédiaire d'un Wizard.

La suite de ce document décrit comment créer et utiliser des composants DCOM avec les ATL et Visual C++ 6.0, ainsi qu'avec Visual Basic 6.0. Vous pourrez constater que Visual Basic exploite pleinement les technologies mises en place par DCOM, et constitue l'environnement de développement idéal pour les petits controles ActiveX.

Créé le 9 juillet 2000  par Christian Casteyde

Il est préférable, pour pouvoir utiliser les ATL, de connaître la notion de template du C++. Ce paragraphe a pour but de vous en faire une présentation minimale pour permettre une utilisation sereine des ATL.

En C++, une classe est un type de donnée complexe, qui est défini à la fois par sa structure de données et par les opérations, encore appelées méthodes, qu'on peut lui appliquer. Les instances de ces classes, c'est à dire les variables de ce type, sont ce qu'on appelle des objets. Il est possible d'appliquer à tous les objets d'une même classe l'ensemble des opérations définies dans cette classe.

Il est courant en conception objet d'utiliser la notion de classe pour regrouper toutes les entités qui ont un comportement commun. Il en va de même pour les classes : il est souvent possible de définir un comportement commun pour différentes classes, et donc de les regrouper dans une classe de classe. On appelle communément ce type de classes des métaclasses. Une classe C++ n'est donc rien d'autre qu'une instance de métaclasse. Le langage C++ ne gère pas en général la notion de métaclasse, et en fait, la manipulation des entités au niveau méta relève plus du modèle objet et de l'outil de développement lui-même que du programme en soi. Ceci signifie que les métaclasses sont généralement manipulées au niveau des outils de conception ou des environnements de développement.

Si vous ne comprenez pas ce qu'est une métaclasse, imaginez que vous disposiez de plusieurs classes gérant des listes chaînées. Quelle que soit la nature des objets que ces listes manipulent, elles ont toutes le même comportement et peuvent être définies de manière générique. Toutes ces classes de gestion des listes appartiennent donc à la métaclasse liste. S'il est possible de définir le comportement général d'une liste chaînée au niveau méta (donc par exemple au niveau de l'environnement de développement), il est inutile de coder chaque classe de liste pour chaque type de donnée stockable. Il suffit d'instancier la métaclasse liste en spécifiant le type de donnée utilisé. Ce travail d'instanciation peut être réalisé par exemple par un générateur de code, ou par l'environnement.

Les classes template du C++ sont un cas particulier de métaclasses. Elles permettent de définir des classes génériques au niveau méta, qui sont paramétrées par les types de données qu'elles manipulent ou par des valeurs constantes. Les classes template permettent donc de définir des classes qui travaillent sur des types génériques. Leur instanciation est réalisée par le compilateur, lorsque l'on précise les types réels à utiliser en lieu et place des types génériques. Pour reprendre l'exemple des listes chaînées, il est possible de définir une classe liste template, qui stocke des objets de type T indéterminé. Les classes de gestion des listes d'entiers, de réels, de chaînes de caractères ou de n'importe quoi peuvent alors être générées automatiquement par le compilateur à partir de la classe template.

Notez que la définition d'une classe template ne génère aucun code, car la métaclasse ainsi définie se trouve au niveau méta. Ce n'est que lorsque l'on instancie cette métaclasse, donc lorsque l'on précise les types réels à utiliser, que le code est généré pour ces types. D'autre part, cette instanciation se fait à la compilation, et non à l'exécution. Il s'agit donc bien d'une instanciation dans le cadre de l'outil de développement (en l'occurrence, le compilateur). Le C++ est incapable de manipuler des métaclasses ou des classes template dynamiquement (c'est à dire à l'exécution).

Pratiquement, une classe template se déclare de la manière suivante :

 
Sélectionnez

template <typename T>
class C
{
    // Définition de C en utilisant T comme s'il s'agissait
    // d'un type normal.
};

La première ligne template permet d'introduire la liste des types génériques que la classe template va utiliser. Dans l'exemple donné ci-dessus, cette liste ne comprend qu'un seul type générique, T. Il est parfaitement faisable d'utiliser plusieurs paramètres template. Il est également possible d'utiliser des valeurs pour les paramètres template à la place de types. Par exemple, on peut définir une classe template paramétrée par la valeur d'un entier.

Il est courant d'utiliser également le mot-clé class à la place du mot-clé typename dans la déclaration des types template.

L'instanciation d'une classe template se fait dès que l'on précise la valeur des paramètres template, que ceux-ci soient des types ou des valeurs. Ceci se fait en donnant la liste de ces valeurs entre crochets. Par exemple, pour définir un type Ci équivalent à l'instance de la classe template C pour les entiers, on écrira :

 
Sélectionnez

typedef C<int> Ci;

Cette écriture revient à remplacer partout dans la définition de la classe template C le type générique T par le type réel int. C'est à ce moment que le code est réellement créé par le compilateur.

La bibliothèque des ATL utilise les template C++ pour de multiples usages. En fait, la raison fondamentale de cette utilisation est que les classes ATL ont besoin de manipuler les interfaces que les composants exposent, ainsi que les classes C++ de ces composants eux-même. Les classes ATL sont donc souvent paramétrées par le type de l'interface à implémenter et par la classe C++ du composant qui l'implémente.

Créé le 9 juillet 2000  par Christian Casteyde

Pour créer un composant ATL, il faut choisir le type de projet ATL COM AppWizard dans la boîte de dialogue ouverte par le menu File | New... de Visual C++. Après avoir donné un nom au projet, par exemple FAQSAMPLE1 et validé, Visual C++ demande le type de serveur que l'on désire créer (DLL, exécutable ou service Windows). Parmi les options les plus importantes, on notera celle qui permet d'utiliser les MFC (Support MFC).

Le Wizard grise les options lorsque l'on choisit de faire un serveur de type exécutable ou service. Par conséquent, les projets de ce type ne pourront pas, par défaut, utiliser les MFC. Si l'on veut malgré tout utiliser les MFC, il faudra l'indiquer dans les options de projets, et modifier substantiellement le code d'initialisation du serveur pour initialiser les MFC. Vous pourrez obtenir la liste des opérations à accomplir dans l'article intitulé HOWTO: Add MFC Support to an ATL Project de la Knowledge Base de Microsoft. Cet article se trouve dans le MSDN et concerne plus Visual C++ que DCOM, par conséquent, les modifications à apporter au projet ne seront pas traitées ici. Nous utiliserons donc systématiquement des serveurs in-process dans la suite de ce document, afin de pouvoir bénéficier des MFC.

La validation des options de projets provoque la création d'un projet vide, contenant :

  • un fichier d'en-tête précompilé stdafx.h, ainsi que le fichier C++ associé stdafx.cpp
  • un fichier de ressources .rc et son fichier d'en-tête associé ressource.h
  • un fichier IDL portant le nom du projet, et dans lequel toutes les définitions d'interfaces et de composant seront placées
  • un fichier de définition d'options de linking .DEF
  • un fichier C++ principal, qui contient tout le code d'initialisation des ATL et de gestion des fabriques de classes des composants qui seront créés par la suite

À ce stade, ce serveur ne contient aucun composant. Pour en ajouter un, il faut cliquer sur l'onglet ClassView de la fenêtre du Workspace afin de faire apparaître le class browser de Visual C++, puis cliquer sur le bouton droit de la souris sur le projet et choisir le menu New ATL Object.... Ce menu fait apparaître une boîte de dialogue permettant de choisir le type d'objet à créer. Nous allons créer un objet ATL simple, et donc sélectionner la rubrique Objects puis le type d'objet Simple Object. Le bouton Next fait apparaître les options de notre objet. Nous pouvons donner son nom, par exemple Adder. Automatiquement, Visual C++ propose des noms dérivés de ce nom simple pour les noms de fichiers sources, du composant, de sa première interface, de son type et de son ProgID. Vous pouvez bien entendu modifier ces options.

L'onglet Attributes permet de spécifier certaines options pour le futur composant que le Wizard va créer. En particulier, il est possible de spécifier son modèle de threading, si ses interfaces seront custom ou duales et s'il supporte l'agrégation. Les options situées en bas de la boîte de dialogue permettent d'activer le support de l'interface ISupportErrorInfo, qui permet fournir aux programmes comme Visual Basic des informations détaillées sur la nature des erreurs que le composant peut renvoyer, de gérer les points de connexion et d'optimiser le code de marshalling au sein d'un même processus. Laissons les options par défaut inchangées et validons.

Automatiquement, le Wizard définit le composant dans l'IDL, crée le code C++ de ce composant, l'ajoute dans la liste des composants dans le fichier principal du projet, et crée un fichier .rgs qui contiendra toutes les informations que le composant enregistrera dans la base de registre lorsqu'il sera enregistré.

Le composant Adder existe à présent, et il gère l'interface IAdder. Il faut maintenant définir cette interface. Vous pouvez pour cela éditer directement le fichier IDL, ou cliquer avec le bouton droit de la souris et choisir l'une des options Add Method... ou Add Property.... Nous allons créer une méthode. Dans la boîte de dialogue qui s'ouvre, tapons Add dans le champ Method Name:, et [in] long x, [in] long y, [out, retval] long *pResult dans la liste des paramètres. Visual C++ présente au fur et à mesure la déclaration IDL qui sera ajoutée dans la définition de l'interface IAdder. Validons pour créer cette méthode.

Maintenant, il ne reste plus qu'à écrire le code de cette méthode. Pour cela, allons dans le fichier d'implémentation du composant Adder.cpp et écrivons le corps de la méthode dans le squelette que le Wizard a créé pour nous. Votre méthode doit ressembler à ceci :

 
Sélectionnez

STDMETHODIMP CAdder::Add(long x, long y, long *pResult)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())
    
    // TODO: Add your implementation code here
    if (pResult == NULL) return E_POINTER;
    *pResult = x + y;
    
    return S_OK;
}

Il ne reste plus qu'à compiler. Et voilà ! Nous avons fait un composant ATL. Comparé à la quantité de code qu'il faut écrire lorsque l'on veut le faire complètement à la main, il n'y a quasiment plus rien à faire En fait, comme vous pourrez le constater en parcourant les fichiers écrits par le Wizard, beaucoup de code a été écrit automatiquement. Voyons de quoi il en retourne.

Pour commencer, la définition de l'interface IAdder a été ajoutée dans le fichier IDL. Nous y retrouvons notre méthode. De même, le composant Adder a été défini dans la section library de ce même fichier.

Ensuite, toutes les informations d'enregistrement du composant ont été écrites dans le fichier Adder.rgs. Ces informations seront compilées par le compilateur de ressources de Visual et placées dans la ressource REGISTRY du fichier exécutable contenant le composant.

Troisièmement, une entrée OBJECT_ENTRY(CLSID_Adder, CAdder) a été ajoutée dans la liste des composants du fichier principal du projet (cette liste commence à la macro BEGIN_OBJECT_MAP et se termine à la macro END_OBJECT_MAP). Ceci n'a l'air de rien, mais ces deux lignes définissent un tableau de données que les ATL utilisent pour implémenter automatiquement les class factories pour les composant qui y sont enregistrés. C'est déjà une bonne partie du travail qui est fait là...

Enfin, la classe C++ CAdder est déclarée dans le fichier Adder.h. Cette classe se présente comme suit :

 
Sélectionnez

class ATL_NO_VTABLE CAdder :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CAdder, &CLSID_Adder>,
    public IDispatchImpl<IAdder, &IID_IAdder, &LIBID_FAQSAMPLE1Lib>
{
public:
    CAdder()
    {
    }
    DECLARE_REGISTRY_RESOURCEID(IDR_ADDER)
    DECLARE_PROTECT_FINAL_CONSTRUCT()
    BEGIN_COM_MAP(CAdder)
        COM_INTERFACE_ENTRY(IAdder)
        COM_INTERFACE_ENTRY(IDispatch)
    END_COM_MAP()
    // IAdder
public:
    STDMETHOD(Add)(/*[in]*/ long x, /*[in]*/ long y,
        /*[out, retval]*/ long *pResult);
};

Parmi tout ce code, nous retrouvons la déclaration de notre méthode Add. Mais il se passe, encore une fois, beaucoup de choses. Premièrement, la classe CAdder hérite de la classe template ATL CComObjectRootEx. Celle-ci contient le code de gestion du compte de références sur l'objet.

CAdder hérite également de la classe CComCoClass. Cette classe est fondamentale : elle contient la définition des fonctions permettant de gérer la création d'une nouvelle instance, la gestion des erreurs et la gestion de l'agrégation. Les méthodes de cette classe de base sont référencées dans la liste des composants du fichier principal pour fournir la fabrique de classe pour le composant Adder.

Dernière classe de base, IDispatchImpl. Cette classe ATL prend en charge la gestion de l'interface IDispatch, ainsi que celle de l'interface IAdder, qu'elle reçoit en paramètre. C'est cette classe qui gère donc complètement notre interface...

Il va de soi que les ATL ont besoin de connaître les interfaces que gèrent le composant, afin d'implémenter correctement la méthode QueryInterface. Ce sont exactement ces informations qui leurs sont fournies par les macros COM_INTERFACE_ENTRY de la liste des interfaces du composant (cette liste est comprise entre les macros BEGIN_COM_MAP et END_COM_MAP).

Enfin, pour terminer ce tour d'horizon, la macro DECLARE_REGISTRY_RESOURCEID contient la définition des fonctions qui permettent à ce composant de s'enregistrer et de se désenregistrer dans la base de registre de Windows. Ces fonctions sont référencées dans la liste des composants déclarées dans le fichier principal du projet, et sont appelées par les ATL lorsque l'on demande l'enregistrement du serveur. Les informations d'enregistrement étant stockées dans les ressources du programme, la macro DECLARE_REGISTRY_RESOURCEID prend en paramètre l'identificateur de la ressource contenant le fichier .RGS du composant.

Créé le 9 juillet 2000  par Christian Casteyde

L'utilisation d'un composant DCOM dans VB est très simple. VB est capable d'utiliser quasiment n'importe quel composant, du moment qu'il dispose d'une type library. Dans ce cas, il se comportera comme un client C++, et ne passera pas par l'interface IDispatch. En revanche, si le composant ne dispose pas de type library, VB utilisera systématiquement l'interface IDispatch, ce qui rendra les performances nettement moins bonnes. D'autre part, il ne pourra pas vérifier les fautes de frappes à la volée, comme il le fait pour les composants dont il peut obtenir une description.

Les serveurs ATL contiennent tous leur propre type library dans leur ressources, ce qui fait qu'il est possible de les utiliser directement dans VB dès qu'ils sont enregistrés dans le système. Cependant, il faut spécifiquement indiquer à VB qu'il doit utiliser cette type library. Ceci se fait en la sélectionnant dans la boîte de dialogue ouverte par le menu Projet | Références....

Pour essayer notre nouveau composant additionneur, créons un projet de type EXE standard dans Visual Basic. Ajoutons la référence sur la type library FAQSAMPLE1 1.0 comme indiqué ci-dessus. Dessinons ensuite un bouton sur la boîte de dialogue, puis double-cliquons dessus. Ceci ouvre la fenêtre de code pour cette action. Tapons le code suivant :

 
Sélectionnez

Private Sub Command1_Click()
    Dim A As New Adder
    Dim Result As Integer
    Result = A.Add(2, 3)
    MsgBox Result
End Sub

Lançons ensuite le programme et cliquons sur le bouton. Ô miracle, le résultat de l'addition, calculé par notre composant, s'affiche !

Créé le 9 juillet 2000  par Christian Casteyde

Visual Basic est certainement l'environnement le plus adapté pour créer des composants DCOM de petite taille. Nous allons faire le composant de multiplication à présent.

Créons un nouveau projet de type EXE ActiveX dans VB. Celui-ci crée automatiquement un nouveau module de classe nommé Class1. Renommons le en Calculator et saisissons le code suivant dans la fenêtre de code :

 
Sélectionnez

Public Function mul(x As Long, y As Long) As Long
    mul = x * y
End Function

C'est tout ce qu'il y a à écrire pour créer notre composant Calculator. Il reste cependant quelques points de détails à régler dans les options de projets pour pouvoir créer un composant réellement utilisable. Ces options se fixent dans la boîte de dialogue des propriétés du projet, qui peut être affichée grâce au menu Projet | Propriétés de XXX, où XXX est le nom courant du projet. Le premier onglet permet de spécifier le nom du projet dans la zone d'édition Nom du projet. Ce nom sera aussi celui de la type library du serveur de notre composant. Dans notre cas, nous avons choisi le nom FAQSAMPLE2.

Nous pouvons à présent compiler l'exécutable avec le menu Fichier | Créer .... Ce menu ouvre une boîte de dialogue permettant de choisir l'emplacement et le nom de l'exécutable à créer. Nous avons utilisé FAQSAMPLE2.EXE comme nom d'exécutable. Visual Basic compilera alors le programme et créera notre serveur. Comme pour Visual C++, l'enregistrement est fait automatiquement à chaque compilation.

La suite des opérations se passe dans l'onglet Composant de la boîte de dialogue des propriétés du projet. Il faut impérativement y activer l'option Compatibilité binaire de la zone Compatibilité des versions. Cette option permet de demander à VB de conserver toujours les mêmes CLSID et IID pour les composants et les interfaces du projet. Par défaut, VB modifie ces valeurs à chaque compilation, ce qui fait que les composants ne sont pas utilisables directement au travers de leur CLSID. Ce n'est pas très pratique en C++, où l'on préfère en général utiliser les CLSID plutôt que les ProgID, et les interfaces custom plutôt que les dispinterfaces. Notez que cette option nécessite d'indiquer le nom du fichier binaire dans lequel se trouve la définition des CLSID et des IID à utiliser, et donc d'avoir compilé au moins une fois le serveur.

Créé le 9 juillet 2000  par Christian Casteyde

Les ATL ne fournissent pas d'aide particulière pour l'utilisation des composants DCOM. Ceux-ci doivent être créés manuellement avec la fonction CoCreateInstance, et détruit avec la méthode Release. Les méthodes des interfaces des composants peuvent être appelées directement, ce qui n'est pas très difficile, sauf si l'interface est une dispinterface.

Notez qu'heureusement pour nous, Visual Basic génère des interfaces duales pour tous ses composants (sauf pour les interfaces sortantes des points de connexions). Ceci signifie qu'il est très facile d'utiliser les composants, même ceux générés par Visual Basic, en C++. Cependant, il y a un petit problème : il faut disposer du fichier d'en-tête de déclaration des composants à utiliser. Pour les composants Visual Basic (ainsi que la plupart des composants qui ne sont distribués que sous forme binaire), nous ne disposons pas de ce fichier, ni de fichier IDL.

Pour résoudre ce problème épineux, Visual C++ fournit une directive de préprocesseur spéciale afin de récupérer toutes les déclarations C++ à partir d'une type library. Il s'agit de la directive #import, qui prend en paramètre le nom du fichier contenant la type library à importer. En fait, cette directive force la création d'un fichier .TLH (Type Library Header), qui sera inclus en lieu et place de la directive #import. Si l'on regarde le contenu de ce fichier, on constatera qu'il contient non seulement la déclaration des interfaces, mais aussi la définition des CLSID et des IID.

Cette directive est très pratique, mais son emploi pose quelques problèmes de portabilité évidents. En effet, elle n'est absolument pas standard. De plus, le code généré par cette directive utilise lui-aussi des extensions non standard au langage C++, en particulier pour résoudre les problèmes de définitions multiples des CLSID et des IID à l'édition de lien, en cas d'import multiples dans différents fichiers du projet. En outre, il faut généralement spécifier les options no_namespace, named_guids et raw_interfaces_only pour avoir du code qui ressemble à celui que MIDL aurait généré à partir d'un fichier IDL. Il faut également tenir compte du fait que ces fichiers peuvent parfois être la source de problèmes assez étranges lorsque l'on utilise le mécanisme des en-têtes précompilés de Visual C++. Mais le défaut majeur de cette technique est qu'elle nécessite que le composant binaire que l'on désire utiliser soit installé sur la machine de compilation, ce qui n'est généralement pas le cas.

C'est pour pour ces raisons qu'on aura intérêt à utiliser une autre technique, plus classique et plus dans les règles de l'art. Celle-ci est très simple : il suffit de récupérer les informations de la type library du composant à utiliser et de les inclure dans un fichier IDL. Ainsi, on se ramène au cas « normal », qui est celui où l'on dispose du fichier IDL de chaque composant. Pour cela, on utilisera l'outil OLE/COM Object Viewer, accessible dans le menu Tools de Visual C++. Ce programme permet de visualiser les interfaces des composants du système, aussi bien dynamiques que celles définies dans les type libraries. Ce sont ces dernières qui nous intéressent.

Ouvrons par exemple la type library du composant FAQSAMPLE2.EXE (menu File | View Typelib...). Voici le fichier IDL correspondant :

 
Sélectionnez

// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: FAQSAMPLE2.exe
[
    uuid(E2D64385-4FFD-11D4-B3BC-9FEBDB931851),
    version(1.0)
]
library FAQSAMPLE2
{
    // TLib :
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("StdOle2.tlb");
    // Forward declare all types defined in this typelib
    interface _Calculator;
    [
        odl,
        uuid(E2D64386-4FFD-11D4-B3BC-9FEBDB931851),
        version(1.0),
        hidden,
        dual,
        nonextensible,
        oleautomation
    ]
    interface _Calculator : IDispatch {
        [id(0x60030000)]
        HRESULT mul(
            [in, out] long* x,
            [in, out] long* y,
            [out, retval] long* );
    };
    [
        uuid(E2D64387-4FFD-11D4-B3BC-9FEBDB931851),
        version(1.0)
    ]
    coclass Calculator {
        [default] interface _Calculator;
    };
};

C'est simple : il suffit de récupérer la définition de l'interface _Calculator et du composant Calculator et de les inclure dans le fichier IDL du projet où l'on désire les utiliser. Ce fichier IDL contiendra alors toutes les définitions nécessaires à l'utilisation de ce composant, sans avoir recours à des artifices douteux ni avoir besoin d'installer ce composant.

Créé le 9 juillet 2000  par Christian Casteyde

L'utilisation de CoCreateInstance peut être assez contraignante lorsque l'on doit utiliser un composant du même projet. En particulier, il peut être intéressant de créer ces composants comme des objets C++ classiques (après tout, un composant ATL est aussi une classe C++), afin de pouvoir les manipuler à l'aide de méthode C++ standard en interne.

C'est exactement ce que permet de faire la classe template ATL CComObject. Cette classe permet de manipuler les composants ATL comme des objets C++, en implémentant les fonctions de IUnknown et en assurant une gestion cohérente de la durée de vie de l'objet C++ par rapport à son compte de références. L'utilisation de CComObject est très simple. Il suffit de lui fournir en paramètre le nom du composant ATL à utiliser comme une classe C++ :

 
Sélectionnez

CComObject<CAdder>

On aura intérêt à définir un typedef dans le fichier d'en-tête de la définition de la classe du composant :

 
Sélectionnez

typedef CComObject<CAdder> CAdderObj;

Dès lors, il sera possible de créer un additionneur directement à l'aide d'une expression du type :

 
Sélectionnez

CAdderObj *pAdder = new CAdderObj;

Notez que la création d'un composant ne demande pas d'interface. Le compte de référence de ce composant vaut donc 0, et le pointeur pAdder ne doit pas être renvoyé à un client en tant que pointeur d'interface. Si l'on désire une interface sur notre additionneur, on devra d'abord appeler la méthode QueryInterface. Notez également que le dernier Release sur l'objet provoquera sa destruction automatiquement. Le pointeur pAdder ne sera donc plus valide. Pour empêcher ce genre de problème, on pourra demander une interface à titre privé. Enfin, n'utilisez par directement l'opérateur delete sur un CComObject. En effet, la destruction des composants doit se faire via la méthode Release (bien entendu, vous pouvez toujours utiliser delete si vous ne faites pas de QueryInterface sur cet objet).

Créé le 9 juillet 2000  par Christian Casteyde

Réaliser un composant contenant des points de connexions n'est pas plus difficile que de réaliser une composant normal avec les Wizards de Visual C++ et les ATL. Cependant, il est conseillé de prévoir dès la création du composant la gestion des points de connexions, faute de quoi il faut réaliser une partie du travail du Wizard manuellement.

Nous allons profiter de cet exemple pour compliquer un peu les choses, et voir comment on peut ajouter des propriétés à un composant. Soit un composant permettant de faire des soustractions. Ce composant fonctionne de la manière suivante : il dispose de deux propriétés, dans lesquelles les valeurs des deux paramètres à soustraire doivent être placées, d'une méthode Sub, qui permet de demander le calcul, et d'une propriété Result, accessible en lecture seule, qui contiendra le résultat. Ce composant génère un événement Done lorsqu'il a fini son calcul.

Créons avant tout un nouveau projet ATL de type DLL nommé FAQSAMPLE3, comme nous l'avons fait pour le projet FAQSAMPLE1. Ajoutons un nouveau composant Substract à ce projet, à l'aide du menu contextuel New ATL Object... du class browser de Visual C++. Choisissons un objet simple, auquel nous donnerons le nom Substract. Dans l'onglet Attributes, cochons la case Support Connection Points et créons le composant.

Qu'est-ce que l'option Support Connection Points a ajouté au composant ? Si l'on regarde le fichier IDL, nous verrons que le Wizard a créé une dispinterface _ISubstractEvents, et que le composant Substract utilise cette interface comme interface sortante par défaut. Ceci signifie que les outils comme Visual Basic ne pourront utiliser que les événements définis dans cette interface.

Il est parfaitement légal d'utiliser des interfaces duales pour les points de connexion. Cependant, ces points de connexion ne seront pas utilisables dans Visual Basic, parce que celui-ci ne sait gérer que des dispinterfaces sur les points de connexions. Il est donc conseillé de laisser le code généré par le Wizard tel quel. Si toutefois on désire utiliser des interfaces duales, il faut transformer la dispinterface en interface duale. Ceci nécessite de procéder comme suit :

  • déclarer l'interface en tant qu'interface normale plutôt qu'en tant que dispinterface
  • faire hériter cette interface de l'interface IDispatch
  • supprimer les champs properties et methods de la définition de l'interface
  • rajouter les attributs object, dual pour cette interface en plus de son uuid. Le mot-clé dual est nécessaire : sans lui, le code d'enregistrement des ATL n'enregistre pas l'IID de l'interface dans la clé Interfaces, et elle ne peut donc pas être marshallée automatiquement. Notez que c'est aussi ce mot-clé qui empêchera Visual Basic de recevoir les événements de cette interface

Par ailleurs, s'il existe déjà des points de connexions utilisant cette interface dans le projet, il va falloir modifier le code de certains composants. En pratique, il est plus simple de refaire tous les points de connexions, et de supprimer les références à l'ancienne dispinterface.

Notez que le Wizard définit les interfaces événementielles dans la section library de l'IDL. Il fait ceci afin de forcer une référence sur cette interface, ce qui permet de définir cette interface dans le .h généré par MIDL. Ceci n'est pas nécessaire si l'interface sortante n'est pas implémentée en tant qu'interface normale par un composant du projet.

La deuxième modification apportée par le Wizard se trouve dans la déclaration de la classe du composant. Cette classe dérive à présent de la classe template ATL IConnectionPointContainerImpl, qui permet de gérer la liste des points de connexion pour ce composant. D'autre part, cette liste a été ajoutée dans la classe. Elle est définie par les macros BEGIN_CONNECTION_POINT_MAP et END_CONNECTION_POINT_MAP.

Si on désire ajouter la gestion des points de connexions à un composant existant pour lequel ceci n'avait pas été prévu initialement, il faut refaire ces opérations manuelles. Autrement dit, il faut définir l'interface dans l'IDL et l'ajouter en tant qu'interface source dans le coclass du composant, faire dériver le composant de IConnectionPointContainerImpl et lui ajouter la liste des points de connexions.

Nous pouvons maintenant ajouter les propriétés à notre interface ISubstract. Pour cela, il suffit d'utiliser le menu contextuel Add Property... sur cette interface dans le class Browser de Visual C++. Nous pouvons ensuite donner un type pour cette propriété, choisissons ici le type long. La propriété sera nommée Operand1. Vous pouvez indiquer les modes d'accès de la propriété dans la section Function Type de la boîte de dialogue d'ajout des propriétés. Pour la propriété résultat, nous ne cocherons que la case Get Function, ce qui permettra de la rendre accessible en lecture seule.

Les propriétés sont implémentées dans Visual C++ comme des fonctions. Il existe une fonction pour la lecture de la propriété, et une fonction pour l'écriture. Comme on peut le constater dans le code de la classe CSubstract, ces deux accesseurs portent le nom de la propriété, préfixé par get_ et put_ respectivement pour la lecture et l'écriture.

Nous pouvons créer de même les propriétés Operand2, Result et la méthode Sub. Il ne reste plus qu'à écrire le code dans le fichier Substract.cpp :

 
Sélectionnez

STDMETHODIMP CSubstract::get_Operand1(long *pVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())
        if (pVal == NULL) return E_POINTER;
    *pVal = m_lOperand1;
    return S_OK;
}
STDMETHODIMP CSubstract::put_Operand1(long newVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())
        m_lOperand1 = newVal;
    return S_OK;
}
STDMETHODIMP CSubstract::get_Operand2(long *pVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())
        if (pVal == NULL) return E_POINTER;
    *pVal = m_lOperand2;
    return S_OK;
}
STDMETHODIMP CSubstract::put_Operand2(long newVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())
        m_lOperand2 = newVal;
    return S_OK;
}
STDMETHODIMP CSubstract::get_Result(long *pVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())
        if (pVal == NULL) return E_POINTER;
    *pVal = m_lResult;
    return S_OK;
}
STDMETHODIMP CSubstract::Sub()
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())
        m_lResult = m_lOperand1 - m_lOperand2;
    return S_OK;
}

Notez que ce code utilise les données membres m_lOperand1, m_lOperand2 et m_lResult de la classe CSubstract. Ces données devront donc bien entendu être ajoutée dans la déclaration de cette classe.

Il faut ensuite ajouter notre événement dans l'interface événementielle. Pour cela, il suffit d'ajouter la méthode Done, qui ne prend aucun paramètre, dans cette interface. Il est nécessaire de recompiler le fichier IDL à ce stade.

Si des paramètres doivent être passés à des méthodes d'interfaces événementielles, il est vivement recommandé de les passer par pointeurs. C'est ce que fait Visual Basic par défaut. En fait, c'est une contrainte imposée par un bug au sein des bibliothèques de OLE, qui fait que certains clients ne pourront pas capter les événements correctement si on ne la respecte pas.

Il faut maintenant implémenter le point de connexion pour le composant Substract. Ceci se fait en utilisant le menu Implement Connection Point... sur la classe CSubstract dans le class browser de Visual C++. Visual C++ nous donne la liste des points de connexions que cette classe est susceptible de gérer. Le Wizard détermine cette liste sur la base de ce qui est écrit dans le fichier IDL, dans la définition du composant Substract. Si l'on veut implémenter une autre interface événementielle, il faut donc ajouter cette interface en tant qu'interface source dans le coclass de Substract.

Le Wizard effectue encore plusieurs modifications lorsqu'il implémente un point de connexion. Premièrement, il crée une classe template CProxy_ISubstractEvents, qui gère le point de connexion pour l'interface _ISubstractEvents. Ensuite, il fait dériver la classe du composant de cette classe, parce que le composant contient ce point de connexion. Finalement, il ajoute une entrée dans la liste des points de connexions de la classe à l'aide de la macro CONNECTION_POINT_ENTRY. Prenez bien garde à ne pas confondre la notion de proxy du marshalling avec les classes CProxyXXX générées par le Wizard. Ce sont deux notions qui n'ont absolument rien à voir.

Notez que la génération de la classe CProxy_ISubstractEvents utilise la type library du projet. C'est pour cette raison qu'il faut impérativement compiler le fichier IDL avant de générer un point de connexion. De plus, toute modification d'une interface événementielle nécessite de regénérer ce point de connexion. Ceci ne modifie absolument rien à votre code.

Il ne reste plus qu'à signaler les événements lorsque c'est nécessaire. Dans notre cas, ceci doit être fait lorsque le calcul du résultat est terminé, dans la méthode Sub de l'interface ISubstract. L'envoi des événements se fait simplement en appelant la méthode Fire_XXX, où XXX est le nom de l'événement à générer. Cette méthode est définie dans la classe d'implémentation du point de connexion, et est héritée par la classe du composant. Dans notre cas, l'utilisation de l'événement Done se fait comme suit :

 
Sélectionnez

STDMETHODIMP CSubstract::Sub()
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())
        m_lResult = m_lOperand1 - m_lOperand2;
    Fire_Done();
    return S_OK;
}

Voilà, nous avons créé un composant un peu plus complexe avec les ATL...

Créé le 9 juillet 2000  par Christian Casteyde

Visual Basic peut récupérer les événements des objets déclarés en portée globale. Pour cela, il suffit de déclarer ces objets avec le mot-clé WithEvents :

 
Sélectionnez

Dim WithEvents S as Substract

S est alors déclaré comme une référence sur un composant Substract, et est capable de récupérer les événements de ce type de composant. Ces événements seront reçus dans des fonctions portant le nom de l'événement, préfixé par le nom de l'objet pour lequel cet événement a été généré. Par exemple, pour l'objet S, l'événement Done sera traité par la fonction S_Done.

Notez que Visual Basic n'accepte pas les interfaces duales comme source d'événements. Il faut impérativement des dispinterfaces, faute de quoi il indique une erreur Mécanisme de bibliothèque d'objets non géré.

Créé le 9 juillet 2000  par Christian Casteyde

Créer un composant gérant des propriétés est immédiat en Visual Basic. Il suffit de définir des méthodes avec les mots-clés Property Let et Property Get. De même, il est beaucoup plus facile de générer des événements en Visual Basic qu'en C++. Il suffit simplement de les déclarer dans les fichiers de classe, avec la syntaxe suivante :

 
Sélectionnez

Public Event XXX()

où XXX est le nom de l'événement à gérer. Cet événement pourra ensuite être généré à l'aide du mot-clé RaiseEvent.

Par exemple, si l'on désire créer un composant Division, on écrira le code suivant :

 
Sélectionnez

Public Event Done()
Private x As Long
Private y As Long
Private r As Long
Public Property Let Operand1(Value As Long)
    x = Value
End Property
Public Property Get Operand1() As Long
    Operand1 = x
End Property
Public Property Let Operand2(Value As Long)
    y = Value
End Property
Public Property Get Operand2() As Long
    Operand1 = y
End Property
Public Property Get Result() As Long
    Result = r
End Property
Public Sub Divide()
    r = x / y
    RaiseEvent Done
End Sub
Créé le 9 juillet 2000  par Christian Casteyde

Du fait que Visual Basic génère des dispinterfaces pour les points de connexion de ses composants, la gestion des événements avec le C++ est relativement pénible. En effet, cela nécessite d'implémenter l'interface IDispatch et d'analyser les DISPIDs des événements reçus. Heureusement, les ATL fournissent une classe template IDispEventImpl, qui permet de récupérer les événements Automation des dispinterfaces. Cependant, aucun Wizard n'existe pour générer le code nécessaire à la gestion des événements Automation, il faut donc l'écrire soi-même.

La première étape pour capter les événements Automation est avant tout de créer un composant qui servira de cible pour les événements. En général, le composant intéressé par les événements d'un point de connexion n'est pas lui-même la cible de ces événements. Le fait de distinguer ces deux composants permet de s'assurer qu'il n'y aura pas de cycle dans le compte des références. Supposons par exemple que le composant intéressé par les notifications détienne une interface sur l'objet source, ce qui est souvent le cas. S'il était également l'objet cible de ces notifications, la source détiendrait une référence sur une interface de l'objet cible, et les deux objets se bloqueraient mutuellement en mémoire.

En général, le composant cible s'appelle un sink, du terme puits en anglais, par opposition à l'objet source des événements (les événements vont de la source vers le puits). Il nous faut donc simplement créer un composant ATL simple, nommé par exemple DivisionSink si l'on veut recevoir les événements du composant Division de l'exemple FAQSAMPLE4.

Le Wizard va automatiquement créer une interface IDivisionSink pour ce composant. En fait, nous n'avons pas besoin de cette interface, puisque notre composant ne gérera que la dispinterface événementielle de Division, à savoir la dispinterface __Division. Nous devons donc modifier le code généré pour supprimer l'interface IDivisionSink dans le fichier IDL, et indiquer que le composant DivisionSink gère en fait la dispinterface événementielle __Division. La définition de notre composant dans le fichier IDL doit donc ressembler à ceci :

 
Sélectionnez

[
    uuid(1910BF90-502F-11D4-B3BC-9FEBDB931851),
    helpstring("DivisionSink Class")
]
coclass DivisionSink
{
    [default] dispinterface __Division;
};

Notez que ceci suppose que nous ayons récupéré les informations de la type library du serveur FAQSAMPLE4.EXE. Ceci ajoute donc une dépendance des projets clients envers les projets serveurs.

Il faut ensuite faire le ménage dans la définition de la classe CDivisionSink et remplacer les références à l'interface IDivisionSink par des références équivalentes à la dispinterface __Division.

La deuxième étape est de faire dériver la classe CDivisionSink de la classe de base IDispEventImpl. Il faut renseigner les paramètres template suivant à cette classe :

  • un identificateur unique dans le projet, qui caractérise la classe CDivisionSink (en fait, cet identificateur est utilisé pour identifier les contrôles, mais ici nous ne cherchons pas à faire un contrôle ATL)
  • le nom de la classe qui dérive de IDispEventImpl
  • l'adresse sur le GUID de la dispinterface à implémenter
  • l'adresse sur le GUID de la type library contenant la définition de cette interface
  • le numéro de version majeur de cette type library
  • son numéro de version mineur

Dans notre cas, cela donne :

 
Sélectionnez

IDispEventImpl<0, CDivisionSink, &DIID___Division,
    &LIBID_FAQSAMPLE4Lib, 1, 0>

Notez bien qu'on référence ici la type library de la source, pas du projet en cours.

La troisième étape est de créer une méthode C++ standard pour chaque événement Automation susceptible de se produire. Ces méthodes doivent utiliser la convention d'appel __stdcall, car elles seront appelées en callback par les ATL. Dans notre cas, nous utiliserons la méthode C++ OnDone, déclarée comme suit dans la classe CDivisionSink :

 
Sélectionnez

void __stdcall OnDone();

Enfin, la dernière étape est de créer une map d'association entre les DISPIDs des événements Automation susceptibles de se produire et les fonctions callback à appeler. Cette map est créée à l'aide des macros BEGIN_SINK_MAP, END_SINK_MAP et SINK_ENTRY_EX. Cette dernière macro prend en paramètre le même numéro qui a été utilisé pour identifier la classe CDivisionSink dans IDispEventImpl, le GUID de la dispinterface à gérer, le DISPID de l'événement Automation et l'adresse de la fonction callback à appeler pour chaque occurrence de cet événement.

Pour récapituler, la déclaration complète de notre classe CDivisionSink est la suivante :

 
Sélectionnez

/////////////////////////////////////////////////////////////////////
// CDivisionSink
class ATL_NO_VTABLE CDivisionSink :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CDivisionSink, &CLSID_DivisionSink>,
    public IDispEventImpl<0, CDivisionSink, &DIID___Division,
    &LIBID_FAQSAMPLE4Lib, 1, 0>,
    public IDispatchImpl>__Division, &DIID___Division,
    &LIBID_FAQSAMPLE4Lib>
{
public:
    CDivisionSink();
    virtual ~CDivisionSink();
    void __stdcall OnDone();
    DECLARE_REGISTRY_RESOURCEID(IDR_DIVISIONSINK)
    DECLARE_PROTECT_FINAL_CONSTRUCT()
    BEGIN_COM_MAP(CDivisionSink)
        COM_INTERFACE_ENTRY(IDispatch)
        COM_INTERFACE_ENTRY(__Division)
    END_COM_MAP()
    BEGIN_SINK_MAP(CDivisionSink)
        SINK_ENTRY_EX(0, DIID___Division, 0x00000001, &OnDone)
    END_SINK_MAP()
    // IDivisionSink
public:
};
typedef CComObject<CDivisionSink> CDivisionSinkObj;

La méthode OnDone des objets de cette classe sera appelée à chaque fois qu'un événement Done sera généré par les objet auxquels ils seront connectés. Il reste à savoir comment on peut connecter une instance de CDivisionSink à un objet gérant la dispinterface événementielle __Division. Eh bien, ceci se fait simplement à l'aide de la méthode DispEventAdvise de la classe cible. Par exemple, une séquence d'appel typique serait la suivante :

 
Sélectionnez

// Crée un objet Division :
_Division *pDivider;
CoCreateInstance(CLSID_Division, NULL, CLSCTX_SERVER,
    IID__Division, (void **) &pDivider);
// Crée un objet cible :
CDivisionSinkObj *pSink = new CDivisionSinkObj;
// Demande l'interface IUnknown de l'objet Division :
IUnknown *pDivUnk;
pDivider->QueryInterface(IID_IUnknown, (void **) &pDivUnk);
// Abonne l'objet cible aux événements de l'objet division :
pSink->DispEventAdvise(pDivUnk);
// Effectue une simple opération :
long lTemp = 18;
pDivider->put_Operand1(&lTemp);
lTemp = 3;
pDivider->put_Operand2(&lTemp);
pDivider->Divide();
// Désabonne l'objet cible :
pSink->DispEventUnadvise(pDivUnk);
// Libère les ressources :
pDivUnk->Release();
pDivider->Release();

La méthode OnDone de la classe CDivisionSink sera appelée à la fin du traitement de la méthode Divide du composant Division. Notez que l'objet cible est automatiquement détruit lorsqu'on le désabonne de sa source d'événements.

Créé le 9 juillet 2000  par Christian Casteyde

Il est beaucoup plus simple de capter les événements provenant d'une interface custom ou une interface duale. En effet, le composant cible doit simplement implémenter l'interface duale en question, et s'abonner à l'objet source des événements.

La seule difficulté réside donc dans la manière de réaliser cette connexion. Ceci se fait à l'aide de la fonction globale AtlAdvise des ATL. Cette fonction prend en paramètre :

  • l'interface IUnknown du point de connexion auquel on désire s'abonner
  • l'interface IUnknown de l'objet qui doit recevoir les événements
  • l'IID de l'interface événementielle à utiliser
  • un pointeur sur un DWORD qui recevra l'identificateur de cette connexion

Ce dernier paramètre devra être utilisé pour détruire cette connexion. Ceci se fait avec la fonction AtlUnadvise. Cette dernière fonction prend en paramètre l'interface IUnknown du point de connexion, l'IID de l'interface utilisée pour cette connexion et l'identificateur de la connexion renvoyée par AtlAdvise.

Créé le 9 juillet 2000  par Christian Casteyde

Même pour la réception des événements Automation, il peut être plus pratique de définir une interface duale plutôt qu'une dispinterface en tant que client d'un point de connexion. Les appels fonctionneront dans tous les cas, qu'ils soient effectués par l'intermédiaire de l'interface IDispatch ou par l'interface custom. Pour que cette technique fonctionne, il faut toutefois que l'interface duale soit déclarée exactement avec le même IID que l'interface IDispatch, et faire l'AtlAdvise sur cet IID. Dans ce cas de configuration, le serveur appellera l'interface duale sur sont interface Automation, et les ATL redirigeront automatiquement ces appels sur les méthodes de l'interface duale.

Cette technique est substantiellement plus simple que l'utilisation de la classe IDispEventImpl, mais elle implique malheureusement que la définition des interfaces des points de connexions ne peut pas être la même entre les clients et les serveurs. Ceci signifie également qu'on ne peut pas, dans un même projet, être à la fois son propre client et serveur. En effet, cela imposerait d'avoir une interface duale et une dispinterface définies avec le même IID dans le fichier IDL du projet.

Créé le 9 juillet 2000  par Christian Casteyde

L'interface ISupportErrorInfo permet à un composant de détailler la nature de l'erreur qui s'est produite lorsqu'il s'en produit une. Lorsqu'un appel à une méthode d'une interface échoue, le client peut récupérer ces informations complémentaires afin de préciser l'erreur à l'utilisateur. Ces informations sont plus pratiques que les codes de résultats HRESULT classiques, et les outils comme Visual Basic en font un usage intensif.

La gestion de cette interface est quasiment complètement automatisée par les ATL. Pour gérer cette interface, il suffit de cocher l'option Support ISupportErrorInfo dans les attributs du composant lors de sa création. Dès lors, il est possible d'appeler la fonction AtlReportError pour donner un message d'erreur explicite à chaque fois qu'un code d'erreur doit être retourné. Cette méthode prend en paramètre l'adresse sur la structure contenant le CLSID du composant générant l'erreur, le texte de l'erreur en Unicode, l'adresse sur la structure contenant l'IID de l'interface contenant la méthode dans laquelle l'erreur s'est produite, et enfin le code d'erreur qui sera retourné. Il existe différentes surcharges de cette fonction pour gérer différents aspects relatifs aux ressources de l'application et à l'aide en ligne.

Malheureusement, il est nécessaire de prévoir la gestion de l'interface ISupportErrorInfo dès la création du composant. Si l'on veut ajouter le support de cette interface a posteriori, il faut écrire manuellement le code que le Wizard génère pour gérer cette interface. Pour cela, il faut procéder comme suit :

  • faire dériver la classe du composant de l'interface ISupportErrorInfo
  • ajouter une entrée dans la liste des interfaces du composant pour cette interface à l'aide de la macro COM_INTERFACE_ENTRY
  • déclarer la méthode InterfaceSupportsErrorInfo dans la classe comme suit :
 
Sélectionnez

STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);
  • définir cette interface comme suit dans le fichier .cpp du composant :
 
Sélectionnez

STDMETHODIMP CError::InterfaceSupportsErrorInfo(REFIID riid)
{
    static const IID* arr[] =
    {
        &IID_IError
    };
    for (int i=0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
        if (InlineIsEqualGUID(*arr[i],riid))
            return S_OK;
    }
    return S_FALSE;
}

Ce code suppose que le composant modifié ici se nomme Error et est implémenté par la classe CError. Vous remarquerez que cette méthode contient la définition d'un tableau statique de pointeurs sur les IID des interfaces susceptibles d'être spécifiées dans les appel à AtlReportError. Il faut donc compléter ce tableau avec la liste des adresse des IID des interfaces du composant.

Créé le 9 juillet 2000  par Christian Casteyde

Les composants Automation peuvent gérer, s'ils le désirent, un point de connexion pour l'interface IPropertyNotifySink. Cette interface permet à un composant serveur de demander à l'ensemble de ses clients l'autorisation de modifier la valeur d'une de ses propriétés, et de leur notifier les changements de valeurs si ceux-ci se produisent.

L'interface IPropertyNotifySink est déclarée comme suit :

 
Sélectionnez

interface IPropertyNotifySink : IUnknown
{
    HRESULT OnChanged(DISPID DispID);
    HRESULT OnRequestEdit(DISPID DispID);
};

Cette interface est très simple. À chaque fois qu'il désire modifier la valeur d'une propriété, le serveur appelle la méthode OnRequestEdit sur les interfaces IPropertyNotifySink de chacun de ses clients qui s'est connecté à son point de connexion, en fournissant le DISPID de cette propriété. Les clients peuvent répondre S_OK pour signaler qu'ils sont d'accord pour que le serveur modifie cette propriété, ou S_FALSE s'ils veulent rendre cette propriété en lecture seule. Remarquez que cette interface ne peut pas être utilisée pour effectuer la validation de la nouvelle valeur de la propriété, puisque celle-ci n'est pas communiquée par le serveur. Le but de cette méthode est ici de donner la possibilité à un client de contrôler les accès en écriture sur les propriétés d'un objet Automation.

La méthode OnChanged, quant à elle, permet au serveur de notifier ses clients qu'une de ses propriétés a changé de valeur. Le DISPID de la propriété est passé en paramètre. Les clients peuvent récupérer cette nouvelle valeur s'il le désirent, mais le serveur ne la fournit pas non plus lors de la notification.

Remarquez qu'un serveur Automation n'est pas obligé d'appeler ces deux méthodes pour toutes ses propriétés, même s'il gère un point de connexion pour l'interface IPropertyNotifySink. En fait, il ne doit demander l'autorisation de modification que pour les propriétés marquées de l'attribut requestedit dans le fichier IDL. De même, il ne doit notifier les changements de valeur que pour les propriétés marquées de l'attribut bindable dans le fichier IDL. Il est bien entendu possible d'utiliser les deux attributs pour une même propriété.

L'implémentation de l'interface IPropertyNotifySink ne pose pas de problème particulier pour les clients, car c'est une interface custom standard comme toutes les autres. De même, l'implémentation du point de connexion sur un composant serveur n'est pas difficile et peut être réalisée comme tous les autres points de connexions. Cependant, les ATL fournissent une implémentation standard pour ce point de connexion, et il est recommandé de l'utiliser. C'est en effet cette implémentation qui est utilisée par les contrôles ActiveX, et si vous désirez gérer ce point de connexion, autant se conformer aux mécanismes des contrôles ActiveX.

L'implémentation du point de connexion pour l'interface IPropertyNotifySink n'est pas réalisée automatiquement par les Wizards. Il faut donc modifier soi-même le code du composant ATL qui doit le gérer. Heureusement, ce n'est pas compliqué. Les opérations à effectuées sont les suivantes :

  • il faut ajouter le support pour les points de connexions au composant si ce n'est pas déjà fait
  • il faut déclarer l'interface IPropertyNotifySink comme interface source dans le fichier IDL pour ce composant
  • il faut ajouter le support du point de connexion standard des ATL pour l'interface IPropertyNotifySink
  • il faut déclarer ce point de connexion dans la liste des points de connexion du composant
  • il faut faire dériver le composant de la classe ATL CComControl, pour qu'il puisse utiliser les mécanismes standard des contrôles ATL
  • il faut définir une liste des propriétés du composant dont les notifications par l'interface IPropertyNotifySink doivent être gérées

La première opération est effectuée automatiquement par le Wizard lors de la création du composant si l'on a indiqué qu'il gérait les points de connexions. Dans le cas contraire, il suffit de faire dériver la classe du composant de la classe template publique IConnectionPointContainerImpl, qui prend en paramètre le nom de la classe qui en dérive. Il faut également ajouter une entrée pour l'interface IConnectionPointContainer dans la liste des interfaces du composant.

L'ajout du support de l'interface IPropertyNotifySink dans le fichier IDL ne pose pas de problème. Pour implémenter le point de connexion pour cette interface, il faut faire dériver la classe du composant de la classe ATL IPropertyNotifySinkCP, qui prend en paramètre le nom de la classe qui en dérive. L'ajout de l'interface IPropertyNotifySink dans la liste des points de connexion se fait en ajoutant une ligne CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink) entre les macros BEGIN_CONNECTION_POINT_MAP et END_CONNECTION_POINT_MAP.

La partie importante est de faire dériver la classe du composant de la classe template ATL CComControl. Cette classe prend en paramètre le nom de la classe qui en dérive, et est définie dans le fichier d'en-tête atlctl.h. Ce fichier devra donc être ajouté, soit dans le fichier d'en-tête du composant, soit dans le fichier d'en-tête précompilé. Cette classe contient les fonctionnalités nécessaires aux contrôles ActiveX pour ATL. Elle suppose que le composant contient une liste des propriétés que le contrôle doit gérer. Il faut donc impérativement définir cette liste, ce qui se fait à l'aide des macros BEGIN_PROP_MAP, END_PROP_MAP et PROP_ENTRY. La première macro marque le début de cette liste, et prend en paramètre le nom de la classe qui la contient. La deuxième macro marque la fin de cette liste, et la troisième macro doit être utilisée pour définir chaque propriété entre le début et la fin de cette liste. Elle prend en paramètre une chaîne de caractère qui décrit la propriété, le DISPID de la propriété et le CLSID d'un composant gérant la page de propriété associée à ce contrôle, s'il en dispose d'une. Pour les composant ne disposant pas de pages de propriétés, on peut utiliser le CLSID nul, identifié par la constante CLSID_NULL.

L'en-tête typique d'une classe gérant un point de connexion pour l'interface IPropertyNotifySink est donc le suivant :

 
Sélectionnez

class ATL_NO_VTABLE CPropertyContainer :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComControl<CPropertyContainer>,
    public CComCoClass<CPropertyContainer, &CLSID_PropertyContainer>,
    public IConnectionPointContainerImpl<CPropertyContainer>,
    public IPropertyNotifySinkCP<CPropertyContainer>,
    public IDispatchImpl<IPropertyContainer,
    &IID_IPropertyContainer, &LIBID_PROPERTYLibv,
    ...

Les différentes listes contenues dans la classe se présenteront comme suit :

 
Sélectionnez

BEGIN_COM_MAP(CPropertyContainer)
    COM_INTERFACE_ENTRY(IPropertyContainer)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY(IConnectionPointContainer)
END_COM_MAP()
BEGIN_CONNECTION_POINT_MAP(CPropertyContainer)
    CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
END_CONNECTION_POINT_MAP()
BEGIN_PROP_MAP(CPropertyContainer)
    PROP_ENTRY("Ma propriété", 1, CLSID_NULL)
END_PROP_MAP()

Dans l'exemple donné ci-dessus, le composant est supposé gérer une interface duale IPropertyContainer, qui contient les méthodes get_Prop et put_Prop permettant d'accéder à une propriété Prop. La partie intéressante est maintenant l'implémentation de l'accesseur put_Prop, parce que c'est lui qui va effectuer les notifications via l'interface IPropertyNotifySink.

Si la propriété est marquée de l'attribut requestedit dans le fichier IDL contenant la définition de l'interface, il doit appeler la méthode FireOnRequestEdit avec le DISPID de la propriété avant toute tentative de modification de la valeur de cette propriété. Si la réponse renvoyée est S_OK, il peut effectuer la modification. Dans le cas contraire, il doit ignorer la nouvelle valeur. De la même manière, il doit appeler la méthode FireOnChanged avec le DISPID de la propriété en paramètre dès que la valeur de cette propriété est modifiée, si celle-ci est marquée de l'attribut bindable dans le fichier IDL. Le code d'exemple ci-dessus conviendrait pour la propriété Prop de notre composant CPropertyContainer, si elle est marquée des deux attributs requestedit et bindable dans le fichier IDL :

 
Sélectionnez

STDMETHODIMP CPropertyContainer::put_Prop(long newVal)
{
    AFX_MANAGE_STATE(AfxGetStaticModuleState())
        if (FireOnRequestEdit(1) == S_OK)
        {
            m_lValue = newVal;
            FireOnChanged(1);
        }
        return S_OK;
}

Remarquez que les deux méthodes FireOnRequestEdit et FireOnChanged ne sont pas implémentées par la classe d'implémentation du point de connexion IPropertyNotifySinkCP, mais par la classe de base CComControl. En fait, la classe CComControl utilise les informations stockées dans la property map pour effectuer les notifications, property map qui elle-même utilise les fonctions fournies par le point de connexion IPropertyNotifySink. L'utilisation de la classe CComControl impose donc la création de la property map. Afin de s'assurer que celle-ci existe, la classe CComControl utilise un type défini par un typedef dans la macro BEGIN_PROPERTY_MAP. Celle-ci utilise également un type défini dans la classe d'implémentation du point de connexion IPropertyNotifySinkCP. En fin de compte, les ATL imposent d'être un contrôle pour gérer ce point de connexion (bien qu'en théorie, ce ne soit pas strictement nécessaire).

Créé le 9 juillet 2000  par Christian Casteyde
  

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 © 2013 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.