Développer un projet aujourd’hui : L’application CaveAVins pour Windows Phone 7.1

 

 

Développer un projet aujourd’hui : L’application CaveAVins pour Windows Phone 7.1

  • Comments 6

Update 18/10/2011: l’url du service OData devient http://stephecaveavins.cloudapp.net/CaveAVinsDataService.svc

Cet article décrit le fonctionnement de la première maquette de l’application Cave A Vins pour Windows Phone. Il explique les points sensibles de l’application et détaille leur réalisation, sans rentrer autant dans le détail qu’un tutoriel.

Pas si basique que cela, notre application va mettre en œuvre les concepts suivants:

Nous allons également modifier le service WCF Data Services pour le rendre plus intelligent et effectuer automatiquement la transformation d’une adresse en coordonnées latitude/longitude que l’on ajoutera aux informations sur le vin.

Ampoule L’application est développée pour Windows Phone 7.1. Pour découvrir les nouveautés par-rapport à la version 7.0, c’est ici.
Prérequis :
Windows Phone SDK 7.1 Release Candidate

Un petit aperçu du résultat:

Get Microsoft Silverlight

 

 

Résumé des épisodes précédents

Articles:

Tutoriels:

Présentation des écrans

Voilà à quoi ressemble notre application de gestion de Cave A Vins.
L’application utilise un contrôle Panorama, avec un en-tête déroulant affichant autant d’images de bouteilles que vous avez de vins différents dans votre cave. Si vous avez des yeux de lynx (‘achement dur ne pas écrire Linq ou Lync Extra-terrestre), vous remarquerez qu’elles portent le nom du vin sur leur étiquette. Bon c’est pas fait pour être lisible, juste décoratif : les bouteilles défilent quand vous passez d’un item panorama à un autre Tire la langue (cf vidéo).

1 image 2 image 3 image

1 - La première page affiche la liste des vins présents dans la cave triés par date décroissante d’achat.

2 - La seconde utilise le contrôle LongListSelector du toolkit Silverlight pour WP7 pour afficher la même liste, groupée par année. On pourrait imaginer ajouter les informations sur les cépages et proposer le même type de vue groupée ce qui serait très pratique.

3 - La troisième page utilise le contrôle Bing Maps pour afficher l’origine des vins et le nombre de bouteilles associées. Cela permet de repérer facilement ce qu’il manque à la cave pour être suffisamment variée géographiquement parlant.

4 image 5 image

4 – le menu contextuel permet de décrémenter le nombre de bouteilles du vin sélectionné, d’ajouter/éditer/supprimer un vin de sa cave.

5 – l’édition d’un vin permet de saisir les informations qui lui sont relatives ainsi que de prendre la photo de son étiquette

Architecture de la solution

Rappel de l’architecture existante après la migration dans Azure:

image48

Et l’architecture qui sera mise en place dans le contexte de cet article :

image

Ampoule Le service proxy d’authentification “CaveAVins Blob Authentifier” est facultatif mais conseillé pour ne pas divulguer les clés d’accès au storage Azure.

A ce stade, plusieurs questions restent encore en suspens :

  • Quelle architecture vais-je mettre en place dans mon application WP ?
  • Comment identifier le propriétaire de la cave pour retrouver quelles sont ses bouteilles dans la base de données ?
  • Comment situer géographiquement mes vins sur un contrôle Bing Map à partir d’une simple adresse ?
  • Comment accéder aux blobs Azure depuis le téléphone, pour y stocker les photos ?
  • Comment modifier la structure des données créée avec Entity Framework Code First pour y ajouter de nouvelles informations ?
  • Existe-t-il des contrôles gratuits pour faciliter le développement ? Liste groupée, contrôle rating pour la note associée à la bouteille, menu contextuel, animations …

L’architecture logique de mon application WP

De mon point vue, la question est moins triviale pour une application Silverlight WP7 que pour du Silverlight classique.

Pour ma part, en Silverlight je pars toujours sur du “MVVM-like” plus ou moins strict, selon l’importance et la durée de vie de mon application. Je pars soit de mes classes de base et méthodes d’extension qui constituent ma boite à outils que j’ai créé au fil des projets, soit d’un framework ou partie de framework existant type MVVMLight, Caliburn, Prism…

AmpouleEn 3 mots, si vous ne savez pas ce qu’est MVVM (Model – View – ViewModel) : c’est une architecture 3-tiers avec

  • Model = le modèle (les données)
  • View = une vue (une page ou morceau de page, contrôle, …bref, un truc qui s’affiche à l’écran et se décrit en xaml Tire la langue)
  • ViewModel = un cas d’utilisation associé à une ou plusieurs vues. Un ViewModel est une classe qui présente des propriétés “notifiables” et bindables à la vue. En principe il n’y a pas ou peu de code-behind dans les vues et ainsi tout le code lié au cas d’utilisation exprimé par la vue est concentré dans la classe ViewModel qui lui est associé.

La bible de ce qu’est l’architecture MVVM, avec un petit exemple et le code associé qui va bien : http://msdn.microsoft.com/en-us/magazine/dd419663.aspx

Le choix est moins immédiat sur WP7 qu’en Silverlight classique ou WPF, car beaucoup de contrôles standards “metro styled” n’intègrent pas complètement les bindings (ex : les boutons de la barres de menu). Heureusement, plusieurs variantes de frameworks MVVM ont été portées pour WP7, comme MVVMLight, Caliburn, …

Dans mon cas, je n’utiliserai pas de framework prêt à l’emploi – ce n’est pas le sujet de cet article -, mais je reste sur une architecture de type MVVM, sans être trop stricte, en conservant du code behind, notamment pour la navigation, et la gestion des boutons de la barre de menus.

image

La couche View est composée de vues : MainView qui contient le panorama à 3 items et EditWineView qui permet d’éditer un vin.

La couche ViewModel contient les 2 ViewModels  associés aux vues : MainViewModel et EditWineViewModel.

La couche Model est composée de la classe CaveAVinsModel qui est générée automatiquement lors de l’ajout du service CaveAVins, à partir des metadata, ce qui fait gagner un temps fou. D’autant plus que ces classes proxy implémentent déjà les mécanismes de notification (INotifyPropertyChanged). Du coup, mon MainviewModel va pouvoir directement exposer ces collections à la vue, sans passer par une classe WineViewModel intermédiaire. C’est un premier raccourci par-rapport à du MVVM.strict, mais dans ce cas là, c’est nettement plus productif.

La classe métier UploadPhoto prend en charge la sauvegarde des photos dans l’Azure Blob Storage, permettant ainsi que les couches Viewmodel et View ne soient pas couplées au mécanisme des blobs. On pourrait ainsi facilement stocker les photos ailleurs sans retoucher aux couches de présentation.

La couche DAL correspond à l’accès aux données : aux blobs Azure et à notre service WCF Data Services hébergé lui aussi dans Azure.

Identifier le propriétaire de la cave

Pour pouvoir associer des bouteilles à un propriétaire, plusieurs options s’offrent à nous et en l’occurrence, c’est la plus simple qui a été retenue.

Le téléphone stocke un numéro associé au Live Id du propriétaire (renseigné dès le téléchargement de la première application sur le MarketPlace). Nous pouvons donc utiliser cet identifiant pour rattacher un vin à son propriétaire, sans pour autant lui demander d’informations d’authentification ou de connexion. Et si vous changez de téléphone, vous ne perdez pas pour autant votre cave à vins.

Voici un exemple de code permettant d’accéder au numéro de l’utilisateur:

 public string GetUserAnId()
        {
            string myId = string.Empty;
            string anid = UserExtendedProperties.GetValue("ANID") as string;
            if (anid != null)
            { 
                myId = anid.Substring(2, 32);
            }

            return myId;
        }

Une autre solution serait d’utiliser la fédération d’identité, en utilisant Azure Access Control Services (ACS) et un compte provenant d’un fournisseur d’identité comme Hotmail, Facebook, Google,… Cela évite à l’utilisateur de créer un énième compte dédié à l’application de cave à vins et ça permet au développeur de ne pas réinventer la roue en créant un mécanisme de gestion de comptes qui lui est propre.

Le Toolkit Azure pour Windows Phone permet de simplifier cette opération en fournissant un template tout prêt pour ce genre d’applications. Benjamin a réalisé une vidéo de quelques minutes pour vous montrer comment utiliser le toolkit pour la gestion des authentifications.

Localiser les vins sur un contrôle Bing Map

Une des vues de mon panorama est une carte Bing Map matérialisant les quantités restantes de chacun des vins de ma cave. C’est une bonne aide à l’achat pour garantir une cave variée et cela me permet de vérifier rapidement si je ne manque pas de vin d’Alsace !

Le contrôle Bing Map fait cela très bien en affichant des punaises correspondant à une liste de coordonnées géographiques (latitude, longitude). Dans notre cas, la fiche d’un vin contient une adresse et il faut donc passer par une étape intermédiaire avant de pouvoir afficher la punaise : la transformation de l’adresse en latitude et longitude. Bing Map fournit un service de résolution utilisable avec un compte développeur gratuit. Pour plus d’information sur les conditions d’utilisation : http://www.microsoft.com/maps/product/licensing.aspx.

L’application Windows Phone pourrait réaliser cette résolution dynamiquement lors du lancement de l’application, mais cela signifie que je ferais autant de résolutions que de vins dans ma cave, à chaque chargement des données. Une meilleure idée serait de modifier directement la fiche du vin lors de l’ajout ou de la modification d’un vin. Mais si c’est mon application WP7 qui s’en charge, cela signifie que tout autre client potentiel qui ajouterait des vins dans la cave ne disposerait pas de la latitude et longitude. L’idéal serait que quiconque utilise le service bénéficie de cette résolution d’adresse, même un simple client http.

Nous allons donc déporter cette résolution au niveau du service WCF Data Services à l’aide d’”intercepteurs”'. Voici comment cela se met place dans notre solution, dans la classe CaveAVinsDataService :

 public class CaveAVinsDataService : DataService<CaveAVins.Db.CaveAVinsContext>
    {
        // This method is called only once to initialize service-wide policies. public static void InitializeService(DataServiceConfiguration config)
        {
            config.SetEntitySetAccessRule("Wines", EntitySetRights.All);
            config.SetEntitySetAccessRule("Bottles", EntitySetRights.All);
            config.DataServiceBehavior.MaxProtocolVersion = DataServiceProtocolVersion.V3;
        }


        // Define a change interceptor for the Products entity set. [ChangeInterceptor("Wines")]
        public void OnChangeProducts(Wine updatedWine, UpdateOperations operations)
        {
            CurrentDataSource.ChangeTracker.DetectChanges();
            foreach (var uw in CurrentDataSource.ChangeTracker.Entries<Wine>())
            {
                string oldValue = null;
                if (operations == UpdateOperations.Change)
                {
                    oldValue = CurrentDataSource.ChangeTracker.Entries<Wine>().First().OriginalValues.GetValue<string>("Address");
                }

                if (oldValue != updatedWine.Address)
                {
                    // Update Latitude & longitude var geoloc = MakeGeocodeRequest(updatedWine.Address);
                    if (geoloc != null)
                    {
                        uw.Entity.Latitude = geoloc.Latitude;
                        uw.Entity.Longitude = geoloc.Longitude;
                    }
                }
            }

        }

On ajoute une méthode préfixée d’un custom attribute ChangeInterceptor("Wines")qui permet d’intercepter les requêtes de modification et d’ajout sur la table Wine. Si cette opération concerne un ajout ou qu’une modification de la propriété Address a été détectée, on appelle le service BingMap qui nous renverra une latitude et une longitude à partir de l’adresse. Ces informations sont alors répercutées directement dans l’entité Wine.

C’est tout côté serveur.

Dans l’application, il suffit de binder la collection de vins au contrôle Bing Map.

     <my:Map CredentialsProvider="Your credentials"
             Center="46.642000079154968,2.3379997909069061"
             ZoomLevel="5">
          <my:MapItemsControl ItemsSource="{Binding Model.Bottles}">
               <my:MapItemsControl.ItemTemplate>
                    <DataTemplate>
                         <my:Pushpin Location="{Binding Converter={StaticResource BottlesToLocationConv}}"                                             
                                     Tap="Image_Tap"
                                     Background="DarkRed" FontWeight="Bold">
                                    
                             <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" 
                                        Text="{Binding Count}"/>
    
                             <my:Pushpin.Template>
                                  …
                             </my:Pushpin.Template>
                         </my:Pushpin>
                    </DataTemplate>
              </my:MapItemsControl.ItemTemplate>
          </my:MapItemsControl>
      </my:Map>

Pour cela, j’utilise un converter qui me renvoit une instance de Geocoordinates (latitude, longitude) à partir d’une instance de Wine. Puis je personnalise le PushPin pour qu’il affiche le nombre de bouteilles, c’est à dire la propriété Count de la classe MyWine.

public class BottlesToLocationConverter : IValueConverter
{
    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        MyWine wine = value as MyWine;
        GeoCoordinate result = null;

        if (wine != null)
        {
            result = new GeoCoordinate(wine.WineInfos.Latitude.GetValueOrDefault(), 
                wine.WineInfos.Longitude.GetValueOrDefault());
        }

        return result;
    }

    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        return null;
    }
}

Le tour est joué !

Accéder au service OData

Le SDK Windows Phone 7.1 fournit un client WCF Data Services (vive Linq) ainsi que la possibilité de générer les classes proxy d’un service directement dans Visual Studio : bref, c’est tout pareil qu’en Silverlight classique.

On commencer par ajouter la référence du service de cave à vins : http://stephecaveavins.cloudapp.net/CaveAVinsDataService.svc. Puis on instancie la classe de contexte qui vient d’être générée. Ensuite on construit sa requête de sélection ou on effectue les opérations CUD et on exécute tout cela de manière asynchrone.

Reste la gestion du tombstoning du contexte WCF Data Services qui permettra de retrouver un état cohérent de nos entités : How to persist the state of an OData client for Windows Phone. En effet, depuis Windows Phone 7.1, la persistence des données est facilitée par la nouvelle méthode “Serialize” au niveau du contexte WCF Data Services.

Tous les détails dans le tutoriel associé à paraitre.

Modification de la structure de la base de données

Pour permettre de réaliser ces fonctionnalités, nous avons besoin d’ajouter des informations dans la base de données et donc de modifier sa structure. Rappelons que celle-ci a été créée automatiquement à partir de nos classes POCOs, grâce à Entity Framework Code First.

Il est également possible d’associer des classes POCOs à une base de données existante, et c’est ce qui a été fait dans notre cas, pour ajouter de nouvelles colonnes/propriétés. La structure de la base a été modifiée directement à partir du portail SQL Azure et les nouvelles propriétés de nos POCOS sont associées aux nouvelles colonnes de nos tables grâce à l’API Fluent pour Entity Framework Code First.

Avant :

image

Maintenant:

image

L’entité MyWine est complétée par

  • la date d’achat du vin : pour le tri dans l’application et pouvoir visualiser facilement la dernière bouteille achetée

L’entité Wine est complétée par:

  • Latitude: pour effectuer 1 seule fois la transformation Addresse/Coordonnées pour chaque vin affiché sur contrôle BingMap
  • Longitude : idem latitude
  • BarCode : pour identifier une bouteille à partir de la photo de son code barre – à venir

Avec Fluent, cela se fait en quelques lignes dans le contructeur du contexte, après avoir complété les POCOs par les nouvelles propriétés:

public class CaveAVinsContext : DbContext {
    public DbSet<MyWine> Bottles { get; set; }
    public DbSet<Wine> Wines { get; set; }

    public CaveAVinsContext()
        : base()
    {
        Database.SetInitializer<CaveAVinsContext>(null);
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Entity<MyWine>().Property(p => p.UserId);
        modelBuilder.Entity<MyWine>().Property(p => p.AddedDate);

        modelBuilder.Entity<Wine>().Property(p => p.BarCode);

        modelBuilder.Entity<Wine>().Property(p => p.Latitude);
        modelBuilder.Entity<Wine>().Property(p => p.Longitude);
        base.OnModelCreating(modelBuilder);
    }
}

Prendre la bouteille en photo

C’est très facile grâce à l’API WP7 dédiée et cela se fait en quelques lignes, du genre :

void TakeAPic()
{
    CameraCaptureTask task = new CameraCaptureTask();
    task.Completed += (sender, photoResult) =>
    {
        if (photoResult.TaskResult == TaskResult.OK)
        {
            BitmapImage bmp = new BitmapImage();
            bmp.SetSource(photoResult.ChosenPhoto);
            PhotoSource = bmp;                                
        }
    };
    task.Show();
}

Attention de pas oublier de :

  • faire une rotation de l’image pour la stocker en mode portrait. On pourrait le faire à l’affichage par un RenderTransform mais du coup il faudrait l’effectuer dans chaque application cliente => factorisation métier côté serveur quand on peut !
  • réduire la taille de la photo avant de la stocker dans le blob storage, puisque nous n’utiliserons que des vignettes.

Stocker les photos dans le blob Azure

Une fois la photo prise et la fiche enregistrée, il faut :

  • soumettre la requête de modification au service WCF Data Services de notre Cave à Vins
  • uploader la photo (nommée avec un nouveau Guid pour pouvoir l’identifier de manière unique) dans les blobs Azure.

Une fois de plus, le Toolkit Azure pour Windows Phone va nous aider en facilitant l’utilisation des blobs à partir de notre smartphone grâce à la librairie WindowsPhoneCloud.StorageClient.dll

Pour commencer simplement, vous pouvez partir d’un sample de code issu du toolkit qui ne cible que la partie storage (merci Wade). Les credentials sont placés dans les ressources de l’application et sont donc visibles par tout un chacun, ce qui n’est pas recommandé. Mais dans un premier temps, on s’en contentera pour simplifier.

Grâce à la classe CloudBlobClient fournie dans WindowsPhoneCloud.StorageClient.dll, l’upload de la photo (propriété PhotoStream de type Stream) s’effectue ainsi :

    public void UploadPhoto(Action callback = null)
    {
        this.IsUploading = true;

        this.blobClient.Upload(
            this.BlobName,
            this.PhotoStream,
            r => this.dispatcher.BeginInvoke(
                    () =>
                    {
                        this.IsUploading = false;

                        if (r.Exception == null)
                        {
                            MessageBox.Show(
                                string.Format(CultureInfo.InvariantCulture, 
                                "Image file {0} successfully uploaded!", this.BlobName),
                                "Upload Photo Result",
                                MessageBoxButton.OK);

                            if (callback != null)
                            {
                                callback.Invoke();
                            }
                        }
                        else
                        {
                            MessageBox.Show(
                                string.Format(
                                    CultureInfo.InvariantCulture,
                                    "Error: {0}",
                                    r.Exception.Message),
                                "Upload Photo Result",
                                MessageBoxButton.OK);
                        }
                    }));
    }
}

AmpouleLa version complète du toolkit vous permet de réaliser la même opération en version sécurisée, en ajoutant un service qui fait office de proxy d’authentification. C’est lui qui contiendra les credentials plutôt que votre application WP7 !

Plus de contrôles Silverlight pour Windows Phone

Le toolkit le plus utilisé est sans conteste le Silverlight Toolkit pour WP disponible sur codeplex.

J’ai utilisé :

  • le LongListSelector permettant d’afficher une liste découpée selon un group by
  • une animation sur les listes :le TiltEffect, qui permet d’avoir un retour visuel sur sélection d’un élément
  • le menu contextuel : indispensable pour une bonne expérience utilisateur et éviter d’avoir trop de boutons partout

J’avais également besoin d’un contrôle Rating et j’ai réutilisé celui-ci (sur codeplex) qui n’est pas prévu pour WP7, mais qui fonctionne bien du moment que l’on désactive le contrôle pendant une gesture.
Un Rating control est également disponible dans le
Silverlight Toolkit, mais je ne l’ai pas essayé dans le cadre d’une application WP.

Le Coding4fun Tools est également très sympa (surtout qu’un de mes propres contrôles y a été intégré Clignement d'œil). Vous y trouverez un menu about, un colorpicker, etc, …

Faites votre marché Rire

Les tuiles dynamiques de l’écran d’accueil

Avec WP7.1, il est ultra simple de rendre vos tuiles dynamiques.

Un simple appel de méthode permet de mettre à jour une ou les deux faces de la tuile associée à votre application. Pour la Cave à Vins, la 1ère face affiche le nom de l’application, un petit dessin ainsi que le nombre total de bouteilles de votre cave. La 2ème face affiche le nom de la dernière bouteille achetée. Et c’est aussi simple que :

public void CreateApplicationTile(IEnumerable<MyWine> bottles = null)
{            
    var appTile = ShellTile.ActiveTiles.First();

    if (appTile != null)
    {
        var standardTile = new StandardTileData {
            Title = "Cave A Vins",
            BackgroundImage = new Uri("/icons/tileBackground2.png", UriKind.Relative),                    
            BackTitle = "Dernier achat",
        };

        if (bottles != null)
        {
            standardTile.Count = bottles.Sum(b => b.Count);
            if (bottles.Any())
            {
                standardTile.BackContent = 
                    bottles.OrderByDescending(w => w.AddedDate)
                            .First()
                            .WineInfos.Name;
            }
        }

        appTile.Update(standardTile);
    }
}  

Côté pile, un MAGNIFIQUE petit dessin ainsi que le nombre total de bouteilles de votre cave :

image

Côté face : la dernière bouteille achetée

image

 

 

Voilà, tous les points sensibles de l’application sont maintenant exposés et détaillés. Je ne ferai pas de tutoriel pas à pas pour chacun des points, mais si vous rencontrez des difficultés avec l’un d’entre eux, je peux tout à fait développer le sujet dans un autre article.

Reste à peaufiner l’application, à la localiser et à gérer le tombstoning correctement (notamment en ce qui concerne WCF Data Services).

Dans le prochain article, nous verrons comment retrouver une bouteille à partir de la photo de son code barre et profiter ainsi des bouteilles déjà saisies par la communauté des utilisateur de l’application CaveAVins.

Leave a Comment
  • Please add 3 and 3 and type the answer here:
  • Post
  • Trop top! Félicitations! Par contre, que viens faire un Mojito dans ta cave à vin ?!?

    David

  • Il faut des mojitos partout, c'est comme ça...un peu comme les rhums arrangés...

    Mais spécialement pour toi, on peut aussi y mettre des bouteilles de coca !

  • Merci pour la maj de l'app !

    PS : Je pense que par LongListPicker tu voulais dire LongListSelector !

  • Julien : mais oui c'est effectivement le LongListSelector, je corrige ça de ce pas !

    Merci beaucoup

  • Cela aurais été sympa d'avoir les sources de l'appli WP.

    Par contre les 1er chapitres sont trés clairs et bien commentés. mais la fatigue s'est faite sentier !!!

  • Bonjour Claude,

    Vous avez raison, difficile d'adresser tous les sujets de manière détaillée dans un post.

    En ce sens, un parcours exclusif d'une journée est réservé au développement de la solution applicative CaveAVins aux TechDays 2012 : www.microsoft.com/.../parcours.aspx

    Sur 5 sessions, nous détaillerons les choix technologiques et les implémentations des différentes parties de l'application.

    L'application Windows Phone sera d'ailleurs largement améliorée et complétée pour l'occasion !

    A bientôt

Page 1 of 1 (6 items)
Page 1 of 4 (87 items) 1234