Archive

Posts Tagged ‘Silverlight’

Partie 3 : WCF Data Services – Création d’une application Silverlight consommant notre service de données OData

31 juillet 2011 1 commentaire

Dans la partie 1 et la partie 2 nous avons respectivement parlé du protocole OData et créé notre service de données en utilisant WCF Data Services. Dans cette partie nous allons créer notre application cliente en utilisant Silverlight mais je rappelle que notre service de données OData est indépendant de toute technologie vous pouvez utiliser les technologies telles que PHP, Java ou tout simplement du javascript pour communiquer avec le service.

Le SDK de Silverlight fournit une bibliothèque cliente nous permettant d’interroger toute source de données OData que cette dernière soit créée avec WCF Data Services ou non. Cette bibliothèque inclut pas mal de classes et de méthodes nous permettant d’accéder et de modifier les ressources.

L’application que nous allons créer doit nous permettre :

  • d’afficher la liste des produits en fonction d’une catégorie,
  • modifier un produit
  • ajouter un produit
  • supprimer un produit : cette fonctionnalité renverra toujours une exception vu que nous avons défini un intercepteur qui refuse tout changement lié à la suppression d’un produit. Nous la mettrons juste pour tester que notre intercepteur fonctionne correctement.

L’application affichera une seule page xaml qui contiendra toutes nos fonctionnalités et ressemblera à l’image ci-dessous :

Interface de notre application

Interface de notre application

Le code XAML de la page MainPage.xaml est le suivant :

<UserControl xmlns:toolkit="http://schemas.microsoft.com/winfx/2006/xaml/presentation/sdk"
             x:Class="SilverlightAndWCFDataServices.MainPage"
             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"
             mc:Ignorable="d"
             d:DesignHeight="600"
             d:DesignWidth="1000">

    <Grid x:Name="LayoutRoot"
          Width="600"
          Background="White"
          HorizontalAlignment="Center">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition Height="400" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <StackPanel Grid.Row="0"
                    Margin="0,5,0,5"
                    Orientation="Horizontal">

            <ComboBox x:Name="cbCategories"
                      ItemsSource="{Binding}"
                      Width="200"
                      Margin="0,0,2,0"
                      DisplayMemberPath="Name" />
            <Button x:Name="btnMaj"
                    Grid.Row="0"
                    Content="Mettre à jour"
                    Margin="0,0,2,0"
                    Click="btnMaj_Click" />
            <Button x:Name="btnSupprimer"
                    Grid.Row="0"
                    Content="Supprimer"
                    Margin="0,0,2,0"
                    Click="btnSupprimer_Click" />
            <Button x:Name="btnAjouter"
                    Grid.Row="0"
                    Content="Nouveau produit"
                    Margin="0,0,2,0"
                    Click="btnAjouter_Click" />
        </StackPanel>

        <toolkit:DataGrid x:Name="myDataGrid"
                          AutoGenerateColumns="False"
                          Grid.Row="1"
                          Margin="0,0,5,0"
                          ItemsSource="{Binding ElementName=cbCategories, Path=SelectedItem.Products, Mode=TwoWay}">
            <toolkit:DataGrid.Columns>
                <toolkit:DataGridTextColumn Header="Numéro du produit"
                                            Binding="{Binding ProductNumber}" />
                <toolkit:DataGridTextColumn Header="Nom"
                                            Binding="{Binding Name}"
                                            Width="300" />
                <toolkit:DataGridTextColumn Header="Prix unitaire"
                                            Binding="{Binding ListPrice}" />
                <toolkit:DataGridTextColumn Header="Couleur"
                                            Binding="{Binding Color}" />
                <toolkit:DataGridTextColumn Header="Taille"
                                            Binding="{Binding Size}" />
            </toolkit:DataGrid.Columns>
        </toolkit:DataGrid>
    </Grid>
</UserControl>

Nous avons 3 boutons et la fonction de chacun se passe de commentaires. Chacun des boutons est abonné à l’évènement Click. Nous avons un contrôle DataGrid qui a sa propriété ItemsSource renseigné grâce à une liaison de données sur la liste des Produits rattachés à la catégorie sélectionnée dans notre ComboxBox prévu à cet effet. Notre ComboBox est abonné à l’évènement SelectionChanged.

Pour nous faciliter la communication avec notre service de donnés, Visual Studio nous permet de générer une classe qui servira de proxy que nous utilisons pour communiquer avec la source de données. Le proxy sera créé dans le projet de l’application Silverlight SilverlightAndWCFDataServices. Pour cela faîtes un clic droit sur le nom du projet puis cliquez sur Ajouter un service de référence et suivre les étapes ci-dessous :

Génération du Proxy

Pour voir ce qui a été généré, dans le dossier Service Reference faîtes un clic droit sur DemoODataServiceReference puis sur Afficher dans l’explorateur d’objets et une boîte de dialogue s’affiche comme dans l’image ci-dessous :

Exploration de la classe servant de proxy

La génération du proxy nous a créé trois classes comme le montre la partie gauche de l’image. La classe AdventureWorksEntites est la plus importante parmi les classes générées. Elle représente notre contexte de données côté client et permettra le suivi de ces données. Elle nous permet grâce à des méthodes asynchrones d’envoyer nos requêtes sous formes d’URI généré à partir d’une chaine de caractères ou à partir d’une instance de classe spéciale DataServiceQuery. Les méthodes d’envoi des requêtes commencent toutes par le préfixe Begin.

Pour ne pas avoir à saisir les requêtes en utilisant les URLs qui peuvent être source d’erreurs nous passerons par la classe DataServiceQuery qui nous générera l’URL correspondante. Cette classe expose une propriété RequestUri qui continent l’URL qui est générée avant l’envoi de la requête à notre service de données grâce aux différentes méthodes asynchrones disponibles dans notre contexte AdventureWorksEntites.

3.1. Chargement de la liste des catégories et des produits
Le code (des commentaires sont insérés pour comprendre ce qui est fait) suivant nous permet de charger les différentes catégories et montre aussi comment sera récupérer les produits associé à une catégorie sélectionnée :

public partial class MainPage : UserControl
{
	private AdventureWorksEntities context;

	public MainPage()
	{
		InitializeComponent();
		this.Loaded += this.userControlLoaded;
		this.cbCategories.SelectionChanged += this.cbCategoriesSelectionChanged;
		this.context = new AdventureWorksEntities(new Uri("http://localhost:3000/AdventureWorksDataService.svc"));
	}

	private void userControlLoaded(object sender, RoutedEventArgs e)
	{
		this.Loaded -= this.userControlLoaded;

		// Nous créons une requête Linq que nous convertissons en un objet du type DataServiceQuery<ProductCategory>
		DataServiceQuery<ProductCategory> query = (from c in this.context.ProductCategories
												   orderby c.Name
												   where c.ParentProductCategoryID != null
												   select c) as DataServiceQuery<ProductCategory>;
		try
		{
			// Nous utilisons la méthode asynchrone BeginExecute (méthode permettant d'exécuter toute requête avec le verbe HTTP GET) pour exécuter la requête précédemment construite.
			// la méthode réçoit en paramètres :
			// -    une URI en l'occurence celle générée par l'instance query
			// -    une méthode de rappel : la méthode qui sera exécutée une fois que une réponse est réçue de la part de notre service de données
			// -    tout type d'objet que nous voudrions par la suite récupérer dans lorsque la méthode de rappel sera exécutée
			context.BeginExecute<ProductCategory>(query.RequestUri, this.getCategoriesCallback, null);
		}
		catch (Exception ex)
		{
			MessageBox.Show(string.Format("Erreur survenue : {0}", ex.Message));
		}
	}

	private void getCategoriesCallback(IAsyncResult result)
	{
		this.Dispatcher.BeginInvoke(() =>
		{
			try
			{
				// Nous appelons la méthode EndExecute pour pouvoir récupérer le résultat. 
				// Ici le récultat correspond à la liste de catégories
				var categories = this.context.EndExecute<ProductCategory>(result);
				this.cbCategories.DataContext = categories;
			}
			catch (Exception ex)
			{ 
				// Dans ce bloc nous gérons les erreurs rencontrées 
				// Une gestion un tout petit particulier pour récupérer le message exacte de l'erreur 
				// s'il s'agit d'une exception du type DataServiceRequestException
				string message;
				if (ex is DataServiceRequestException)
					message = this.getMessage(ex as DataServiceRequestException);
				else message = ex.Message;
				MessageBox.Show("Erreur : " + message);
			}
		});
	}

	private void cbCategoriesSelectionChanged(object sender, SelectionChangedEventArgs e)
	{
		try
		{
			// Si aucune catégorie n'est sélectionnée on ne fait rien
			if (this.cbCategories.SelectedIndex == -1) return; 

			// On récupère la catégorie concernée
			ProductCategory category = this.cbCategories.SelectedItem as ProductCategory;

			// On appelle la méthode BeginLoadProperty permettant le chargement des données d'une propriéte particulière
			this.context.BeginLoadProperty(category, "Products", getProductByCategoryCallback, null);
		}
		catch (Exception ex)
		{
			MessageBox.Show(string.Format("Erreur survenue : {0}", ex.Message));
		}
	}

	private void getProductByCategoryCallback(IAsyncResult result)
	{
		this.Dispatcher.BeginInvoke(() =>
		{
			try
			{
				// Nous appelons juste la méthode EndLoadProperty. 
				// la liste des produits associées à la catégorie concernée sera bien remplie
				// vu que nous l'avons spécifié lors de l'appel BeginLoadProperty. 
				this.context.EndLoadProperty(result);
			}
			catch (Exception ex)
			{
				string message;
				if (ex is DataServiceRequestException)
					message = this.getMessage(ex as DataServiceRequestException);
				else message = ex.Message;
				MessageBox.Show("Erreur : " + message);
			}
		});
	}

	private string getMessage(DataServiceRequestException dataServiceRequestException)
	{
		if (dataServiceRequestException.InnerException == null) return dataServiceRequestException.Message;

		// Nécessite l'ajout de la DLL System.Xml.Linq
		// Le message exacte à récupérer lorsqu'une exception générée par le service de données est interceptée par 
		// le client. Le type de l'exception sera du type DataServiceRequestException sauf que le message que nous voulons
		// ne se trouve pas dans la propriété Message de l'exception mais dans celle de la propriété innerException. 
		// Le message est par contre cpntenu dans une structure XML d'où le fait que nous utilisons Linq To XML pour récupére le texte
		// de l'élément message
		XDocument xDoc = XDocument.Parse(dataServiceRequestException.InnerException.Message);
		XElement xElement = xDoc.Root;
		XNamespace ns = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";
		var message = (from elt in xElement.Elements(ns + "message")
					   select elt.Value).FirstOrDefault();
		return message;
	}
}

3.2. Ajout, mise à jour et suppression d’un produit
Dans l’extrait de code suivant nous avons les gestionnaires de l’évènement Click des différents boutons :

private void btnMaj_Click(object sender, RoutedEventArgs e)
{
    try
    {
        // On verifie qu'un produit est bien sélectionné sinon on ne fait rien
        if (this.myDataGrid.SelectedIndex == -1) return;

        // On récupère l'instance du produit sélectionné
        Product product = this.myDataGrid.SelectedItem as Product; 

        // On spécifie à notre contexte que le produit doit être mis à jour grâce 
        // à la méthode UpdateObject de notre contexte 
        this.context.UpdateObject(product);  

        // Une fois le produit marqué comme à mettre à jour alors
        // nous devons valider la modification vers notre service de données
        // en appelant la méthodde BeginSaveChanges
        this.context.BeginSaveChanges(this.saveChangeCallback, null);
    }
    catch (Exception ex)
    {
        MessageBox.Show("Erreur : " + ex.Message);
    }
}

private void btnSupprimer_Click(object sender, RoutedEventArgs e)
{
    try
    {
        // On verifie qu'un produit est bien sélectionné sinon on ne fait rien
        if (this.myDataGrid.SelectedIndex == -1) return;

        // On récupère l'instance du produit sélectionné
        Product product = this.myDataGrid.SelectedItem as Product;

        // On spécifie à notre contexte que le produit doit être supprimé grâce 
        // à la méthode DeleteObject de notre contexte 
        this.context.DeleteObject(product);

        // Une fois le produit marqué comme à supprimer alors
        // nous devons valider la modification vers notre service de données
        // en appelant la méthodde BeginSaveChanges
        this.context.BeginSaveChanges(this.saveChangeCallback, null);
    }
    catch (Exception ex)
    {
        MessageBox.Show("Erreur : " + ex.Message);
    }
}

private void btnAjouter_Click(object sender, RoutedEventArgs e)
{
    if(this.cbCategories.SelectedIndex == -1) return;
    try
    {
        // On récupère la catégorie à associer au nouveau produit
        ProductCategory category = this.cbCategories.SelectedItem as ProductCategory;

        // On renseigne les différentes propriétés requises 
        Random random = new Random();
        int n = random.Next(999);
        Product newProduct = new Product()
        {
            Name = string.Format("Added product {0}", n),
            Color = "White",
            ListPrice = 150m,
            StandardCost = 99,
            ProductNumber = string.Format("Added product - {0}", n),
            SellStartDate = DateTime.Now,
            rowguid = Guid.NewGuid(),
            ModifiedDate = DateTime.Now,
            Size = "XL" // Mettre cette propriété à null si vous voulez tester si notre intercepteur empêchant l'ajout
                        // d'un produit appartenant à la catégorie parent Clothing Ex : la catgorie Socks est enfant de Clothing.
        };

        // Mettre à jour les propriétes Products et ProductCategory respectivement pour category et newProductCategory
        category.Products.Add(newProduct);
        newProduct.ProductCategory = category;

        // On crée la relation entre la catégorie et le produit. Cela n'est pas fait de façon automatique lorsqu'on 
        // met à jour les propriétés de nos données avec les deux lignes de codes précédentes
        this.context.AddRelatedObject(category, "Products", newProduct);

        // Nous appelons la méthode BeginSaveCahnges pour mettre à jour le service de données.
        this.context.BeginSaveChanges(this.saveChangeCallback, null);
    }
    catch (Exception ex)
    {
        MessageBox.Show("Erreur : " + ex.Message);
    }
}

private void saveChangeCallback(IAsyncResult result)
{
        this.Dispatcher.BeginInvoke(() =>
        {
            try
            {
                // Nous appelons la méthode EndSaveChanges 
                this.context.EndSaveChanges(result);
                MessageBox.Show("Mise à jour effectuée avec succcès");
            }
            catch (Exception ex)
            {
                string message;
                if (ex is DataServiceRequestException)
                    message = this.getMessage(ex as DataServiceRequestException);
                else message = ex.Message;
                MessageBox.Show("Erreur : " + message);
            }
        });
            
}

Nous pouvons tester notre application pour récupérer les données, ajouter, mettre à jour et essayer de supprimer un produit.
La solution Visual Studio 2010 se trouve ici.
Sources :
• Site du protocole OData : http://www.odata.org
• MSDN WCF Data Services : http://msdn.microsoft.com/fr-fr/library/cc668792.aspx
• MSDN WCF Data Services (Silverlight) : http://msdn.microsoft.com/en-us/library/cc838234(VS.95).aspx

Publicités
%d blogueurs aiment cette page :