Introduction
Comment créer sa première application Modern UI, avec C# XAML?
Les applications Modern UI connaissent un véritable essor dans le monde du développement. Notamment, avec l'arrivée imminente du nouvel OS Windows 8 de chez Microsoft, ainsi qu'une multitude d’appareils et de supports tactiles, sur lesquels nous allons justement pouvoir utiliser ces applications.
Sur la toile, vous trouverez un bon nombre de tutoriels pour commencer à développer une application pour Windows 8. Je pense au blog de Loïc Rebours, que je trouve complet.
Dans cette article, j’insisterai sur la manière dont nous pouvons développer une application, en utilisant le pattern MVVM, au travers d’un exemple concret. Puis, dans un second article, je vous présenterai une manière de déployer une application sans passer par le Windows Store. Quels genres de contraintes et de restrictions allons-nous rencontrer ?
Prérequis
Avant de rentrer dans le vif du sujet, nous allons commencer par les prérequis.
Pour concevoir des applications Windows Store, il faut :
- être doté d’une machine équipée de l’OS Windows 8, et d’un compte Microsoft (nécessaire et pour faciliter les déploiements)
- avoir une licence Développeur Microsoft (gratuite) ; cette licence est proposée lors de la première compilation d’un projet Windows Store
- être équipé de l’IDE Visual Studio 2012, avec le Framework 4.5 (à minima)
Contexte
Notre application consiste à afficher le profil d’un utilisateur depuis un back-end existant :

Dans le monde de l’entreprise, on a souvent besoin de récupérer les informations depuis une base de données ou un Active Directory, par exemple.
Pour rappel, une application Modern UI évolue dans un environnement dit « Sandbox ». C’est-à-dire qu’une telle application ne peut communiquer directement avec une application tierce.
Les applications Windows Store sont soumises à des restrictions. Et, par conséquent, elles ne peuvent interagir directement avec une base de données ou un annuaire LDAP. Pour cela, la solution du Web service va nous permettre de répondre à notre besoin.
Le but de cet article n’est pas d’apprendre comment créer un Web Service avec WCF. Je pourrais très bien créer une « data source », pour simplifier les développements. Mais, je pense qu’on passerait à côté de certaines informations.
Préliminaires
Nous sommes partis du principe que la partie Middleware et Back-end existaient déjà. En effet, cette partie a été précédemment développée pour les besoins d’une application interne. Il s’agit du trombinoscope de Neos-SDI développé à partir d’un des templates de Windows Store Apps (« Je dis ça, je dis rien ! »).
Voici, la solution finale avec la logique MVVM. Cette solution sera disponible en interne.

Qu’est-ce que le MVVM ?
Le MVVM est un « pattern » de conception architecturale qui permet une séparation indépendante entre les couches :
- métier : le Model
- de présentation : la View
- et, on va dire le contrôleur (par analogie au design pattern MVC) : la ViewModel ; cette dernière couche va jouer le rôle du liant entre la couche métier et la couche de présentation.
Pour bien commencer…
Avant de mettre en place l’architecture MVVM dans notre projet, souvenez-vous qu’une application Windows Store est une application Sandbox (restrictive). Une application est soumise à des contraintes déclaratives.
Pour pouvoir communiquer avec l’extérieur, ou consommer un Web Service, il va donc falloir déclarer cette fonctionnalité dans notre application. Cela se passe dans notre fichier de manifeste, Package.appxmanifest (situé à la racine de notre projet) :
NB : Pour pouvoir faire évoluer notre application et gérer les ressources de déploiement, il faut donc passer par ce fichier de manifeste.
Rentrons dans le vif du sujet, il va y avoir du code…
Dans la famille MVVM, je demande le…
...Model :

Le Model représente les objets métier de notre back-end (cf. Contexte). Ici, notre source de données provient de l’annuaire LDAP de notre société. Pour notre besoin, la classe Neostee représente un utilisateur commun à Neos-SDI. Cette classe est visible dans notre projet, si et seulement si nous intégrons la référence au Web Service (Middleware).
NB : Cette classe hérite d’une classe de base qui implémente l’interface INotifyPropertyChanged. Nous verrons un exemple de code, par la suite.
Dans la famille ViewModel, je demande le…
…ViewModel :

Le ViewModel représente la liaison entre le Model et la View. L’idée est d’implémenter aussi l’interface INotifyPropertyChanged (comme dans le Model) afin d’utiliser le mécanisme de « Binding » entre la vue et l’objet métier. Nous verrons comment faire le lien entre nos objets métier et la vue.
Pour détailler la classe ViewModelBase, comme pour la classe ModelBase, voici une implémentation de l’interface INotifyPropertyChanged :
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
namespace AppLoadProfil.ViewModel
{
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public void OnChangedProperty(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public void OnChangedProperty<T>(Expression<Func<T>> expression)
{
MemberExpression body = expression.Body as MemberExpression;
if (body == null)
{
throw new ArgumentException("erreur dans l'expression");
}
string propertyName = body.Member.Name;
if (!string.IsNullOrEmpty(propertyName))
{
OnChangedProperty(propertyName);
}
}
}
}
La classe de base implémente l’évènement : event PropertyChangedEventHandler PropertyChanged;
Les 2 surcharges de méthodes OnChangedProperty permettent de détecter le changement de valeur d’une propriété rattachée à notre ViewModel (ou notre Model).
Comment allons-nous utiliser cette classe de base ?
La classe ProfilViewModel hérite de cette classe de base
using AppLoadProfil.BusinessLayer.Impl;
using AppLoadProfil.BusinessLayer.Interfaces;
using AppLoadProfil.Common;
using AppLoadProfil.LdapService;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;
using Windows.Storage.Streams;
using Windows.UI.Popups;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Media.Imaging;
namespace AppLoadProfil.ViewModel
{
public class ProfilViewModel : ViewModelBase
{
#region properties
private Neostee _neostee = null;
public Neostee TheNeostee
{
get { return _neostee; }
set
{
_neostee = value;
//base.OnChangedProperty("TheNeostee");
base.OnChangedProperty<Neostee>(() => this.TheNeostee);
}
}
private string _commonName;
public string CommonName
{
get { return _commonName; }
set
{
_commonName = value;
base.OnChangedProperty("CommonName");
}
}
private ObservableCollection<Neostee> _Neostees = null;
public ObservableCollection<Neostee> Neostees
{
get { return _Neostees; }
set
{
_Neostees = value;
base.OnChangedProperty("Neostees");
}
}
#endregion
#region ctors
public ProfilViewModel()
{
_Neostees = new ObservableCollection<Neostee>();
LoadAsync();
}
#endregion
#region private methods
/// <summary>
/// Méthode asynchrone qui permet le chargement du profil du neostee
/// </summary>
private async void LoadAsync()
{
string firstName = await Windows.System.UserProfile.UserInformation.GetFirstNameAsync();
string name = await Windows.System.UserProfile.UserInformation.GetLastNameAsync();
var commonName = "Amadou Camara";
byte[] flux = null;
StorageFolder folder = null;
try
{
folder = Windows.ApplicationModel.Package.Current.InstalledLocation;
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
StorageFile file = await folder.GetFileAsync(@"Assets\SmallLogo.png");
if (file != null)
{
flux = await GetByteFromFile(file);
}
var defaultNeostee = new Neostee() { CommonName = "Inconnu", DepartmentName = string.Empty, Description = string.Empty, Email = string.Empty, Login = string.Empty, Manager = string.Empty, ThumbnailPhoto = flux };
if (!string.IsNullOrEmpty(firstName) && !string.IsNullOrEmpty(name))
{
commonName = string.Format("{0} {1}", firstName, name);
}
else
{
TheNeostee = defaultNeostee;
_Neostees.Add(defaultNeostee);
return;
}
INeosteeRepository _repository = new NeosteeRepository();
try
{
var result = await _repository.Get(commonName);
if (result != null)
{
foreach (var item in result)
{
_Neostees.Add(item);
if (TheNeostee == null)//le 1er neostee qui correspond au critère de recherche
{
TheNeostee = item;
CommonName = _neostee.CommonName;
break;
}
}
}
}
catch (Exception ex)
{
throw new Exception("erreur dans la methode LoadAsync", ex);
}
}
private async Task<byte[]> GetByteFromFile(StorageFile storageFile)
{
var stream = await storageFile.OpenReadAsync();
using (var dataReader = new DataReader(stream))
{
var bytes = new byte[stream.Size];
await dataReader.LoadAsync((uint)stream.Size);
dataReader.ReadBytes(bytes);
return bytes;
}
}
#endregion
}
}
Dans cette classe, nous allons définir l’ensemble des propriétés que nous souhaitons afficher dans la vue (la couche de présentation).
Il est même possible de définir des actions telles que les ICommand, qui représentent une action (une interaction), lors d’un évènement provoqué par utilisateur, tel que le clic sur un bouton. Mais ceci est une autre histoire.
Dans notre cas, j’ai défini dans le ViewModel une méthode private async void LoadAsync()
Cette méthode permet de faire appel asynchrone au Web service.
Mais attention, je ne fais pas appel directement à ce Web service. Je passe par une couche métier :

Cette classe est responsable de la pertinence des données et va nous permettre de renseigner les propriétés à afficher dans la Vue. Cette classe définit une méthode Get qui va renvoyer les informations sur un employé de Neos-SDI en fonction de son Common Name.
Dans la propriété TheNeostee, j’ai volontairement commenté la première surcharge de la méthode de base OnChangedProperty, au profit de la deuxième surcharge :
private Neostee _neostee = null;
public Neostee TheNeostee
{
get { return _neostee; }
set
{
_neostee = value;
//base.OnChangedProperty("TheNeostee");
base.OnChangedProperty<Neostee>(() => this.TheNeostee);
}
}
Cette surcharge prend en paramètre de type de la forme Lambda Expression. Cette méthode comporte ainsi moins de risques d’erreur de saisie, comparé à la méthode de base qui prend une chaîne de caractères en paramètres.
Dans la famille ViewModel, je demande la…
…View :
Pour afficher les données, on se base sur le concept lié au pattern MVVM : le databinding.
Ce mécanisme permet de lier une donnée entre notre couche de présentation et le ViewModel. Pour cela, il faut définir le contexte du ViewModel au niveau de la Vue et déclarer notre objet métier à l’aide du mot clé Binding.
Voici une vue partielle de la View, avec la déclaration du contexte pour le ViewModel :

Voici une vue partielle de la page qui montre comment la « binder » avec notre objet métier TheNeostee.

Et la partie que je trouve intéressante dans cette vue :

Comment afficher une image dans notre vue ? Sachant que la propriété TheNeostee.ThumbnailPhoto est homogène à un flux binaire.
La solution est de passer par la classe ImageConverter :

La classe ImageConverter implémente l’interface IValueConverter qui définit 2 méthodes Converter et ConverterBack. Seule la première méthode est implémentée pour justement convertir notre source de données (qui provient de l’annuaire) en objet de type WriteableBitmap qui permettra de charger l’image dans notre application Windows Store.
using AppLoadProfil.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Data;
using AppLoadProfil.StreamExtensions;
using Windows.UI.Xaml.Media.Imaging;
namespace AppLoadProfil.Converters
{
public class ImageConverter : IValueConverter
{
WriteableBitmap _imageSource = null;
private readonly int _pixelWidth = 1024;
private readonly int _pixelHeight = 1024;
/// <summary>
/// to load an image from byte[] stream
/// </summary>
/// <param name="value">value which as byte[]</param>
private async void LoadImage(object value)
{
var flux = value as byte[];
if (flux != null)
{
_imageSource = new WriteableBitmap(_pixelWidth, _pixelHeight);
var stream = flux.ToAccessStream();
await _imageSource.SetSourceAsync(stream);
}
}
public object Convert(object value, Type targetType, object parameter, string language)
{
LoadImage(value);
return _imageSource;
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
throw new NotImplementedException();
}
}
}
La méthode ToAccessStream est une méthode d’extension qui va renvoyer un objet de type IRandomAccessStream : interface de gestion des flux pour les applications Modern UI.
using AppLoadProfil.Common;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage.Streams;
namespace AppLoadProfil.StreamExtensions
{
public static class MemoryStreamExtensions
{
public static IRandomAccessStream ToAccessStream(this byte[] flux)
{
return new ImageStream(flux);
}
}
}
Cette méthode d’extension renvoie une instance de la classe ImageStream qui prend en charge une implémentation de l’interface IRandomAccessStream :
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage.Streams;
namespace AppLoadProfil.Common
{
public class ImageStream : IRandomAccessStream
{
private MemoryStream _memoryStream = null;
public ImageStream()
{
}
public ImageStream(byte[] inputStream)
{
if (inputStream != null)
{
_memoryStream = new MemoryStream(inputStream, 0, inputStream.Count(), true);
}
}
public bool CanRead
{
get
{
if (_memoryStream != null)
{
return _memoryStream.CanRead;
}
else
{
return false;
}
}
}
public bool CanWrite
{
get
{
if (_memoryStream != null)
{
return _memoryStream.CanWrite;
}
else
{
return false;
}
}
}
public IRandomAccessStream CloneStream()
{
return this.MemberwiseClone() as IRandomAccessStream;
}
public IInputStream GetInputStreamAt(ulong position)
{
_memoryStream.Position = (long)position;
return _memoryStream.AsInputStream();
}
public IOutputStream GetOutputStreamAt(ulong position)
{
_memoryStream.Position = (long)position;
return _memoryStream.AsOutputStream();
}
public ulong Position
{
get { return (ulong)_memoryStream.Position; }
}
public void Seek(ulong position)
{
_memoryStream.Seek((long)position, SeekOrigin.Begin);
}
public ulong Size
{
get
{
return (ulong)_memoryStream.Length;
}
set
{
_memoryStream.SetLength((long)value);
}
}
public void Dispose()
{
_memoryStream.Dispose();
}
public Windows.Foundation.IAsyncOperationWithProgress<IBuffer, uint> ReadAsync(IBuffer buffer, uint count, InputStreamOptions options)
{
var inputStream = this.GetInputStreamAt(0);
return inputStream.ReadAsync(buffer, count, options);
}
public Windows.Foundation.IAsyncOperation<bool> FlushAsync()
{
var outputStream = this.GetOutputStreamAt(0);
return outputStream.FlushAsync();
}
public Windows.Foundation.IAsyncOperationWithProgress<uint, uint> WriteAsync(IBuffer buffer)
{
var outputStream = this.GetOutputStreamAt(0);
return outputStream.WriteAsync(buffer);
}
}
}
Et voilà, nous savons comment utiliser le pattern MVVM dans un projet Windows Store. Ce pattern a pour avantage de séparer les concernes et permet le travail avec un designer pour l’interface utilisateur.
Pour aller plus loin, comment naviguer avec le pattern MVVM ? Voici une première réponse proposé par Microsoft Services.