Frédéric Colin
WCF : Introspection dynamique
Maintenant que WCF est bien ancré chez les développeurs d’applications .NET, de nouveaux besoins de dynamicité naissent. L’introspection de services est tout à fait possible et c’est ce que je me propose de vous présenter dans cet article.
Par Frédéric Colin publié le 16/04/2009 à 22:28, lu 2172 fois, 11 pages
 9 | L’IHM cliente
L’IHM cliente est définie sous la forme d’une application Windows Forms dont l’interface a été décrite précédemment. Voici le diagramme de classe mis en place (hors IHM) :
 
/content/9de5dea6-fde2-4cd9-a392-5296e67a197f/image9.png
 
« Helper » : classe contenant une méthode d’extension pour la gestion de la mise à jour des contrôles de l’IHM sur le thread de création dudit contrôle
« Factory » : classe chargée de créer les proxies dynamiques à partir des informations récupérées de l’introspection d’un MexEndPoint
« Instrospector » : classe chargée de la récupération asynchrone des données d’un MexEndPoint et de la notification de l’IHM une fois les données récupérées.
Les principaux points clés de ce projet IHM sont les suivants :
  • Pour le chargement des métadonnées concernant les propriétés "Contracts" et "EndPoints" (classe « Introspector »)
Tout se passe au niveau de la méthode asynchrone « Start ». Cette dernière se contente d’utiliser le « ThreadPool » pour exécuter de manière asynchrone la récupération des métadonnées d’un Mex EndPoint donné. La méthode privée « GetContractsAndEndPoints » est celle exécutée sur un thread à part. La première chose à faire consiste à gérer le type de transport (http ou tcp) sur lequel le Mex EndPoint est défini et de créer un « CustomBinding » paramétré avec ce dernier :

// Create and parametrize binding to address Mex EndPoint (Http or Tcp)

TransportBindingElement binding = null;

 

if ( this.MexEndPointAddress.StartsWith("http") )

{

    binding = new HttpTransportBindingElement();

    binding.MaxReceivedMessageSize *= 10;

}

else

{

    binding = new TcpTransportBindingElement();

}

 

CustomBinding cusBinding = new CustomBinding(binding);

L’étape suivante consiste à interroger le Mex EndPoint retenu. Pour cela, nous utilisons la classe « MetaDataExchangeClient » paramétrée avec le « CustomBinding » précédent contenant l’url du Mex EndPoint et paramétré en conséquence. Ensuite nous lançons l’interrogation du Mex EndPoint par l’appel à la méthode « GetMetadata » qui renvoi un jeu de données (« Metadataset ») en XML dont nous simplifions l’accès par l’intermédiaire de la classe « WsdlImporter » qui elle possède les méthodes nécessaires et suffisantes pour constituer les collections de contrats et de EndPoints :

// Parametrize metadaLta download

MetadataExchangeClient client = new MetadataExchangeClient(cusBinding);

MetadataSet metadata = client.GetMetadata(new EndpointAddress(MexEndPointAddress));

MetadataImporter wsdlImporter = new WsdlImporter(metadata);

 

// Get all Contracts

Contracts = wsdlImporter.ImportAllContracts();

// Get all EndPoints

EndPoints = wsdlImporter.ImportAllEndpoints();

Voici les collections qui sont stockées :

public Collection<ContractDescription> Contracts { get; set; }

public ServiceEndpointCollection EndPoints { get; set; }

Il ne nous reste ensuite plus qu’à notifier les éventuels abonnés de l’arrivée des données concernant les contrats et les EndPoints. Pour cela, deux événements ont été créés :

public event EventHandler<ParametrizedEventArgs<Collection<ContractDescription>>> ContractsFilled;

public event EventHandler<ParametrizedEventArgs<ServiceEndpointCollection>> EndPointsFilled;

public class ParametrizedEventArgs<T> : EventArgs

{

    public T Items;

}

Et voici le code de la notification des éventuels abonnés :

// Client notification for Contracts

if (ContractsFilled != null)

    ContractsFilled.Invoke(

        this,

        new ParametrizedEventArgs<Collection<ContractDescription>>() { Items = Contracts });

 

// Client notification for EndPoints

if (EndPointsFilled != null)

    EndPointsFilled.Invoke(

        this,

        new ParametrizedEventArgs<ServiceEndpointCollection>() { Items = EndPoints });

  • Pour l’invocation des méthodes de récupération des données
Pour cela, nous utilisons la classe « Factory » et sa méthode « GetObject » qui à partir d’un Binding donné et de l’adresse d’un EndPoint crée le canal qui sera utilisé pour la communication. Cette méthode générique étant paramétrée à l’aide du type du contrat de service souhaité.

public class Factory

{

    public static T GetObject<T>(System.ServiceModel.Channels.Binding binding, EndpointAddress epAddress)

    {

        ChannelFactory<T> factory = new ChannelFactory<T>(binding, epAddress);

 

        return factory.CreateChannel();

    }

}

Pour ce qui est ensuite de l’utilisation de cette fabrique, tout est un savant dosage de récupération du binding sélectionné dans l’interface (en fonction d’un BindingSource) et de paramétrage par réflexion du transport en terme de limites (taille des tableaux, etc.). Ce qui nous donne :

private void Execute(Int32? productID)

{

    ServiceEndpoint ep = (ServiceEndpoint)bindingSource2.Current;

 

    if (ep != null)

    {

        System.ServiceModel.Channels.BindingElementCollection elements = ep.Binding.CreateBindingElements();

 

        // Inside selected binding get the corresponding transport

        System.ServiceModel.Channels.TransportBindingElement elem

            = elements.Find<System.ServiceModel.Channels.TransportBindingElement>();

 

        // Manage binding Element genericity

        PropertyInfo prop = elem.GetType().GetProperty("MaxBufferSize");

        if (prop != null)

        {

            prop.SetValue(elem, 65000000, null);

        }

 

        // Arbitrary increase of MaxReceivedMessageSize

        elem.MaxReceivedMessageSize = 65000000;

 

        // Get the encoding if existing to increase quotas and MaxArrayLength

        System.ServiceModel.Channels.MessageEncodingBindingElement elem2

            = elements.Find<System.ServiceModel.Channels.MessageEncodingBindingElement>();

 

        prop = elem2.GetType().GetProperty("ReaderQuotas");

        if (prop != null)

        {

            prop.PropertyType.GetProperty("MaxArrayLength").SetValue(prop.GetValue(elem2, null), 1000000, null);

        }

 

        // Re-create a custom binding with all these parameters to use it with our Factory

        ep.Binding = new System.ServiceModel.Channels.CustomBinding(elements);

 

        // Call through created channel by the factory

        THB.Sample.ServiceContracts.IProductService service

            = Factory.GetObject<ServiceContracts.IProductService>(ep.Binding, ep.Address);

 

        if ( !productID.HasValue )

            bindingSource1.DataSource = service.Get();

        else

            bindingSource1.DataSource = service.Get(productID.Value);

 

        // Close the channel

        ((System.ServiceModel.Channels.IChannel)service).Close();

    }

}

Dans le code précédent, il va de soit que le codage en dur des différentes limites est mal venu et peut-être amélioré.
  • Pour le rafraichissement de l’interface graphique
Au démarrage du formulaire, nous créons une instance unique de la classe « Introspector ». Cette dernière nous servira pour le requêtage sur le Mex EndPoint considéré, ainsi que pour le stockage des métadonnées récupérées :

// Parametrize introspection

private void CreateIntrospector()

{

    IntrospectorInstance = new Introspector();

    IntrospectorInstance.ContractsFilled

        += new EventHandler<Introspector.ParametrizedEventArgs<Collection<ContractDescription>>>

            (IntrospectorInstance_ContractsFilled);

    IntrospectorInstance.EndPointsFilled

        += new EventHandler<Introspector.ParametrizedEventArgs<ServiceEndpointCollection>>

            (IntrospectorInstance_EndPointsFilled);

}

Ensuite la gestion de la notification est réalisée par les deux abonnements que vous voyez dans le code précédent.
Les deux méthodes « IntrospectorInstance_ContractsFilled » et « IntrospectorInstance_EndPointsFilled » fonctionnent de la même manière. Il s’agit en effet d’invoquer sur le thread ayant créé les contrôles, une méthode de remplissage et/ou de databinding ; sachant que la méthode reçoit en paramètre la collection qui vient d’être mise à jour sur l’instance de la classe « Introspector » :

void IntrospectorInstance_ContractsFilled(object sender

    , Introspector.ParametrizedEventArgs<Collection<ContractDescription>> e)

{

    try

    {

        treeView1.Invoke<System.Collections.ObjectModel.Collection<ContractDescription>>(FillContracts

            , e.Items);

 

        toolStripStatusLabel1.Text = "Getting EndPoints ...";

    }

    catch (Exception ex)

    {

        MessageBox.Show(ex.ToString());

    }

}

 

void IntrospectorInstance_EndPointsFilled(object sender

    , Introspector.ParametrizedEventArgs<ServiceEndpointCollection> e)

{

    treeView1.Invoke<ServiceEndpointCollection>(FillEndPoints, e.Items);

}

private void FillContracts(System.Collections.ObjectModel.Collection<ContractDescription> items)

{

    BuildTree(treeView1.Nodes.Add(SERVICES), items);

}

 

private void FillEndPoints(ServiceEndpointCollection items)

{

    BuildTree(treeView1.Nodes.Add(ENDPOINTS), items);

 

    toolStripStatusLabel1.Text = "";

}

Pour réaliser cela, j’ai donc créé une méthode d’extension paramétrée (Invoke). J’avoue avoir bien pris goût aux méthodes d’extension dont le concept initial m’avait gêné (étendre des classes avec des méthodes, sans héritage et même si le concepteur ne l’a pas prévu), mais qui sont d’une praticité sans équivoque :

public static class Helper

{

    public static void Invoke<T>(this Control control, Action<T> action, T param)

    {

        if (control.InvokeRequired)

        {

            control.Invoke(action, param);

        }

        else

        {

            action(param);

        }

    }

}

  • Pour le dump dans la treeview
Une méthode récursive et fonctionnant par réflexion sur un modèle objet est chargée de réaliser le dump dans la treeview. Il faut avouer que ce dump est incomplet car il ne prend pas en compte tous les types d’éléments (les tableaux, tout ce qui n’est pas IEnumerable, etc.) :

private void BuildTree(TreeNode node, object o)

{

    if (o != null)

    {

        Type t = o.GetType();

 

        if (t.GetInterface("IEnumerable") != null)

        {

            foreach (object item in (IEnumerable)o)

            {

                BuildTree(node.Nodes.Add(item.ToString()), item);

            }

        }

        else

        {

            foreach (PropertyInfo p in t.GetProperties())

            {

                if (p.PropertyType.GetInterface("IEnumerable") != null && p.PropertyType != typeof(String))

                {

                    BuildTree(node.Nodes.Add(p.Name), p.GetValue(o, null));

                }

                else

                {

                    object value = p.GetValue(o, null);

                    TreeNode n = node.Nodes.Add(p.Name, String.Format("{0} = {1}", p.Name, value));

                    n.Tag = value;

                }

            }

        }

    }

}

Le reste du code de l’IHM n’est là que pour gérer les databindings divers et variés et la gestion des contrôles de l‘IHM et ne présente aucune difficulté particulière.
 
» Démarrer une discussion