- Cours OpenClassRooms
- Documentation .NET
- Documentation c#
- Guide programmation c#
- Exemple MVVM_Demo Github
- Projet Transports Grenoble
Language de programmation orienté objet récent (début 2000) inspiré de Java (donc typé) et développé par Microsoft. C'est le langage recommandé pour développer sous Windows.
C'est le framework ou environnement d'exécution (aussi écrit Dot NET) pour programmer en C# sous Windows : accès réseau, création de fenêtres, appel à une base de données...
Pour développer sous MacOS ou Linux il faut utiliser .NET Core.
C# est un langage proche de Java. Le framework .NET regroupe un ensemble de bibliothèques pour faciliter le développement.
La compilation permet de traduire le code en source binaire. Mais la compilation en C# a une particularité.
La compilation en C# ne donne pas un programme binaire, contrairement au C et au C++. Le code C# est en fait transformé dans un langage intermédiaire (appelé CIL ou MSIL) que l'on peut ensuite distribuer à tout le monde. Ce code, bien sûr, n'est pas exécutable lui-même, car l'ordinateur ne comprend que le binaire.
Le code en langage intermédiaire (CIL) correspond au programme qui sera distribué. Sous Windows, il prend l'apparence d'un .exe comme les programmes habituels, mais il ne contient en revanche pas de binaire.
Lorsqu'on exécute le programme CIL, celui-ci est lu par un autre programme (une machine à analyser les programmes, appelée CLR) qui le compile cette fois en vrai programme binaire. Le CLR permet également de vérifier le code (sécurité).
NuGet est un gestionnaire de packages.
Utilisation de l'IDE Visual Studio 2017.
- Lors du choix du fond de couleurs et du paramètre de développement choisir : "Visual C#"
Quelques raccourcis utiles :
Raccourcis | Résultats |
---|---|
ctrl + k et ctrl + c | Commenter |
ctrl + k et ctrl + u | Décommenter |
ctrl + k et ctrl + d | Indenter |
prop (puis double tabulation) | Créer une Property |
propfull (puis double tabulation) | Créer une Property et son attribut associé |
Dans l'IDE Visual Studio 2017 :
- Fichier / Nouveau / Projet
- Chosir 'Application console (.NET Framework)'
- Nommer le projet et choisir l'emplacement
- Utiliser ensuite le Main
- Une chaîne de caractères se trouve entre
guillemets ( " )
. - Un caractère se trouve entre des
apostrophes ( ' )
.
"Chaîne de caractères" et 'C'
- Une variable doit être déclarée en indiquant devant son Type (boolean, integer, string...) et en l'initialisant :
String MaVariable = "Caracteres";
- En .NET tous les types de variables (String, Int) commence par une majuscule : String, Int, Boolean...
Comme en Java, il est possible de créer des énumérations qui permettent de regrouper un ensemble de valeurs de type unique.
Exemple :
enum Jours
{
Lundi,
Mardi,
Mercredi,
Jeudi,
Vendredi,
Samedi,
Dimanche
}
Les énumérations peuvent être appelées par la suite dans le code :
Jours monJour = Jours.Vendredi;
Opérateur | Définition |
---|---|
< | plus petit que |
<= | plus petit ou égal à |
> | plus grand que |
>= | plus grand ou égal à |
== | égal à |
!= | différent de |
&& | et |
|| | ou |
Syntaxe :
string[] jours = new string[] { "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche" };
Console.WriteLine(jours[3]); // affiche Jeudi
Console.WriteLine(jours[0]); // affiche Lundi
Console.WriteLine(jours[10]); // provoque une erreur d'exécution car l'indice n'existe pas
Trier rapidement un tableau :
Array.Sort(jours);
Syntaxe :
List<int> chiffres = new List<int>(); // création de la liste
chiffres.Add(8); // chiffres contient 8
chiffres.Add(9); // chiffres contient 8, 9
chiffres.Add(4); // chiffres contient 8, 9, 4
//Récupérer la position d'un élément dans une liste
int indice = jours.IndexOf(4); // indice vaut 2
//Supprimer un élément par sa position dans la liste
chiffres.RemoveAt(1); // chiffres contient 8, 4
//Parcourir une liste
foreach (int chiffre in chiffres)
{
Console.WriteLine(chiffre);
}
Les Dictionary fonctionnent avec une "Key" et une "Value" et il ne peut pas y avoir de doublons dans les clés.
Syntaxe :
// Création du dictionnaire.
Dictionary<string, string> openWith = new Dictionary<string, string>();
// Ajout de quelques éléments. Il ne peut pas y avoir
// deux clefs identiques mais les valeurs peuvent l'être.
openWith.Add("txt", "notepad.exe");
openWith.Add("bmp", "paint.exe");
openWith.Add("dib", "paint.exe");
openWith.Add("rtf", "wordpad.exe");
Les Dictionary possédent des propriétés utiles :
monDictionnaire.Keys
: collection qui contient les clés du dictionnaire.monDictionnaire.Values
: est une collection qui contient les valeurs du dictionnaire.
Afficher les clés d'un dictionnaire :
//Afficher les clés du Dictionary (liste sans doublons)
foreach (String key in listeSansDoublons.Keys)
{
Console.WriteLine(key);
}
Parcourir un Dictionary
//Parcourir la lite sans doublons (type Dictionary) pour afficher la paire "key - value"
foreach (KeyValuePair<String, List<String>> kvp in listeSansDoublons)
{
//Afficher ma clé
Console.WriteLine(kvp.Key);
//Afficher les values (car ma valeur est une Liste)
foreach (String val in kvp.Value)
{
Console.WriteLine(val);
}
}
Vérifier si le Dictionary contient une clé déjà
monDictionnaire.ContainsKey("keyName");
String[] jours = new String[] { "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche" };
int indice;
for (indice = 0; indice < jours.Length; indice++)
{
Console.WriteLine(jours[indice]);
}
String[] jours = new String[] { "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche" };
foreach (String jour in jours)
{
Console.WriteLine(jour);
}
int i = 0;
while (i < 50)
{
Console.WriteLine("Bonjour C#");
i++;
}
int i = 0;
do
{
Console.WriteLine("Bonjour C#");
i++;
}
while (i < 50);
Il est possible de sortir prématurément d’une boucle grâce à l’instruction break. Dès qu’elle est rencontrée, elle sort du bloc de code de la boucle. L’exécution du programme continue alors avec les instructions situées après la boucle. Par exemple :
int i = 0;
while (true)
{
if (i >= 50)
{
break;
}
Console.WriteLine("Bonjour C#");
i++;
}
Il est également possible de passer à l’itération suivante d’une boucle grâce à l’utilisation du mot-clé continue. Exemple :
for (int i = 0; i < 20; i++)
{
if (i % 2 == 0)
{
continue;
}
Console.WriteLine(i);
}
//L'exemple n'affichera que les chiffres impairs car s'il tombe sur un chiffre paire il passera à l'itération suivante et donc n'affichera pas le chiffre pair.
Point d'entrée d'un programme exécutable (c'est l'endroit où le contrôler du programme commence et se termine).
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Helloworld
{
class Program
{
static void Main(string[] args)
{
System.Console.WriteLine(message.GetHelloMessage());
}
}
}
Hybride entre attribut et méthode possédant des getters et setters. Il est possible de rendre le setter en visibilité 'private'.
Les propriétés permettent à une classe d'exposer un moyen public d'obtenir et de définir des valeurs, tout en masquant le code d'implémentation ou de vérification.
Dans les setters, le mot clé "value" correspond à ce que l'utilisateur entrera en paramètre du setter comme nouvelle valeur pour l'attribut.
Exemple :
using System;
public class Person
{
private string firstName;
private string lastName;
public Person(string first, string last)
{
firstName = first;
lastName = last;
}
//Exemple d'une propriété
public string Name => $"{firstName} {lastName}";
//Autre exemple d'une propriété
public String Name
{
get => name;
set => name = value;
}
}
public class Example
{
public static void Main()
{
var person = new Person("Camille", "Onette");
Console.WriteLine(person.Name);
// résultat "Camille Onette"
}
}
Exemple : La méthode GetHelloMessage peut être une propriété
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Helloworld
{
public class Message
{
//Attributs déterminant les différentes tranches horaires
protected int matin = 9;
protected int midi = 13;
protected int soir = 18;
//Propriété GetHelloMessage
public String GetHelloMessage
{
get
{
//Récupérer la date actuelle
DateTime date = DateTime.Now;
//date = new DateTime(2018, 08, 20, 8, 52, 12);
String message = "";
//Pour la tranche vendredi soir au lundi matin
if (date.DayOfWeek == DayOfWeek.Friday && date.Hour >= this.soir || date.DayOfWeek == DayOfWeek.Saturday || date.DayOfWeek == DayOfWeek.Sunday || date.DayOfWeek == DayOfWeek.Monday && date.Hour < this.matin)
{
message = "Bon week-end";
}
else if (date.Hour >= this.soir || date.Hour < this.matin)
{
//Tranche "semaine" pour le soir
message = "Bonsoir";
}
else if (date.Hour < this.midi)
{
//Tranche "semaine" pour le matin
message = "Bonjour";
}
else
{
//Tranche "semaine" pour l'après-midi
message = "Bon après-midi";
}
//Message complet
String messageComplet = message + " " + Environment.UserName;
return messageComplet;
}
}
//Constructeur prenant en paramètres les différentes tranches horaires
public Message (int matin, int midi, int soir)
{
this.matin = matin;
this.midi = midi;
this.soir = soir;
}
}
}
Syntaxe | Action | Exemple |
---|---|---|
System.Console.WriteLine("") | Ecrire dans la console | System.Console.WriteLine("Chaîne caractères" + maVariable); |
System.Console.ReadLine() | Récupérer l'entrée utilisateur | String saisie = System.Console.ReadLine(); |
Pour créer rapidement une interface à partir de la classe qui l'implémentera :
- Dans la classe se positionner sur le nom de la classe et cliquer sur le tournevis (tout à gauche)
- Choisir "Extraire l'interface"
Résultat :
- l'interface sera créée ainsi que la ou les méthode(s) qui étaient dans la classe
- dans la classe il y aura déjà la syntaxe pour l'implémentation de l'interface
Il est possible de stocker des ressources, comme par exemple des données JSON, dans une classe spécifique et pouvoir ensuite faire appel à cette classe.
- Créer une nouvelle Classe de type "Fichier de ressources" : "Resource.resx"
- Renseigner un nom pour la variable
- Copier tel quel le JSON (provenant d'une API par exemple)
- Pour l'utiliser dans une classe
NomResource.maVariable;
Créer un test unitaire : se positionner dans la classe, faire clic droit et sélectionner "Créer des tests unitaires".
Attention : la classe doit être "public"
Lorsqu'on test une classe elle est très souvent liée à d'autres classes ou d'autres éléments externes (lien avec une API). Pour pouvoir maîtriser les éléments externes à la classe, nous avons besoind de l'injection de dépendance.
L'injection de dépendance permet d'isoler les éléments externes à la classe pour pouvoir les gérer.
La doublure de tests fait suite à l'injection de dépendance et permet d'utiliser des interfaces pour créer de "faux" objets pour simuler une dépendance lors des tests unitaires.
Ces objets doubles sont classés en plusieurs catégories selon leur degré d'intelligence et selon la manière donc ils vont imiser le comportement de la classe remplacée.
Dans l'exemple ci-dessous, ces objets seront utilisés dans la classe FakeTime qui implémente l'interface ITime. Ils permettront d'implémenter la propriété "Date" de l'interface.
Il s'agit du plus simple (niveau degré d'intelligence), c'est une classe dont on se fiche comment elle est utilisée.
Le Dummy est juste là pour implémenter l'interface sans logique spécifique.
Dans l'exemple ci-dessous, le dummy correspondrait à un simple "new DateTime()" dans la classe "FakeTime" :
namespace HelloworldTests
{
public class FakeTime : ITime
{
public DateTime Date
{
get
{
return new DateTime();
}
}
}
}
Alors pour les stubs, il s’agit d’implémenter une classe qui va répondre exactement ce que j’attends.
Dans l'exemple ci-dessous, dans la classe "FakeTime", le stub correspond à une date précise "new DateTime(2018,08,20,15,12,11)" :
namespace HelloworldTests
{
public class FakeTime : ITime
{
public DateTime Date
{
get
{
return new DateTime(2018,08,20,15,12,11);
}
}
}
}
Le Fake permet d'implémenter la classe comme on le souhaite.
Dans l'exemple ci-dessous, dans la classe "FakeTime", le fake permet de retourner une propriété qui sera ensuite modifié comme on le souhaite dans les tests (on pourra choisir la date que l'on souhaite).
namespace HelloworldTests
{
public class FakeTime : ITime
{
//Créer une propriété qui sera retourné dans le getter de Date
public DateTime DateToReturn { get; set; }
public DateTime Date
{
get
{
return DateToReturn;
}
}
}
}
Le Spy fonctionne comme un Stub mais en enregistrant les informations pendant le test que l'on pourra aller chercher ensuite.
Le Spy peut être très utile pour savoir combien de fois une méthode a été appelée ou savoir pourquoi le test échou.
Le Mock est un objet qui est une substitution complète de l'implémentation originale d'une classe concrète.
Le Mock a le même but que le Fake mais permet de ne plus écrire la classe qui remplace l'originale. Le mock utilise des librairies qui vont permettre de générer dynamiquement l'objet de doublure dans le corps du code de tests.
Il existe 2 références de librairies :
- NMock3 : simple mais malheureusement plus maintenu
- Moq : mêmes fonctionnalités mais avec une syntaxe moins évidente
Ces librairies s'ajoutent via les packages NuGet.
Exemple : Avoir une fonction GetHelloMessage dans Message qui retourne un message selon l'heure et le jour.
Le problème est que dans cette fonction, on initialise une date à la date actuelle. Cela empêche de pouvoir tester le code avec des tests unitaires.
Solution :
- Créer une interface pour isoler la dépendance :
- Super astuce : Voir la super astuce dans "Interface" pour créer une interface rapidement
- nom du fichier : ITime.cs dans le projet "Helloworld"
- contient une propriété "Date" de type DateTime :
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Helloworld { public interface ITime { DateTime Date { get; } } }
- Dans la classe Message :
- ajouter un attribut du type de l'interface "ITime _time"
- Utiliser l'attribut dans la méthode 'GetHelloMessage' : _time.Date.DayOfWeek pour la date du jour par exemple
namespace Helloworld
{
public class Message
{
//Attributs déterminant les différentes tranches horaires
protected int matin = 9;
protected int midi = 13;
protected int soir = 18;
private ITime _time;
public String GetHelloMessage()
{
//date = new DateTime(2018, 08, 20, 8, 52, 12);
String message = "";
//Pour la tranche vendredi soir au lundi matin
if (_time.Date.DayOfWeek == DayOfWeek.Friday && _time.Date.Hour >= this.soir || _time.Date.DayOfWeek == DayOfWeek.Saturday || _time.Date.DayOfWeek == DayOfWeek.Sunday || _time.Date.DayOfWeek == DayOfWeek.Monday && _time.Date.Hour < this.matin)
{
message = "Bon week-end";
}
//...
}
}
}
- Créer une classe concrète pour impléter l'interface ITime permettant ainsi de donner la date actuelle :
- nom fichier : Time.cs dans projet 'Helloworld'
- Pour dire que la classe implémente une interface : 'public class MaClasse : MonInterface'
- Implémenter "Date" en complémentant le getter :
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace Helloworld { public class Time : ITime { public DateTime Date { get { return DateTime.Now; } } } }
- Dans la classe Message, ajouter dans le constructeur l'initialisation de l'attribut ITime _time :
A ce niveau, nous avons isolé la dépendance et le code fonctionne correctement. Maintenant, pour réaliser les tests unitaires, il faut injecter les dépendances.
-
Dans la classe Message
- ajouter un constructeur contenant un paramètre supplémentaire qui sera la Date souhaitée de type ITime
- Initialiser l'attribut _time avec le nouveau paramètre
- A la place de "public" mettre "internal" pour la visibilité du constructeur, cela empêchera le client d'utiliser ce constructeur réservé aux tests unitaires
- Pour pouvoir utiliser ce nouveau constructeur, ajouter dans le fichier "AssemblyInfo.cs" (projet Helloworld), à la fin la ligne :
[assembly: InternalsVisibleTo("HelloworldTests")]
- Nouveau constructeur dans la classe Message :
//Constructeur pour les tests unitaires internal Message(ITime time, int matin, int midi, int soir) { this.matin = matin; this.midi = midi; this.soir = soir; _time = time; }
-
Créer un Fake pour pouvoir implémenter l'interface ITime avec les données que l'on souhaite :
- Créer dans le projet test "HelloworldTests" un dossier "Fakes" et ajouter une classe "FakeTime"
- Cette classe FakeTime implémente l'interface ITime
- Ajouter une propriété avec une visibilité public "DateTime DateToReturn {get;set;}"
- Implémenter Date (qui est dans l'interface) en complétant le getter avec un return de la propriété DateToReturn
using Helloworld; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace HelloworldTests { public class FakeTime : ITime { public DateTime DateToReturn { get; set; } public DateTime Date { get { return DateToReturn; } } } }
-
Dans la classe Test "MessageTests.cs", créer un test pour chaque message que l'on souhaite tester. Dans un test :
- Créer une instance de FakeTime pour générer une date précise
- Attribuer à la propriété DateToReturn (de la classe FakeTime) la valeur de la date souhaitée
- Instancier un objet Message (classe que l'on souhaite tester)
- Stocker le résultat de la méthode que l'on test "GetHelloMessage" dans une variable
- Comparer cette variable au résultat que l'on attend (message "bonjour" par exemple pour le matin en semaine)
using Microsoft.VisualStudio.TestTools.UnitTesting; using Helloworld; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using HelloworldTests; namespace Helloworld.Tests { [TestClass()] public class MessageTests { [TestMethod()] public void MessageTest() { //Retourner un échec du test //Assert.Fail(); } //Tester que le message retourne "Bonjour" le matin [TestMethod()] public void GetHelloMessageTest_Bonjour() { //Créer une instance de FakeTime pour générer une date précise FakeTime fake = new FakeTime(); fake.DateToReturn = new DateTime(2018, 08, 21, 9, 0, 0); //Créer une instance de l'objet Message avec la date que je souhaite Message message = new Message(fake, 9, 13, 18); //Stocker le résultat de l'appel à la méthode que je test String resultat = message.GetHelloMessage(); //Je m'attends à avoir le résultat suivant StringAssert.Contains(resultat, "Bonjour"); } } }
Refactor du constructeur de la classe Message
- Pour le constructeur classique étant utilisé pour le programme, indiquer qu'il utilise le constructeur pour les tests avec comme paramètre "date" une instance de la classe "Time" (classe générant la date actuelle)
- ":this" (à positionner avant les crochets) indique qu'on utilise l'autre constructeur
//Constructeur prenant en paramètres les différentes tranches horaires
public Message(int matin, int midi, int soir)
:this (new Time(), matin, midi, soir)
{
}
//Constructeur pour les tests unitaires
internal Message(ITime time, int matin, int midi, int soir)
{
this.matin = matin;
this.midi = midi;
this.soir = soir;
_time = time;
}
Exemple : Avoir une fonction GetDetailsLigne
dans la classe DataTypeTransport qui retourne les détails sur une ligne (appel API métromobilité).
Le problème est que dans cette fonction, on se connecte à une API pour obtenir les données. Il faut donc isoler cette dépendance, pour pouvoir tester la méthode, même lorsque l'API ne fonctionne pas.
L'idée est qu'on utilise une classe ConnectApi
avec une méthode ConnexionApi
qui permet de se connecter à une API et de retourner le résultat au format JSON.
Le principe est donc de créer une classe "Fake" qui retourne simplement un résultat JSON pour ensuite pouvoir tester notre méthode GetDetailsLigne
.
Solution :
- Créer une interface pour isoler la dépendance :
- Super astuce : Voir la super astuce dans "Interface" pour créer une interface rapidement
- nom du fichier : IConnectApi.cs dans le projet "TransportLibrary"
- contient une méthode
ConnexionApi
(comme dans la classe concrèteConnectApi
) - interface IConnectApi.cs :
using System;
namespace TransportLibrary
{
public interface IConnectApi
{
String ConnexionApi(String url);
}
}
- Avoir une classe concrète
ConnectApi
implémentant l'interface et permettant la connexion à l'API - Dans la classe
DataTypeTransport
:- Ajouter un attribut du type de l'interface
- Se servir de l'attribut pour appeler la méthode de connexion : monAttribut.ConnexionApi();
- Ajouter un constructeur permettant d'initialiser l'attribut
- Classe
DataTypeTransport
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TransportLibrary
{
public class DataTypeTransport
{
//Attribut du type de l'interface
private IConnectApi connect;
//Obtenir les détails d'une ligne
public TypeTransport GetDetailsLigne(String idLigne)
{
String url = "http://data.metromobilite.fr/api/routers/default/index/routes?codes=" + idLigne;
//Appeler la méthode de connexion en passant par l'attribut 'connect'
String responseFromServer = this.connect.ConnexionApi(url);
List<TypeTransport> listDetailsLigne = JsonConvert.DeserializeObject<List<TypeTransport>>(responseFromServer);
return listDetailsLigne[0];
}
//Constructeur initialisant l'attribut connect
public DataTypeTransport(IConnectApi connect)
{
this.connect = connect;
}
}
}
- Dans le fichier "Program.cs" corriger le code pour que le programme fonctionne toujours en ajoutant un paramètre lors de l'instanciation de la classe
DataTypeTransport
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using TransportLibrary;
namespace TransportsGrenoble
{
class Program
{
static void Main(string[] args)
{
//...//
//Parcourir la lite sans doublons (type Dictionary) pour afficher la paire "key - value"
foreach (KeyValuePair<String, List<String>> kvp in listeSansDoublons)
{
Console.WriteLine("******* " + kvp.Key + " *******");
foreach (String idLigne in kvp.Value)
{
//Instancier DataTypeTransport avec en paramètre la vraie connexion à l'API
DataTypeTransport dataTypeTransport = new DataTypeTransport(new ConnectApi());
TypeTransport detailsLigne = dataTypeTransport.GetDetailsLigne(idLigne);
Console.WriteLine(detailsLigne.mode + " - " + detailsLigne.shortName + " : " + detailsLigne.longName);
}
}
}
}
}
- Créer la classe Fake :
FakeConnectApi.cs
- Créer un dossier "Fakes" dans le projet de test
TransportLibraryTest
- Créer la classe
FakeConnectApi.cs
dans le dossier Fakes - Implémenter la méthode
ConnexionApi
pour qu'elle retourne un JSON spécifique (étant la vraie réponse de l'appel à l'API) - Créer un attribut qui sera retourné par la méthode et plus tard lors des tests nous initialiserons cet attribut
- Classe FakeConnectApi.cs
- Créer un dossier "Fakes" dans le projet de test
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TransportLibrary;
namespace TransportLibraryTests.Fakes
{
class FakeConnectApi : IConnectApi
{
public String resultatJson { get; set; }
public String ConnexionApi(String url)
{
return resultatJson;
}
}
}
- Utiliser une classe de type
Fichier de Ressources
pour stocker le JSON (voir dans le mémo un peu plus haut) - Créer la classe de test
- Instancier une FakeConnectApi
- Initialiser la valeur de l'attribut en lui donnant celle de la ressource
- Instancier la classe que l'on test DataTypeTransport avec en paramètre l'instance de la fausse connexion
- Comparer les résultat avec des
Assert
pour tester le résultat
using Microsoft.VisualStudio.TestTools.UnitTesting;
using TransportLibrary;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TransportLibraryTests.Fakes;
using TransportLibraryTests;
namespace TransportLibrary.Tests
{
[TestClass()]
public class DataTypeTransportTests
{
[TestMethod()]
public void GetDetailsLigneTest()
{
FakeConnectApi fake = new FakeConnectApi();
fake.resultatJson = Resource.detailLigne12;
DataTypeTransport dataTypeTransport = new DataTypeTransport(fake);
TypeTransport resultat = dataTypeTransport.GetDetailsLigne("SEM:12");
Assert.AreEqual(resultat.shortName, "12");
Assert.AreEqual(resultat.longName, "Eybens Maisons Neuves / Saint-Martin-d'Hères Les Alloves");
Assert.AreEqual(resultat.color, "009930");
Assert.AreEqual(resultat.mode, "BUS");
Assert.AreEqual(resultat.type, "PROXIMO");
}
}
}
- Lancer les tests (Test/Executer/Tous les tests) et prier pour voir du vert
La première ligne est importante pour éviter les erreurs de communication. Elle doit être placée en première dans le programme.
Exemple : liste des lignes de transport à proximité d'un point
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace TransportsGrenoble
{
class Program
{
static void Main(string[] args)
{
//Permet d'éviter les erreurs de communication avec l'API
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
Console.WriteLine("Transports à pied c'est mieux de Grenoble");
//Définir les éléments : latitude, longitude et distance pour ensuite faire appel à l'API
String latitude = "45.185476";
String longitude = "5.727772";
//Int32 est important pour fonctionner en .NET
Int32 distance = 500; //périmètre de recherche
//Créer une connexion à l'API
WebRequest request = WebRequest.Create("http://data.metromobilite.fr/api/linesNear/json?x="+longitude+ "&y="+latitude+ "&dist=" + distance + "&details=true");
//Récupérer la réponse de la connexion à l'API
WebResponse response = request.GetResponse();
//Afficher le status de la réponse pour la connexion à l'API
Console.WriteLine(((HttpWebResponse)response).StatusDescription);
// Obtenir le flux contenant les données de réponse envoyées par le serveur
Stream dataStream = response.GetResponseStream();
// Ouvrir le flux de données avec le StreamReader pour faciliter la lecture
StreamReader reader = new StreamReader(dataStream);
// Lire le contenu
string responseFromServer = reader.ReadToEnd();
// Afficher le contenu
Console.WriteLine(responseFromServer);
// Fermer les différentes connexions
reader.Close();
dataStream.Close();
response.Close();
}
}
}
Avec le gestionnaire de packages NuGet, il faut installer "Newtonsoft.Json" pour pouvoir transformer les données JSON en objets C#.
Nom du package : "Newtonsoft.Json" à installer avec NuGet.
Etapes :
- Télécharger le package
- Créer une classe qui a les mêmes propriétés que les données JSON. Pour cela, utiliser ce traducteur pour transformer les données JSON en classe C#
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TransportLibrary
{
public class Ligne
{
public string id { get; set; }
public string name { get; set; }
public double lon { get; set; }
public double lat { get; set; }
public List<string> lines { get; set; }
}
}
- Stocker dans une liste (si nécessaire) du type de la classe les données récupérées de l'API
// (Voir plus haut les éléments précédents)
//Lire le contenu
string responseFromServer = reader.ReadToEnd();
//Stocker dans une liste d'objets Ligne l'ensemble des informations au format JSON pour les convertir en objet c#
List <Ligne> listeArrets = JsonConvert.DeserializeObject<List<Ligne>>(responseFromServer);
- Afficher les données de la liste
//Boucler sur la liste d'objets de type Ligne
foreach (Ligne arret in listeArrets)
{
//un arret contient une liste de lignes : arret.lines (d'où le 2ème foreach)
foreach (String ligne in arret.lines)
{
//Avoir uniquement le numéro de ligne sans le "SEM:"
int delimiter = ligne.IndexOf(":");
Console.WriteLine("Id : "+ arret.id+ " Arret : " + arret.name + " - ligne " + ligne.Substring(delimiter + 1));
}
}
La librairie est un nouveau projet de type "Librairie" permettant d'avoir des classes qui seront utilisées pour les projets à venir (ex : traitement des données d'une API).
Il faut créer un projet "Librairie" dans notre solution et ajouter les classes nécessaires.
Ces classes contiendront, par exemple, toutes les méthodes nécessaires pour effectuer les requêtes et de récolter les données provenant d'une API.
Exemple : Projet Transports en Commun, avoir un programme qui affiche à partir d'une localisation, l'ensemble des arrêts à proximité avec les lignes disponibles à ces arrêts.
- Créer un projet "Class Library" (ou "Bibliothèque de classes (.NET Framework)")
- Nommer ce nouveau projet : par exemple "TransportLibrary"
- Une classe "Class1.cs" est déjà créée, elle peut donc servir de première classe
Le principe est de faire :
- une classe généraliste avec une méthode pour se connecter à l'API et récupérer les données
- une classe pour traiter les données (chaque URL de l'API aura sa classe puisque les données traitées ne seront pas les mêmes)
Concernant l'exemple, en l'état, la solution contient :
- un projet de type "Console" : contient uniquement la classe principale "Program.cs"
- un projet de type "Librairie" qui contient l'ensemble des classes (y compris la classe "Ligne" par exemple)
Recupérer les arrêts à proximité d'un point et les lignes qui sont liées à ces arrêts
- Classe Ligne.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TransportLibrary
{
public class Ligne
{
public string id { get; set; }
public string name { get; set; }
public double lon { get; set; }
public double lat { get; set; }
public List<string> lines { get; set; }
}
}
- Classe ConnectApi.cs (classe généraliste permettant la connexion à une API)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
namespace TransportLibrary
{
public class ConnectApi
{
//Méthode pour se connecter à l'API et renvoyer les données
public String ConnexionApi(String url)
{
//Permet d'éviter les erreurs de communication avec l'API
ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls;
//Créer une connexion à l'API
WebRequest request = WebRequest.Create(url);
//Récupérer la réponse de la connexion à l'API
WebResponse response = request.GetResponse();
//Afficher le status de la réponse pour la connexion à l'API
//Console.WriteLine(((HttpWebResponse)response).StatusDescription);
// Obtenir le flux contenant les données de réponse envoyées par le serveur
Stream dataStream = response.GetResponseStream();
// Ouvrir le flux de données avec le StreamReader pour faciliter la lecture
StreamReader reader = new StreamReader(dataStream);
// Lire le contenu
String responseFromServer = reader.ReadToEnd();
// Fermer les différentes connexions
reader.Close();
dataStream.Close();
response.Close();
//Retourner les données
return responseFromServer;
}
}
}
- Classe DataLignesProximite.cs
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace TransportLibrary
{
public class DataLignesProximite
{
//Récupérer une liste de type Ligne
private List<Ligne> GetDataLignesProximite(String latitude, String longitude, Int32 distance)
{
ConnectApi connexion = new ConnectApi();
String url = "http://data.metromobilite.fr/api/linesNear/json?x=" + longitude + "&y=" + latitude + "&dist=" + distance + "&details=true";
String responseFromServer = connexion.ConnexionApi(url);
//Stocker dans une liste d'objets Ligne l'ensemble des informations au format JSON pour les convertir en objet c#
List<Ligne> listeArrets = JsonConvert.DeserializeObject<List<Ligne>>(responseFromServer);
return listeArrets;
}
//Supprimer les doublons dans les arrets et lignes
public Dictionary<String, List<String>> GetDataLignesProximiteSansDoublons (String latitude, String longitude, Int32 distance)
{
List<Ligne> listeArrets = GetDataLignesProximite(latitude, longitude, distance);
//Liste sans doublons : Créer un objet de type Dictionary contenant comme Key : Arret et Value : listeLignes
Dictionary<String, List<String>> listeSansDoublons = new Dictionary<string, List<String>>();
//Boucler sur les données pour créer un Dictionary(NomArret, Lignes)
foreach (Ligne arret in listeArrets)
{
if (!listeSansDoublons.ContainsKey(arret.name))
{
//Ajouter dans la liste sans doublons la paire "Key : nom arret, value : listeLignes"
listeSansDoublons.Add(arret.name, arret.lines);
}
//Mon arret est déjà dans la liste, je vais donc vérifier que toutes les lignes de l'arrêt sont déjà dans la liste de cet arret
else
{
//Boucler sur les lignes de l'arrêt
foreach (String ligne in arret.lines)
{
//Si la ligne n'est pas déjà dans la liste des lignes de l'arret dans le Dictionary (liste sans doublons)
if (!listeSansDoublons[arret.name].Contains(ligne))
{
//J'ajoute maligne dans le "Value" de mon arrêt à la liste des lignes
listeSansDoublons[arret.name].Add(ligne);
}
}
}
}
return listeSansDoublons;
}
}
}
- Program.cs (Dans le projet Console)
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Data;
using System.IO;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using TransportLibrary;
namespace TransportsGrenoble
{
class Program
{
static void Main(string[] args)
{
//Définir les éléments : latitude, longitude et distance pour ensuite faire appel à l'API
String latitude = "45.185476";
String longitude = "5.727772";
Int32 distance = 500; //périmètre de recherche
//Instance pour traitement des données
DataLignesProximite dataLignesProximite = new DataLignesProximite();
//Récupérer les lignes et arrêts sans doublons à proximité
Dictionary<String, List<String>> listeSansDoublons = dataLignesProximite.GetDataLignesProximiteSansDoublons(latitude, longitude, distance);
//Foreach pour afficher les données...
}
}
}
A noter: les classes qui ne seront pas utilisées dans le Program.cs doivent être en "private"
Une librairie peut être utilisée depuis un autre projet. Il faut donc générer un fichier .dll qui pour être utilisé depuis un autre projet.
- Dans Visual Studio, dans le petit menu déroulant "Gestionnaire de configurations" choisir "Release" au lieu de "Debug"
- Générer / Générer NomLibrairie (MAJ+F6)
- Les fichiers sont créés dans NomLibrairie/bin/release/
Ressource présentation MVVM Petit exemple dans la doc Autre petit exemple
Le design pattern MVVM :
- Model (données)
- View (la vue)
- ViewModel (l'interface permettant de transmettre les données à la vue)
Une application MVVM s'appuie sur l'utilisation de WPF pour l'interface graphique.
WPF (Windows Presentation Foundation) dernière approche de Windows en terme de framework GUI (Graphical User Interface).
Fonctionne avec le XAML (eXtensible Application Markup Language - Langage extensible de description d'application). Fonctionne comme avec HTML.
- Fichier / Nouveau Projet
- Dans "Visual C# / Windows Desktop" et choisir
Application WPF (.NET Framework)
- Nommer le projet et choisir le dossier d'emplacement
Il faut également créer 3 dossiers pour ordonner les classes :
- Model
- ViewModel
- Views
L'exemple utilisé dans le mémo est disponible sur GitHub : MVVM_Demo
Définir le projet Interface Graphique comme projet de démarrage.
Si notre solution comporte également un projet console, lorsqu'on lance avec CTRL + F5 le programme, c'est le projet console qui s'affiche.
Pour modifier cela et afficher notre interface graphique, il faut :
- se positionner sur le projet de l'interface graphique
- Click droit
- Choisir "Définir comme projet de démarrage"
App.xaml
: Définit une application WPF et les ressources. Se servir de ce fichier pour spécifier l'interface utilisateur qui s'affiche automatiquement quand l'application s'ouvre (StartupUri).
<Application x:Class="ExpenseIt.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
<Application.Resources>
</Application.Resources>
</Application>
MainWindow.xaml
: Fenêtre principale de l'application qui affiche le contenu créé dans les pages. LeWindow
définit les propriétés d'une fenêtre (titre, taille, icône, évènements...).MainWindow.xaml.cs
: Ce fichier est un fichier code-behind qui contient le code pour gérer les événements déclarés dans MainWindow.xaml. Cette classe estpartial
car lors de la compilation, le code dans la Window sera compilé et traduit en C# pour créer une classe complète.
Une fenêtre WPF est la combinaison d'un fichier XAML (.xaml), dans lequel l'élément est la racine, et un fichier de CodeBehind (littéralement "code derrière") (.cs).
Module très utile pour ajouter des éléments facilement à notre page : la boîte à outils.
Affichage/Boite à outils
pour ajouter le module. Pour l'utiliser cliquer sur l'élément souhaité puis cliquer sur l'endroit de notre page où l'on souhaite le mettre.
La classe principale MainWindow.xaml
est composée d'une Window
(balises) avec à l'intérieure une ou plusieurs Grid
.
Il est possible d'insérer des vues qui correspondent à des UserControl
(des contrôles utilisateurs). Ces vues sont insérées de la manière suivante :
<Window x:Class="MVVMDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MVVMDemo"
xmlns:views = "clr-namespace:MVVMDemo.Views"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<views:StudentView x:Name= "StudentViewControl" Loaded = "StudentViewControl_Loaded" />
</Grid>
</Window>
- Dans la 1ère balise
Window
il faut déclarer le NameSpace des vues (le dossier où les vues se situent) :xmlns:views = "clr-namespace:MVVMDemo.Views"
- Appeler la vue avec la balise
views:NomVue
x:Name
correspond au nom qu'aura notre vue dans le code c# (ex: Dans la classeMainWindow.xaml.cs
on utilisera ce nom pour déclarer le DataContext)Loaded
: Méthode qui sera appelée au chargement de la page (permet d'injecter les vraies données via le DataContext)
Lorsque dans notre classe principale MainWindow.xaml
on insére une vue, il faut déclarer le DataContext.
Le DataContext indique les vraies données qui seront utilisées (par exemple, une collection).
Dans la balise , permettant d'insérer une vue, il y a un attribut Loaded
qui indique une méthode à appeler lors du chargement de la vue.
Les vraies données sont déclarées dans cette méthode.
Code MainWindow.xaml.cs
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace MVVMDemo
{
/// <summary>
/// Logique d'interaction pour MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
//Méthode permettant d'indiquer les données à utiliser pour la vue
private void StudentViewControl_Loaded(object sender, RoutedEventArgs e)
{
//Instance de la vue 'StudentViewModel' (dans le constructeur de la ViewModel nous avons ajouté la création d'une ObservableCollection)
ViewModel.StudentViewModel studentViewModelObject = new ViewModel.StudentViewModel();
//Définir le DataContext en utilisant le nom StudentViewControl donné dans la balise <views x:Name="StudentViewControl">
StudentViewControl.DataContext = studentViewModelObject;
}
}
}
Une Grid
est très puissante et permet de positionner les éléments grâce à des lignes et colonnes. Par défaut, la grid possède une ligne et une colonne.
Il faut donc préciser combien de colonnes et lignes nous souhaitons :
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="2*" />
<RowDefinition Height="1*" />
<RowDefinition Height="1*" />
</Grid.RowDefinitions>
</Grid>
- Il est possible de définir la taille des colonnes et lignes via "2*", ce qui équivaut à 2 fois plus qu'une colonne ou ligne normale.
- On peut également écrire "0.5*" qui équivaut à "la moitié de l'espace disponible".
Ensuite lorsqu'on ajoute des éléments, on peut préciser quelles lignes et colonnes ils occupents (index commençant par 0) avec les attributs Grid.Row
et Grid.Column
:
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70*"></ColumnDefinition>
<ColumnDefinition Width="110*"></ColumnDefinition>
<ColumnDefinition Width="70*"></ColumnDefinition>
<ColumnDefinition Width="110*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0" FontSize="14" HorizontalAlignment="Right" >Latitude:</Label>
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Path=Latitude}" FontSize="14" VerticalAlignment="Center" Padding="5 0 5 0"/>
<Label Grid.Column="2" Grid.Row="0" FontSize="14" HorizontalAlignment="Right">Longitude:</Label>
<TextBox Grid.Column="3" Grid.Row="0" Text="{Binding Path=Longitude}" FontSize="14" VerticalAlignment="Center" Padding="5 0 5 0"/>
</Grid>
Dans la Grid il est possible d'ajouter de nombreux éléments (listBox, checkbox, button,...).
Un élément très utile pour contenir plusieurs objet StackPanel
, il s'agit d'un container.
<Grid>
<StackPanel HorizontalAlignment = "Left">
<TextBox Text = "Bonjour" Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "Aurevoir" Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "Tcho" Margin = "0 5 3 5"/>
</StackPanel>
</Grid>
Lorsque l'on crée des vues, il faut créer un type d'élèment spécifique nommé User Control
:
- Click droit : ajouter
- Choisir
Controle Utilisateur
- Choisir le type de la classe
Controle utilisateur WPF
Les vues seront composées d'une Grid contenant l'ensemble de l'interface graphique.
Lorsqu'on souhaite afficher des données en utilisant des données existantes, on utilise le binding.
Le Binding
ne se fait que sur des propriétés
(property), c'est-à-dire qu'on ne peut pas binder des attributs d'une classe.
L'avantage des propriétés est qu'elles ont des getters et setters permettant d'accèder et modifier les données.
Le Binding se fait en écrivant Content="{Binding Path = NomProperty }"
Exemple : binding d'une property ButtonText dans la StudentView.xaml
<Grid>
<StackPanel HorizontalAlignment = "Left">
<Button Content="{Binding Path = ButtonText }"/>
</StackPanel>
</Grid>
Code : StudentView.xaml.cs
using MVVMDemo.Model;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMDemo.ViewModel
{
public class StudentViewModel
{
public StudentViewModel()
{
Students = new ObservableCollection<Student>();
Students.Add(new Student { FirstName = "Marion", LastName = "Chapuis" });
Students.Add(new Student { FirstName = "Sergio", LastName = "Custodio" });
Students.Add(new Student { FirstName = "Romain", LastName = "Joubert" });
//Initialiser la valeur de la property ButtonText
ButtonText = "Click me";
_text = "Default";
}
//property ButtonText
public string ButtonText { get; set; }
}
}
Pour pouvoir itérer sur une collection d'objets, il faut que cette collection soit de type ObservableCollection
.
- Créer une ObservableCollection dans la classe
StudentViewModel.cs
using MVVMDemo.Model;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace MVVMDemo.ViewModel
{
public class StudentViewModel
{
//Constructeur permettant d'ajouter des données dans notre ObservableCollection
public StudentViewModel()
{
Students = new ObservableCollection<Student>();
Students.Add(new Student { FirstName = "Marion", LastName = "Chapuis" });
Students.Add(new Student { FirstName = "Sergio", LastName = "Custodio" });
Students.Add(new Student { FirstName = "Romain", LastName = "Joubert" });
}
//Property Students étant une ObservableCollection de Student
public ObservableCollection<Student> Students
{
get;
set;
}
}
}
- Afficher la collection dans la vue StudentView.xaml
<Grid>
<StackPanel HorizontalAlignment = "Left">
<ItemsControl ItemsSource = "{Binding Path = Students}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation = "Horizontal">
<TextBox Text = "{Binding Path = FirstName, Mode = TwoWay}" Width = "100" Margin = "3 5 3 5"/>
<TextBox Text = "{Binding Path = LastName, Mode = TwoWay}" Width = "100" Margin = "0 5 3 5"/>
<TextBlock Text = "{Binding Path = FullName, Mode = OneWay}" Margin = "0 5 3 5"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
ItemControl
: permet d'afficher des informations, très utile pour des données BindéesItemSource
: permet de définir la property servant de source (ici une ObservableCollection)Binding Path = NomProperty
: liaison avec la property (ici une ObservableCollection) disponible dans la classeStudentViewModel.cs
ItemsControl.ItemTemplate
: définir la structure qu'aura chaque élémentDataTemplate
: Contient la structure d'un élémentStackPanel
: container WPF permettant de contenir plusieurs éléments (ici plusieurs TextBox et TextBlock)TextBox
etTextBlock
: balises WPF pour afficher du texteMode
: TwoWay permet d'utiliser le getter et setter de la property alors que OneWay utilise juste le getter (on ne pourra donc pas modifier)
L'interface INotifyPropertyChanged permet de notifier aux vues lorsqu'un élément a été modifié pour permettre aux vues de s'actualiser.
Il est possible d'implémenter cette interface soit dans le Model, soit dans le ViewModel.
Une très bonne pratique à réaliser est de créer une classe mère abstraite implémentant l'interface INotifyPropertyChanged puis de créer nos Models en indiquant qu'ils héritent de cette classe mère.
Code de la classe mère
public abstract class NomClasseMere : INotifyPropertyChanged
{
//évnènement permettant de détecter lorsqu'un élément change
public event PropertyChangedEventHandler PropertyChanged;
//Lorsqu'une property change, notifier aux vues le changement
private void RaisePropertyChanged(string property)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(property));
}
}
}
Code de la classe Model Student.cs
(le nom StudentModel dans l'exemple n'est pas génial)
public class Student : NomClasseMere
{
//Student hérite de l'ensemble des éléments de la classe mère
//...
}
Exemple : Dans la classe StationViewModel.cs
qui implémente la classe Mere
(implémentant elle-même INotifyPropertyChanged), utilisation de la méthode RaisePropertyChanged
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using TransportInterfaceGraphique.Model;
using TransportLibrary;
namespace TransportInterfaceGraphique.ViewModel
{
public class StationViewModel : Mere
{
private String latitude;
private String longitude;
private Int32 distance;
private ObservableCollection<DataStation> stations;
public ObservableCollection<DataStation> Stations
{
get
{
return stations;
}
set
{
if (stations != value)
{
stations = value;
RaisePropertyChanged("Stations");
}
}
}
public String Latitude
{
get
{
return latitude;
}
set
{
if (latitude != value)
{
latitude = value;
DoRequest();
}
}
}
public String Longitude
{
get
{
return longitude;
}
set
{
if (longitude != value)
{
longitude = value;
DoRequest();
}
}
}
public Int32 Distance
{
get
{
return distance;
}
set
{
if (distance != value)
{
distance = value;
DoRequest();
}
}
}
public StationViewModel()
{
latitude = "45.185476";
longitude = "5.727772";
distance = 400;
DoRequest();
}
private void DoRequest()
{
DataLignesProximite dataLignesProximite = new DataLignesProximite(new ConnectApi());
Dictionary<String, List<Ligne>> dicoLignesProximite = dataLignesProximite.GetDataDetailsLigneProximite(latitude, longitude, distance);
Stations = ConvertInObsCollection(dicoLignesProximite);
}
private ObservableCollection<DataStation> ConvertInObsCollection(Dictionary<String, List<Ligne>> dicoLignes)
{
ObservableCollection<DataStation> listLignesProx = new ObservableCollection<DataStation>();
foreach (KeyValuePair<String, List<Ligne>> kvp in dicoLignes)
{
DataStation data = new DataStation(kvp.Key, kvp.Value);
listLignesProx.Add(data);
}
return listLignesProx;
}
}
}
Explications :
- Il faut que toutes les Property soient liées à des attributs
- Utiliser les attributs dans le code de la classe (par exemple, pour passer en paramètre la latitude, longitude et distance)
- Dans les setters des property, on relance la méthode permettant de mettre à jour l'ObservableCollection
Stations
- Il faut notifier aux vues que
Stations
(property que la vue utilise pour afficher les lignes à proximité) a changé et qu'il faut que les vues s'actualisent : méthodeRaisePropertyChanged("Stations")
- la méthode
RaisePropertyChanged(string)
est décrite dans la classe Mere - La classe
StationViewModel.cs
hérite de la classe Mere contenant la méthodeRaisePropertyChanged(string)
Pour compléter l'exemple : code de la vue StationView.xaml
<UserControl x:Class="TransportInterfaceGraphique.Views.StationView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:TransportInterfaceGraphique.Views"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="800">
<Grid>
<StackPanel>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="70*"></ColumnDefinition>
<ColumnDefinition Width="110*"></ColumnDefinition>
<ColumnDefinition Width="70*"></ColumnDefinition>
<ColumnDefinition Width="110*"></ColumnDefinition>
<ColumnDefinition Width="70*"></ColumnDefinition>
<ColumnDefinition Width="110*"></ColumnDefinition>
<ColumnDefinition Width="110*"></ColumnDefinition>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition></RowDefinition>
</Grid.RowDefinitions>
<Label Grid.Column="0" Grid.Row="0">Latitude:</Label>
<TextBox Grid.Column="1" Grid.Row="0" Text="{Binding Path=Latitude}"/>
<Label Grid.Column="2" Grid.Row="0">Longitude:</Label>
<TextBox Grid.Column="3" Grid.Row="0" Text="{Binding Path=Longitude}"/>
<Label Grid.Column="4" Grid.Row="0">Distance:</Label>
<TextBox Grid.Column="5" Grid.Row="0" Text="{Binding Path=Distance}"/>
</Grid>
</StackPanel>
<StackPanel>
<TextBlock Text="Liste des lignes à proximité" FontSize="20" Foreground="DarkBlue" Margin="10 40 0 10"/>
<ItemsControl ItemsSource="{Binding Path= Stations}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="16 2 2 2">
<TextBlock Text="{Binding Path= Arret}" FontSize="14" FontStyle="Italic"/>
<ListBox Name="listeLignes" ItemsSource="{Binding Path=Lignes}">
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text= "{Binding Path=shortName}"/>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</UserControl>
Permet d'effectuer une action, par exemple, lors d'un click sur un bouton.
- Créer une classe
RelayCommand.cs
qui implémente l'interfaceICommand
- Copier le code (ci-dessous)
- Dans notre classe
ViewModel.cs
:- Créer une property
- Créer une méthode avec les actions qu'on souhaite effectuer
- Dans le Constructeur initialiser la property en lui donnant une instance de RelayCommand avec en paramètre la méthode effectuant les actions
- Dans la vue
View.xaml
:- Ajouter à un bouton l'attribut
Command = "{ Binding Path = NomProperty }"
- Ajouter à un bouton l'attribut
Code RelayCommand.cs
:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;
namespace TransportInterfaceGraphique.Model
{
public class RelayCommand : ICommand
{
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
private Action methodToExecute;
private Func<bool> canExecuteEvaluator;
public RelayCommand(Action methodToExecute, Func<bool> canExecuteEvaluator)
{
this.methodToExecute = methodToExecute;
this.canExecuteEvaluator = canExecuteEvaluator;
}
public RelayCommand(Action methodToExecute)
: this(methodToExecute, null)
{
}
public bool CanExecute(object parameter)
{
if (this.canExecuteEvaluator == null)
{
return true;
}
else
{
bool result = this.canExecuteEvaluator.Invoke();
return result;
}
}
public void Execute(object parameter)
{
this.methodToExecute.Invoke();
}
}
}
Code de StationViewModel.cs
:
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using TransportInterfaceGraphique.Model;
using TransportLibrary;
namespace TransportInterfaceGraphique.ViewModel
{
public class StationViewModel : Mere
{
//...
//Constructeur
public StationViewModel()
{
latitude = "45.185476";
longitude = "5.727772";
distance = 400;
DoRequest();
//Initialiser la property avec une instance de RelayCommand avec pour paramètre la méthode contenant les actions à mener
FindLignes = new RelayCommand(RechercherLignes);
//ou alors directement avec DoRequest() qui effectivement n'est pas static mais ça fonctionne
FindLignes = new RelayCommand(DoRequest);
}
//Méthode effectuant une action : donner à Stations les lignes à proximité
private void DoRequest()
{
DataLignesProximite dataLignesProximite = new DataLignesProximite(new ConnectApi());
Dictionary<String, List<Ligne>> dicoLignesProximite = dataLignesProximite.GetDataDetailsLigneProximite(latitude, longitude, distance);
Stations = ConvertInObsCollection(dicoLignesProximite);
}
//La property qu'on appellera dans notre vue
public RelayCommand FindLignes { get; set; }
//Méthode effectuant une action simple
private static void RechercherLignes()
{
MessageBox.Show("Hello");
}
}
}
Code de la vue StationView.xaml
:
<Grid>
<Button Command="{Binding Path=FindLignes}">Rechercher</Button>
</Grid>
Pour ajouter un scroll sur l'ensemble de la page, il faut ajouter les balises <ScrollViewer>
dans la vue principale MainWindow.xaml
<ScrollViewer>
<Grid>
<StackPanel >
<TextBlock Text ="Transports Grenoble" Grid.Row="1" FontSize="30" FontWeight="Bold" Foreground="MidnightBlue" HorizontalAlignment="Center"/>
</StackPanel>
<views:StationView x:Name="StationViewControl" Loaded="StationViewControl_Loaded" Margin="30,60,20,40" Grid.RowSpan="2"/>
</Grid>
</ScrollViewer>
Pour que le scroll soit disponible dans l'ensemble de la page (il peut être annulé quand la souris est sur une ListBox par exemple).
- Ajouter dans la Vue (ici StationView.xaml), au dessus de la grid, un code spécifique
<UserControl.Resources>
<ControlTemplate x:Key="NoScroll">
<ItemsPresenter></ItemsPresenter>
</ControlTemplate>
</UserControl.Resources>
<Grid>
//...
</Grid>
- Dans notre ListBox, ajouter dans l'attribut
Template
en StaticResource, le NoScroll qu'on vient de créer
<ListBox Name="listeLignes" ItemsSource="{Binding Path=Lignes}" Template="{StaticResource NoScroll}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<TextBlock Width="50" Text="{Binding Path=mode}" FontSize="16" Margin="5 0 5 0" Padding="4 2 4 2"/>
<TextBlock Width="50" TextAlignment="Center" Text= "{Binding Path=shortName}" Padding="4 2 4 2" Background="{Binding Path=realColor}" FontWeight="Bold" FontSize="16"/>
<TextBlock Text="{Binding Path=longName}" FontSize="16" Margin="10 0 0 0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
- Pour garder les bordures qui sont disponibles dans la ListBox, il faut ajouter un
DockPanel
avec une baliseBorder
autour de la ListBox
<ItemsControl ItemsSource="{Binding Path= Stations}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Margin="16 2 2 2">
<TextBlock Text="{Binding Path= Arret}" FontSize="18" FontStyle="Italic" FontWeight="DemiBold" Margin="0 10 0 4" Foreground="DarkBlue"/>
<DockPanel>
<Border BorderThickness="1"
BorderBrush="Gray"
CornerRadius="10"
Padding="5"
Background="GhostWhite">
<ListBox Name="listeLignes" ItemsSource="{Binding Path=Lignes}" Template="{StaticResource NoScroll}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" >
<TextBlock Width="50" Text="{Binding Path=mode}" FontSize="16" Margin="5 0 5 0" Padding="4 2 4 2"/>
<TextBlock Width="50" TextAlignment="Center" Text= "{Binding Path=shortName}" Padding="4 2 4 2" Background="{Binding Path=realColor}" FontWeight="Bold" FontSize="16"/>
<TextBlock Text="{Binding Path=longName}" FontSize="16" Margin="10 0 0 0"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</DockPanel>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Dans le fichier App.xaml
il est possible de créer des styles.
Exemple : Styliser les boutons
<Application.Resources>
<Style TargetType="Button">
<Setter Property="Width" Value="90"/>
<Setter Property="Margin" Value="10"/>
<Setter Property="Background" Value="#FF141415"/>
<Setter Property="FontWeight" Value="Bold"/>
<Setter Property="Foreground" Value="White"/>
<Setter Property="Opacity" Value="0.8"/>
</Style>
</Application.Resources>
Pour ajouter une image dans le projet :
- Dans notre projet, créer un dossier "Images"
- Placer les images souhaitées dans ce dossier
- Dans l'IDE, clique droit sur le dossier "Images" et choisir
Ajouter / Element existant
(puis l'image à ajouter)
- Dans la classe
Ligne.cs
, avoir une Image différente selon le mode de transport :
public Uri Image
{
get
{
if (mode == "BUS")
{
return new Uri(@"\Images\logoGeneral.png", UriKind.RelativeOrAbsolute);
}
else
{
return new Uri(@"\Images\logoGeneral.png", UriKind.RelativeOrAbsolute);
}
}
}
- Dans la vue
StationView.xaml
, avoir les balisesImage
et faire un binding surUriSource
:
<StackPanel Orientation="Horizontal" >
<Image Width="30" Height="30">
<Image.Source>
<BitmapImage UriSource="{Binding Image}"/>
</Image.Source>
</Image>
</StackPanel>
- Dans la vue
StationView.xaml
, déclarer l'image dans unWindow.Resources
au dessus du grid :
<Window.Resources>
<BitmapImage x:Key="MyLogo" UriSource="\Images\logoGeneral.png"/>
</Window.Resources>
<ScrollViewer>
<Grid>
<Image Source="{StaticResource MyLogo}" HorizontalAlignment="Right" Width="100" Margin="0,10,25,0"/>
</Grid>
</ScrollViewer>
- Afficher l'image dans la vue
StationView.xaml
en utilisantStaticResource
et lax:Key
<Image Source="{StaticResource MyLogo}" HorizontalAlignment="Right" Width="100" Margin="0,10,25,0"/>