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.
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:
Articles:
Tutoriels:
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 ), 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 (cf vidéo).
1 2 3
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 5
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
Rappel de l’architecture existante après la migration dans Azure:
Et l’architecture qui sera mise en place dans le contexte de cet article :
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 :
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…
En 3 mots, si vous ne savez pas ce qu’est MVVM (Model – View – ViewModel) : c’est une architecture 3-tiers avec
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.
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.
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.
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é !
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.
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 :
Maintenant:
L’entité MyWine est complétée par
L’entité Wine est complétée par:
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); } }
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 :
Une fois la photo prise et la fiche enregistrée, il faut :
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); } })); } }
La 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 !
Le toolkit le plus utilisé est sans conteste le Silverlight Toolkit pour WP disponible sur codeplex.
J’ai utilisé :
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é ). Vous y trouverez un menu about, un colorpicker, etc, …
Faites votre marché
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 :
Côté face : la dernière bouteille achetée
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.
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