Mvvm pour Wpf

Transcription

Mvvm pour Wpf
1
Mvvm pour Wpf
J. ROMAGNY
I.
MVVM ..................................................................................................................................................... 5
1.
VUES ............................................................................................................................................................ 5
a. « View First » ......................................................................................................................................... 5
b. Fenêtre principale de l’application (MainWindow/ Shell) ..................................................................... 6
Régions ........................................................................................................................................................................ 6
c.
d.
e.
Vues ....................................................................................................................................................... 6
Ressources ............................................................................................................................................. 6
Source de données ................................................................................................................................. 7
DataContext ................................................................................................................................................................. 7
ViewModelLocator....................................................................................................................................................... 7
Sans conteneur ....................................................................................................................................................... 7
Avec conteneur IoC ................................................................................................................................................. 8
ViewModelLocator « générique » ........................................................................................................................... 9
Bootsrapper … Prévention .................................................................................................................................... 10
f.
g.
h.
Converters ............................................................................................................................................ 10
TargetNullValue ................................................................................................................................... 11
Animations ........................................................................................................................................... 11
Transitions ................................................................................................................................................................. 11
Contrôle de chargement ............................................................................................................................................ 12
VisualStates................................................................................................................................................................ 13
2.
a.
b.
c.
d.
e.
VIEWMODEL................................................................................................................................................ 14
Classes de base de ViewModel ............................................................................................................ 14
ViewModel Liste et détails ................................................................................................................... 15
Filtrer, trier, grouper (CollectionViewSource) ...................................................................................... 18
Commandes ......................................................................................................................................... 20
Injection de dépendance ...................................................................................................................... 22
Unity .......................................................................................................................................................................... 22
f.
g.
h.
i.
j.
k.
« ViewModel First » ............................................................................................................................. 23
Navigation ........................................................................................................................................... 26
Messenger ........................................................................................................................................... 28
ServiceLocator...................................................................................................................................... 29
DialogService ....................................................................................................................................... 30
Design Time data ................................................................................................................................. 30
3. MODELS...................................................................................................................................................... 32
a. ObservableBase ................................................................................................................................... 32
b. Validation ............................................................................................................................................ 33
1.
2.
3.
4.
5.
6.
II.
Validation sur exceptions ................................................................................................................................. 33
ValidationrRule ................................................................................................................................................. 33
IDataErrorInfo .................................................................................................................................................. 35
INotifyDataErrorInfo ........................................................................................................................................ 36
Une classe de validation réutlisable ................................................................................................................. 37
Templates ......................................................................................................................................................... 40
MVVM LIGHT ......................................................................................................................................... 41
1-Installation ................................................................................................................................................ 41
a.
b.
c.
d.
Templates Mvvm Light ..................................................................................................................................... 42
Items Mvvm Light ............................................................................................................................................. 42
Snippets............................................................................................................................................................ 42
Projet « from scratch » ..................................................................................................................................... 43
1
2
2-Model ........................................................................................................................................................ 43
3. ViewModel ................................................................................................................................................ 44
a-Commandes ............................................................................................................................................................ 44
b-Messenger .............................................................................................................................................................. 44
4. SimpleIoC .................................................................................................................................................. 45
III.
PRISM .................................................................................................................................................... 47
1.
2.
a.
b.
c.
d.
INSTALLATION .............................................................................................................................................. 47
MEMENTO .................................................................................................................................................. 47
Shell ..................................................................................................................................................... 48
Boostrapper ......................................................................................................................................... 48
Lancement de l’application .................................................................................................................. 48
Régions ................................................................................................................................................ 48
Toolbar et navigation ................................................................................................................................................. 49
Adaptation de région ................................................................................................................................................. 50
c.
d.
ShellViewModel ................................................................................................................................... 50
Modules ............................................................................................................................................... 52
Avoir plus de contrôle sur la vue affichée (Activate/ Deactivate) .............................................................................. 53
« ViewModel First » ................................................................................................................................................... 54
Module chargé à la demande .................................................................................................................................... 56
e.
Navigation ........................................................................................................................................... 56
Navigation avec le « regionManager » ...................................................................................................................... 56
Avec passage de paramètre ....................................................................................................................................... 57
Avec NavigationParameters ....................................................................................................................................... 57
Création d’une commande de navigation globale ..................................................................................................... 58
Navigation Journal ..................................................................................................................................................... 59
Region Context........................................................................................................................................................... 59
Confirmer, Annuler la navigation (IConfirmNavigationRequest) ............................................................................... 60
RegionMemberLifetime ............................................................................................................................................. 61
Navigation grâce à VisualStateManager .................................................................................................................... 61
3.
MVVM (PRISM.MVVM) ................................................................................................................................. 62
DelegateCommand .............................................................................................................................. 62
CompositeCommand ........................................................................................................................... 62
BindableBase ....................................................................................................................................... 63
ViewModelLocator ............................................................................................................................... 63
4. PUBSUBEVENTS (PRISM.PUBSUBEVENTS) ......................................................................................................... 65
5. SERVICES ..................................................................................................................................................... 66
a. Service de module ................................................................................................................................ 66
b. Services partagés ................................................................................................................................. 67
6. PROJET INFRASTRUCTURE ............................................................................................................................... 69
7. INTERACTIVITY (PRISM.INTERACTIVITY).............................................................................................................. 70
a. Notification .......................................................................................................................................... 70
a.
b.
c.
d.
Avec « InteractionRequest » ...................................................................................................................................... 70
Avec « DefaultNotificationWindow » ........................................................................................................................ 71
b.
Confirmation ........................................................................................................................................ 71
Avec « InteractionRequest » ...................................................................................................................................... 71
Avec « DefaultConfirmationWindow » ...................................................................................................................... 72
c.
Custom ................................................................................................................................................. 73
Custom popup ........................................................................................................................................................... 73
Custom Notification ................................................................................................................................................... 74
d.
e.
BusyIndicator ....................................................................................................................................... 77
InvokeCommandAction ........................................................................................................................ 79
2
3
« Vue d’ensemble »
Vues
L’application a une fenêtre principale (MainWIndow ou « Shell »).
Les vues sont des contrôles utilisateurs/ fenêtres. Celles-ci ont un un viewmodel en source de
données et les contrôles sont bindés.
On définit la source avec le DataContext, CollectionViewSource si besoin de trier/filtrer/grouper, on
peut utiliser également un ViewModelLocator
L’application a des dictionnaires de ressources (styles, templates, fonts, brushes, colors, etc.).
…Converter, TargetNullValue
ViewModels
Ensemble des propriétés que la vue aura besoin d’afficher + déclencheurs (commandes)
... utilise les services … méthodes pour aller chercher les données pour remplir les propriétés
(chargement de la page souvent).
Plusieurs « types » de viewmodels : liste, détails affichant le détail d’un élément/ élément courant
Navigation : entre vues, avec passage de paramètre, historique de navigation
Messenger sert à échanger des messages. L’emeteur envoie un message, les abonnés recoivent une
notification
Models
Model défini la structure des données (lecture seule, nom affiché, etc.), les stocke et sont bindées
dans la vue. Implémente INotifyPropertyChanged si besoin de notifier la vue des changements, data
annotations si besoin de validation et ieditableobject si besoin d’annuler les changements.
Prism
Utile pour l’UI Composition, c’est-à-dire avoir une « page unique » divisée en régions (conteneurs
pour vues), avec navigation.
-
Bootstrapper : sert à définir la vue de base, IoC, à afficher la page
Shell qui est la vue/ page de base, le conteneur, divisé en régions
Modules divisent les features
Chaque vue est enregistrée pour une région avec le regionManager.
Les viewmodels et services sont enregistrés dans un container.
-
Navigation avec le regionManager on indique la region ciblée pour le changement de vue,
une uri avec le nom de la nouvelle vue et enfin les paramètres
Modularité : permet de diviser les features et de charger à la demande les modules au lieu
de tout charger au lancement de l’application
3
4
Différences entre Mvvm pour Windows / Windows Phone/Windows Phone SliverLight … Pourquoi on
est obligé de différencier Mvvm pour Wpf et Windows, Windows Phone, etc.
WPF
 CommandManager pour RelayCommand
 DialogService
WinRT :
 Pas de propetychanging
 Pas de listCollectionView
 Pas de IDataErrorInfo > INotifyDataErrorInfo
 Reflection légérement différente
 Pas de commandManager pour relayCommand
WP8
 Ajouter une référence aux data annotations (C:\Program Files (x86)\Microsoft
SDKs\Silverlight\v5.0\Libraries\Client )
WP8 SL
 Frame > PhoneApplicationFrame
 Pas de paramètre  dans uri
 NavigationService existe déjà (dans la Page de base)
 Problème avec les data annotations…
4
5
I.
Mvvm
On peut se créer ses propres classes d’aides et pas forcément utiliser un Framework :
1. Vues
a. « View First »
Fenêtre principale
Membres,
commandes du shell
et de l’application
Vues (UserControls) ou
ContentControls
App
Enregistrement
possible services,
viewmodels dans
conteneur.
Fenêtre de départ
(StartupUri)
Ressources
ShellViewModel
ViewModel deView1
Membres affichés
par la vue,
commandes et
utilisation services
(injection possible)
IService
Service
View1
(UserControl / fenêtre)
Echange de messages
possible par Messenger
entre ViewModels
Autre ViewModel
View2
Views
Un ViewModelLocator peut faire
l’intermédiaire. Retourne une
instance du ViewModel (avec
injection) .Utilisation conteneur
possible
Models
ViewModels
5
6
b. Fenêtre principale de l’application (MainWindow/ Shell)
StartupUri
<Application x:Class="MvvmDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
StartupUri="MainWindow.xaml">
</Application>
Bootstrapping … Définir et lancer soi-même sa fenêtre
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
// enregistrement des services avec un container possible avant
var shell = new Shell();
On supprimer « MainWindow » et
shell.Show();
}
ajoute une fenêtre appelée « Shell »
}
par exemple
On peut supprimer le paramètre StartupUri
Régions
Utiliser des ContentControls qui recevront les vues (UserControls)
MainWindow
Une région qui pourra recevoir plusieurs vues,
par exemple la vue détails
c. Vues
Ce sont soit des contrôles utilisateurs, soit des fenêtres.
d. Ressources
(Styles, templates, fonts, brushes, colors, etc.)
<Application x:Class="MvvmDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:MvvmDemo.ViewModels"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<vm:ViewModelLocator x:Key="ViewModelLocator"/>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary
Source="pack://application:,,,/EasyMvvm;component/Resources/Styles.xaml" />
<ResourceDictionary Source="Resources/Styles.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
Ressource d’un
autre projet
Ressource du
projet
6
7
e. Source de données
DataContext
Xaml
Namespace
<UserControl …
xmlns:vm ="clr-namespace:MvvmDemo.ViewModels">
<UserControl.DataContext>
DataContext ici pour un « UserControl » ,
<vm:PeopleViewModel />
</UserControl.DataContext>
cela serait la même chose pour « Window »
<Grid>
</Grid>
</UserControl>
Ou code-behind
public partial class Shell : Window
{
public Shell()
{
InitializeComponent();
DataContext = new PeopleViewModel();
}
}
CollectionViewSource (pour filtrer, trier, grouper) sur listes
ViewModelLocator
 (Enregistrement des services injectés dans un conteneur IoC)
 Listing des ViewModels
 Pour chaque ViewModel : Création de l’instance (grâce au conteneur ou non) du ViewModel
et retour de l’instance
Sans conteneur
public class ViewModelLocator
{
private static ViewModelLocator _default;
public static ViewModelLocator Default
{
get
{
return _default ?? (_default = new ViewModelLocator());
}
}
private MainWindowViewModel _mainWindowViewModel;
public MainWindowViewModel MainWindowViewModel
{
Si l’instance du ViewModel est nulle,
get
on la créé ainsi que les services
{
injectés dans le constructeur
if (_mainWindowViewModel == null)
{
IService service = new Service();
_mainWindowViewModel = new MainWindowViewModel(service);
}
return _mainWindowViewModel;
}
}
}
7
8
Exemple de ViewModel et services pour les exemples
public class MainWindowViewModel
{
public string Message { get; set; }
private IService _service;
public MainWindowViewModel(IService service)
{
_service = service;
Message = "Bonjour!";
}
}
public interface IService { }
public class Service : IService { }
Avec conteneur IoC
Enregistrement des services injectés et des VioewModels dans un conteneur IoC (dans « App » ..
« OnStartup » ou dans le constructeur du ViewModelLocator)
Création de l’instance du ViewModel grâce au conteneur (avec injection des dépendances) et retour
de l’instance
public class ViewModelLocator
{
private static ViewModelLocator _default;
public static ViewModelLocator Default
{
get
{
return _default ?? (_default = new ViewModelLocator());
}
Enregistrement des services
}
puis des ViewModels dans le
public ViewModelLocator()
{
conteneur
EasyIoC.Default.Register<IService, Service>();
EasyIoC.Default.Register<MainWindowViewModel>();
}
public MainWindowViewModel MainWindowViewModel
Le conteneur se charge de créer
{
l’instance que l’on retourne
get
{
return EasyIoC.Default.Resolve<MainWindowViewModel>();
}
}
}
Utilisation du ViewModelLocator dans les 2 cas idem (avec et sans conteneur)
Dans le code-behind de la vue
DataContext = ViewModelLocator.Default.MainWindowViewModel;
8
9
Xaml
Référencement dans « App »
<Application x:Class="MvvmDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:MvvmDemo.ViewModels"
StartupUri="MainWindow.xaml">
<Application.Resources>
<vm:ViewModelLocator x:Key="ViewModelLocator" />
</Application.Resources>
</Application>
Puis dans la vue
<Window …
DataContext="{Binding Source={StaticResource ViewModelLocator},Path=MainWindowViewModel}">
<Grid>
</Grid>
</Window>
On pourrait également définir le DataContext sur l’instance des UserControls d’une fenêtre/vue
<Views:PeopleView DataContext="{Binding PeopleViewModel, Source={StaticResource
ViewModelLocator}}" />
ViewModelLocator « générique »
public class ViewModelLocator
{
private readonly static Dictionary<string, object> _viewModels = new
Dictionary<string, object>();
public object this[string key]
{
get
{
object viewModel;
_viewModels.TryGetValue(key, out viewModel);
return viewModel;
}
}
private static ViewModelLocator _default;
public static ViewModelLocator Default
{
get
{
return _default ?? (_default = new ViewModelLocator());
}
}
public void Register(string key, object viewModel)
{
_viewModels.Add(key, viewModel);
}
public void Register<T>(object viewModel)
{
Register(typeof(T).Name, viewModel);
}
public void Register(object viewModel)
{
Register(viewModel.GetType().Name, viewModel);
}
}
9
10
Enregistrement des services et ViewModels
IService service = new Service();
var vm = new MainWindowViewModel(service);
ViewModelLocator.Default.Register("MainWindowViewModel",vm);
Utilisation
Code-behind de la vue
DataContext = ViewModelLocator.Default["MainWindowViewModel"];
Xaml
Référencer le ViewModelLocator dans « App » comme précédemment puis dans les vues
DataContext="{Binding Source={StaticResource ViewModelLocator},Path=[MainWindowViewModel]}"
Les crochets permettents de récupérer la valeur avec « this »
Bootsrapper … Prévention
Avec le ViewModelLocator référencé dans « App » … Si on supprime StartupUri (en gros on fait un
bootstrapper) il se peut qu’il y ait ensuite une erreur « impossible de retrouver ViewModelLocator »
(du à un problème de msbuild qui ne génére tout simplement pas le fichier). Dans ce cas il faut au
moins avoir un fichier de resources (Styles par exemple) ou tout simplement rajouter StartupUri
f.
Converters
Implémente IValueConverter
public sealed class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
CultureInfo culture)
{
return (value is bool && (bool)value) ? Visibility.Visible :
Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter,
CultureInfo culture)
{
return value is Visibility && (Visibility)value == Visibility.Visible;
}
}
Utilisation dans la vue
Déclarer en ressource
<Window.Resources>
<Views:BooleanToVisibilityConverter x:Key="BooleanToVisibiltyConverter"/>
</Window.Resources>
… Puis
<Views:LoadingView Visibility="{Binding IsLoading, Converter={StaticResource
BooleanToVisibiltyConverter}}" />
10
11
g. TargetNullValue
TargetNullValue permet d’éviter les champs vides et apporter une indication à l’utilisateur sur les
données à saisir.
On peut définir directement
<TextBox Text="{Binding Description,TargetNullValue='Entrez la description'}" />
On peut églement définir les chaines en ressources, cela permet également de contourner le
problème des « apostrophes »
<sys:String x:Key="NullName">Entrez le nom de l'article</sys:String>
(namespace xmlns:sys="clr-namespace:System;assembly=mscorlib" )
On peut ainsi accéder facilement à la ressource depuis l’application et contourner le problème des
apostrophes
<TextBox Text="{Binding ArticleName,TargetNullValue={StaticResource NullName} }"/>
h. Animations
Transitions
Opacité
private void OpacityAnimate(UIElement elementToAnimate, double from, double to, int
milliseconds, EventHandler completed = null)
{
var animation = new DoubleAnimation(from, to, new
Duration(TimeSpan.FromMilliseconds(milliseconds)));
if (completed != null)
animation.Completed += completed;
elementToAnimate.BeginAnimation(Control.OpacityProperty, animation);
}
Utilisation
OpacityAnimate(old, 1, 0, 100);
« Slide »
private void HorizontalSlide(UIElement elementToAnimate, double from, double to, int
milliseconds, IEasingFunction ease = null, EventHandler completed = null)
{
var transform = new TranslateTransform();
elementToAnimate.RenderTransform = transform;
var animation = new DoubleAnimation(from, to, new
Duration(TimeSpan.FromMilliseconds(milliseconds)));
if (ease != null)
animation.EasingFunction = ease;
if (completed != null)
animation.Completed += completed;
transform.BeginAnimation(TranslateTransform.XProperty, animation);
}
Utilisation
HorizontalSlide(old, 0, 30, 700, new CubicEase { EasingMode = EasingMode.EaseOut },
(s, Ev) =>
{
old.Visibility = Visibility.Hidden;
});
11
12
Contrôle de chargement
Créer un UserControl avec une progressbar par exemple
<UserControl x:Class="MvvmDemo.Views.LoadingView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d" >
<Grid Background="#99959292">
<ProgressBar Height="39" HorizontalAlignment="Center"
VerticalAlignment="Center" Width="210" IsIndeterminate="True"/>
</Grid>
</UserControl>
Placer ce contrôle en dernier élément du conteneur pricipal de la fenêtre
<Views:LoadingView Visibility="{Binding IsLoading, Converter={StaticResource
BooleanToVisibiltyConverter}}" />
Utilisation d’un converter pour l’afficher ou le masquer.
Bindé à une propriété du ViewModel de la fenêtre principale (« ShellViewModel »)
public class ShellViewModel : ViewModelBase
{
private bool _isLoading;
public bool IsLoading
{
get { return _isLoading; }
set { SetProperty(ref _isLoading, value); }
}
Utilisation d’un messenger
pour être notifié
public ShellViewModel()
{
MessengerInstance.Subscribe<bool>(Messages.IS_LOADING, (isLoading) =>
{
IsLoading = isLoading;
RaisePropertyChanged("IsLoading");
});
}
}
12
13
VisualStates
<Window x:Class="VisualStateManagerDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid x:Name="_layout">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Normal"></VisualState>
<VisualState x:Name="Fade">
<Storyboard>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00"
Storyboard.TargetName="rectangle"
Storyboard.TargetProperty="(UIElement.Opacity)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="1" />
<SplineDoubleKeyFrame KeyTime="00:00:00.700" Value="0" />
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<StackPanel>
<Button Content="Go" Click="Button_Click"/>
<Rectangle x:Name="rectangle" Fill="#FF3232DE" Height="300"/>
</StackPanel>
</Grid>
</Window>
Code-behind (sur le click du bouton)
VisualStateManager.GoToElementState(this._layout, "Fade", true);
Avec un UserControl ajouté dans la fenêtre dans l’instance se nommerait « uc » et avec les mêmes
VisualStates
VisualStateManager.GoToState(uc, "Fade", true);
On pourrait également utiliser le comportement blend « GoToStateAction » également.
13
14
2. ViewModel
Ensemble des propriétés que la vue aura besoin d’afficher + déclencheurs (commandes)
... utilise les services … méthodes pour aller chercher les données pour remplir les propriétés
(chargement de la page souvent).
Plusieurs « types » de viewmodels : liste, détails affichant le détail d’un élément/ élément courant
Navigation : entre vues, avec passage de paramètre, historique de navigation
Messenger sert à échanger des messages. L’emeteur envoie un message, les abonnés recoivent une
notification
a. Classes de base de ViewModel
ViewModelBase
Classe iméplement
INotifyPropertyChanged (voir Models)
public class ViewModelBase : ObservableBase, IWpfNavigation
{
public static IEasyMessenger MessengerInstance
{
Il peut être bon d’ajouter un
get
messenger de base et la
{
navigation
return EasyMessenger.Default;
}
}
public virtual void OnNavigatedTo(WpfNavigationEventArgs e)
{ }
public virtual void OnNavigatedFrom(WpfNavigationCancelEventArgs e)
{ }
}
ListViewModelBase : classe de base pour les ViewModels affichant une liste
public abstract class ListViewModelBase<T> : ViewModelBase where T : class
{
Liste de « Model » ou liste de
private ObservableCollection<T> _items;
public ObservableCollection<T> Items
« ViewModel »
{
get { return _items; }
set { SetProperty(ref _items, value); }
}
Pour pouvoir filtrer, trier,
public ListCollectionView DefaultView
{
grouper
get { return
(ListCollectionView)CollectionViewSource.GetDefaultView(Items); }
}
public T CurrentItem
{
get { return (Items != null) ? DefaultView.CurrentItem as T : null; }
set
{
Elément courant et méthode
DefaultView.MoveCurrentTo(value);
permettant d’être notifié du
RaisePropertyChanged();
CurrentItemChanged();
changement
}
}
protected virtual void CurrentItemChanged() { }
}
14
15
b. ViewModel Liste et détails
1 possibilité : On fait deux vues / ViewModels indépendants et on informe le ViewModel détails du
changement d’élément courant
ère
ViewModel détails
ViewModel liste
Et datacontext
Message current
changed
Et datacontext
public class PeopleViewModel : ViewModelBase
{
private IPeopleService _peopleService;
public ObservableCollection<Person> People { get; set; }
private Person _currentPerson;
public Person CurrentPerson
{
get { return _currentPerson; }
On notifie les abonnés du
set
{
changement d’élément courant
SetProperty(ref _currentPerson, value);
avec un Messenger
MessengerInstance.Publish(Messages.CURRENT_PERSON_CHANGED,
_currentPerson);
}
}
public PeopleViewModel(IPeopleService peopleService)
{
_peopleService = peopleService;
LoadPeopleAsync();
}
public async void LoadPeopleAsync()
{
MessengerInstance.Publish(Messages.IS_LOADING, true);
var peopleList = await _peopleService.getAllAsync();
People = new ObservableCollection<Person>(peopleList);
RaisePropertyChanged("People");
MessengerInstance.Publish(Messages.IS_LOADING, false);
}
}
Dans la vue Liste on binde « SelectedItem » à l’élément courant du « ViewModel Liste »
<ListBox ItemsSource="{Binding People}" SelectedItem="{Binding CurrentPerson}"/>
15
16
ViewModel Détails
public class PersonDetailsViewModel : ViewModelBase
{
private Person _currentPerson;
public Person CurrentPerson
{
get { return _currentPerson; }
set { SetProperty(ref _currentPerson, value); }
}
Abonnement à l’évènement
public PersonDetailsViewModel()
changement d’élément courant
{
avec un Messenger
MessengerInstance.Subscribe<Person>(Messages.CURRENT_PERSON_CHANGED,
(person) =>
{
CurrentPerson = person;
RaisePropertyChanged("Person");
});
}
}
La vue Détails, les éléments sont bindés sur l’élément courant du « ViewModel details »
<UserControl … >
<StackPanel>
<Grid>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Label>Nom</Label>
<TextBlock Grid.Column="1" Text="{Binding CurrentPerson.FirstName}" />
<Label Grid.Row="1">Prénom</Label>
<TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding
CurrentPerson.LastName}" />
<!-- etc.-->
</Grid>
</StackPanel>
</UserControl>
On définit le DataContext pour chaque Vue
<Views:PeopleView DataContext="{Binding PeopleViewModel, Source={StaticResource ViewModelLocator}}" />
<Views:PersonDetailsView DataContext="{Binding PersonDetailsViewModel, Source={StaticResource
ViewModelLocator}}"/>
16
17
2ème possibilité
Soit on crée une vue « master details » avec un ViewModel MasterDetails, la vue détail ayant son
DataContext branché sur l’élément courant
Vue MasterDetails » avec pour DataContext le
ViewModel « MasterDetails »
Vue Details bindée sur le CurrentItem du
ViewModel « MasterDetails »
Vue liste, éléments bindés au DataContext du « parent »
ViewModel « MasterDetails »
public class MasterDetailsViewModel : ListViewModelBase<Person>
{
private IPeopleService _peopleService;
public MasterDetailsViewModel(IPeopleService peopleService)
{
_peopleService = peopleService;
LoadPeopleAsync();
}
public async void LoadPeopleAsync()
{
MessengerInstance.Publish(Messages.IS_LOADING, true);
var peopleList = await _peopleService.getAllAsync();
Items = new ObservableCollection<Person>(peopleList);
RaisePropertyChanged("Items");
MessengerInstance.Publish(Messages.IS_LOADING, false);
}
}
Pas de ViewModel « Details » … la vue est bindée sur CurrentItem du ViewModel « MasterDetails »
Vue « MasterDetails »
<UserControl …
DataContext="{Binding MasterDetailsViewModel, Source={StaticResource ViewModelLocator}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<local:PeopleView />
<local:PersonDetailsView Grid.Column="1" DataContext="{Binding CurrentItem}"/>
</Grid>
</UserControl>
Adaptation du binding, vue « Liste »
<ListBox ItemsSource="{Binding Items}" SelectedItem="{Binding CurrentItem}"/>
Et pour la vue d étails on se binde directement aux propriéts de CurrentItem
<Label>Nom</Label>
<TextBlock Grid.Column="1" Text="{Binding FirstName}" />
17
18
c. Filtrer, trier, grouper (CollectionViewSource)
Pour l’exemple on a une liste de personnes avec nom (name), email et ville (city). On trie par ordre
alphabétique, on filtre et et groupe par ville.
Dans le ViewModel
public class MainWindowViewModel : ObservableBase
{
public ObservableCollection<Person> People { get; set; }
public ListCollectionView DefaultView
{
get { return
(ListCollectionView)CollectionViewSource.GetDefaultView(People); }
}
// etc.
}
On peut binder la ListBox sur la CollectionViewSource ou sur la liste des éléments
<ListBox ItemsSource="{Binding People}">
<ListBox ItemsSource="{Binding DefaultView}" />
On définit le DataContext de la vue sur le ViewModel.
(On pourrait aussi définir le DataContext de la vue avec une CollectionViewSource)
<Window.DataContext>
<vm:MainWindowViewModel />
</Window.DataContext>
<Window.Resources>
<CollectionViewSource x:Key="ViewSource" Source="{Binding People}"/>
</Window.Resources>
…
<ListBox ItemsSource="{Binding Source={StaticResource ViewSource}}" />
Filtrer
public ICommand FilterCommand { get; set; }
FilterCommand = new RelayCommand(() =>
{
DefaultView.Filter = (item) =>
{
var person = item as Person;
return person.City == "Londres" ? true : false;
};
});
Trier
public ICommand SortCommand { get; set; }
SortCommand = new RelayCommand(() =>
{
DefaultView.SortDescriptions.Add(new SortDescription("Name", ListSortDirection.Ascending));
});
18
19
Grouper
public ICommand GroupCommand { get; set; }
GroupCommand = new RelayCommand(() =>
{
DefaultView.GroupDescriptions.Add(new PropertyGroupDescription("City"));
});
On adapte la ListBox afin d’afficher l’en-tête du groupe
<ListBox ItemsSource="{Binding People}">
<ListBox.GroupStyle>
<GroupStyle />
</ListBox.GroupStyle>
<ListBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
Les personnes sont groupées par ville dans la ListBox
19
20
d. Commandes
public class RelayCommandBase : ICommand
{
protected readonly Delegate _executeMethod;
protected readonly Func<Object, bool> _canExecuteMethod;
public RelayCommandBase(Delegate executeMethod)
: this(executeMethod, (args) => true)
{ }
public RelayCommandBase(Delegate executeMethod, Func<Object, bool>
canExecuteMethod)
{
if (executeMethod == null) throw new ArgumentNullException("execute");
_executeMethod = executeMethod;
_canExecuteMethod = canExecuteMethod;
}
public bool CanExecute(object parameter)
{
return _canExecuteMethod == null || _canExecuteMethod(parameter);
}
public virtual void Execute(object parameter)
{
_executeMethod.DynamicInvoke(parameter);
}
public event EventHandler CanExecuteChanged
{
add { CommandManager.RequerySuggested += value; }
remove { CommandManager.RequerySuggested -= value; }
}
}
RelayCommand
public class RelayCommand : RelayCommandBase
{
public RelayCommand(Action executeMethod): this(executeMethod, () => true) { }
public RelayCommand(Action executeMethod, Func<bool> canExecuteMethod)
: base(executeMethod, (args) => canExecuteMethod()) { }
public bool CanExecute()
{
return CanExecute(null);
}
public void Execute()
{
Execute(string.Empty);
}
public override void Execute(object parameter)
{
_executeMethod.DynamicInvoke();
}
}
RelayCommand « Generic »
public class RelayCommand<T> : RelayCommandBase
{
public RelayCommand(Action<T> execute): this(execute, (args) => true) { }
public RelayCommand(Action<T> executeMethod, Func<Object, bool>
canExecuteMethod): base(executeMethod, (args) => canExecuteMethod((T)args))
{ }
public virtual bool CanExecute(T parameter)
{
return base.CanExecute(parameter);
}
}
20
21
CompositeCommand
public class EasyCompositeCommand : ICommand
{
private readonly IList<ICommand> _commands = new List<ICommand>();
public IList<ICommand> Commands
{
get { return _commands; }
}
public virtual void RegisterCommand(ICommand command)
{
if (command == null) throw new ArgumentNullException("command");
lock (_commands)
{
if (_commands.Contains(command))
throw new InvalidOperationException();
_commands.Add(command);
}
}
public virtual void UnregisterCommand(ICommand command)
{
if (command == null) throw new ArgumentNullException("command");
lock (_commands)
{
_commands.Remove(command);
}
}
public virtual void Execute(object parameter)
{
foreach (var command in _commands)
{
command.Execute(parameter);
}
}
public virtual bool CanExecute(object parameter)
{
bool result = false;
ICommand[] commandList;
lock (_commands)
{
commandList = _commands.ToArray();
}
foreach (ICommand command in commandList)
{
if (!command.CanExecute(parameter))
{
return false;
}
result = true;
}
return result;
}
public event EventHandler CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
var handler = CanExecuteChanged;
if (handler != null)
{
handler(this, EventArgs.Empty);
}
}
}
21
22
e. Injection de dépendance
Un conteneur marche ainsi :
1. On enregistre dans le conteneur les services, instances, viewmodels, etc.
2. Le conteneur se charge de créer une instance (en prenant en compte les paramètres injectés
dans le constructeur) et la retourner
En simplifiant, un conteneur c’est un dictionnaire, un object builder et une variable static permettant
d’avoir accès depuis n’ importe où dans l’application aux mêmes éléments enregistrés. Il peut y avoir
en plus un cache (permettant une stratégie singleton par exemple).
Unity
NuGet
PM> Install-Package Unity
PM> Install-Package CommonServiceLocator
Dans « app » « startup » par exemple
IUnityContainer container = new UnityContainer();
container.RegisterType<IPeopleService, PeopleService>();
container.RegisterType<PeopleViewModel>();
Pour pouvoir retrouver une instance depuis une autre vue, viewmodel, … (puisque l’instance du
conteneur est définie localement on ne retrouverait pas les éléments enregistrés) on utilise
ServiceLocator (Microsoft.Practices.ServiceLocation). (ServiceLocator n’est pas compris avec Unity, il
faut donc l’installer séparément avec les packages NuGet)
IUnityContainer container = new UnityContainer();
ServiceLocator.SetLocatorProvider(() => new UnityServiceLocator(container));
container.RegisterType<IPeopleService, PeopleService>();
container.RegisterType<PeopleViewModel>();
Retrouver une instance
var vm = ServiceLocator.Current.GetInstance<PeopleViewModel>();
Exemple de ViewModel
public class PeopleViewModel : ViewModelBase
{
private IPeopleService _peopleService;
Injection dans le constructeur
du service
public PeopleViewModel(IPeopleService peopleService)
{
_peopleService = peopleService;
}
}
22
23
f.
« ViewModel First »
Fenêtre principale
avec conteneurs
ViewModel avec
injection d’une IView1
ShellViewModel
App
Public IView1 view1
Enregistrement
vues,
viewmodels,
services, dans
conteneur
Public IView2 view2
… retournent la vue
du viewmodel
(conteneur)
Affectation du
datacontext de la vue à
ce viewmodel (this)
ContentControl , contenu
bindé à une vue de
« ShellViewModel »
IView1
View1
(UserControl par ex
implémentant
IView1)
Views
ViewModels
« App »
Enregistrement des vues, viewmodels et services avec un conteneur
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
IUnityContainer container = new UnityContainer();
var unity = new UnityServiceLocator(container);
ServiceLocator.SetLocatorProvider(() => unity);
container.RegisterType<IPeopleView, PeopleView>();
container.RegisterType<PeopleViewModel>();
}
}
23
24
ViewModel
public class PeopleViewModel :ObservableBase
{
public IPeopleView View { get; private set; }
public ObservableCollection<Person> People { get; set; }
public ICommand AddPersonCommand { get; set; }
public PeopleViewModel(IPeopleView view)
{
View = view;
View.DataContext = this;
Vue injectée
AddPersonCommand = new RelayCommand(() =>{ });
LoadPeople();
}
public void LoadPeople()
{
var peopleList = new List<Person>();
peopleList.Add(new Person("Marie", "[email protected]"));
peopleList.Add(new Person("Joey", "[email protected]"));
People = new ObservableCollection<Person>(peopleList);
RaisePropertyChanged("People");
}
}
Vues
Interface
public interface IPeopleView
{
object DataContext { get; set; }
}
UserControl
Implémente l’interface
public partial class PeopleView : UserControl, IPeopleView
{
public PeopleView()
{
InitializeComponent();
}
}
Xaml
<UserControl x:Class="ViewModelFirstDemo.Views.PeopleView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="25"/>
<RowDefinition />
</Grid.RowDefinitions>
<Button Content="Ajouter" Command="{Binding AddPersonCommand}" />
<ListBox ItemsSource="{Binding People}" Grid.Row="1"/>
</Grid>
</UserControl>
24
25
Fenêtre principale
<Window xmlns:Views="clr-namespace:ViewModelFirstDemo.Views"
x:Class="ViewModelFirstDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ContentControl Content="{Binding Main}"></ContentControl>
</Grid>
</Window>
Contenu des conteneurs
bindé aux vues du
ShellViewModel
Le ViewModel de la fenêtre principale
public
{
class ShellViewModel
public IPeopleView Main
{
get
{
var vm = ServiceLocator.Current.GetInstance<PeopleViewModel>();
return vm.View;
}
}
}
25
26
g. Navigation
Navigation possible
- de vue à vue
de vue à viewmodel ou de ViewModel à vue
- de viewmodel à viewmodel : Les ViewModels héritent de ViewModelBase. Les vues
doivent hériter d’une vue spéciale (WpfView) permettant le lien entre le service de
navigation et les viewmodels
La classe WpfView
public class WpfView : ContentControl,IWpfNavigation
{
public virtual void OnNavigatedFrom(WpfNavigationCancelEventArgs e)
{
Récupère le ViewModel
var vm = DataContext as ViewModelBase;
vm.OnNavigatedFrom(e);
défini en DataContext et
}
appelle ses méthodes
public virtual void OnNavigatedTo(WpfNavigationEventArgs e)
« OnNavigated… »
{
var vm = DataContext as ViewModelBase;
vm.OnNavigatedTo(e);
}
}
Exemple de vue permettant Héritant de « WpfView » qui permettra la navigation vers le ViewModel
défini en DataContext (avec passage de paramètre par exemple)
26
27
<wlw:WpfView x:Class="MvvmDemo.Views.PersonEditView"
…
xmlns:wlw="clr-namespace:EasyMvvm.Navigation;assembly=EasyMvvm"
DataContext="{Binding PersonEditViewModel,Source={StaticResource
ViewModelLocator}}">
<Grid>
</Grid>
</wlw:WpfView>
Le service au plus simple se contente de créer instance de la vue, puis remplace le contenu du
conteneur (Application.Current.MainForm) et si la vue implémente IWpfNavigation alors appelle la
méthode OnNavigated… soit c’est juste une vue qui implémente IWpfNavigation et la navigation
s’arrête à la vue, soit c’est une vue qui hérite de WpfView (classe implémentant IWpfNavigation)
alors la navigation est faite vers le viewmodel de la vue.
Exemple simplifié de Service de Navigation
public class WpfNavigationService
{
private static WpfNavigationService _default;
public static WpfNavigationService Default
{
get
{
return _default ?? (_default = new WpfNavigationService());
}
}
public void Navigate(Type page)
{
Navigate(page, null);
}
public void Navigate(Type page, object parameter)
{
// création d'instance de la page
var pageToGo = Activator.CreateInstance(page);
// remplacement du contenu de la fenêtre principale de l'application
Application.Current.MainWindow.Content = pageToGo;
// passer le paramètre à la nouvelle vue, vm
DoNavigatedTo(pageToGo, parameter);
}
private void DoNavigatedTo(object pageToGo, object parameterToGo = null)
{
var context = new WpfNavigationEventArgs(pageToGo.GetType(),
parameterToGo);
if (typeof(IWpfNavigation).IsAssignableFrom(pageToGo.GetType()))
{
((IWpfNavigation)pageToGo).OnNavigatedTo(context);
}
}
}
A cela on pourrait ajouter un historique de navigation (Journal) pour permettre la navigation retour
par exemple
27
28
h. Messenger
Messenger sert à échanger des messages. L’emeteur envoie un message, les abonnés recoivent une
notification. Un « bon » Messenger utilise également les « WeakReference » pour savoir si les
abonnés n’ont pas été collectés par le GC et éviter les fuites de mêmoire.
Exemple de Messenger en version simplifiée
public class Messenger : IMessenger
{
private static Dictionary<string, List<Delegate>> _container = new
Dictionary<string, List<Delegate>>();
// pour être juste notifié sans paramètre
public void Subscribe(string message, Action callback)
{
if (!_container.ContainsKey(message))
{
_container[message] = new List<Delegate>();
}
_container[message].Add(callback);
}
// notifié et passage de paramètre
public void Subscribe<T>(string message, Action<T> callback)
{
if (!_container.ContainsKey(message))
{
_container[message] = new List<Delegate>();
}
_container[message].Add(callback);
}
public void Publish(string message)
{
if (_container.ContainsKey(message))
{
foreach (Delegate execute in _container[message])
{
execute.DynamicInvoke();
}
}
}
public void Publish(string message, object parameter)
{
if (_container.ContainsKey(message))
{
foreach (Delegate execute in _container[message])
{
execute.DynamicInvoke(parameter);
}
}
}
}
S’abonner
_messenger.Subscribe<string>("message", (response) => { });
Publier
_messenger.Publish("message", "Nofication …");
28
29
i. ServiceLocator
Version simplifiée
public class ServiceLocator
{
private Dictionary<Type, object> _container = new Dictionary<Type, object>();
private static readonly ServiceLocator instance = new ServiceLocator();
public static ServiceLocator Instance
{
get { return instance; }
}
public void Register<T>(T element)
{
_container.Add(typeof(T), element);
}
public T Retrieve<T>()
{
object retrieved;
if (_container.TryGetValue(typeof(T), out retrieved))
return (T)retrieved;
return default(T);
}
public void Unregister<T>()
{
if (_container.ContainsKey(typeof(T)))
{
_container.Remove(typeof(T));
}
}
}
Utilisation
Enregistrement (Dans « OnStartup » de App.xaml)
ServiceLocator.Instance.Register<IMessenger>(new Messenger());
Retrouver un service
IMessenger _messenger = ServiceLocator.Instance.Retrieve<IMessenger>();
29
30
j. DialogService
Exemple de service de boite de dialogues
public interface IDialogService
{
bool YesNo(string title, string message);
string OpenFile(string title);
void ShowMessage(string title, string message);
}
public class DialogService : IDialogService
{
public bool YesNo(string title, string message)
{
return MessageBoxResult.Yes ==
MessageBox.Show(message, title, MessageBoxButton.YesNo,
MessageBoxImage.Question);
}
public void ShowMessage(string title, string message)
{
MessageBox.Show(message, title, MessageBoxButton.OK,
MessageBoxImage.Information);
}
public string OpenFile(string title)
{
OpenFileDialog dialog = new OpenFileDialog() { Title = title, Multiselect
= false };
if (dialog.ShowDialog().HasValue)
return dialog.FileName;
else
return string.Empty;
}
}
k. Design Time data
Dans le ViewModel
public class PeopleViewModel : ObservableBase
{
public ObservableCollection<Person> People { get; set; }
Affichage de données en mode
design
public PeopleViewModel()
{
if (DesignerProperties.GetIsInDesignMode(new DependencyObject()))
{
var peopleList = new List<Person>();
peopleList.Add(new Person(1, "Marie", "Bellin", "[email protected]"));
peopleList.Add(new Person(2, "Luc", "Blanc", "[email protected]"));
People = new ObservableCollection<Person>(peopleList);
}
}
}
On peut également le définir dans le code-behind des vues par exemple.
30
31
<Window
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"
xmlns:local="clr-namespace:InDesign" mc:Ignorable="d"
x:Class="MvvmDemo.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:PeopleViewModel />
</Window.DataContext>
<Grid>
<ListBox ItemsSource="{Binding People}" />
</Grid>
</Window>
SampleData avec Blend
<Window
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"
xmlns:local="clr-namespace:InDesign" mc:Ignorable="d"
x:Class="InDesign.MainWindow"
Title="MainWindow" Height="350" Width="525">
<Window.DataContext>
<local:PeopleViewModel />
</Window.DataContext>
<StackPanel>
<ListBox ItemsSource="{Binding People}"
d:DataContext="{Binding Source={StaticResource SampleDataSource}}"
ItemTemplate="{DynamicResource PeopleItemTemplate}" />
</StackPanel>
</Window>
On peut utiliser la création de classe pour binder une seule valeur, à un TextBlock par exemple. On
peut également créer sa propre classe :
<data:Person xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:data = "clr-namespace:MvvmDemo"
Name="Jerome" Email="[email protected]">
</data:Person>
<TextBlock d:DataContext="{d:DesignData Source=SampleData/MyData.xaml}" Text="{Binding Name}"/>
31
32
3. Models
a. ObservableBase
Classe de base pour les « Models » et utilisée par « ViewModelBase » pour la notification de
l’interface utilisateur qu’une propriété a changé.
public abstract class ObservableBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual bool SetProperty<T>(ref T storage, T value,
[CallerMemberName] string propertyName = null)
{
if (object.Equals(storage, value)) return false;
storage = value;
RaisePropertyChanged(propertyName);
return true;
}
protected virtual void RaisePropertyChanged([CallerMemberName] string
propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Utilisation
public class Person : ObservableBase
{
private string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
SetProperty(ref _firstName, value);
RaisePropertyChanged("FullName");
}
}
// etc.
private string _email;
Enregistrement et notification
On raise avec un nom de
propriété pour notifier du
changement de FullName
public string Email
{
get { return _email; }
set { SetProperty(ref _email, value); }
}
public string FullName
{
get { return string.Format("{0} {1}", FirstName, LastName); }
}
}
32
33
b. Validation
1. Validation sur exceptions
1. Model : Déclencher une exception si une valeur ne correspond pas à celle attendue
private string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
if (string.IsNullOrEmpty(value))
{
throw new ArgumentException("Prénom requis.");
}
SetProperty(ref _firstName, value);
}
}
2. Vue : Ajouter « ValidatesOnException » au binding
<TextBox Grid.Column="1" Text="{Binding CurrentPerson.FirstName,
ValidatesOnExceptions=True}" />
3. Désactiver l’exception
2. ValidationrRule
Spécifique à Wpf
1. On crée une classe héritant de ValidationRule avec une méthode validate retournant un
« ValidationResult »
public class TwitterValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo
cultureInfo)
{
var regex = new Regex("^@([A-Za-z0-9_]+)");
var match = regex.Match(value.ToString());
if (match == null || match == Match.Empty)
{
return new ValidationResult(false, "Twitter invalide");
}
else
{
return ValidationResult.ValidResult;
}
}
}
33
34
2. Vue
<Label Grid.Row="2">Twitter</Label>
<TextBox Grid.Column="1" Grid.Row="2">
<TextBox.Text>
<Binding Path="CurrentPerson.Twitter">
<Binding.ValidationRules>
<local:TwitterValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
ValidationRule réutilisable
public class RegexValidationRule : ValidationRule
{
public string Expression { get; set; }
public string ErrorMessage { get; set; }
public override ValidationResult Validate(object value, CultureInfo
cultureInfo)
{
if (Expression == null)
return ValidationResult.ValidResult;
if (string.IsNullOrEmpty(ErrorMessage))
ErrorMessage = "Format invalide";
var regex = new Regex(Expression);
var match = regex.Match(value.ToString());
if (match == null || match == Match.Empty)
{
return new ValidationResult(false, ErrorMessage);
}
else
{
return ValidationResult.ValidResult;
}
}
}
<TextBox Grid.Column="1" Grid.Row="2">
<TextBox.Text>
<Binding Path="CurrentPerson.Twitter">
<Binding.ValidationRules>
<local:RegexValidationRule Expression="^@([A-Za-z09_]+)" ErrorMessage="Twitter invalide!"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
34
35
3. IDataErrorInfo
public class Person :INotifyPropertyChanged, IDataErrorInfo
{
private string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
if (_firstName != value)
{
_firstName = value;
RaisePropertyChanged();
}
}
}
// etc.
public string Error
{
get { return string.Empty; }
}
Erreur pour l’objet ayant les propriétés
public string this[string propertyName]
{
get { return GetErrorForProperty(propertyName); }
}
public string GetErrorForProperty(string propertyName)
{
switch (propertyName)
On teste la valeur de la propriété
{
case "FirstName":
if (string.IsNullOrEmpty(FirstName))
{
return "Prénom requis";
}
else
{
return string.Empty;
}
default:
return string.Empty;
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged([CallerMemberName] string
propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
Dans la vue
Ajouter « ValidatesOnDataErrors »
<TextBox Text="{Binding CurrentPerson.FirstName,ValidatesOnDataErrors=True}"
/>
35
36
4. INotifyDataErrorInfo
public class Person : INotifyPropertyChanged, INotifyDataErrorInfo
{
public int Id { get; private set; }
private string _firstName;
public string FirstName
{
get { return _firstName; }
set
{
if (_firstName != value)
{
_firstName = value;
RaisePropertyChanged();
GetErrorsForFirstName(FirstName).ContinueWith((errorsTask) =>
{
lock (_propertyNameAndErrors)
{
if (errorsTask.Result.Count > 0)
{
_propertyNameAndErrors["FirstName"] =
errorsTask.Result;
RaiseErrorsChanged("FirstName");
}
}
});
}
}
}
public Task<List<string>> GetErrorsForFirstName(string value)
{
return Task.Factory.StartNew<List<string>>(() =>
{
var result = new List<string>();
if (string.IsNullOrEmpty(value))
{
result.Add("Prénom requis!!!!!!");
}
return result;
});
}
private Dictionary<string, List<string>> _propertyNameAndErrors = new
Dictionary<string, List<string>>();
public IEnumerable GetErrors(string propertyName)
{
lock (_propertyNameAndErrors)
{
if (_propertyNameAndErrors.ContainsKey(propertyName))
{
return _propertyNameAndErrors[propertyName];
}
return null;
}
}
36
37
public bool HasErrors
{
get { return _propertyNameAndErrors.Count > 0; }
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void RaiseErrorsChanged(string propertyName)
{
var handler = ErrorsChanged;
if (handler != null)
{
handler(this, new DataErrorsChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void RaisePropertyChanged([CallerMemberName] string
propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
5. Une classe de validation réutlisable
public class ValidableBase : ObservableBase, INotifyDataErrorInfo, IEditableObject
{
private Dictionary<string, List<string>> _propertyNameAndErrors = new
Dictionary<string, List<string>>();
public void ValidateProperty(string propertyName)
{
if (string.IsNullOrEmpty(propertyName))
throw new ArgumentNullException("propertyName");
var propertyInfo = this.GetType().GetProperty(propertyName);
if (propertyInfo == null)
throw new ArgumentException("Invalid property name", propertyName);
// validation
var validationResults = new List<ValidationResult>();
var context = new ValidationContext(this, null, null) { MemberName =
propertyInfo.Name };
var value = propertyInfo.GetValue(this, null);
var propertyErrors = new List<string>();
bool isValid = Validator.TryValidateProperty(value, context,
validationResults);
if (validationResults.Any())
{
propertyErrors.AddRange(validationResults.Select(c =>
c.ErrorMessage));
}
// erreurs ou clear?
var hasCurrentValidationResults =
_propertyNameAndErrors.ContainsKey(propertyName);
var hasNewValidationResults = propertyErrors != null &&
propertyErrors.Count() > 0;
if (hasCurrentValidationResults || hasNewValidationResults)
{
37
38
if (hasNewValidationResults)
{
_propertyNameAndErrors[propertyName] = propertyErrors;
}
else
{
_propertyNameAndErrors.Remove(propertyName);
}
RaiseErrorsChanged(propertyName);
}
}
protected override bool SetProperty<T>(ref T storage, T value,
[CallerMemberName] string propertyName = null)
{
var result = base.SetProperty(ref storage, value, propertyName);
if (result && !string.IsNullOrEmpty(propertyName))
{
ValidateProperty(propertyName);
}
return result;
}
#region INotifyDataError implemented
public IEnumerable GetErrors(string propertyName)
{
// retourne la ou les erreurs pour la propriété (exemple : email)
if (string.IsNullOrEmpty(propertyName)) return new List<string>();
var propertyErrors = new List<string>();
if (_propertyNameAndErrors.TryGetValue(propertyName, out propertyErrors))
{
return propertyErrors;
}
return new List<string>();
}
// retourne si l'élément courant?? a des erreurs
public bool HasErrors
{
get { return _propertyNameAndErrors.Count > 0; }
}
// déclenché quand une erreur est trouvée
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void RaiseErrorsChanged(string propertyName)
{
var handler = ErrorsChanged;
if (handler != null)
{
handler(this, new DataErrorsChangedEventArgs(propertyName));
}
}
#endregion
#region IEditableObject implemented
private Dictionary<string, object> _propertyNameAndValues;
public void BeginEdit()
38
39
{
_propertyNameAndValues = new Dictionary<string, object>();
var properties = this.GetType().GetProperties().Where(p => p.CanRead &&
p.CanWrite);
foreach (var property in properties)
{
_propertyNameAndValues.Add(property.Name, property.GetValue(this,
null));
}
}
public void CancelEdit()
{
if (_propertyNameAndValues != null)
{
var properties = this.GetType().GetProperties().Where(p => p.CanRead
&& p.CanWrite);
foreach (var property in properties)
property.SetValue(this, _propertyNameAndValues[property.Name],
null);
_propertyNameAndValues = null;
}
}
public void EndEdit()
{
_propertyNameAndValues = null;
RaisePropertyChanged(string.Empty);
}
#endregion // IEditableObject
}
Utilisation ajouter des data annotations sur le modèle héritant de « ValidableBase »
Dans le ViewModel on peut vérifier que que l’élément n’a pas d’erreurs avant par exemple une
modification vers une base de données
ValidateCommand = new RelayCommand(() =>
{
if ( !_currentPerson.HasErrors)
{
}
});
39
40
6. Templates
Style pour afficher les erreurs en ToolTip
<Style TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="true">
<Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static
RelativeSource.Self},
Path=(Validation.Errors).CurrentItem.ErrorContent}" />
</Trigger>
</Style.Triggers>
</Style>
On peut définir également un template …
<TextBox Validation.ErrorTemplate="{StaticResource TextBoxErrorTemplate}"
Text="{Binding CurrentPerson.Twitter,ValidatesOnDataErrors=True}" />
40
41
II.
Mvvm Light
http://www.galasoft.ch/mvvm/
1-Installation
Plusieurs possibilités :


Installer les Templates Mvvm Light pour Visual Studio
Ou simplement installer les libraires dans son projet via le gestionnaire de Package NuGet
(Depuis la console)
41
42
PM> install-package mvvmlight
a. Templates Mvvm Light
Pour Wpf(WPF4 et WPF45), SilverLight(SL5),Windows Phone(WP8) et Windows(Win8 et
Win81)
b. Items Mvvm Light
disponibles pour Windows , Windows Phone, Wpf et Silverlight
 MvvmView : crée une vue avec le DataContext défini sur le ViewModelLocator
 MvvmViewModel : crée un ViewModel qui hérite de ViewModelBase
 MvvmViewModelLocator : crée un ViewModelLocator + Utilisation ServiceLocator
c. Snippets
42
43
d. Projet « from scratch »
Installer les librairies Mvvm Light puis utiliser les items proposés par Mvvm Light.
2-Model
Les modèles dérivent de « ObservableObject »
ObservableObject :



Implémente INotifyPropertyChanging et INotifyPropertyChanged
Offre des méthodes pour simplement notifier d'un changement de valeur
(RaisePropertyChanging et RaisePropertyChanged)
Offre des méthodes effectuant la mise à jour de la valeur de la propriété puis déclenchant
RaisePropertyChanged (Set<T>)
public class Person : ObservableObject
{
}
1ère possibilité – Notification simple
RaisePropertyChanged();
RaisePropertyChanged("FullName");
RaisePropertyChanged<string>(() => FirstName);
2ème possibilité – Modification de la valeur + Notification
(Quelques-unes des écritures possibles)
Set(ref _firstName, value);
Set(ref _firstName, value,"FirstName");
Set(() => FirstName, ref _firstName, value);
Set<string>(ref _firstName, value);
Set<string>(ref _firstName, value,"FirstName");
Set<string>("FirstName", ref _firstName, value);
Set<string>(() => FirstName, ref _firstName, value);
Concrètement on aura tendance à utiliser
Set<string>(() => FirstName, ref _firstName, value);
Set retourne un booléen indiquant si PropertyChanged a été déclenché.
if (Set<string>(() => FirstName, ref _firstName, value))
{
// IsDirty = true;
}
43
44
3. ViewModel
Les ViewModels héritent de « ViewModelBase ».
ViewModelBase :


Hérite de « ObservableObject »
Offre de nouvelles signatures pour RaisePropertyChanged et Set<T>
Exemple de ViewModel simple
using GalaSoft.MvvmLight;
using MvvmLightDemo.Models;
using System.Collections.ObjectModel;
namespace MvvmLightDemo.ViewModels
{
public class PeopleViewModel : ViewModelBase
{
private ObservableCollection<Person> _people;
public ObservableCollection<Person> People
{
get { return _people; }
set { Set<ObservableCollection<Person>>(() => People, ref _people, value);
}
}
}
}
a-Commandes
RelayCommand et RelayCommand<T> (T étant le type du paramètre passé) .Implémentent
ICommand.
+ RaiseCanExecuteChanged
b-Messenger
Communication de « messages » entre ViewModels
Exemple On a deux vues, chacune ayant un ViewModel .On s’abonne dans le ViewModel « détail »
afin d’afficher l’élément couramment sélectionné.
Register
Messenger.Default.Register<Person>(this, p =>
{
this.Person = p;
});
Send
Messenger.Default.Send<Person>(CurrentPerson);
 On pourrait aussi créer une classe générique.
44
45
Register
Messenger.Default.Register<NotificationMessage<Person>>(this, n =>
{
this.Person = n.Content;
});
Send
Messenger.Default.Send<NotificationMessage<Person>>(new
NotificationMessage<Person>(CurrentPerson,"Key01"));
4. SimpleIoC
Utilisation de Microsoft.Practices.ServiceLocator
Constructor Injection et Property Injection
public class ViewModelLocator
{
public PeopleViewModel PeopleViewModel
{
get { return SimpleIoc.Default.GetInstance<PeopleViewModel>(); }
}
public PersonViewModel PersonViewModel
{
get { return SimpleIoc.Default.GetInstance<PersonViewModel>(); }
}
public ViewModelLocator()
{
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
// Services
SimpleIoc.Default.Register<IDialogService, DialogService>();
SimpleIoc.Default.Register<IPeopleService, PeopleService>();
// ViewModels
SimpleIoc.Default.Register<PersonViewModel>();
SimpleIoc.Default.Register<PeopleViewModel>();
}
}
Injection de dépendances .Le ViewModel n’a pas de constructeur par défaut
 Utilisation du ViewModelLocator pour définir le DataContext des vues
o App.xaml
45
46
<Application x:Class="MvvmLightDemo.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:MvvmLightDemo.ViewModels"
StartupUri="MainWindow.xaml">
<Application.Resources>
<vm:ViewModelLocator x:Key="ViewModelLocator" />
</Application.Resources>
</Application>
o
Vue
Référencement du ViewModelLocator
Puis utilisation dans les vues
<Window x:Class="MvvmLightDemo.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525"
DataContext="{Binding PeopleViewModel, Source={StaticResource
ViewModelLocator}}">
46
47
III.
Prism
1. Installation
PM> Install-Package Prism
PM> Install-Package Prism.UnityExtensions
Documentation : Prism 5.0 (Wpf/ .Net 4.5), Prism 4.1 (Silverlight 5, Windows Phone 7, et Wpf/.Net
4.0)
Lirairies portables (Prism 5.0):
 « Prism.Mvvm » : commandes (Delegate et composite commands), BindableBase, IView,
etc.
 « Prism.PubSubEvents » : Envoi de messages aux abonnés
2. Mémento
Un module par
« feature »
Un module
Vue pour la navigation
« Main » projet
App (startup)
Lancement de l’application
(création d’une instance du
boostrapper)
Bootstrapper
(Unity ou MEF)
Enregistrement, création du
shell. Enregistrement des
modules, services partagés
Shell
Une région
Vues (implémentent IView), DataContext
sur le viewmodel injecté
ViewModel (peut
implémenter une interface)
Classe de configuration du module
(implémentant IModule). Enregistrement
des services, viewmodels dans le
conteneur. Enregistrement des vues pour
une région
Une autre
région
ShellViewModel (peut
implémenter une interface)
Interface du
viewmodel
Les régions sont des
ContentControls,
ItemsControls ou autre
(avec adaptation de région)
Projet « Infrastructure », classes
partagées par les différents projets
Projet de services partagés par
plusieurs modules
47
48
a. Shell
Shell- Création de la fenêtre principale de l’application
Supprimer « MainWindow » et créer une nouvelle fenêtre nomée « Shell ». Supprimer « StartupUri »
du fichier « App »
b. Boostrapper
Pour utiliser un boostrapper avec Unity installer …
PM> Install-Package Prism.UnityExtensions
public class Bootstrapper : UnityBootstrapper
{
protected override DependencyObject CreateShell()
{
return Container.TryResolve<Shell>();
}
Définition et affichage de la
fenêtre principale
protected override void InitializeShell()
{
base.InitializeShell();
App.Current.MainWindow = (Window)Shell;
App.Current.MainWindow.Show();
}
}
c. Lancement de l’application
public partial class App : Application
{
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
var bootstrapper = new Bootstrapper();
bootstrapper.Run();
}
}
Processus du bootstrapper :
LoggerFacade Module CatalogContainerRegion Adapter mappingsRegion
BehaviorsException TypesCreate ShellInitialize ShellInitialize Modules
d. Régions
Définir les regions du Shell
Ajouter le namespace
<Window …
xmlns:prism="http://www.codeplex.com/prism">
<Grid>
<Grid.RowDefinitions>
Exemple enregistrement de 2 régions
<RowDefinition Height="30"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ContentControl prism:RegionManager.RegionName="ToolbarRegion" />
<ContentControl prism:RegionManager.RegionName="ContentRegion" Grid.Row="1" />
</Grid>
</Window>
48
49
Pour éviter les erreurs de saisie on peut créer une classe de variables static dans le projet
« Infrastructure »
public class RegionNames
{
public static string ToolbarRegion = "ToolbarRegion";
public static string ContentRegion = "ContentRegion";
}
… Adaptation du code du Shell
<Window …
xmlns:prism="http://www.codeplex.com/prism"
xmlns:inf ="clr-namespace:PrismDemo.Infrastructure;assembly=PrismDemo.Infrastructure"
>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition/>
</Grid.RowDefinitions>
<ContentControl prism:RegionManager.RegionName="{x:Static inf:RegionNames.ToolbarRegion}" />
<ContentControl prism:RegionManager.RegionName="{x:Static inf:RegionNames.ContentRegion}"
Grid.Row="1" />
</Grid>
</Window>
QuickStart : UI Composition
Toolbar et navigation
Utiliser un ItemsControl ou créer une adaptation de régions
pour que les différentes vues de navigation s’ajoutent dans
une région (« ToolbarRegion » par exemple)
Exemple de région
<ItemsControl prism:RegionManager.RegionName="{x:Static inf:RegionNames.ToolbarRegion}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
49
50
Adaptation de région
Pour les autres conteneurs que « ContentControl » et « ItemsControl », il faut faire une adaptation
de région pour qu’ils sachent comment ajouter les vues à la région.
-
Dérive de RegionAdapter<T> T étant le type de « conteneur » à adapter)
Implemente les méthodes « Adapt » et « CreateRegion »
Exemple avec StackPanel
public class StackPanelRegionAdapter : RegionAdapterBase<StackPanel>
{
public StackPanelRegionAdapter(IRegionBehaviorFactory regionBehaviorFactory)
: base(regionBehaviorFactory)
{ }
protected override void Adapt(IRegion region, StackPanel regionTarget)
{
region.Views.CollectionChanged += (s, e) =>
{
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (FrameworkElement element in e.NewItems)
{
regionTarget.Children.Add(element);
}
}
};
}
protected override IRegion CreateRegion()
{
return new AllActiveRegion();
}
}
Shell
<StackPanel Orientation="Horizontal"
prism:RegionManager.RegionName="{x:Static inf:RegionNames.ToolbarRegion}"/>
Bootstrapper
protected override RegionAdapterMappings ConfigureRegionAdapterMappings()
{
RegionAdapterMappings mappings = base.ConfigureRegionAdapterMappings();
mappings.RegisterMapping(typeof(StackPanel), Container.Resolve<StackPanelRegionAdapter>());
return mappings;
}
c. ShellViewModel
public interface IShellViewModel : IViewModel
{
}
public class ShellViewModel :ViewModelBase , IShellViewModel
{
private readonly IRegionManager _regionManager;
public ICommand NavigateCommand { get; private set; }
50
51
public ShellViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
NavigateCommand = new DelegateCommand<object>(Navigate);
GlobalCommands.NavigateCommand.RegisterCommand(NavigateCommand);
}
private void Navigate(object navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate("ContentRegion",
navigatePath.ToString(), NavigationComplete);
}
private void NavigationComplete(NavigationResult result)
{
//log
}
}
Code-behind du Shell
public partial class Shell : Window
{
public Shell(IShellViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
Dans le bootstrapper
protected override void ConfigureContainer()
{
base.ConfigureContainer();
((UnityContainer)Container).RegisterType<IShellViewModel, ShellViewModel>();
}
51
52
d. Modules
Installation pour chaque module
PM> Install-Package Prism
PM> Install-Package Prism.UnityExtensions
Et référencer le projet « Infrastructure »
Configuration du module
public class TeachersModule : IModule
{
private IUnityContainer _container;
private IRegionManager _regionManager;
Injection du conteneur et de regionManager
public TeachersModule(IUnityContainer container, IRegionManager regionManager)
{
_container = container;
_regionManager = regionManager;
}
Enregistrement des
« viewmodels » et services dans le
conteneur
public void Initialize()
{
// conteneur : viewmodels, services du module,...
_container.RegisterType<IAddTeacherViewModel, AddTeacherViewModel>();
_container.RegisterType<ITeachersViewModel, TeachersViewModel>();
Enregistrement des vues pour
// vues
// navigation
une région et la navigation
_regionManager.RegisterViewWithRegion(RegionNames.ToolbarRegion,
typeof(TeachersNavigationView));
// enregistrement de chaque vue pour une région
_regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(AddTeacherView));
_regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(TeachersView));
}
}
52
53
Avoir plus de contrôle sur la vue affichée (Activate/ Deactivate)
La méthode « RegisterViewWithRegion » offre peu de contrôle sur la vue.
Exemple on a deux vues enregistrées pour la region « ContentRegion ». On choisit quelle vue afficher
public void Initialize()
{
// …
_regionManager.RegisterViewWithRegion(RegionNames.ContentRegion, typeof(AddTeacherView));
var view = _container.Resolve<TeachersView>();
IRegion contentRegion = _regionManager.Regions[RegionNames.ContentRegion];
contentRegion.Add(view);
contentRegion.Activate(view);
On peut activer, désativer
la vue
}
On récupère la vue.
« ViewModel First »
on récupèrerait la vue
du viewmodel
Module Lifetime
Register Modules  Discover Modules  Load Modules  Initialize Modules



Register/Discover Modules : par code, Module Catalog,Xaml, Configuration (Xaml),Wpf
Loading Modules : Wpf, load On demand/When available
Initialize Modules : IModule.Initialize(), register types/services/views, subscribe to
services/events
On peut créer une interface base IViewModel dans le projet « Infrastructure »
public interface IViewModel
{
}
ViewModel
On pourrait avoir plusieurs « ViewModels » implémentant
« ITeachersViewModel » et décider avec le conteneur quel
viewmodel injecter dans la vue « TeachersView »
public interface ITeachersViewModel : IViewModel
{
}
public class TeachersViewModel : ViewModelBase, ITeachersViewModel
{
// etc.
Le viewmodel implémente l’interface.
}
Injecter les services dont le viewmodel a
besoin
Vue
La implémente IView (Microsoft.Practices.Prism.Mvvm)
public partial class TeachersView : UserControl, IView
{
public TeachersView(ITeachersViewModel viewModel)
{
InitializeComponent();
DataContext = viewModel;
}
}
53
54
Adaptation du Bootstrapper
Référencer le « Module »
Dans le « Bootstrapper »
protected override IModuleCatalog CreateModuleCatalog()
{
var catalog = new ModuleCatalog();
catalog.AddModule(typeof(TeachersModule));
// autres modules ..
return catalog;
}
« ViewModel First »
Vue
IView
ITeachersView
public interface ITeachersView : IView { }
TeachersView
public partial class TeachersView : UserControl, ITeachersView
{
public TeachersView()
{
InitializeComponent();
}
}
54
55
ViewModel
IViewModel
public interface IViewModel
{
IView View { get; set; }
}
ITeachersViewModel
public interface ITeachersViewModel : IViewModel
{
}
TeachersViewModel
public class TeachersViewModel : ViewModelBase, ITeachersViewModel
{
private ITeachersService _teachersService;
public ObservableCollection<Teacher> Teachers { get; set; }
public ITeachersView View { get; set; }
public TeachersViewModel(ITeachersView view, ITeachersService teachersService)
{
_teachersService = teachersService;
View = view;
View.DataContext = this;
var list = _teachersService.GetAll();
Teachers = new ObservableCollection<Teacher>(list);
OnPropertyChanged("Teachers");
}
}
Enregistrement ( configuration du module)
public void Initialize()
{
_container.RegisterType<ITeachersView, TeachersView>();
_container.RegisterType<ITeachersViewModel, TeachersViewModel>();
var vm = _container.Resolve<ITeachersViewModel>();
IRegion contentRegion = _regionManager.Regions[RegionNames.ContentRegion];
contentRegion.Add(vm.View);
contentRegion.Activate(vm.View);
}
55
56
Module chargé à la demande
1. Dans le bootstrapper
protected override IModuleCatalog CreateModuleCatalog()
{
var catalog = new ModuleCatalog();
catalog.AddModule(typeof(TeachersModule));
// On demand
var moduleType = typeof(CoursesModule);
catalog.AddModule(new ModuleInfo()
{
ModuleName = "CoursesModule",
ModuleType = moduleType.AssemblyQualifiedName,
InitializationMode = InitializationMode.OnDemand
});
return catalog;
}
2. Pour charger le module il faut injecter « IModuleManager moduleManager » dans le
ViewModel
3. Charger le module puis naviguer vers la vue désirée du module
_moduleManager.LoadModule("CoursesModule");
_regionManager.RequestNavigate("ContentRegion", "CoursesView");
QuickStart: Modularity
e. Navigation
QuickStart : ViewSwitchingNavigation
Navigation avec le « regionManager »
Il faut injecter IRegionManager dans le ViewModel
public class TeachersViewModel : ViewModelBase, ITeachersViewModel
{
private IRegionManager _regionManager;
public DelegateCommand AddTeacherCommand { get; private set; }
public TeachersViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
Utilisation de la méthode
« RequestNavigate » du
RegionManager à laquelle on
passe le nom de la vue (ou
l’uri) vers laquelle naviguer
AddTeacherCommand = new DelegateCommand(() =>
{
regionManager.RequestNavigate(RegionNames.ContentRegion, "AddTeacherView");
});
}
}
56
57
Avec passage de paramètre
var uri = string.Format("AddTeacherView?title={0}", "Ajout d'un nouvel enseignant");
_regionManager.RequestNavigate(RegionNames.ContentRegion,uri);
Pour récupèrer le paramètre, le viewmodel « récepteur » doit implémenter INavigationAware
public class AddTeacherViewModel : ViewModelBase, IAddTeacherViewModel,
INavigationAware
{
public string Title { get; set; }
// etc.
public bool IsNavigationTarget(NavigationContext navigationContext)
{
return true;
}
public void OnNavigatedFrom(NavigationContext navigationContext)
{
}
public void OnNavigatedTo(NavigationContext navigationContext)
{
var parameters = navigationContext.Parameters;
if (parameters != null)
{
Title = parameters["title"].ToString();
OnPropertyChanged("Title");
}
}
}
Processus de navigation avec INavigationAware :
RequestNavigateOnNavigatedFromIsNavigationTargetResolveViewOnNavigatedToNaviga
tionComplete
Avec NavigationParameters
var parameters = new NavigationParameters();
parameters.Add("title", "Ajout d'un nouvel enseignant!");
_regionManager.RequestNavigate("ContentRegion", new Uri("AddTeacherView", UriKind.Relative), parameters);
Dans le ViewModel Récepteur
public void OnNavigatedTo(NavigationContext navigationContext)
{
var parameters = navigationContext.Parameters as NavigationParameters;
if (parameters != null && parameters["title"] != null)
{
Title = parameters["title"].ToString();
OnPropertyChanged("Title");
}
}
Avec Prism 4.1 on peut utiliser UriQuery
57
58
Création d’une commande de navigation globale
public class GlobalCommands
{
public static CompositeCommand NavigateCommand = new CompositeCommand();
}
Dans ShellViewModel
public class ShellViewModel :ViewModelBase , IShellViewModel
{
private readonly IRegionManager _regionManager;
public ICommand NavigateCommand { get; private set; }
public ShellViewModel(IRegionManager regionManager)
{
_regionManager = regionManager;
//
NavigateCommand = new DelegateCommand<object>(Navigate);
GlobalCommands.NavigateCommand.RegisterCommand(NavigateCommand);
}
private void Navigate(object navigatePath)
{
if (navigatePath != null)
_regionManager.RequestNavigate("ContentRegion",
navigatePath.ToString(), NavigationComplete);
}
private void NavigationComplete(NavigationResult result)
{
//log
}
}
Utilisation dans Xaml (depuis une vue de navigation)
<UserControl …
xmlns:inf="clr-namespace:PrismDemo.Infrastructure;assembly=PrismDemo.Infrastructure"
xmlns:views="clr-namespace:PrismDemo.Teachers.Views">
<StackPanel Orientation="Horizontal">
<Button Content="Liste" Command="{x:Static inf:GlobalCommands.NavigateCommand}"
CommandParameter="{x:Type views:TeachersView}" />
<Button Content="Ajouter" Command="{x:Static inf:GlobalCommands.NavigateCommand}"
CommandParameter="{x:Type views:AddTeacherView}" />
<!-- ect. -->
</StackPanel>
</UserControl>
58
59
Navigation Journal
On récupère le journal de navigation depuis « navigationContext »
private IRegionNavigationJournal _journal;
// etc.
public void OnNavigatedTo(NavigationContext navigationContext)
{
_journal = navigationContext.NavigationService.Journal;
}
On peut utiliser le journal pour revenir en arrière par exemple.
if (_journal.CanGoBack)
_journal.GoBack();
SI le journal est vide, c’est parce qu’il faut appeler la commande pour naviguer vers la page
« d’accueil » au lancement de l’application depuis l’initialisation du module
GlobalCommands.NavigateCommand.Execute("PeopleView");
Region Context
Affichage du détail de la
personne sélectionnée
dans la liste avec
RegionContext.
Créer une région
« PersonDetailsRegion »
<ListBox x:Name="lsPeople" ItemsSource="{Binding People}" />
<ContentControl Grid.Row="1"
prism:RegionManager.RegionName="PersonDetailsRegion"
prism:RegionManager.RegionContext="{Binding SelectedItem, ElementName=lsPeople}"/>
59
60
Dans la vue « PersonDetailsView »
public partial class PersonDetailsView : UserControl, IView
{
public PersonDetailsView(IPersonDetailsViewModel viewModel)
{
InitializeComponent();
ViewModel = viewModel;
RegionContext.GetObservableContext(this).PropertyChanged += (s, e) =>
{
var context = (ObservableObject<object>)s;
var currentPerson = (Person)context.Value;
(ViewModel as PersonDetailsViewModel).Person = currentPerson;
};
}
public IViewModel ViewModel
{
get { return (IViewModel)DataContext; }
set { DataContext = value; }
}
}
Confirmer, Annuler la navigation (IConfirmNavigationRequest)
IConfirmNavigationRequest Process
RequestNavigateConfirmNavigationRequestOnNavigatedFromcontine navigation process
Utile par exemple pour demander à l’utilisateur s’il désire sauvegarder des changements avant de
quitter une page.
public class PersonDetailsViewModel :
ViewModelBase,
IPersonDetailsViewModel,
INavigationAware,
IConfirmNavigationRequest
{
//
}
public void ConfirmNavigationRequest(NavigationContext navigationContext, Action<bool>
continuationCallback)
{
bool result = true;
if (MessageBox.Show("Navigation…",
"Désirez-vous vraiment quitter la page?",
MessageBoxButton.YesNo) == MessageBoxResult.No)
{
result = false;
}
continuationCallback(result);
}
60
61
RegionMemberLifetime
La View/ViewModel sont supprimés de la région si KeepAlive réglé à false, à chaque navigation vers
une autre page.
public class PersonDetailsViewModel :
ViewModelBase,
IPersonDetailsViewModel,
INavigationAware,
IRegionMemberLifetime
{
public bool KeepAlive
{
get { return false; }
}
}
Navigation grâce à VisualStateManager
QuickStart: Stated-Based Navigation
A utiliser pour afficher les mêmes données avec un style différent.
Exemple un affichage « liste » et un affichage « avatars » pour la même View.
On change la vue par l’intermédiaire du VisualStateManager (exemple ici avec un ToggleButton)
61
62
3. Mvvm (Prism.Mvvm)
a. DelegateCommand
Sans paramètre
public ICommand AddTeacherCommand { get; private set; }
AddTeacherCommand= new DelegateCommand(() =>
{
});
Avec paramètre
public ICommand AddTeacherCommand { get; private set; }
AddTeacherCommand= new DelegateCommand<Teacher>((teacher) =>
{
});
Avec condition d’exécution
On déclare avec DelegateCommand afin de pouvoir déclencher
RaiseCanExecuteChanged
public DelegateCommand AddTeacherCommand { get; private set; }
AddTeacherCommand = new DelegateCommand(() =>
{
}, () =>
{
return !CurrentTeacher.HasErrors;
});
Déclencher l’éxécution de « CanExecute »
AddTeacherCommand.RaiseCanExecuteChanged();
b. CompositeCommand
Commande « globale », on peut y enregistrer une ou plusieurs commandes
« simples »(DelegateCommand)
1. Création de commandes et enregistrement dans la commande globale
SaveCommand1 = new DelegateCommand(Save, CanSave);
SaveCommand2 = new DelegateCommand(Save, CanSave);
GlobalCommands.SaveAllCommand.RegisterCommand(SaveCommand1);
GlobalCommands.SaveAllCommand.RegisterCommand(SaveCommand2);
2. La commande composite ne peut s’exécuter que si toutes les commandes enregistrées
peuvent s’exécuter.
3. En exécutant la commande globale, toutes les commandes enregistrées sont exécutées
GlobalCommands.SaveAllCommand.Execute();
Depuis le Xaml (inf étant le namespace du projet)
<Button Command="{x:Static inf:ApplicationCommands.SaveAllCommand}" Content="Enregistrer tout" />
QuickStart : Commanding
62
63
c. BindableBase
Les Models et ViewModels peuvent utiliser BindableBase (INotifyPropertyChanged)
public class Teacher : BindableBase
{
}
Mise à jour de la valeur et notification de changement avec la méthode « SetProperty »
private string _name;
public string Name
{
get { return _name; }
set { SetProperty(ref _name, value); }
}
Notification de changement
OnPropertyChanged("Teachers");
Ou avec expression
OnPropertyChanged(() => Teachers);
d. ViewModelLocator
Prism utilise la convention par défaut pour retrouver les correspondances entre View/ViewModel.
Cette convention est plus basée sur les namespaces que les « dossiers » :
-
Vues dans dossier « Views » (namespace « *.Views »)
ViewModels dans dossier « ViewModels » (namespace « *.ViewModels »)
Nom du ViewModel = nom de la View + « ViewModel »
(Exemple pour « Shell », son ViewModel se nomme « ShellViewModel »)
« AutoWireViewModel »
En indiquant ViewModelLocator.AutoWireViewModel="True" sur la fenêtre principale, le
ViewModelLocator va utiliser sa convention pour essayer de retrouver le « ViewModel » de la vue
sans qu’on ne l’ait enregistré. Les vues doivent implémenter IView
<Window …
xmlns:prism="clrnamespace:Microsoft.Practices.Prism.Mvvm;assembly=Microsoft.Practices.Prism.Mvvm.Desktop"
xmlns:p="http://www.codeplex.com/prism"
prism:ViewModelLocator.AutoWireViewModel="True">
<Grid>
<ContentControl p:RegionManager.RegionName="ContentRegion" />
</Grid>
</Window>
Exemple dans la configuration d’un module, on enregistre la vue pour une région mais pas de
ViewModel dans le conteneur. (Note pour l’exemple le ViewModel n’implémente pas d’interface
sinon le conteneur serait dans l’impossibilité de résoudre la correspondance)
public void Initialize()
{
_regionManager.RegisterViewWithRegion("ContentRegion", typeof(TeachersView));
}
63
64
Méthode appelée à chaque fois que le
ViewModelLocator essaie de résoudre le
ViewModel d’une vue
Changer la convention avec le ViewModelLocatorProvider
(Toujours sans enregistrer les ViewModels dans le conteneur)
Exemple ViewModels et Views rangés dans un même dossier et namespace « *.Views »
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
Type de la vue .. exemple …Name : « Shell », FullName
« PrismViewModelLocatorDemo.Views.Shell »
ViewModelLocationProvider.SetDefaultViewTypeToViewModelTypeResolver((viewType) =>
{
var viewName = viewType.FullName;
var viewAssemblyName = viewType.GetTypeInfo().Assembly.FullName;
// PrismViewModelLocatorDemo, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
var viewModelName = String.Format(CultureInfo.InvariantCulture,
"{0}ViewModel, {1}", viewName, viewAssemblyName);
// PrismViewModelLocatorDemo.Views.ShellViewModel, PrismViewModelLocatorDemo,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null
return Type.GetType(viewModelName);
});
var bootstrapper = new Bootstrapper();
bootstrapper.Run();
Retourne le type du ViewModel retrouvé
par son « chemin »
}
Création d’instance des « ViewModels »… (Dans « OnStartup »)
ViewModelLocationProvider.SetDefaultViewModelFactory((type) =>
{
return Activator.CreateInstance(type);
});
64
65
Problème ne peut résoudre les paramètres injectés …
Création d’instance avec un conteneur (exemple Unity)
public partial class App : Application
{
IUnityContainer _container = new UnityContainer();
protected override void OnStartup(StartupEventArgs e)
{
base.OnStartup(e);
_container.RegisterType<IPeopleService,PeopleService>();
ViewModelLocationProvider.SetDefaultViewModelFactory((type) =>
{
return _container.Resolve(type);
});
}
}
4. PubSubEvents (Prism.PubSubEvents)
QuickStart: EventAggregation
Permet l’échange de messages entre les composants de l’application
Dans le projet Infrastructure on crée les « Events ». Exemple
public class MessageEvent : PubSubEvent<string>
{ }
Injecter « IEventAggregator » pour les abonnés et publieurs
private IEventAggregator _eventAggregator;
public PersonDetailsViewModel(IEventAggregator eventAggregator)
{
_eventAggregator = eventAggregator;
}
Abonnement à l’event
_eventAggregator.GetEvent<MessageEvent>().Subscribe((message) => {
//
});
Publication d’un message
_eventAggregator.GetEvent<MessageEvent>().Publish("Un message ...");
65
66
5. Services
a. Service de module
« Couple interface et service »
public interface ITeachersService
{
IEnumerable<Teacher> GetAll();
void Add(Teacher teacher) ;
}
public class TeachersService : ITeachersService
{
private static List<Teacher> teachers = new List<Teacher>() {
new Teacher("Ellen Poe"),
new Teacher("Marie Bellin")
};
public IEnumerable<Teacher> GetAll()
{
return teachers;
}
public void Add(Teacher teacher)
{
teachers.Add(teacher);
}
}
On enregistre les services dans la classe de configuration du module
_container.RegisterType<ITeachersService, TeachersService>();
On injecte ensuite le service dans les ViewModels l’utilisant
public class TeachersViewModel : ViewModelBase, ITeachersViewModel
{
private IRegionManager _regionManager;
private ITeachersService _teachersService;
public ObservableCollection<Teacher> Teachers { get; set; }
public TeachersViewModel(IRegionManager regionManager, ITeachersService teachersService)
{
_regionManager = regionManager;
_teachersService = teachersService;
var list = _teachersService.GetAll();
Teachers = new ObservableCollection<Teacher>(list);
OnPropertyChanged("Teachers");
}
}
}
66
67
b. Services partagés
On crée un projet pour les services partagés par plusieurs modules.
Création d’un projet avec les « Models » partagés
Interface pour le service dans le projet
« Infrastructure »
Module avec le service
Configuration du module
public class TeachersServiceModule : IModule
{
private IUnityContainer _container;
public TeachersServiceModule(IUnityContainer container)
{
_container = container;
}
« Singleton »
public void Initialize()
{
_container.RegisterType<ITeachersService, TeachersService>(new
ContainerControlledLifetimeManager());
}
}
67
68
Référencer le module dans le boostrapper
protected override IModuleCatalog CreateModuleCatalog()
{
var catalog = new ModuleCatalog();
catalog.AddModule(typeof(TeachersServiceModule));
catalog.AddModule(typeof(TeachersModule));
catalog.AddModule(typeof(CoursesModule));
return catalog;
}
Les modules utilisant le service n’ont pas de référence directe au service mais seulement au projet
« Business » et « Infrastructure ». On injecte le service dans les ViewModels.
68
69
6. Projet Infrastructure
IViewModel : interface de base pour les ViewModels
public interface IViewModel
{
}
ViewModelBase : classe de base pour les ViewModels
public class ViewModelBase : BindableBase, IViewModel{ }
ModuleBase : classe de base pour la configuration des modules
public abstract class ModuleBase : IModule
{
protected IRegionManager RegionManager { get; private set; }
protected IUnityContainer Container { get; private set; }
public ModuleBase(IUnityContainer container, IRegionManager regionManager)
{
Container = container;
RegionManager = regionManager;
}
public void Initialize()
{
RegisterTypes();
InitializeModule();
}
protected abstract void InitializeModule();
protected abstract void RegisterTypes();
}
Events : Events pour EventAggregator
public class MessageEvent : PubSubEvent<string>
{ }
69
70
GlobalCommands : les commandes composites de l’application
public class GlobalCommands
{
public static CompositeCommand NavigateCommand = new CompositeCommand();
}
RegionNames : pour éviter les erreurs de saisies dans le nom des régions.
public class RegionNames
{
public static string ToolbarRegion = "ToolbarRegion";
public static string ContentRegion = "ContentRegion";
}
Etc.
7. Interactivity (Prism.Interactivity)
PM> Install-Package Prism.Interactivity
Ajouter une référence à « System.Windows.Interactivity »
QuickStart: Interactivity
a. Notification
Avec « InteractionRequest »
<Window …
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:prism="http://www.codeplex.com/prism">
<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding NotificationRequest}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True" />
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
<StackPanel>
<Button Content="Notification" Command="{Binding NotificationCommand}"></Button>
</StackPanel>
</Window>
public class MainWindowViewModel
{
public InteractionRequest<INotification> NotificationRequest { get; set; }
public ICommand NotificationCommand { get; set; }
public MainWindowViewModel()
{
NotificationRequest = new InteractionRequest<INotification>();
NotificationCommand = new DelegateCommand(() =>
70
71
{
NotificationRequest.Raise(new Notification() { Title = "Notification", Content = "Message
...", }, (c) =>
{
// callback
});
});
Calllback en 3ème
paramètre
}
}
Avec « DefaultNotificationWindow »
NotificationCommand = new DelegateCommand(() =>
{
var window = new DefaultNotificationWindow()
{
Width = 200,
Height = 150d,
WindowStartupLocation = WindowStartupLocation.CenterScreen
};
window.Notification = new Notification()
{
Title = "Notification",
Content = "Message..."
};
window.ShowDialog();
});
b. Confirmation
Avec « InteractionRequest »
Seule la source change par
<Window …
rapport à l’exemple précédent
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:prism="http://www.codeplex.com/prism">
<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding ConfirmationRequest}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True" />
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
<StackPanel>
<Button Content="Confirmation" Command="{Binding ConfirmationCommand}"></Button>
</StackPanel>
</Window>
public class MainWindowViewModel
{
71
72
public InteractionRequest<IConfirmation> ConfirmationRequest { get; set; }
public ICommand ConfirmationCommand { get; set; }
public MainWindowViewModel()
{
ConfirmationRequest = new InteractionRequest<IConfirmation>();
ConfirmationCommand = new DelegateCommand(() =>
{
ConfirmationRequest.Raise(new Confirmation { Title = "Confirmation", Content =
"Confirmez-vous? ..." }, (c) =>
{
if (c.Confirmed)
{
//
}
});
});
}
}
Avec « DefaultConfirmationWindow »
ConfirmationCommand = new DelegateCommand(() =>
{
var window = new DefaultConfirmationWindow()
{
Width = 200,
Height = 150d,
WindowStartupLocation = WindowStartupLocation.CenterScreen
};
window.Confirmation = new Confirmation() { Content = "Confirm ?",
Title = "Confirmez-vous ? …" };
window.ShowDialog();
var dialogResult = window.Confirmation.Confirmed;
if (dialogResult)
{
}
});
72
73
c. Custom
Custom popup
<Window …
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:prism="http://www.codeplex.com/prism">
<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding CustomPopupRequest}">
<prism:PopupWindowAction IsModal="True" CenterOverAssociatedObject="True">
<prism:PopupWindowAction.WindowContent>
<local:MyUserControl />
On crée un « UserControl » que
</prism:PopupWindowAction.WindowContent>
l’on ajoute
</prism:PopupWindowAction>
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
<StackPanel>
<Button Content="Custom" Command="{Binding CustomPopupCommand}"></Button>
</StackPanel>
</Window>
public class MainWindowViewModel
{
public InteractionRequest<INotification> CustomPopupRequest { get; set; }
public ICommand CustomPopupCommand { get; set; }
public MainWindowViewModel()
{
CustomPopupRequest = new InteractionRequest<INotification>();
CustomPopupCommand = new DelegateCommand(() =>
{
CustomPopupRequest.Raise(new Notification { Title = "Custom!", Content = "Message... " },
(c) =>
{
MessageBox.Show("Intéraction finie!");
});
});
}
}
Le « userControl » peut implémenter « IInteractionRequestAware »
public partial class MyUserControl : UserControl, IInteractionRequestAware
{
public MyUserControl()
{
InitializeComponent();
}
public Action FinishInteraction { get; set; }
public INotification Notification { get; set; }
private void Button_Click(object sender, RoutedEventArgs e)
{
if (FinishInteraction != null)
Le callback sera déclenché
FinishInteraction();
}
}
73
74
Custom Notification
Dans le Shell ou vue désirée
<i:Interaction.Triggers>
<prism:InteractionRequestTrigger SourceObject="{Binding ShowMessageRequest}">
<inf:ShowNotificationAction TargetName="NotificationList" />
</prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
La zone affichée
<Grid x:Name="NotificationList"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Visibility="{Binding Count, Converter={StaticResource SizeToVisibilityConverter},
Mode=OneWay, FallbackValue=Collapsed}">
<ItemsControl ItemTemplate="{StaticResource MessageNotificationTemplate}"
ItemsSource="{Binding}" />
</Grid>
ShellViewModel
public InteractionRequest<INotification> ShowMessageRequest { get; set; }
ShowMessageRequest = new InteractionRequest<INotification>();
Notification notification = new Notification();
notification.Title = "PrismOverview :";
notification.Content = message;
ShowMessageRequest.Raise(notification);
(template)
<DataTemplate x:Key="MessageNotificationTemplate">
<Border BorderThickness="2" BorderBrush="SkyBlue" Background="White"
CornerRadius="5" IsHitTestVisible="False" Opacity="0.6">
<StackPanel Orientation="Vertical" Width="200">
<TextBlock Padding="5"><Run Text="{Binding Content, Mode=OneWay}"
FontStyle="Italic" FontSize="14"/></TextBlock>
</StackPanel>
</Border>
</DataTemplate>
74
75
(et converter)
public class SizeToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (value is int)
{
if ((int)value > 0)
{
return Visibility.Visible;
}
}
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
75
76
(Trigger)
public class ShowNotificationAction : TargetedTriggerAction<FrameworkElement>
{
public static readonly DependencyProperty NotificationTimeoutProperty =
DependencyProperty.Register("NotificationTimeout", typeof(TimeSpan),
typeof(ShowNotificationAction), new PropertyMetadata(new TimeSpan(0, 0, 5)));
private ObservableCollection<object> notifications;
public ShowNotificationAction()
{
this.notifications = new ObservableCollection<object>();
}
public TimeSpan NotificationTimeout
{
get { return (TimeSpan)GetValue(NotificationTimeoutProperty); }
set { SetValue(NotificationTimeoutProperty, value); }
}
protected override void OnTargetChanged(FrameworkElement oldTarget, FrameworkElement newTarget)
{
base.OnTargetChanged(oldTarget, newTarget);
if (oldTarget != null)
{
this.Target.ClearValue(FrameworkElement.DataContextProperty);
}
if (newTarget != null)
{
this.Target.DataContext = this.notifications;
}
}
protected override void Invoke(object parameter)
{
var args = parameter as InteractionRequestedEventArgs;
if (args == null)
{
return;
}
var notification = args.Context;
this.notifications.Insert(0, notification);
var timer = new DispatcherTimer { Interval = this.NotificationTimeout };
EventHandler timerCallback = null;
timerCallback =
(o, e) =>
{
timer.Stop();
timer.Tick -= timerCallback;
this.notifications.Remove(notification);
};
timer.Tick += timerCallback;
timer.Start();
args.Callback();
}
}
76
77
d. BusyIndicator
Création d’un « UserControl »
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Rectangle Fill="LightGray" Opacity="0.5"/>
<Border BorderBrush="Black" BorderThickness="2" HorizontalAlignment="Center"
VerticalAlignment="Center">
<StackPanel Background="White" >
<TextBlock Margin="20,20,20,10" Name="Message">Loading...</TextBlock>
<ProgressBar Margin="20,0,20,20" IsIndeterminate="True" Height="15" Width="200" />
</StackPanel>
</Border>
</Grid>
 Avec VisualStateManager (Shell)
<VisualStateManager.VisualStateGroups>
<VisualStateGroup>
<VisualState x:Name="Normal"/>
<VisualState x:Name="Loading">
<Storyboard>
<ObjectAnimationUsingKeyFrames Storyboard.TargetProperty="Visibility"
Storyboard.TargetName="busyIndicator">
<DiscreteObjectKeyFrame KeyTime="0:0:0">
<DiscreteObjectKeyFrame.Value>
<Visibility>Visible</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
Le behavior
<i:Interaction.Behaviors>
<ei:DataStateBehavior Binding="{Binding IsBusy}" Value="True" TrueState="Loading" FalseState="Normal"/>
</i:Interaction.Behaviors>
Ajout du busyIndicator.. En bas de la page
<inf:BusyIndicator x:Name="busyIndicator" Visibility="Collapsed" />
77
78
 Avec un converter « BooleanToVisibilityConverter »
public sealed class BooleanToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
return (value is bool && (bool)value) ? Visibility.Visible : Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value is Visibility && (Visibility)value == Visibility.Visible;
}
}
<views:BusyIndicator Visibility="{Binding IsBusy,Converter={StaticResource
BooleanToVisibilityConverter}}"/>
Ajouter une propriété « IsBusy » par exemple dans le ViewModel
public bool IsBusy { get; set; }
… Changer l’état
public async void Refresh()
{
IsBusy = true;
await Task.Delay(2000); //
var result = _peopleRepository.GetAll();
People = new ObservableCollection<Person>(result);
OnPropertyChanged("People");
IsBusy = false;
OnPropertyChanged("IsBusy");
}
78
79
e. InvokeCommandAction
Invoque une ICommand sur un évenement
Exemple :
Invoque la commande SelectPersonCommand quand la sélection change
dans la ListBox
<ListBox ItemsSource="{Binding People}">
<i:Interaction.Triggers>
<i:EventTrigger EventName="SelectionChanged">
<prism:InvokeCommandAction Command="{Binding SelectPersonCommand}"
TriggerParameterPath="AddedItems" />
</i:EventTrigger>
</i:Interaction.Triggers>
</ListBox>
<TextBlock Text="{Binding CurrentPerson}" />
Dans le ViewModel de la View
public ICommand SelectPersonCommand { get; set; }
SelectPersonCommand = new DelegateCommand<object[]>((items) => {
CurrentPerson = items.FirstOrDefault() as Person;
});
Ici la commande récupère un tableau d’objets en parameter avec
TriggerParameterPath. On peut definir le mode de selection de la ListBox sur single
SelectionSelectionMode="Single"
79