1.Introduction

Vous êtes un développeur C#, débutant avec Silverlight et vous souhaitez utiliser Silverlight avec une base de données MySQL ?
Alors ce tutoriel est pour vous.

A travers cet article, nous allons présenter une méthode pour accéder à une base de données MySQL, exécuter des requêtes de sélection, d'insertion, etc ...
Tout au long de ce cours, je vais utiliser Visual C# 2008.

Notez qu'au moment où j'écris cet article, Silverlight est encore en version 2 béta 2 et j'utilise certains hacks pour contourner des problèmes qui n'existeront sans doute plus dans la version finale de Silverlight.

2.Préambule

Dans ce tutoriel, je considère que vous avez quelques notions de Silverlight et les outils correctement installés.
Pour plus de renseignements sur l'installation des outils Silverlight, vous pouvez aller consulter l'introduction à Silverlight 2 de Benjamin Roux.

3.Le besoin : utiliser MySQL et PHP avec Silverlight

On voit un peu partout des utilisations de Silverlight avec Sql Serveur, notamment à travers l'utilisation de LINQ et de WCF. Je vais vous montrer comment on peut utiliser Silverlight avec une base de données MySQL par exemple.

L'intérêt est de pouvoir utiliser des applications Silverlight dans un environnement open source (LAMP par exemple) ou de les héberger chez votre fournisseur d'accès qui en général propose un serveur apache avec MySQL (comme Free par exemple).

Comme on ne peut pas utiliser Linq To Sql avec autre chose qu'Sql Server, l'idée est d'utiliser un script PHP qui accèdera à la base de données MySQL qui servira d'interface avec notre application Silverlight.
Ainsi, quand on aura besoin d'informations, notre script PHP ira lire dans la base de données et nous renverra les informations en format XML que l'on pourra exploiter avec Linq To Xml.
De même, quand on aura besoin de faire des insertions ou des mises à jour, on enverra des données à un script PHP qui s'occupera de communiquer avec la base de données MySQL.

4.Lire une table de la base MySQL

Imaginons que nous voulions faire une application qui affiche une todolist. Le premier besoin est d'être capable de lire dans une table (qui s'appellera todolist) avec un select. Cette table contient 2 champs :

  • id, qui est un entier auto incrémenté
  • libelle, qui est une chaîne de caractères (varchar(500))

Le but est de récupérer la liste des éléments de ma todolist avec un select.

4.1.Préambule : création de la table

Le script SQL suivant permet de créer la table todolist.

 
Sélectionnez
CREATE TABLE `todolist` (`id` INT NOT NULL AUTO_INCREMENT , `libelle` VARCHAR( 500 ) NOT NULL , PRIMARY KEY ( `id` )) ENGINE = MYISAM 
				

Tant qu'on est dans le SQL, on va faire un petit insert into pour remplir la table avec une ligne, pour que notre select renvoi quelque chose :

 
Sélectionnez
INSERT INTO `todolist` (`id`, `libelle`) VALUES (NULL , 'Mettre le projet de demo en telechargement');
				

Ce qui nous donne :

id libelle
1 Mettre le projet de demo en telechargement

4.2.Le script PHP de lecture

Nous allons donc créer un script PHP qui va nous retourner l'ensemble des éléments de ma todolist sous format XML.
L'idée est de retourner cette liste sous cette forme :

 
Sélectionnez
<datas>
	<data>
		<id>1</id>
		<libelle>Mettre le projet de démo en téléchargement</libelle>
	</data>
	...
</datas>

Voici le script PHP qui fait ca :

gettodolist.php
Sélectionnez
<?php
	header('Content-type: text/xml');

	$host = "localhost"; // le nom ou l'adresse du host
	$user = "admin"; // user de la base
	$pass = ""; // mot de passe
	$connexion = mysql_connect($host,$user,$pass) or die('Erreur de connexion');
	$bdd = "mysqldb"; // nom de la base de données
	if (!mysql_select_db($bdd,$connexion))
		return 0;
	if (!$connexion)
		return 0;

	$query = "SELECT * FROM `todolist`";
	
	$result = mysql_query($query);
	if (!$result)
		return 0;
	echo "<datas>";
	while ($line = mysql_fetch_assoc($result))
	{
		echo "<data>";
		$id = $line["id"];
		echo "<id>".$id."</id>";
		echo "<libelle>".$line["libelle"]."</libelle>";
		echo "</data>";
	}
	mysql_free_result($result);	// Libération des résultats	
	echo "</datas>";

	mysql_close($connexion);	// Fermeture de la connexion, cela ne libère pas les résultats
?>

Ce script commence par créer la connexion à la base de données et fait un select pour retourner tous les éléments de la liste.
Ensuite, il s'occupe de créer les balises XML en bouclant sur tous les éléments retournés par le select.
Notez l'utilisation de

gettodolist.php
Sélectionnez
header('Content-type: text/xml');

pour indiquer que l'on retourne du XML.

Cliquez ici pour tester le script.

4.3.Téléchargement asynchrone

Nous allons donc devoir appeler ce script depuis Silverlight pour pouvoir ensuite interpréter le xml généré.

Pour ce faire, on va utiliser l'objet WebClient pour faire un appel asynchrone au script PHP. On utilisera la méthode DownloadStringAsync pour déclencher le téléchargement asynchrone et la surcharge de l'événement DownloadStringCompletedEventHandler permettra d'agir lors de la fin de téléchargement de la réponse renvoyée par le script PHP.

J'ai créé à cet effet une petite classe helper toute simple :

 
Sélectionnez
public class WebClientHelper
{
    public event DownloadStringCompletedEventHandler DownloadComplete;
    private void OnDownloadComplete(object sender, DownloadStringCompletedEventArgs e)
    {
        if (DownloadComplete != null)
        {
            DownloadComplete(sender, e);
        }
    }

    private readonly string _url;

    public WebClientHelper(string url)
    {
        var random = new Random();
        _url = url;
        if (_url.Contains("?"))
            _url = _url + "&trick=" + random.Next();
        else
            _url = _url + "?trick=" + random.Next();
    }

    public void Execute()
    {
        var webClient = new WebClient();
        webClient.DownloadStringCompleted += webClient_DownloadStringCompleted;
        webClient.DownloadStringAsync(new Uri(_url));
    }

    void webClient_DownloadStringCompleted(object sender, DownloadStringCompletedEventArgs e)
    {
        if (e.Error == null)
        {
            OnDownloadComplete(sender, e);
        }
    }
}

Dans cette classe Helper, vous pouvez remarquer que je passe un paramètre en GET à mon script PHP qui est un nombre aléatoire (paramètre trick). L'utilisation de ce paramètre permet de tromper le cache de Silverlight.
Aujourd'hui, il n'y a aucun mécanisme qui permet de définir les options de cache (cela sera surement ajouté dans la version définitive). Mon objectif est de me passer des fonctionnalités de cache, c'est pour cela que je rajoute cet argument, ce qui va forcer le script PHP a être réinterprété.

Pour l'utiliser, on fera :

 
Sélectionnez
private void ChargementDonnees()
{
	try
	{
		var helper = new WebClientHelper(string.Format("{0}/gettodolist.php", Config.BASEPATH));
		helper.DownloadComplete += helper_DownloadComplete;
		helper.Execute();
	}
	catch (Exception ex)
	{
		HtmlPage.Window.Alert(ex.Message);
	}
}

Vous pouvez constater que j'ai choisi de mettre l'url du serveur dans une classe de configuration :

 
Sélectionnez
public static class Config
{
	public const string BASEPATH = "http://nico-pyright.developpez.com/tutoriel/vs2008/csharp/silverlightandmysql";
}

On associe l'événement DownloadComplete du helper à une méthode : helper_DownloadComplete. C'est dans cette méthode que l'on va pouvoir faire le traitement du résultat du script, c'est à dire le traitement du XML.

Remarque : n'oubliez pas de référencer l'assembly System.Net.

4.4.Linq to Xml

L'appel au script PHP nous renvoi donc du XML. Nous allons le parser grâce aux méthodes de Linq To Xml.
Dans un premier temps, nous allons créer un objet qui va représenter un élément de la todolist :

 
Sélectionnez
public class TodoElement
{
	public int Id { get; set; }
	public string Libelle { get; set; }
}

Comme on l'a vu au dessus, c'est dans la méthode helper_DownloadComplete que nous pourrons effectuer notre select avec Linq :

 
Sélectionnez
void helper_DownloadComplete(object sender, DownloadStringCompletedEventArgs e)
{
	try
	{
		XDocument xmlElements = XDocument.Parse(e.Result);
		var elements = from data in xmlElements.Descendants("data")
					   select new TodoElement
					   {
						   Id = (int)data.Element("id"),
						   Libelle = ((string)data.Element("libelle")).Trim()
					   };
		ListeTodo.ItemsSource = elements;
	}
	catch (Exception ex)
	{
		HtmlPage.Window.Alert(ex.Message);
	}
}

Remarque : N'oubliez pas de rajouter la référence à System.Xml.Linq.

Le résultat de l'exécution du script PHP, s'il est correct, est disponible dans e.Result. On peut donc s'en servir pour le parser avec l'object XDocument. Nous pourrons alors faire notre requête Linq.
Notez qu'on affecte la liste des éléments construits à la propriété ItemSource d'un objet ListeTodo.
Il s'agit d'une ListBox qui sera déclarée dans le XAML de notre page, nous y reviendrons plus tard.


Vous avez donc vu, à travers ces différentes étapes, comment on pouvait faire une requête SQL de type select dans notre base MySQL.

5.Mise à jour de la table

De la même façon, on veut pouvoir effectuer d'autres opérations sur la table, comme un insert into ou un update, afin de rajouter ou de supprimer des éléments dans notre todolist. Pour ça, on va passer encore une fois par un script PHP. Sauf que cette fois ci, nous allons devoir lui passer des paramètres. Pour ce faire, nous allons les lui envoyer en POST.

5.1.Script PHP d'ajout d'un élément dans la liste

add.php
Sélectionnez
<?php
	$host = "localhost"; // le nom ou l'adresse du host
	$user = "admin"; // user de la base
	$pass = ""; // mot de passe
	$connexion = mysql_connect($host,$user,$pass) or die('Erreur de connexion');
	$bdd = "mysqldb"; // nom de la base de données
	if (!mysql_select_db($bdd,$connexion))
		return 0;
	if (!$connexion)
	{
		echo "ERR1";
		return 0;
	}

	if (isset($_POST["data"]))
	{
		$libelle = mysql_real_escape_string($_POST["data"]);
        $query = "INSERT INTO `todolist` (`id`, `libelle`) VALUES (NULL , '".$libelle."')";
		
		$result = mysql_query($query);
		if (!$result)
			echo "ERR2";
	}
	else
		echo "ERR";

	mysql_close($connexion);	// Fermeture de la connexion, cela ne libère pas les résultats
?>

Dans un premier temps, ce script se connecte à la base de données. Ensuite, il vérifie s'il y a dans les données postées quelque chose qui l'intéresse. Si c'est le cas, on l'insère dans la table todolist.
Ce qui a pour but d'ajouter un nouvel élément dans la todolist.

5.2.Envoi de données en POST

Le principe est d'utiliser l'objet HttpWebRequest (n'oubliez pas de rajouter la référence à System.Net) et d'envoyer les données de manière asynchrone en POST via BeginGetRequestStream.
J'utilise ici un helper inspiré du site d'Albert Cameron.

Voici son implémentation :

 
Sélectionnez
public class HttpHelper
{
    private HttpWebRequest Request { get; set; }
    public Dictionary<string, string> PostValues { get; private set; }

    public event HttpResponseCompleteEventHandler ResponseComplete;
    private void OnResponseComplete(HttpResponseCompleteEventArgs e)
    {
        if (ResponseComplete != null)
        {
            ResponseComplete(e);
        }
    }

    public HttpHelper(Uri requestUri, string method, params KeyValuePair<string, string>[] postValues)
    {
        Request = (HttpWebRequest)WebRequest.Create(requestUri);
        Request.ContentType = "application/x-www-form-urlencoded";
        Request.Method = method;
        PostValues = new Dictionary<string, string>();
        if (postValues != null && postValues.Length > 0)
        {
            foreach (var item in postValues)
            {
                PostValues.Add(item.Key, item.Value);
            }
        }
    }

    public void Execute()
    {
        Request.BeginGetRequestStream(BeginRequest, this);
    }

    private static void BeginRequest(IAsyncResult ar)
    {
        var helper = ar.AsyncState as HttpHelper;
        if (helper != null)
        {
            if (helper.PostValues.Count > 0)
            {
                using (var writer = new StreamWriter(helper.Request.EndGetRequestStream(ar)))
                {
                    foreach (var item in helper.PostValues)
                    {
                        writer.Write("{0}={1}&", item.Key, item.Value);
                    }
                }
            }
            helper.Request.BeginGetResponse(BeginResponse, helper);
        }
    }

    private static void BeginResponse(IAsyncResult ar)
    {
        var helper = ar.AsyncState as HttpHelper;
        if (helper != null)
        {
            var response = (HttpWebResponse)helper.Request.EndGetResponse(ar);
            if (response != null)
            {
                var stream = response.GetResponseStream();
                if (stream != null)
                {
                    using (var reader = new StreamReader(stream))
                    {
                        helper.OnResponseComplete(new HttpResponseCompleteEventArgs(reader.ReadToEnd()));
                    }
                }
            }
        }
    }
}

public delegate void HttpResponseCompleteEventHandler(HttpResponseCompleteEventArgs e);
public class HttpResponseCompleteEventArgs : EventArgs
{
    public string Response { get; set; }

    public HttpResponseCompleteEventArgs(string response)
    {
        Response = response;
    }
}

Pour l'utiliser :

 
Sélectionnez
var helper = new HttpHelper(new Uri(string.Format("{0}/add.php", Config.BASEPATH)), "POST", 
				new KeyValuePair<string, string>("data", nouvelleTache.Text));
helper.ResponseComplete += AddComplete;
helper.Execute();
 
Sélectionnez
private void AddComplete(HttpResponseCompleteEventArgs e)
{
	string retour = e.Response;
	if (retour == "ERR")
		HtmlPage.Window.Alert("Erreur dans l'ajout");
	else
	{
		if (retour == "ERR1")
			HtmlPage.Window.Alert("Problème de connexion à la base de données");
		else
		{
			if (retour == "ERR2")
				HtmlPage.Window.Alert("Problème d'insertion");
			else
				ChargementDonnees();
		}
	}
}	

La méthode AddComplete est appelée lorsque l'envoi des données en POST est terminé. On récupère le retour de cet appel dans e.Response, ce qui me permettra de vérifier que tout s'est bien passé.
Puis on rappelle le chargement des données pour prendre en compte la nouvelle donnée.

6.L'application : une todo liste

Je commence par créer un nouveau projet de type Silverlight Application.

Image non disponible


Ensuite je choisis d'utiliser un nouveau site web qui contiendra le contrôle silverlight.

Image non disponible


NB : Si vous n'utilisez pas ce type de projet, vous aurez une exception de type "security error" lors de l'utilisation de l'objet WebClient.

6.1.Code Xaml

 
Sélectionnez
<UserControl x:Class="demoTodoList.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="800" Height="600">
    <UserControl.Resources>
        <Style x:Key="MaList" TargetType="ListBox">
            <Setter Property="Margin" Value="5"/>
            <Setter Property="Template">
                <Setter.Value>
                    <ControlTemplate>
                        <ScrollViewer x:Name="ScrollViewerElement" 
					VerticalScrollBarVisibility="Visible" Loaded="ScrollViewerElement_Loaded" >
                            <ItemsPresenter/>
                        </ScrollViewer>
                    </ControlTemplate>
                </Setter.Value>
            </Setter>
        </Style>
    </UserControl.Resources>
    <Canvas>
        <Rectangle Width="600" Height="50" Stroke="Orange" StrokeThickness="5" />
        <TextBlock Text="Ajouter une nouvelle tache : " Canvas.Left="20" Canvas.Top="10"/>
        <TextBox x:Name="nouvelleTache" Width="200" Canvas.Left="240" Canvas.Top="10"/>
        <Button Click="Button_Click" Width="80" Canvas.Left="470" Canvas.Top="10" Content="Ajouter"/>
        <TextBlock Text="Liste des taches en cours : " Canvas.Left="15" Canvas.Top="60"/>
        <ListBox x:Name="ListeTodo" Height="400" Canvas.Top="100" 
		Style="{StaticResource MaList}" SelectionChanged="OnSelectionChanged" >
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <TextBlock Text="{Binding Libelle}" Margin="5"/>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
        <TextBlock x:Name="textSupprimer" Canvas.Left="500" Canvas.Top="150" 
		Text="Voulez vous supprimer cet élément ?" Visibility="Collapsed"/>
        <Button x:Name="boutonSupprimer" Content="Supprimer" Canvas.Top="200" 
		Canvas.Left="600" Click="Button_Click_1" Visibility="Collapsed"/>
    </Canvas>
</UserControl>

On a donc un cadre composé d'un TextBlock, d'un TextBox et d'un Button qui permettra de faire l'ajout d'un nouvel élément dans la todolist.
En dessous, nous avons une ListBox qui contiendra la liste des éléments de la todolist qui sont sauvegardés en base.
Le click sur un élément de la ListBox provoquera l'affichage du TextBlock et du bouton qui permettront de supprimer un élément de la liste, comme on le verra au 6.3.

Image non disponible

Comme il s'agit d'une béta, j'utilise une astuce pour afficher la scrollbar de notre ListBox en utilisant l'événement Loaded pour ajuster la visibilité de la Scrollbar verticale.
Gageons que ce hack sera corrigé dans la version finale.

 
Sélectionnez

private void ScrollViewerElement_Loaded(object sender, RoutedEventArgs e)
{
	((ScrollViewer)sender).VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
}

6.2.Binding avec la ListBox

On remarque dans le xaml ci-dessus le TextBox dans le DataTemplate de la ListBox :

 
Sélectionnez
<TextBlock Text="{Binding Libelle}" Margin="5"/>

grâce à la MarkupExtension Binding, Silverlight est capable de binder la propriété Libelle d'un élément de la source de données.

6.3.Supprimer un élément de la todolist

On veut aussi pouvoir supprimer un élément de la todolist. Pour ce faire, on va réagir à un événement de sélection d'un élément de la ListBox et afficher le bouton de suppression.

 
Sélectionnez
private void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
	if (ListeTodo.SelectedItem != null)
	{
		textSupprimer.Visibility = Visibility.Visible;
		boutonSupprimer.Visibility = Visibility.Visible;
	}
}

Ensuite, lors du click sur ce bouton, on récupère l'élément sélectionné et on passe au script de suppression PHP (en POST) la valeur de l'ID de l'item sélectionné, de la même façon que précédemment. Ce qui nous donne :

 
Sélectionnez
private TodoElement _current;
private void Button_Click_1(object sender, RoutedEventArgs e)
{
	_current = ((TodoElement)ListeTodo.SelectedItem);
	if (_current == null)
	    return;
	try
	{
	    var helper = new HttpHelper(new Uri(string.Format("{0}/delete.php", Config.BASEPATH)), "POST",
	                                       new KeyValuePair<string, string>("data", _current.Id.ToString()));
	    helper.ResponseComplete += DeleteComplete;
	    helper.Execute();
	}
	catch (Exception ex)
	{
	    HtmlPage.Window.Alert(ex.Message);
	}
}

private void DeleteComplete(HttpResponseCompleteEventArgs e)
{
	string retour = e.Response;
	if (retour == "ERR")
		HtmlPage.Window.Alert("Erreur dans la suppression");
	else
	{
		if (retour == "ERR1")
			HtmlPage.Window.Alert("Problème de connexion à la base de données");
		else
		{
			if (retour == "ERR2")
				HtmlPage.Window.Alert("Problème de suppression");
			else
			{
				Dispatcher.BeginInvoke(() =>
				{
					textSupprimer.Visibility = Visibility.Collapsed;
					boutonSupprimer.Visibility = Visibility.Collapsed;
				});
				nouvelleTache.Focus();
				ChargementDonnees();

			}
		}
	}
}

Notez à nouveau le hack que j'utilise pour contourner des bugs de la version béta. En effet, les changements de visibilité des contrôles provoquent souvent des erreurs.
On utilise ici le Dispatcher pour gérer cette visibilité dans un thread qui s'occupe de mettre à jour l'interface utilisateur.
De même, je place le focus sur un autre élément que la LisBbox, sinon, j'aurai une belle exception : HRESULT E_FAILED has been returned from a call to a COM component.

Voici le script PHP qui va s'occuper de faire la suppression d'un élément de la todolist :

delete.php
Sélectionnez
<?php
	$host = "localhost"; // le nom ou l'adresse du host
	$user = "admin"; // user de la base
	$pass = ""; // mot de passe
	$connexion = mysql_connect($host,$user,$pass) or die('Erreur de connexion');
	$bdd = "mysqldb"; // nom de la base de données
	if (!mysql_select_db($bdd,$connexion))
		return 0;
	if (!$connexion)
	{
		echo "ERR1";
		return 0;
	}

	if (isset($_POST["data"]))
	{
        $id = (int)$_POST["data"];
        $query = "DELETE FROM `todolist` where `id` = ".$id;	
		$result = mysql_query($query);
		if (!$result)
			echo "ERR2";
	}
	else
		echo "ERR";

	mysql_close($connexion);	// Fermeture de la connexion, cela ne libère pas les résultats
?>

6.4.Autoriser le cross domain

Si votre script PHP à appeler est sur un domaine différent de celui où est hébergée votre application Silverlight, il faudra autoriser explicitement l'application à appeler une url externe.
Pour ce faire, le site de destination devra posséder un fichier crossdomain.xml à la racine du serveur, dont le contenu autorise le cross-domain.
L'exemple suivant autorise tous les appels :

 
Sélectionnez
<cross-domain-policy>
	<allow-access-from domain="*"/>
</cross-domain-policy>

Plus d'infos sur msdn.

6.5.Téléchargement

Vous pouvez télécharger ici les sources du projet : version rar (656 Ko) , version zip (782 Ko).

7.Démo en ligne

Vous trouverez ci-dessous la démo en ligne du projet d'exemple de l'article, mais je l'ai bridé pour ne permettre que la consultation.
Voir la démo.

8.Conclusion

Cet article montre comment utiliser une base de données MySQL à partir d'une application silverlight, en utilisant le PHP et Linq To Xml. Ainsi, vous pourrez utiliser votre application Silverlight chez votre hébergeur préféré.
Nous avons également vu un peu de Linq To Xml et la manipulation élémentaire d'une listbox.

Remerciements

Je remercie particulièrement Yogui pour ses conseils experts sur mon code PHP ainsi que l'équipe Dotnet pour leurs relectures attentives du document.

Contact

Si vous constatez une erreur dans le tutorial, dans le source, dans la programmation ou pour toutes informations, n'hésitez pas à me contacter par mail, ou par le forum.