Trouver une ressource (Nouvelle version du moteur, plus rapide & pertinent, essayez le !)
Vous ne trouvez pas de réponse à votre problème ? Alors posez la question dans le forum.
Souvenez-vous qu'il n'y a jamais de question bête, mais rester dans l'ignorance parce que l'on n'ose pas poser une question, ça c'est une erreur !
C# DANS VISUAL STUDIO 2005: EXPLICATIONS SUR DES POINTS OBSCURS
Information sur le tutorial
Description
L'objectif de ce tutorial est de vous éclairer sur des points difficiles, et rarement expliqués de C# dans Visual Studio 2005. Ce n'est pas un cours intégral de C#. Je complèterai ce tuto au fur et à mesure.
Tutorial
SOMMAIRE I) Généralités I.1) Un langage puissant et accessible I.2) Créer ses classes en c# II) CREER SES PROPRES EVENEMENTS EN C# .NET II.1) Rappel sur les procédures liées( aux évènements) II.2) La procédure à suivre pour créer ses évènements III) Les services windows III.1) Généralités III.2) Création d'un service Windows III.3) La classe Service1 III.4) La classe Program III.5) Installation d'un service Windows III.6) Ajouter la classe ProjectInstaller III.7) Création du programme d'installation III.8) Génération et utilisation de l'installation III.9) Installation en ligne de commande IV) Le .net remoting IV.I) L'application serveur IV.II) L'objet serveur IV.III) L'application cliente IV.IV) Remarques sur .net remoting COURS VISUAL C# .NET 2005 I ) Généralités Ce cours est volontairement incomplet. Il vous éclaircira certains sujets de C#. Mon objectif est surtout d'indiquer des explications sur des points qu'on n'explique pas ailleurs, et qui m'ont posé problème. J'enrichirai le cours au fur et à mesure. I.1) Un langage puissant et accessible C# est un langage qui allie la puissance de C++, et la facilité d'approche de Visual Basic. Il se rapproche beaucoup aussi du langage Java. Il permet d'éviter les redondances du C++, et la lourdeur du code C++. Une des différences principales entre C# et C++, est qu'on cache, comme en VB et en Java, les références des objets. Ainsi, en C++, on écrira, au sujet d'une instanciation d'une classe personne: (C++) personne^ Michel = gcnew personne( ); alors qu'en C#: (C#) personne Michel = newpersonne( ); ( ce qui ressemble à VB .net) En c++, Michel est une variable référence sur un objet de la classe personne. Alors qu'en c#, Michel est un objet de la classe personne( tout en étant aussi, selon le contexte, une référence sur un objet personne). On évite ainsi la lourdeur du c++, qui nous force à se trimbaler les top-level(^). Et cela évite aussi les -> du C++ pour accéder aux membres des objets. Ainsi, en C++, on fera Michel->age pour accéder à l'age de Michel En c#: On écrira Michel.age : on utilise le point(.), car Michel est considéré comme un objet de la classe personne. On peut dès lors se concentrer uniquement sur la programmation orientée objet, et éviter de s'encombrer inutilement l'esprit avec la manière dont sont implémentés les objets. Cependant, la puissance du C++ n'est pas entachée. Les concepteurs du C# .net ont fait des choix qui montre clairement leur volonté de garder toute la richesse du C++. Ainsi, le main est visible dès la création d'un projet. Ainsi que le Application.Run( pas comme en Visual Basic où on nous cache la création du formulaire principal): Application.Run( newForm1( )); Le :: du C++ a été remplacé par le point(.), ce qui fait penser à VB. Le :: c++ nous fait bien comprendre ce qu'on fait, mais complique la lisibilité du programme. Le gcnew du c++, spécial pour les objets managés, est remplacé par le new en c# . I.2) Créer ses classes en c# Contrairement à c++ .net, c# vous permet de créer des modules de classes, comme en VB .net . Le fichier est un fichier d'extension .cs, comme tout fichier source. Et vous pourrez utiliser votre classe dans tous les fichiers sources de votre projet. En c++, vous devez écrire vos classes dans un fichier .h, puis faire un #include de ce fichier à chaque fois que vous désirez utiliser votre classe quelque part. La démarche est donc très simple en C#, et est parfaitement adaptée à la programmation objet. II) CREER SES PROPRES EVENEMENTS EN C# .NET Il est bon de savoir créer ses propres évènements, afin de pouvoir faire sa propre programmation évènementielle. Il est possible de réaliser cela en C# .net, mais aussi en C++ et en Visual Basic .net . Voici les différentes étapes à suivre. Prenons l'exemple d'une classe voiture, qui crée des objets voiture. On veut pouvoir disposer d'un évènement FREINE, qu'on déclenchera au freinage d'une voiture. II.1) Rappel sur les procédures liées( aux évènements) On peut observer très facilement, par rapport aux lignes générées automatiquement par visual studio en cas d'évènement windows, que la procédure liée est de la forme suivante
private void button1_Click(object sender, EventArgs e)
De plus, le code suivant est auto-généré lui-aussi, dans la fonction initializecomponent( ) de form1.Designer.cs :
this.button1.Click += new System.EventHandler(this.button1_Click);
Cette ligne permet de lier la fonction button1_Click à l'évenement Click du bouton 1. Nous l'expliquerons plus en détail bientôt.
Revenons à la ligne précédente:
private void button1_Click(object sender, EventArgs e) { }
Ceci est donc une fonction qui sera appelée en cas de click sur le bouton1. Cette fonction possède deux paramètres.
- sender, qui est un objet. sender est l'objet qui a émis l'évènement. L'émetteur de l'évènement peut y mettre ce qu'il veut. C'est juste une information pour la fonction liée, pour la renseigner. Ici, le sender est l'objet button1, donc on n'a pas l'impression que ce paramètre est utile. Mais parfois, il est nécessaire. Par exemple, dans notre cas, on pourrait y mettre la voiture qui freine.
- L'argument e est un objet, qui encapsule toutes les informations intéressantes concernant l'évènement. Par exemple, dans le cas d'un appui sur une touche, le code de la touche pressée. Dans notre cas, cela peut être les circonstances au moment du freinage( pluie, soleil, etc), la vitesse au moment de freiner, etc. Cet argument doit forcément être d'une classe dérivée de la classe System.EventArgs . Par exemple, dans le cas de l'évènement KeyDown:
private void Form1_KeyDown(object sender, KeyEventArgs e) { }
Le e ici est de la classe KeyEventArgs, qui est une classe qui hérite de la classe EventArgs. La classe KeyEventArgs est spécialement adaptée pour cet évènement. Elle possède notamment un attribut e.KeyCode, qui est le code de la touche pressée. Ce KeyCode ne figure pas, par contre, dans la classe EventArgs. Cet objet e est là pour aider la fonction liée. Pour lui donner des informations que l'évènement KeyDown seul, ne suffit pas à donner( il nous indique juste qu'une touche a été enfoncée). Le e est donc nécessaire. II.2) La procédure à suivre pour créer ses évènements Pour créer ses propres évènements, la façon de procéder est composée de plusieurs parties.
- Créer sa propre classe EventArgs - Se créer une classe EmetEvent, qui permettra de créer un objet émetteur d'évènements. - Procéder à quelques déclarations et initialisations, à l'extérieur, par exemple dans la classe Form1( votre classe de formulaire principal). Par exemple se créer un objet émetteur d'évènements.
Nous étudierons un exemple complet, celui des objets voiture et de l'évènement FREINE. II.2.1) Créer sa propre classe EventArgs Nous devons tout d'abord nous créer notre propre classe EventArgs, qui tient compte des particularités de notre évènement. Faisons la dans un module de classe appelé VoitEventArgs.cs
namespace CreerEventCsharp { classVoitEventArgs : EventArgs { public string type_freinage; //pluie, etc public int vitesse_fr; //vitesse au moment de freiner public VoitEventArgs( ) { } public VoitEventArgs(string type_fr) { this.type_freinage = type_fr; } } //fin classe } //fin namespace
Notre classe VoitEventArgs hérite de la classe System.EventArgs, bien sûr.
Puis nous avons les 2 propriétés type_freinage et vitesse_fr, qui vont donner au programmeur des renseignements sur l'évènement FREINE. Enfin nous avons 2 constructeurs de la classe. L'objet e nous concernant sera donc une instanciation de la classe VoitEventArgs. II.2.2) Créer une classe EmetEvent Créons cette classe dans un module de classe EmetEvent.cs . Cette classe servira à créer un objet émetteur d'évènement. Cet objet sera utilisé pour émettre l'évènement FREINE. Et un de ses membres sera un event FREINE. On ne peut émettre des évènements qu'à partir de la classe qui contient le membre event. C'est pour cela que pour émettre des évènements FREINE, nous prévoirons des méthodes à l'intérieur de l'objet émetteur d'évènements. Ainsi la méthode emet_freine_pluie émet un évènement FREINE, avec un e.type_freinage à "pluie". On pourra appeler cet méthode de n'importe où, donc émettre des évènements de n'importe où.
Le constructeur de EmetEvent commence par créer un objet e_voit( une des propriétés de EmetEvent).
VoitEventHandler est en réalité une classe qui hérite de la classe System.EventHandler . Les objets EventHandler contiennent les adresses des fonctions liées aux évènements. Ainsi le framework saura quelles méthodes appeler dans le cas du déclenchement de l'évènement. Les fonctions liées mises dans les objets de la classe EventHandler doivent avoir obligatoirement 2 paramètres: un sender de type object, et un objet e d'une classe dérivée de la classe EventsArgs. Et ces fonctions ne doivent rien retourner( void).
Cependant, le VoitEventHandler est déclaré dans la classe comme une fonction "déléguée". Et on fait une sorte de déclaration de son prototype. Cette notion de déléguée est juste une vue de l'esprit, pour simplifier les choses. VoitEventHandler n'est pas une fonction en réalité, mais un objet( quand la classe sera instanciée) contenant toutes les adresses des fonctions liées. On peut voir VoitEventHandler comme UNE fonction déléguée, représentant toutes les fonctions liées. Et on déclare ainsi le prototype de cette fonction déléguée. Grâce à cet déclaration de prototype, le framework saura le prototype de toutes les fonctions liées( qui auront toutes ce prototype). Je vous conseille de voir l'objet de la classe VoitEventHandler comme un objet de la classe System.EventHandler( un 'traiteur' d'évènements ); et de ne pas voir VoitEventHandler comme une fonction, car ce n'en est pas une. D'ailleurs les membres event, tel FREINE, sont déclarés comme des objets de la classe EventHandler( dans notre cas de la classe VoitEventHandler).
On déclare ensuite un membre event. Event n'est pas un type, c'est un genre de membre. Il y a les attributs, les méthodes, et les events! . On appelle cet event: FREINE. Cet event est un objet de la classe VoitEventHandler. Ainsi, un évènement, est juste un objet traiteur d'évènements, qui contient les références de toutes les fonctions liées. Ce qui est logique.
Enfin, on se prévoit des méthodes, telles emet_freine_pluie, qui sauront capables d'émettre des évènements de n'importe où dans notre programme. Et avec un objet e rempli d'une manière qui nous arrange. Emet_freine_pluie, par exemple, non seulement émet un évènement FREINE, mais en plus fournit un e avec le champ e.type_freinage = "pluie". On pourra appeler par exemple emet_freine_pluie à partir de la form1!
namespace CreerEventCsharp { classEmetEvent { privateVoitEventArgs e_voit; public delegate void VoitEventHandler(object sender, VoitEventArgs e); public eventVoitEventHandler FREINE; public void EmetEvent( ){ this.e_voit = newVoitEventArgs(); } public void emet_freine_pluie( object sender){
this.e_voit.type_freinage = "pluie"; this.FREINE( sender, this.e_voit ); }
} //Fin classe
} //Fin namespace II.2.3) Les déclarations à l'extérieur- Se créer un objet émetteur d'évènement, de la classe EmetEvent, par exemple comme attribut de la classe Form1.
publicEmetEvent Emetteur_Voit
Puis, dans le form1_load(ou autre):
this.Emetteur_voit = newEmetEvent( );
- Dans le form1_load, par exemple, ajouter notre évènement à notre objet de la classe VoitEventHandler
this.Emetteur_voit.FREINE += newEmetEvent.VoitEventHandler( ma_fct_liee);
Ceci se fait avec une syntaxe spécial( "+=" ), qui a un sens particulier ici. On "ajoute" à notre membre FREINE, qui est un objet VoitEventHandler, un nouvel objet VoitEventHandler. Ce nouvel objet est initialisé avec un constructeur à 1 paramètre. Le paramètre est l'adresse de la fonction liée, c'est donc un pointeur de fonction: il suffit donc d'écrire le nom de la fonction liée, et C# comprendra qu'il doit donner la référence, car les fonctions se passent par référence.. Tout ceci est une syntaxe spéciale, que visual studio comprendra, et il comprendra que vous voulez ajouter cet fonction à votre objet VoitEventHandler. Vous pouvez ajouter, de cette manière, autant de fonctions liées que vous le souhaitez. Et vous pouvez aussi utiliser le "-=" pour en retirer. Remarque: cette syntaxe est utilisée dans les lignes auto-générées par visual studio dans la classe form1, dans la méthode initializecomponent, pour lier vos fonctions aux évènements windows( par exemple votre fonction button1_click).
- Ensuite, on peut déclencher les évènements comme bon nous semble, en appelant les méthodes de la classe EmetEvent
this.Emetteur_voit.emet_freine_pluie(this);
le this en paramètre correspond au formulaire principal de la classe form1, dans mon exemple. On aurait pu mettre la voiture qui freine.
- Et bien sûr, il faut se faire ses procédures liées, exactement de la même manière que pour les évènements windows.
void ma_fct_liee( object sender, VoitEventArgs e ) {
this.textBox1.Text = "1 event FREINE emis. e.text:=' " + e.type_freinage + " ' . sender: " + sender.ToString(); } III) LES SERVICES WINDOWSIII.1) GENERALITES Les services Windows sont des applications particulières. On les appelait auparavant "services NT". Ce sont des programmes qui tournent en tâche de fond, et qui exécutent une certaine tâche. Les services Windows peuvent démarrer soit au démarrage de l'ordinateur, soit à la demande. Cette notion de service Windows répond à un besoin. En effet, dans certains cas, on peut avoir besoin d'un programme qui marche en arrière-plan et qui nous rend un certain service. Il existe un gestionnaire des services windows, accessible par Démarrer->Panneau de configuration->Performances et maintenance->Outils d'administration->Services . On peut aussi l'avoir par Démarrer -> Exécuter -> services.msc . Cet interface s'appelle le gestionnaire de contrôle des services. Grâce à ce gestionnaire, on peut effectuer les tâches courantes sur les services: avoir la liste des services Windows, leur description, leur état( démarré, arrêté ), leur mode de démarrage( automatique, manuel, ...). Windows XP possède ses propres services Windows pour ses besoins personnels. Ainsi le service "Mises à jour automatiques" de Windows, comme l'indique sa description, se charge d'activer le téléchargement et l'installation des mises à jour Windows. Vous saviez bien que ces mises à jour de Windows étaient effectuées par un programme en arrière-plan, car elles avaient lieu en même temps que vous travailliez! Et bien, c'est ce service qui fait cela! Cela vous montre bien que ce concept de "service windows" est nécessaire pour certains besoins. Un service Windows ne possède pas d'interface utilisateur. Il n'est pas fait pour cela. Cet absence d'interface correspond bien à une utilisation sur un serveur. Car les services windows sont souvent utiles sur un serveur( mais pas uniquement). Si vous désirez écrire un résultat, vous pouvez le faire dans le journal des événements Windows( nous en reparlerons, ceci est très utile, et très couramment utilisé. C'est un moyen pour votre service de communiquer). Votre service peut également envoyer un mail à l'administrateur. Il est possible de créer vos propres services en c# .net. Nous allons voir comment procéder.
III.2) Création d'un service Windows Dans Visual Studio 2005, allez dans File -> New -> Project . Dérouler le noeud C#( sinon on n'a pas accès aux services Windows). Dans le noeud C#, choisir la branche "Windows". Puis choisir "Windows Service" dans les modèles proposés. Nous allons donc créer un nouveau projet, correspondant à notre service Windows. Comme nous ne disposions pas de solution, une nouvelle solution sera également créée. Mais nous aurions très bien pu ajouter notre projet à une solution existante. Appelons notre projet "WindowsService1".
Parmi les dossiers créés, nous constatons un dossier "Properties", et un dossier "References". Ces 2 dossiers nous sont familiers par les applications Windows. "Properties" contient uniquement le classique "AssemblyInfo.cs", contenant les informations à propos du projet. Le dossier "References" contient les .dll utilisées par le projet. En-dehors de ces 2 dossiers qui nous intéressent moins, nous avons un fichier Program.cs, qui contient la méthode main, point d'entrée de l'application. C'est une méthode static, qui appartient à une classe appelée "Program".
Ce qui nous concerne surtout, c'est le fichier Service1.cs.
III.3) La classe Service1 Dans le fichier "Service1.cs" figure la classe Service1( appelée Service1 par défaut). Cette classe dérive de la classe ServiceBase( System.ServiceProcess.ServiceBase). ServiceProcess est un espace de nom. ServiceBase est la classe qui représente les services Windows. Pourquoi ne pas créer directement un objet de la classe ServiceBase? C'est parce qu'on va avoir besoin de redéfinir certaines méthodes de la classe ServiceBase. Et on pourra également définir notre propre constructeur.
Nous allons voir plus précisément ces méthodes qui nous concernent, ainsi que le constructeur.
III.3.1) La méthode OnStart Parmi les méthodes générées automatiquement figure la méthode OnStart.
III.3.1.a) DEFINITION: le mot-clé "override": J'en profite pour vous définir le mot-clé "override". Nous remarquons "override" après le "protected". "override" signifie en Anglais: "ne pas tenir compte de", et c'est exactement cela: on indique qu'on redéfinit cette méthode de la classe de base. La méthode redéfinie doit avoir la même signature que la méthode de la classe de base. On ne peut faire "override" sur une méthode que si c'est une méthode virtual ou abstract, ou elle-même override. Ce qui est logique: il faut que le concepteur de la classe de base ait prévu qu'on puisse la rééecrire. "Abstract" signifie que le contenu de la méthode n'a pas été écrit du tout, alors que pour "virtual" le contenu de la méthode est écrit, mais on précise que cette méthode peut être redéfinie par une classe dérivée. On se doute bien que les concepteurs de la classe ServiceBase ne pouvaient pas écrire la méthode OnStart, car elle dépend de chaque service. Donc ils l'ont écrite en abstract, ou virtual.
La méthode OnStart est la méthode qui sera appelée lors du démarrage de votre service Windows. Placez-y le code que vous souhaitez voir exécuter lors du démarrage du service.
III.3.2) La méthode OnStop La méthode OnStop, elle aussi en override, et la méthode qui sera appelée à l'arrêt du service. A vous donc d'y placer ce que vous désirez.
III.3.3) Le constructeur de la classe service1 Le constructeur de la classe service1 contient un appel à la méthode InitializeComponent( ). Cette méthode se trouve dans le fichier "Service1.Designer.cs", qui contient une partie de la définition de la classe Service1.
III.3.3.a) DEFINITION:le mot-clé "partial" Profitons-en pour définir le mot-clé "partial". Il peut être intéressant de découper une définition de classe dans deux ou plusieurs fichiers, afin d'en faciliter la lecture. On peut faire cette séparation d'après le sens de ce qu'on met dans une partie ou dans l'autre. On pourra, par exemple, mettre les éléments secondaires dans un fichier à part, afin de clarifier la classe. Le mot-clé partial veut uniquement dire qu'on ne définit pas la classe dans sa totalité, mais juste une partie de la classe. Le reste de la classe se trouve dans un ou plusieurs autres fichiers. Dans notre cas, la classe Service1 est découpée en deux: une partie( la plus importante et la plus significative) dans le fichier Service1, et l'autre partie dans le fichier Service1.Designer.cs . Le mot clé "partial" figure dans chacune des deux définitions de classe.
Le fichier Service1.Designer.cs possède notamment la méthode InitializeComponent( ). Dans cette méthode d'initialisation, nous trouvons:
this.ServiceName = "Service1"; Ici on définit la propriété name du service Windows. Nommez le comme vous voulez, mettons par exemple this.ServiceName = "NomService"; C'est de cette façon que Windows désignera votre service quand il vous en parlera ( par exemple dans l'observateur d'évènement lors du démarrage ou de l'arrêt). Nous verrons l'observateur d'évènement bientôt.
III.4) La classe program Le fichier program.cs contient la classe program . Cette classe program contient un seul élément: une méthode main. Cette méthode est déclarée static ( elle peut par conséquent être appelée directement, sans avoir d'objet de la classe program). Ce main est le point d'entrée de l'application. Nous savons bien que toute application a besoin d'un point d'entrée, sinon l'ordinateur ne saurait pas où elle commence.
Dans ce main, on aurait pu faire:
//on aurait pu faire ServiceBase monService; monService = newService1( ); ServiceBase.Run(monService);
Dans le code ci-dessus, on crée d'abord un objet de notre classe Service1. Puis on appelle la méthode statique Run, de la classe ServiceBase. Cette méthode est nécessaire pour commencer l'exécution du service. Elle charge en mémoire le code des méthodes du service( même si on aurait pu croire que théoriquement ce code était déjà dans l'objet, car la méthode OnStart est une méthode de l'objet. Mais dans la pratique, le code est chargé au moment du Run, car c'est à ce moment qu'on en aura besoin. Le code des méthodes ne figure pas dans chaque objet en réalité, car ce serait une répétition inutile). Après avoir chargé le code, la méthode Run appelle la méthode OnStart de monService.
Pourquoi ce main est-il indispensable, et pourquoi ne pas appeler directement la méthode OnStart, au lieu d'appeler le main? Et bien, tout simplement parce que l'objet monService n'avait pas encore été créé! Donc on ne pouvait pas appeler sa méthode OnStart!
Revenons au code généré automatiquement par Visual Studio. Il est un peu différent du mien. Car le cas du démarrage de plusieurs services est prévu; moi je ne me suis occupé que du cas d'un unique service. En effet, la méthode Run a une surchage autorisant le démarrage de plusieurs services. Il suffit de lui passer un tableau d'objet de la classe ServiceBase. Il faut ainsi lui donner un ServiceBase[].
ServicesToRun = newServiceBase[] { newService1() };
Cette ligne crée un tableau de services. Et elle initialise ce tableau avec un seul élément: avec un objet de la classe Service1. Le new Service1 crée le service. L'objet créé est rangé dans le tableau de services, à l'indice 0. La méthode Run va lancer chacun des services du tableau qu'on lui passe en paramètre.
On remarquera que notre application peut exécuter plusieurs services en parallèle. Par conséquent, on en conclut que chaque service sera lancé dans un nouveau thread. Mais tous ces services( du tableau ServicesToRun) appartiendront bien sûr à la même application.
III.5) Installation d'un service Windows Nous avons appris comment créer un service, mais pour pouvoir utiliser un service, il faut l'avoir installé auparavant dans Windows XP. Pourquoi cette installation? Parce que le système d'exploitation a besoin qu'on lui déclare les services windows, afin par exemple de pouvoir vous en afficher la liste. L'installation a lieu une fois pour toute, comme lorsque vous installez une nouvelle application avec un programme d'installation. D'ailleurs l'installation se fera avec le classique Windows Installer, car Visual Studio vous aura créé des fichiers pour Windows Installer.
Comment générer ces fichiers pour Windows Installer? C'est ce que nous allons voir.
III.6) Ajouter la classe ProjectInstaller Un service Windows doit contenir, dans son projet, une classe ProjectInstaller qui hérite de la classe Installer. Cette classe contient les informations utiles sur le service, afin de permettre à "celui"( cela peut être un installeur Windows installer) qui voudra l'installer d'avoir les données( sur le service) dont il a besoin pour l'installer. Au moment où le service sera "déclaré" à Windows( pour que, par exemple, on puisse le voir dans la liste des services affichée par le gestionnaire de contrôle des services), "celui" qui l'installera ira chercher dans cette classe les informations( sur le service) lui étant nécessaires. Voilà ce que j'entends ici par "installer", et cela n'a rien à voir avec la création d'un programme d'installation. Cette classe permet uniquement de définir les caractéristiques de notre service. Ces caractéristiques du service sont les suivantes. Par exemple: le type de démarrage( automatique, manuel), le nom du service, la machine sur laquelle s'exécute le service, etc. La classe ProjectInstaller est uniquement, finallement, un complément à la définition de notre service faite par la classe ServiceBase. Dans la classe ProjectInstaller, on y mettra plutôt toutes les informations sur le service qui ont rapport avec son installation. C'est uniquement un découpage en deux objets( Service1 et ProjectInstaller), au lieu d'en faire un seul, par souci de faire une belle programmation objet. Mais les concepteurs des classes auraient pu regrouper ces deux classes en une seule grande classe "Service".
Comment ajouter cette classe? Cela est très simple. Il suffit d'aller dans la vue design de votre service( le fichier service1.cs). Puis vous effectuez un clic droit, et vous choisissez "Add installer". Cela vous crée un fichier ProjectInstaller1.cs, qui contient une classe ProjectInstaller( héritant de la classe Installer). Cette classe contient deux attributs privés, qui sont serviceInstaller1 et serviceProcessInstaller1. Ces deux composants sont d'ailleurs visibles dans la vue design de ProjectInstaller1. Ces deux attributs apparaissent dans le fichier ProjectInstaller1.Designer.cs. Ce sont respectivement des objets de la classe ServiceProcessInstaller et de la classe ServiceInstaller. ServiceInstaller permet de définir des caractéristiques du service lui-même( son type de démarrage, etc), afin de pouvoir "l'installer" au mieux. ServiceProcessInstaller fait de même, mais il définit lui des caractéristiques en rapport avec le processus du service, comme la machine sur laquelle le processus va tourner. Là aussi, c'est un découpage (en deux classes) qui a été choisi par souci de faire une belle( et pratique) programmation orientée objet. Nous allons définir dans le design( mais on peut le faire par code), les propriétés ServiceName et StartType de ServiceInstaller1
ServiceInstaller1.ServiceName => "NomService" ServiceInstaller1.StartType => manual
Nous allons définir les propriétés suivantes de ServiceProcessInstaller1:
ServiceProcessInstaller1.Account = LocalSystem
III.7) Création du programme d'installation Nous devons maintenant créer un programme d'installation. Un exécutable qui installera notre service dans le système d'exploitation. Ce programme d'installation ressemblera à n'importe quel programme d'installation d'application, et sera un programme compatible avec l'installeur Windows Installer. Une fois installé, votre application sera d'ailleurs visible dans la liste des applications de Panneau de configuration -> Ajout/Suppression de programmes. Et vous pourrez aussi le supprimer à partir de cette liste, comme n'importe quelle application.
La marche à suivre est de créer un projet de configuration, qui sera un nouveau projet qui s'ajoutera à notre solution( le 2eme projet de notre solution). Pour créer un projet de configuration, faites File -> Add -> New Project . Puis dérouler le noeud Other Project Types. Puis cliquer sur "setup et deployment". Il faut alors choisir le type "Setup Project". Mettez "SetupService" comme nom de projet. Un nouveau projet est ajouté à la solution "WindowsService1".
En faisant un clic droit sur le nom du projet, nous avons un menu contextuel qui s'ouvre. Il nous donne diverses possibilités. Nous allons choisir "Actions personnalisées"( "Customs Actions"), accessible par "View" puis "Customs Actions". Quatre petits dossiers s'affichent dans le design: "Install", "Commit", "Rollback", "Uninstall". Nous avons alors la possibilité de choisir une action personnalisée à propos notamment de l'installation et de la désinstallation. Faites un clic droit sur "Install", choisissez "Custom actions". Puis double-cliquez sur "Application folder"( dossier d'application), ou sélectionnez le dans la liste déroulante. Puis cliquez sur "Add output', et sur "Primary output"( sortie principale), qui est la sortie principale de l'application, c'est à dire le .exe du projet "WindowsService1". Nous venons simplement de dire que l'installation à faire sera l'installation du .exe du projet ServiceWindows1 ! Nous avons donc personnalisé l'installation, d'où le terme "action personnalisée".
Faites de même pour "Uninstall": clic droit "custom actions", puis "Application folder", puis "Add output", et après "Primary output". Dans le design, sous "Install" se trouve affiché "Primary output from Windows Service1". Et sous "Uninstall" se trouve affiché "Primary output from Windows Service1". Ceci est bien ce que l'on voulait. Cela veut simplement signifier qu'on déclare que lors de l'installation, on souhaite installer l'exécutable généré pour le projet Windows Service1. C'est juste une déclaration de ce qu'on veut installer! Pareil pour la désinstallation: on a juste déclaré qu'on veut désinstaller le .exe du projet Windows Service1, lors de l'action de désinstallation.
III.8) Génération et utilisation de l'installation Il ne reste plus qu'à générer chaque projet( build). D'abord le projet ServiceWindows1( par un clic droit sur le nom du projet dans l'explorateur de solutions, puis "build"). Puis générez le projet "SetupService".
Une fois généré, le projet ServiceWindows1 donne un exécutable: "ServiceWindows1.exe". Et le projet "SetupService" donne "SetupService.msi", et également "SetupService.exe". "SetupService.msi" est strictement identique (en résultat) à SetupService.exe. Donc vous devez lancer soit l'un, soit l'autre pour installer le service. La différence c'est que setup.exe n'a pas besoin du logiciel Windows Installer pour fonctionner, au contraire de SetupService.msi .
Remarques: - si vous modifiez ServiceWindows1, et que vous le générez après, il est nécessaire de générer aussi SetupService, même s'il n'a pas été modifié. En effet, rappelez vous, le projet SetupService se sert de la sortie principale du projet ServiceWindows1. Et, de plus, c'est quand même un programme d'installation de votre exécutable: il sera donc adapté à votre exécutable. Vérifiez, en cas de problème à l'installation( si on vous demande un nom d'utilisateur et un mot de passe), que la propriété Account du ServiceProcessInstaller1 est bien à "LocalSystem", et pas à "user".
III.9) Installation en ligne de commande
Il est possible d'installer les services autrement que par Windows Installer. On peut en effet ne pas créer le deuxième projet "SetupService", et juste créer le projet "ServiceWindows1". L'installation se fera dans une console DOS, grâce à l'utilitaire InstallUtil.exe du framework.
InstallUtil.exe se trouve dans c:\WINDOWS\Microsoft.NET\Framework\v2.0.50727( en dernier c'est votre version de framework).
Ouvrez donc une console DOS par Accessoires->Invite de commandes, ou par Exécuter->cmd . Puis tapez: InstallUtil.exe "c:\(...votre chemin...)\ServiceWindows1.exe(nom de votre exécutable)" Ceci installera votre service.
Pour le désinstaller, il suffit d'ajouter l'option /u :
InstallUtil.exe /u "c:\(...votre chemin...)\ServiceWindows1.exe(nom de votre exécutable)" .
Avec cette méthode, l'application n'est pas visible dans la liste des programmes de Ajout/Suppression de programmes. Mais votre service figurera bien sûr dans le gestionnaire de contrôle des services. IV) LE .NET REMOTING IV.I) L'application serveur IV.II) L'objet serveur IV.III) L'application cliente
Voici un tuto sur .net remoting, que je complèterai petit à petit.
Merci à MSDN pour ses précieux tutoriels, à partir desquels est inspiré largement ce cours.
Créer un serveur à distance:
Le serveur à distance se situe, bien entendu, sur l'ordinateur serveur. Le serveur à distance est divisé en deux parties:
- L'objet serveur.
C'est l'objet avec lequel le client communique.
- L'application serveur
IV.I) L'application serveur L'application serveur sert pour enregistrer l'objet serveur auprès des services d'accès à distance. Pour faire cet enregistrement, elle utilise la méthode RemotingConfiguration.RegisterWellKnownServiceType. Il faut remarquer que l'objet distant n'est pas créé au moment de cet enregistrement( vu sur msdn). L'instanciation de l'objet distant se produit uniquement lorsque le client essaye d'appeler une méthode depuis le client( vu sur msdn). La classe System.Runtime.Remoting.RemotingConfiguration est une classe qui fournit diverses méthodes statiques pour configurer l'infrastructure remoting.
IV.I.1) Le canal( channel)Les canaux: les canaux servent à transporter les messages des objets distants, et aussi les messages vers les objets distants( bidirectionnel). Quand un client appelle une méthode d'un objet distant, les paramètres (et d'autres choses concernant cet appel) sont transportés, à travers ce canal, vers l'objet distant. Les résultats de l'appel sont retournés vers le client de la même façon.
using System.Runtime.Remoting; using System.Runtime.Remoting.Channels; using System.Runtime.Remoting.Channels.Tcp;
//on crée un objet channel, qui écoutera sur le port 8085 TcpChannel channel = newTcpChannel(8085);
//on enregistre, auprès des services de canaux, notre canal
ChannelServices.RegisterChannel(channel);
Explications:
- On crée d'abord un objet channel, de type TcpChannel car on veut utiliser le protocole tcp( car, en réalité, les communications se font par le protocole tcp ou un autre, mais cela est caché par la notion de .net remoting, pour vous simplifier la vie. Il y a donc, en réalité, une couche tcp). On précise un numéro de port, numéro sur 16 bits( car il y a 65536 ports). Il s'agit d'un port logiciel, bien entendu, rien à voir avec un port physique. Un port logiciel, c'est juste pour que le système d'exploitation puissent nommer un "endroit" de communication. Ainsi, les applications qui veulent communiquer, préciseront l'endroit où elles veulent communiquer, et donc avec qui. Ce canal( channel en anglais) est un server channel. Il écoutera le port 8085, par exemple pour pouvoir entendre une demande d'appel de méthode de l'objet distant.
- Puis il faut enregistrer cet objet channel. Car nous avons seulement un objet channel en mémoire. On utilise la méthode ChannelServices.RegisterChannel(channel); Cette méthode va enregistrer notre canal auprès des services de canaux.
Ce canal, maintenant qu'il est enregistré, pourra être utilisé par les clients. Et le canal commence à écouter le port dès cet enregistrement. Il écoute les demandes des clients. D'ailleurs votre pare-feu vous indiquera que votre programme essaye d'agir en tant que serveur.
Comment le système d'exploitation saura que c'est celui-là qu'il faudra utiliser? Eh bien, c'est parce que c'est le seul canal qui aura été enregistré par l'application serveur. Donc le système d'exploitation peut en déduire que c'est celui-là qu'il faut utiliser.
IV.I.1.a) Définition: un serveurUn serveur, c'est comme un serveur de bar. Il attend que le client fasse une demande, et ensuite il le sert. C'est pareil en informatique. Ainsi le canal serveur attend qu'un client lui demande quelque chose: il écoute le port concerné, dans l'attente d'une demande. Puis il lui envoie sa réponse par l'intermédiaire du canal.
IV.I.2) L'enregistrement de l'objet distant Ensuite il faut enregistrer notre objet distant auprès des services d'accès à distance.
RemotingConfiguration.RegisterWellKnownServiceType( System.Type.GetType("MonNameSpace.myRemoteClass, DllObjDistant"), //(peut être remplacé par typeof( myRemoteClass ), si la classe n'est pas dans une dll "RemoteObject", WellKnownObjectMode.Singleton);
On utilise la méthode statique RegisterWellKnownServiceType. Son premier paramètre est le type de votre objet distant, donc sa classe. Il faut donc fournir un objet de type "System.type". Deux méthodes sont possibles:
* soit la classe de votre futur objet distant est dans votre projet, dans ce cas c'est facile, faites seulement typeof( votreClasse), qui retourne un objet de type "System.Type" correspondant.
*soit la classe de votre futur objet distant se trouve dans une dll. Dans ce cas, utilisez la méthode System.Type.GetType qui prend une string en paramètre. Cette string contient, en premier lieu, le nom de la classe de votre objet( y compris les espaces de nom), puis une virgule, puis le nom de votre dll contenant votre classe( sans l'extension ".dll", bien sûr).
*Et d'ailleurs, comme vous allez forcément ajouter la dll à votre projet, vous pourrez toujours vous en sortir avec typeof!
Le deuxième paramètre de RegisterWellKnownServiceType est le nom du "point final"( end point), c'est-à-dire une string qui sera le nom par lequel vos clients accèderont à l'objet. Pourquoi "point final"? C'est parce que votre client indiquera, dans une sorte d'URL( qui s'appelle URI): le protocole, le nom de l'ordinateur serveur, son port, et enfin( à la fin, d'où le nom point final ou point de terminaison) ce nom de votre objet. Exemple d'URI que le client utilisera: "tcp://localhost:8085/RemoteObject".
Le troisième paramètre de RegisterWellKnownServiceType est le mode( SingleCall ou Singleton). Vous trouverez plus loin des explications sur ce mode.
IV.I.3) Laisser actif le processus de l'application serveur- Enfin vous pouvez utiliser la méthode Readline, si votre application serveur est une application console, afin que l'application serveur ne s'arrête pas( afin que le processus continue d'exister).
Il est à remarquer que les canaux et les objets enregistrés sont disponibles uniquement tant que le processus qui les a enregistré est actif. Dès que ce processus prend fin, l'ensemble des canaux et objets enregistrés par ce processus sont automatiquement supprimés des services distants où ils ont été enregistrés. C'est pour cette raison que je vous propose le Readline.
System.Console.Writeline( "Touche 'Entrée' pour arrêter le serveur"); System.Console.Readline( );
IV.II) L'objet serveur L'objet serveur est instancié par l'application cliente. L'application cliente communique avec cet objet serveur, qui est situé sur l'ordinateur serveur. L'application cliente passe par un objet proxy créé sur l'ordinateur client.
IV.II.1) Comment construire l'objet serveur La classe de l'objet serveur( donc la classe de l'objet distant) doit hériter de la classe MarshalByRefObject, qui est la classe des objets distants. Il suffit donc de faire dériver votre classe de la classe MarshalByRefObject.
Par exemple:
public class myRemoteClass : system.MarshalByRefObject { (...) //contenu de votre classe, avec vos méthodes, etc...
public overrideobject InitializeLifetimeService( ) { //pour indiquer que la durée de vie de l'objet est illimitée return null; } }
Cette classe, comme étant utilisée par l'application serveur et par l'application suivante, peut être placée dans une dll, pour simplifier. Donc vous devez créer un projet "bibliothèque de classes"( "class library"), écrire votre classe d'objet distant, puis compiler votre projet. Vous obtiendrez une dll, que vous pourrez ajouter aux références de vos applications serveurs et clientes.
On ajoute une méthode qui surcharge la méthode InitializeLifetimeService de la classe MarshalByRefObject, afin que notre objet ait une durée de vie illimitée. En effet, pour les objets distants en singleton( ce qui est notre cas), on peut préciser la durée de vie de l'objet, dans diverses circonstances. Par exemple, il est possible de définir la durée minimale de vie de l'objet entre son activation et le premier appel de méthode. Cette durée est le InitialiseLeaseTime et est par défaut de cinq minutes. D'autres durées de vie sont définissables, comme le RenewOnCallTime qui est la durée de vie minimale entre deux appels de méthode. Si on souhaite que l'objet distant ne soit jamais détruit pendant toute la durée de l'application serveur, il suffit de surcharger la méthode InitializeLifetimeService, et de retourner null.
IV.II.2) Une autre façon de construire l'objet serveur Une variante consiste à déclarer une interface dans une dll, par exemple une interface IRemoteClass .
public interfaceIRemoteClass { int maMethodeUn( int param1, string param2 ); void maMethodeDeux( int monParam ); }
Puis on déclare notre classe myRemoteClass dans le même projet que celui de l'application serveur, en n'oubliant pas d'ajouter au projet la référence à notre dll contenant l'interface.
public classmyRemoteClass : MarshalByRefObject, IRemoteClass { int maMethodeUn( int param1, string param2 ) { //on écrit le corps de la méthode }
void maMethodeDeux( int monParam ) { //on écrit le corps de la méthode }
public override object InitializeLifetimeService( ) { //pour indiquer que la durée de vie de l'objet est illimitée return null; } }
Dans l'application serveur, on aura, bien entendu:
RemotingConfiguration.RegisterWellKnownServiceType( typeof(myRemoteClass), "RemoteObject", WellKnownObjectMode.Singleton);
Pour ce qui concerne l'application cliente, la déclaration de l'objet proxy se fera de cette façon:
IRemoteClass objet_proxy;
et
objet_proxy = (IRemoteClass) Activator.Getobject( typeof(IRemoteClass), "tcp://localhost:8085/RemoteObject");
Un "avantage" est que l'objet proxy n'est pas un MarshallByRefObject, ce qui est plus en accord avec la logique, car pas besoin qu'il dérive aussi de MarshallByRefObject car le proxy est un objet local. Mais la première méthode est tout aussi valable et efficace, et c'est même celle décrite dans msdn. Dans tout ce tuto, je parle uniquement, sauf quand c'est précisé expressément, de la première méthode(sans les interfaces).
IV.II.3) Remarque sur les accesseurs On peut remarquer qu'on peut accéder aux propriétés d'un objet distant uniquement en appelant des méthodes de cet objet. Les méthodes en question sont get et set. Je ne veux pas dire qu'on ne peut pas obtenir directement les attributs d'un objet distant. Je voulais juste faire cette remarque, je voulais juste remarquer qu'on peut tout effectuer sur un objet, uniquement en utilisant ses méthodes, y compris obtenir et fixer la valeur de ses attributs.
private string nom;
publicstring Nom { get { return nom; } set { nom = value; } }
IV.III) L'application cliente L'application cliente passe par un objet proxy créé sur l'ordinateur client. Le proxy est l'objet créé et retourné par la méthode statique Activator.GetObject. En même temps( vu sur msdn), cette méthode active l'objet distant qui est sur l'ordinateur serveur. Nous verrons que activation ne signifie pas instanciation de l'objet distant.
IV.III.1) Le channelTcpChannel channel = newTcpChannel( ); ChannelServices.RegisterChannel(channel);
Créez un objet TcpChannel et enregistrez votre canal auprès des services de canaux. C'est ce canal qui sera utilisé pour la communication avec le serveur. Comment le système d'exploitation saura que c'est celui-là qu'il faudra utiliser? Eh bien, c'est parce que c'est le seul canal qui aura été enregistré par l'application cliente. Donc le système d'exploitation peut en déduire que c'est celui-là qu'il faut utiliser.
IV.III.2) La création de l'objet proxy et l'activation de l'objet distantRemoteClass objet_proxy; //myRemoteClass est la classe de l'objet distant
objet_proxy = (myRemoteClass) Activator.Getobject( System.Type.GetType("MonNameSpace.myRemoteClass, DllObjDistant"), "tcp://localhost:8085/RemoteObject");
ou, avec la méthode avec les interfaces décrite ci-dessus:
objet_proxy = (IRemoteClass) Activator.Getobject( typeof(IRemoteClass), "tcp://localhost:8085/RemoteObject");
Rappel: L'avantage de cette deuxième méthode est que l'objet proxy n'est pas un MarshallByRefObject, ce qui est plus en accord avec la logique, car pas besoin qu'il dérive aussi de MarshallByRefObject car le proxy est un objet local. Mais la première méthode est tout aussi valable est efficace, et c'est même celle décrite dans msdn.
DllObjDistant est, comme tout-à-l'heure, le nom de l'assembly( la dll) qui contient votre classe d'objet distant, par exemple "maLib"( sans l'extension .dll). Bien entendu, dans le cas où votre classe est dans le même projet que l'application cliente, un typeof( myRemoteClass) peut remplacer le GetType. Et d'ailleurs, comme vous allez forcément ajouter la dll à votre projet, vous pourrez toujours vous en sortir avec typeof!
Bien sûr, "localhost" signifie que le serveur se situe sur l'ordinateur local( le même que le client). Si le serveur est distant, remplacez "localhost" par le nom de l'ordinateur sur lequel se situe le serveur. Exemple: "tcp://monpc:8085/RemoteObject". Remarque: "localhost" peut être remplacé par l'adresse IP 127.0.0.1 .
Dans ce code, j'ai appelé le proxy de l'objet distant "objet_proxy". C'est pour que vous compreniez bien. Mais on aurait pu l'appeler "objet_distant", en "oubliant" qu'il y a un proxy, et en imaginant cette variable comme étant l'objet distant.
A quoi sert l'objet proxy? Il nous permet de passer par un objet local, pour atteindre l'objet distant. C'est donc une copie locale de l'objet distant. Cela présente des intérêts techniques, et aussi pratiques( on peut par exemple exprimer ce qu'on veut faire, ce qu'on veut appeler comme méthode, etc). L'objet proxy n'est donc pas une fin en soi. C'est un intermédiaire, comme tout proxy. Quand on parle de serveur internet, on ne pense d'ailleurs presque jamais aux serveurs internet proxy intermédiaires. On fait comme si l'internaute était directement relié au serveur internet final!
La méthode GetObject ( de la classe Activator) est une méthode d'activation de l'objet distant ( vu sur msdn). Mais, en réalité, l'objet distant n'est pas instancié à ce moment là( vu sur msdn). Aucune connexion réseau n'a lieu. Une connexion réseau a lieu seulement lors de l'appel à une méthode de l'objet. Dans ce cas, l'objet distant est instancié si cela est nécessaire, selon que l'objet distant a été déclaré en singlecall ou pas. Si l'objet est en singlecall, l'objet distant est créé à chaque appel de méthode de cet objet. Et l'objet distant est détruit après chaque appel de méthode effectué( msdn). Les objets singleton sont créés une seule fois. Et dans le cas du singleton, tous les clients utilisent donc le même objet distant( msdn). IV.IV) Remarques sur .net RemotingIV.IV.1) Remarque sur l'accessibilité des objets de l'application cliente Il est possible qu'on ressente le besoin de passer un objet client en paramètre. Ceci est réalisable, à la condition que l'objet soit sérialisable. Sérialisable signifie transformer un objet en une série d'octets. Ainsi, sans aller plus loin dans la manière dont les choses sont implémentées réellement, on peut affirmer que cela facilite le transport de l'objet. Si on veut être plus précis sur la sérialisation, on peut se pencher sur ce qu'en dit MSDN. Le site de Microsoft nous dit ceci : - Dans un même domaine d'application, tous les objets sont passés par référence, et tous les types de données primitifs sont passés par valeur.
- Comme les références d'objet ne sont valides qu'à l'intérieur du même domaine d'application, ces objets ne peuvent être passés par référence lors d'un appel de méthode distant. Par conséquent, ils doivent être, dans ce cas, passés par valeur.
- Pour pouvoir passer un tel objet par valeur, il doit être sérialisable. Pour qu'un objet puisse être sérialisable, on doit le faire précéder de l'attribut [serializable] , oubien cet objet doit implémenter l'interface ISerializable . Autre solution : tous les objets peuvent être changés en objet distant en les dérivant de MarshalByRefObject .
- Les objets qu'il est impossible de sérialiser ne peuvent être passés à un autre domaine d'application. Ils ne peuvent donc pas devenir des objets distants.
Mais une question subsiste pour ma part : pourquoi dit-on que MarshalByRefObject passe les objets par référence ?Et quel est donc la différence entre MarshalByRefObject et MarshalByValueObject ? On peut remarquer que beaucoup de classes .net ont été précédées de l'attribut [serializable] : par conséquent, tous les objets créés avec ces classes sont sérialisables. Une façon simple et claire, lorsqu'on veut passer un objet en paramètre, est de faire dériver cet objet de la classe MarshalByRefObject. De plus, il est indispensable de donner une vie illimitée à cet objet( si on le souhaite), sous peine de le voir détruit au bout de quelques minutes. Pour ceci, il suffit de faire un override, comme d'habitude, de la méthode InitializeLifeTimeService. IV.IV.2) Remarque sur les « dangers » des pare-feu Attention, il est parfois nécessaire d'arrêter les firewalls, y compris le pare-feu Windows, pour que .net remoting fonctionne. J'ai connu une expérience avec un objet serveur sur une machine sous XP, et un client sur un autre PC sous Windows Vista Premium, où j'ai dû arrêter le pare-feu de Windows XP pour que cela marche( tout en pouvant laisser le pare-feu de Vista). IV.IV.3) Remarques générales sur .net Remoting Comme le précise MSDN, le .net Remoting fonctionne quel que soit le type d'application, sur une application Windows classique, mais aussi dans un service Windows( et sur tous les autres types d'applications). Il est d'ailleurs aisé d'écrire un service Windows qui constitue l'application serveur !
Historique
- 06 mai 2007 11:47:06 :
- Tentative de soumission en passant par internet explorer, et non plus par firefox. Merci Coq :)
- 06 mai 2007 12:13:38 :
- Correction de la mise en forme modifiée par la soumission
- 06 mai 2007 14:01:05 :
- Correction mise en forme dû à la soumission
- 06 mai 2007 14:01:31 :
- Correction mise en forme dû à la soumission
- 08 mai 2007 08:15:07 :
- Correction de la mise en forme du sommaire
- 22 mai 2007 21:23:23 :
- Ajout du chapitre III sur les services windows
- 22 mai 2007 21:48:42 :
- Correction du sommaire etc
- 26 mai 2007 22:20:53 :
- Suite services windows
- 10 juin 2007 18:11:08 :
- Ajout début .net remoting
- 11 juin 2007 23:19:58 :
- Suite du cours sur .net remoting: l'application serveur
- 13 juin 2007 07:23:57 :
- ajout partie application cliente .net remoting
- 13 juin 2007 09:34:25 :
- Correction tuto
- 17 juin 2007 14:49:04 :
- Suite et fin( pour l'instant) de .net remoting
- 17 juin 2007 19:06:12 :
- Petites corrections .net remoting
- 19 septembre 2007 19:57:05 :
- Ajout IV.IV Remarques sur .net remoting
Commentaires
|
CalendriCode
| | | L | M | M | J | V | S | D |
| | 1 | 2 | 3 | 4 | 5 | 6 |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 | | | |
|
|