Voici le deuxième article de la série :

- Partager du code entre plate-formes grâce à la Portable Library (1/4)

- Partager du code entre plate-formes : l’application Windows 8 Metro (2/4)

- Partager du code entre plate-formes : l’application Windows Phone (3/4)

- Partager du code entre plate-formes : l’application Silverlight 5 (4/4)

 

Retrouvez le code source des démos:

 

L’application Windows 8 Metro

Je commence par créer un nouveau projet de type Portable Class Library sous Visual Studio 11. Celui-ci sera commun et utilisé par mes 3 applications à venir.

image

Le projet de type Portable Class Library

Dans les propriétés du projet, je sélectionne les 3 plate-formes dans ma Portable Lib.

image

Je prends soin de sélectionner Windows Phone 7.5 sinon je n’aurais pas accès à l’assembly System.Windows.dll qui me permet d’utiliser tout ce qui est associé au INotifyxxx et les éléments observables. En effet, j’en aurai besoin pour la couche ViewModel.

On me prévient gentiment que d’autres plateformes seront aussi compatibles avec celles que j’ai sélectionnées, à savoir .Net 4.5 et Silverlight 4 et >.

Dans mon projet, je crée les répertoires correspondant à chacune des couches supportées par la portable lib: DAL, Model, ViewModel

image_thumb8

Dans cette application simpliste, je n’aurai qu’un type de données à gérer : la classe Wine que je place dans la couche Model :

namespace CaveAVins.Portable.Model
{
    public partial class Wine
    {
        public string Name { get; set; }
        public int Year { get; set; }
        public string PictureUrl { get; set; }
    }
}

La couche d’accès aux données (DAL)

Pour récupérer une collection de vins à partir de mon service OData, je vais procéder de manière différente selon que je travaille sur Win8 ou WP.

C’est WCF Data Services (une implémentation pour un client OData) qui va permettre d’ajouter une référence à mon service OData dans Windows Phone (tout comme on le ferait dans .Net ou Silverlight). Dans ce cas, les classes proxy (comprenant la classe Wine) sont générées automatiquement.

Avec Visual Studio 11, il n’y a pas encore de client OData disponible et nous devrons donc parser manuellement le flux Atom renvoyé par le service pour le mapper dans une collection d’éléments de type Wine.

J’aurai donc plusieurs implémentations de mon modèle matérialisé par la classe Wine : l’une manuelle comme mentionné plus haut, dans Windows 8 et l’autre générée automatiquement par Visual Studio.

Je rajoute donc une interface qui définira la signature de ce qu’est un vin dans notre application : un nom, une année de production et un lien vers la photo de l’étiquette de la bouteille.

namespace CaveAVins.Portable.DAL
{
    public interface IWine
    {
        string Name { get; set; }
        int Year { get; set; }
        string PictureUrl { get; set; }
    }
}
Les classes Wine implémenteront cette interface
namespace CaveAVins.Portable.Model
{
    // Implémentation par défaut
    public partial class Wine : IWine
    {
        public string Name { get; set; }
        public int Year { get; set; }
        public string PictureUrl { get; set; }
    }
}

J’ajoute également l’interface du data provider qui me permettra de manipuler les données :

namespace CaveAVins.Portable.DAL
{
    public interface ICaveAVinsDataProvider
    {
        event Action<IEnumerable<IWine>> WinesLoaded;
        void LoadDataAsync();

        //void UpdateItem(Wine item);
        //void AddItem(Wine item);
        //void RemoveItem(Wine item);
    }
}

Je commence par m’occuper du chargement asynchrone des données, dont la fin est signalée par l’évènement WinesLoaded. Remarquez que c’est un simple event, j’ai choisi de ne faire intervenir les éléments notifiables qu’à partir de la couche ViewModel.

 

La couche ViewModel

La couche ViewModel va permettre à la couche View d’interagir avec le modèle et la DAL par les mécanismes de binding, en utilisant des éléments “notifiables”.

Nous créons une classe de base ViewModelBase qui implémente INotifyPropertyChanged pour en faire bénéficier automatiquement tous les ViewModels.

    public abstract class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propName));
            }
        }
    }

Nous créons ensuite un ViewModel associé à un vin : WineVM

namespace CaveAVins.Portable.ViewModel
{
    public partial class WineVM : ViewModelBase
    {
        
        public WineVM(IWine wine)
        {
            if (!string.IsNullOrEmpty(wine.PictureUrl))
            {
                _pictureUrl = new Uri(wine.PictureUrl);
            }
            _name = wine.Name;
            _year = wine.Year;
        }

        Uri _pictureUrl = null;
        public Uri PictureUrl
        {
            get
            {
                return _pictureUrl;
            }           
        }

        private string _name;
        public string Name
        {
            get { return _name; }
            set 
            {
                if (_name != value)
                {
                    _name = value;
                    OnPropertyChanged("Name");
                }
            }
        }

        private int _year;
        public int Year
        {
            get { return _year; }
            set
            {
                if (_year != value)
                {
                    _year = value;
                    OnPropertyChanged("Year");
                }
            }
        }        
    }
}

Ainsi qu’un ViewModel permettant de manipuler la collection de vins renvoyée par la DAL:

namespace CaveAVins.Portable.ViewModel
{
    public partial class CaveAVinsVM : ViewModelBase
    {
        public CaveAVinsVM(IEnumerable<IWine> wines)
        {
            _wines = new ObservableCollection<WineVM>(wines.Select(w => new WineVM(w)));
        }

        public CaveAVinsVM(IEnumerable<WineVM> wines)
        {
            _wines = new ObservableCollection<WineVM>(wines);
        }

        ObservableCollection<WineVM> _wines;
        public ObservableCollection<WineVM> Wines
        {
            get
            {
                return _wines;
            }
            private set
            {
                _wines = value;
            }
        }

    }
}

Voici la structure de notre projet de type Portable Class Library, qui sera commun aux deux solutions Windows 8 Metro et Windows Phone:

image_thumb9[1]

 

Le projet Windows 8 Metro

Créons un nouveau projet de type Metro: vous pouvez choisir un template qui inclut une navigation et une présentation de collection.

image_thumb121

Moi j’ai choisi une application vide pour me focaliser sur le sujet de cet article:

image_thumb15

J’ajoute une référence au projet CaveAVins.Portable pour pouvoir l’utiliser dans mon application.

 

La couche d’accès aux données (DAL)

Je crée un répertoire DAL ainsi qu’un fichier CaveAVinsDataProvider.cs et sa classe CaveAVinsDataProvider qui implémente IDaveAVinsDataProvider et permet ainsi l’accès au service OData et à la collection de vins.

image_thumb4[1]

A ce jour, il n’existe pas de version de WCF Data Services pour Windows 8 et il faudra donc accéder à mon service OData et parser le flux de données manuellement.

Voici à quoi il ressemble:

image

J’utilise le SyndicationClient qui me permet de récupérer simplement un flux Atom.

Je le parse manuellement pour n’en conserver que les propriétés Name, Year et PictureUrl. J’instancie pour chaque vin, la classe Wine que j’ai créée dans la Portable Lib, puis je déclenche l’évènement WinesLoaded pour signaler que le chargement des données est terminé.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CaveAVins.Portable.DAL;
using CaveAVins.Portable.Model;
using Windows.Web.Syndication;

namespace CaveAVins.Win8Metro.DAL
{
    public class CaveAVinsDataProvider : ICaveAVinsDataProvider
    {        
        public event Action<IEnumerable<IWine>> WinesLoaded;

        public async void LoadDataAsync()
        {
            SyndicationClient client = new SyndicationClient();
            Uri feedUri = new Uri("http://caveavins.cloudapp.net/CaveAVinsDataService.svc/Wines");
            IList<Wine> wines = new List<Wine>();

            SyndicationFeed feed = await client.RetrieveFeedAsync(feedUri);


            foreach (SyndicationItem item in feed.Items)
            {
                try
                {
                    Wine newWine = new Wine();
                    var attributes = item.Content.Xml.FirstChild.ChildNodes.AsEnumerable();
                    Func<string, string> parse = (an) =>
                    {
                        var attr = attributes.FirstOrDefault(a => a.LocalName.ToString() == an);
                        if ((attr != null) && (attr.HasChildNodes()))
                        {
                            return attr.FirstChild.NodeValue.ToString();
                        }
                        return string.Empty;
                    };

                    newWine.Name = parse("Name");
                    newWine.Year = int.Parse(parse("Year"));
                    newWine.PictureUrl = parse("PictureUrl");
                    wines.Add(newWine);
                }
                catch
                {
                    // On ajoute que les items valides
                }
            }
            WinesLoaded(wines);
        }
    }
}

La couche ViewModel

Elle est déjà créée dans la portable lib. Il reste à complémenter celle-ci avec les certaines spécificités de la plate-forme. Dans notre cas, cela revient à compléter la classe WineVM.

image_thumb9

Il faut créer une BitmapImage à partir d’une Uri, pour pouvoir afficher l’image de la bouteille. Je crée donc une classe dérivée de WineVM qui comporte une propriété supplémentaire qui sera bindée à une image dans la vue.

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CaveAVins.Portable.DAL;
using CaveAVins.Portable.ViewModel;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;

namespace CaveAVins.Win8Metro.ViewModel
{
    public class Win8WineVM : WineVM
    {
        public Win8WineVM(IWine wine) : base(wine)
        {
        }

        ImageSource _picture;
        public ImageSource Picture
        {
            get
            {
                if (_picture == null)
                {
                    _picture = new BitmapImage(this.PictureUrl);
                }
                return _picture;
            }
        }
    }
}

 

La couche Vue

La couche Vue est belle et bien spécifique à la plate-forme et n’apparait donc pas dans la Portable Lib. Nous pourrions créer un répertoire View et y glisser les vues mais dans notre application, nous n’avons qu’une seule page qui est déjà présente dans le template d’application que nous avons choisi d’utiliser. C’est le fichier BlankPage.xaml.

image_thumb11

Voici le code que j’utilise pour afficher mes éléments sous la forme d’une grille, avec un scrolling horizontal:

<Page
    x:Class="CaveAVins.Win8Metro.BlankPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CaveAVins.Win8Metro"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d">

    <Grid Background="{StaticResource ApplicationPageBackgroundBrush}">
        <Grid.RowDefinitions>
            <RowDefinition Height="140"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
            
        <TextBlock x:Name="pageTitle" Text="Portable Library Demo" Grid.Column="1" Style="{StaticResource PageHeaderTextStyle}"
                    Margin="116,0,40,46"/>

        <!-- Horizontal scrolling grid used in most view states -->
        <ScrollViewer
            x:Name="itemGridScrollViewer"
            AutomationProperties.AutomationId="ItemGridScrollViewer"
            Grid.Row="1"
            Margin="0,-3,0,0"
            Style="{StaticResource HorizontalScrollViewerStyle}">

            <GridView Grid.Row="1" Grid.Column="1" ItemsSource="{Binding Wines}"
                          Margin="116,0,40,46">
                <GridView.ItemTemplate>
                    <DataTemplate>
                        <Grid HorizontalAlignment="Left" Width="250" Height="250">
                            <Image Source="{Binding Picture}" Stretch="Uniform"/>
                            <StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundBrush}">
                                <TextBlock Text="{Binding Name}" Foreground="{StaticResource ListViewItemOverlayTextBrush}" Style="{StaticResource TitleTextStyle}" Height="60" Margin="15,0,15,0"/>
                                <TextBlock Text="{Binding Year}" Foreground="{StaticResource ListViewItemOverlaySecondaryTextBrush}" Style="{StaticResource CaptionTextStyle}" TextWrapping="NoWrap" Margin="15,0,15,10"/>
                            </StackPanel>
                        </Grid>
                    </DataTemplate>
                </GridView.ItemTemplate>

            </GridView>
        </ScrollViewer>
    </Grid>
</Page>

 

Remarquez que chaque élément de ma grille sera bindé aux propriétés Name, Year et Picture de mes vins projetés dans une collection de type WineVM.

<Grid HorizontalAlignment="Left" Width="250" Height="250">
   <Image Source="{Binding Picture}" Stretch="Uniform"/>
   <StackPanel VerticalAlignment="Bottom" Background="{StaticResource ListViewItemOverlayBackgroundBrush}">
      <TextBlock Text="{Binding Name}" Foreground="{StaticResource ListViewItemOverlayTextBrush}" Style="{StaticResource TitleTextStyle}" Height="60" Margin="15,0,15,0"/>
      <TextBlock Text="{Binding Year}" Foreground="{StaticResource ListViewItemOverlaySecondaryTextBrush}" Style="{StaticResource CaptionTextStyle}" TextWrapping="NoWrap" Margin="15,0,15,10"/>
   </StackPanel>
</Grid>


La propriété ItemsSource de ma GridView sera bindée à la propriété Wines du viewModel CaveAVinsVM.

<GridView Grid.Row="1" Grid.Column="1" ItemsSource="{Binding Wines}"

La couche vue est prête, il faut maintenant charger les données !

 

Initalisation de l’application

Cela se passe dans le fichier app.xaml.cs, dans l’évènement OnLaunched qui correspond au lancement de l’application. Nous ajoutons le code qui nous permet de charger les données:

            CaveAVinsDataProvider dp = new CaveAVinsDataProvider();
            dp.WinesLoaded += dp_WinesLoaded;
            dp.LoadDataAsync();

Ainsi que l’association des données chargées avec la vue sur déclenchement de l’évènement :

 

        void dp_WinesLoaded(IEnumerable<IWine> wines)
        {
            CaveAVinsVM vm = new CaveAVinsVM(wines.Select(w => new Win8WineVM(w)));
            (Window.Current.Content as Frame).DataContext = vm;
        }

Pour chaque vin, on instancie un Win8WineVM et la collection résultante est passée en paramètre dans le constructeur de la classe CaveAVinsVM. Celle-ci est affectée au DataContext de notre page principale pour permettre aux bindings créés dans la vue de fonctionner.

 

Voici le code complet correspondant:

        /// <summary>
        /// Invoked when the application is launched normally by the end user.  Other entry points
        /// will be used when the application is launched to open a specific file, to display
        /// search results, and so forth.
        /// </summary>
        /// <param name="args">Details about the launch request and process.</param>
        protected override void OnLaunched(LaunchActivatedEventArgs args)
        {
            if (args.PreviousExecutionState == ApplicationExecutionState.Terminated)
            {
                //TODO: Load state from previously suspended application
            }

            CaveAVinsDataProvider dp = new CaveAVinsDataProvider();
            dp.WinesLoaded += dp_WinesLoaded;
            dp.LoadDataAsync();

            // Create a Frame to act navigation context and navigate to the first page
            var rootFrame = new Frame();
            rootFrame.Navigate(typeof(BlankPage));

            // Place the frame in the current Window and ensure that it is active
            Window.Current.Content = rootFrame;
            Window.Current.Activate();
        }

        void dp_WinesLoaded(IEnumerable<IWine> wines)
        {
            CaveAVinsVM vm = new CaveAVinsVM(wines.Select(w => new Win8WineVM(w)));
            (Window.Current.Content as Frame).DataContext = vm;
        }

Go !

Nous pouvons démarre l’application qui affichera les bouteilles au bout de quelques secondes :

image

 

La suite : Partager du code entre plate-formes : l’application Windows Phone (3/4)