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 CatalogContainerRegion Adapter mappingsRegion BehaviorsException TypesCreate ShellInitialize ShellInitialize 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 : RequestNavigateOnNavigatedFromIsNavigationTargetResolveViewOnNavigatedToNaviga 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 RequestNavigateConfirmNavigationRequestOnNavigatedFromcontine 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