Liens
Composants
- A ViewModelBase class
- A Messenger class
- RelayCommand
- EventToCommand behavior
- DispatcherHelper class
ObservableObjet
Permet aux classes de la couche Modèle d'appeler RaisePropertyChanged
|
class Data : ObservableObjet
{
private string _myProperty;
public string MyProperty
{
get { return _myProperty; }
set {
_myProperty = value;
RaisePropertyChanged("MyProperty"); // utilise un string pour nommer la propriété
RaisePropertyChanged(() => MyProperty); // utilise la réflexion pour nommer la propriété
// set value + RaisePropertyChanged
Set(() => MyProperty, ref _myProperty, value);
// exécute (set value + RaisePropertyChanged) seulement si (value ≠ _myProperty) et return true
}
}
|
ViewModelBase
Équivalant d'ObservableObjet pour la couche Vue-Modèle
|
class MainWindowVM : ViewModelBase
{
private string _myProperty;
public string MyProperty
{
get { return _myProperty; }
set {
// set value + RaisePropertyChanged + [optionnel] send message (oldValue, newValue, propertyName)
Set(() => MyProperty, ref _myProperty, value, true);
}
}
public void DesignMode()
{
bool dm = this.IsInDesignMode;
bool dms = ViewModelBase.IsInDesignModeStatic;
}
|
RelayCommand
|
<Button Command="{Binding MyCmd}"
CommandParameter="{Binding ...}"/>
|
|
// sans paramètre
private RelayCommand _myCmd;
public RelayCommand MyCmd
{
get
{
return _myCmd ?? (_myCmd = new RelayCommand(
() => { ... }, // Execute
() => { return true; })); // CanExecute
}
}
// avec paramètre
private RelayCommand<int> _myCmd;
public RelayCommand<int> MyCmd
{
get
{
return _myCmd ?? (_myCmd = new RelayCommand<int>(
param => { ... },
param => { return true; }));
}
}
// force la réévaluation de CanExecute
MyCmd.RaiseCanExecuteChanged();
|
EventToCommand
Action attachée à un Interaction.Triggers qui permet d’exécuter une Command.
|
<Window xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:mvvm="http://www.galasoft.ch/mvvmlight">
<GroupBox Header="My Header">
<!-- Lance la commande ClickCmd lors du clique sur le Header du GroupBox -->
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<mvvm:EventToCommand Command="{Binding ClickCmd}" CommandParameter="..." />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
|
Passer un argument de l’événement à la commande: IEventArgsConverter
|
<Window.Resources>
<helpers:MouseButtonEventArgsToPointConverter x:Key="MouseToPointConverter" />
</Window.Resources>
<i:Interaction.Triggers>
<i:EventTrigger EventName="MouseLeftButtonDown">
<mvvm:EventToCommand Command="{Binding ShowPointCommand}"
PassEventArgsToCommand="True"
EventArgsConverter="{StaticResource MouseToPointConverter}"
EventArgsConverterParameter="{Binding ElementName=PageRoot}" />
</i:EventTrigger>
</i:Interaction.Triggers>
|
|
public class MouseButtonEventArgsToPointConverter : IEventArgsConverter
{
// value → EventArgs (MouseButtonEventArgs)
// parameter → EventArgsConverterParameter
// return the command parameter
public object Convert(object value, object parameter) { ... }
}
|
Messenger
Système de distribution de message (event bus) sans couplage entre le destinataire et l'expéditeur.
- un objet envoie un message, sans savoir qui le recevra
- un ou des objets s'inscrivent pour recevoir ces messages, sans savoir qui les envoie
|
Solution de dernier recourt, préférer un IoC container avec un Service. |
Ce mécanisme permet:
- d'appeler une méthode de la vue depuis le Vue-Modèle.
- de communiquer entre Vue-Modèle.
MainWindow.cs
|
public MainWindow() // ctor de la vue
{
// s'enregistre pour recevoir tous les messages du type NotificationMessage<MyType>
Messenger.Default.Register<NotificationMessage<MyType>>(this, message =>
{
MyType myObject = message.Content;
string notification = message.Notification;
object sender = message.Sender;
object target = message.Target;
});
// s'enregistre pour recevoir tous les messages du type NotificationMessage<MyType> avec un token
Messenger.Default.Register<NotificationMessage<MyType>>(this, token, message => { ... });
// s'enregistre pour recevoir tous les messages du type IMessage et de toutes les classes qui l'implémentent
Messenger.Default.Register<IMessage>(this, true, message => { ... });
/* Messenger utilise des weak references,
ainsi l'objet courant peut quand même être collecté par le GarbageCollector même si Unregister n'a pas été appelé */
// désabonne l'objet courant de toutes les tous les types de messages auquel il était abonné
Messenger.Default.Unregister(this);
// désabonne l'objet courant des message de type NotificationMessage<MyType> auquel il était abonné
Messenger.Default.Unregister<NotificationMessage<MyType>>(this);
// désabonne l'objet courant des message de type NotificationMessage<MyType> pour l'action HandleMessage auquel il était abonné
Messenger.Default.Unregister<NotificationMessage<MyType>>(this, HandleMessage);
}
|
MainViewModel.cs
|
MyCmd = new RelayCommand(() =>
{
MyType myObject;
// créé un message du type NotificationMessage<MyType>
var message = new NotificationMessage<MyType>(myObject, "text message");
// créé un message du type NotificationMessage<MyType> contenant le sender
var message = new NotificationMessage<MyType>(this, myObject, "text message");
// envoie le message
Messenger.Default.Send(message);
// envoie le message avec un token unique
public static readonly Guid token = Guid.NewGuid();
Messenger.Default.Send(message, token);
};
|
|
Messenger.Reset(); passe l'instance à null, au prochain accès l'instance sera recréé car elle est nulle. À appeler au début de chaque test unitaire. |
DispatcherHelper
|
WPF dispatche automatiquement les événements PropertyChanged vers le thread principal. |
Le Dispatcher permet à un sous-thread de modifier des éléments graphiques du thread principal.
- il n'est accessible que depuis la couche vue.
- accès aux éléments graphiques depuis un sous-thread sans le Dispatcher:
InvalidOperationException: The calling thread cannot access this object because a different thread owns it.
|
// initialiser le DispatcherHelper, si possible au lancement de l'application (ctor static App.xaml.cs)
DispatcherHelper.Initialize();
DispatcherHelper.CheckBeginInvokeOnUI(() =>
{
// ce code sera exécuté dans le thread principal
});
|
ViewModelLocator
|
Cette classe est à créer. |
Permet de cibler un Vue-Modèle depuis un dictionnaire de ressources.
Permet de contrôler plus facilement la création des Vue-Modèles et de leurs Services.
Par exemple, à la création de MainViewModel, on veut fournir un IDataService qui correspond soit:
- aux véritables données de l'application: DataService
- à des données statiques pour la visualisation dans un Designer: DesignerDataService
Sans SimpleIoc
ViewModelLocator.cs
|
public class ViewModelLocator
{
public static ViewModelLocator Instance
{
get
{
return Application.Current.Resources["Locator"] as ViewModelLocator;
}
}
public MainViewModel Main { get; private set; }
static ViewModelLocator()
{
IDataService dataService;
// choix du IDataService en fonction du DesignMode
if (ViewModelBase.IsInDesignModeStatic)
{
dataService = new DesignDataService();
}
else
{
dataService = new DataService();
}
Main = new MainViewModel(dataService);
}
}
|
App.xaml
|
<Application xmlns:vm="clr-namespace:MonApplication.ViewModel"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008">
<Application.Resources>
<vm:ViewModelLocator x:Key="Locator" d:IsDataSource="True" />
|
MainWindow.xaml
|
<Window DataContext="{Binding Main, Source={StaticResource Locator}}" >
|
Avec SimpleIoc
|
SimpleIoc permet de se passer de ViewModelLocator.
On peut appeler directement SimpleIoc.Default.GetInstance<MainViewModel>(); pour avoir une instance de MainViewModel.
ViewModelLocator reste utile si on veut obtenir MainViewModel depuis du code xaml pour les DesignTime Data. |
ViewModelLocator.cs
|
public class ViewModelLocator
{
public static ViewModelLocator Instance
{
get
{
return Application.Current.Resources["Locator"] as ViewModelLocator;
}
}
public MainViewModel Main
{
get
{
return SimpleIoc.Default.GetInstance<MainViewModel>();
}
}
static ViewModelLocator()
{
// choix du IDataService en fonction du DesignMode
if (ViewModelBase.IsInDesignModeStatic)
{
SimpleIoc.Default.Register<IDataService, DesignDataService>();
}
else
{
SimpleIoc.Default.Register<IDataService, DataService>();
}
// MainViewModel a besoin d'un IDataService, il sera fournit par Dependency Composition car IDataService a été enregistré
SimpleIoc.Default.Register<MainViewModel>();
}
// dés-enregistre les classes et interfaces précédemment enregistrés
public static void Cleanup()
{
SimpleIoc.Default.Unregister<IDataService, DesignDataService>();
SimpleIoc.Default.Unregister<IDataService, DataService>();
SimpleIoc.Default.Unregister<MainViewModel>();
}
}
|
SimpleIoc
Cache global d'objets à injecter.
Register
|
// enregistrement de la classe MainViewModel dans le conteneur, le constructeur par défaut est utilisé
SimpleIoc.Default.Register<MainViewModel>();
// enregistrement de l'interface IDataService en spécifiant l'implémentation à utiliser (ici le constructeur par défaut de DataService)
SimpleIoc.Default.Register<IDataService, DataService>();
SimpleIoc.Default.Register<IDataService>(() => new DataService(param));
// Factory delegate, exécuté à la demande
// permet de passer des paramètres au constructeur
SimpleIoc.Default.Register<MainViewModel>(() => new MainViewModel(param));
// ou d'utiliser un objet existant
var mainViewModel = new MainViewModel();
SimpleIoc.Default.Register<MainViewModel>(() => mainViewModel);
// création immédiate de l'objet et mise en cache
SimpleIoc.Default.Register<MainViewModel>(true);
// utilisation d'une clé, ce qui permet d'avoir plusieurs instance de la même classe
SimpleIoc.Default.Register<MainViewModel>(() => new MainViewModel(), "Key1");
|
GetInstance
Création de l'instance et mise en cache ou récupération de l'instance dans le cache si elle a déjà été crée.
|
var instance = SimpleIoc.Default.GetInstance<MainViewModel>();
var service = SimpleIoc.Default.GetInstance<IDataService>();
// une exception est générée si la classe ou l'interface n'est pas ou plus enregistrée
// avec la clé
var specificInstance = SimpleIoc.Default.GetInstance<MainViewModel>("Key1");
// si aucun clé n'a été utilisé pour Register, GetInstance avec une clé va créer une nouvelle instance pour chaque clé
|
Unregister
|
// dés-enregistrement de la classe MainViewModel du conteneur et suppression de toutes les instances du cache
SimpleIoc.Default.Unregister<MainViewModel>();
// suppression de l'instance du cache mais MainViewModel n'est pas dés-enregistré
// si l'instance n'était pas enregistré il ne se passe rien
SimpleIoc.Default.Unregister<MainViewModel>(instance);
// suppression de l'instance associé à la clé du cache mais MainViewModel n'est pas dés-enregistré
// si l'instance associé à la clé n'était pas enregistré il ne se passe rien
SimpleIoc.Default.Unregister<MainViewModel>("Key1");
|
Exemple
|
public class MainViewModel
{
// Property Injection, utile si IDialogService peut être amener à changer
public IDialogService DialogService
{
get
{
return SimpleIoc.Default.GetInstance<IDialogService>();
}
}
// Constructor Injection
private readonly IDataService _dataService;
public MainViewModel(IDataService dataService)
{
_dataService = dataService;
}
|
Utility Methods
|
// vrai si IDialogService a été enregistré
bool test = SimpleIoc.Default.IsRegistered<IDialogService>();
// vrai si le cache contient une instance de IDialogService
bool test = SimpleIoc.Default.ContainsCreated<IDialogService>();
// retourne toutes les instances de IDialogService qui se trouvent dans le cache
IEnumerable<IDialogService> allInstances = SimpleIoc.Default.GetAllCreatedInstances<IDialogService>();
// force la création des toutes les instances par défaut qui ont enregistrées, puis retourne ces instances
IEnumerable<IDialogService> allInstances = SimpleIoc.Default.GetAllInstances<IDialogService>();
|
ServiceLocator
Modèle d'abstraction permettant de changer facilement d'IoC container.
|
// Définir SimpleIoc comme IoC container à utiliser
ServiceLocator.SetLocatorProvider(() => SimpleIoc.Default);
// utiliser l'IoC container courant pour récupérer une instance
ServiceLocator.Current.GetInstance<MainViewModel>();
|
Tests unitaires
MainViewModel.cs
|
public class MainViewModel : ViewModelBase
{
private readonly IDataService _dataService;
public MainViewModel(IDataService dataService) {...}
|
UnitTest.cs
|
[TestClass]
public class UnitTest
{
[TestMethod]
public void TestMethod()
{
// reset le système le Message pour éviter les interactions entre méthodes de test
Messenger.Reset();
// création d'un IDataService pour nos tests
var testDataService = new TestDataService();
var mainVM = new MainViewModel(testDataService);
mainVM.MyCommand.Execute(null);
Assert.IsTrue(mainVM.MyProperty == "value");
|
Ajout des références avec NuGet
MvvmLightLibs
- GalaSoft.MvvmLight
- GalaSoft.MvvmLight.Extra (SimpleIoc)
- Microsoft.Practices.ServiceLocation
- GalaSoft.MvvmLight.Platform (EventToCommand, IEventArgsConverter)
- System.Windows.Interactivity
Dépendance avec le paquet CommomServiceLocator
MvvmLight
- Ajoute au projet les classes:
- MainViewModel
- ViewModelLocator
- Ajoute ViewModelLocator comme une ressource dans le fichier App.xaml
Dépendance avec le paquet MvvmLightLibs
Extension Visual Studio: Mvvm Light for VS2015
- Project templates (New Projet)
- Item templates (New Item)
- Code snippets
mvvminpc |
ajoute d'une Observable Property
|
mvvmpropa |
ajoute une Attached Property Similaire à propa avec une const PropertyName en plus.
|
mvvmpropdp |
ajoute une Dependency Property. Similaire à propdp avec une const PropertyName en plus.
|
mvvmrelay |
ajoute une RelayCommand
|