MVVM Light Toolkit

De Banane Atomic
Aller à la navigationAller à la recherche
La version imprimable n’est plus prise en charge et peut comporter des erreurs de génération. Veuillez mettre à jour les signets de votre navigateur et utiliser à la place la fonction d’impression par défaut de celui-ci.

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

Csharp.svg
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

Cs.svg
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

Xaml.svg
<Button Command="{Binding MyCmd}"
        CommandParameter="{Binding ...}"/>
Csharp.svg
// 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.

Xaml.svg
<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

Xaml.svg
<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>
Csharp.svg
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.
Csharp.svg
// 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

Csharp.svg
// 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.

Csharp.svg
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

Csharp.svg
// 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

Csharp.svg
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

Csharp.svg
// 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.

Csharp.svg
// 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