Je pense faire quelque chose de révolutionnaire... disons une fenêtre en WPF avec un titre ! Elle suffira à me faire comprendre...
Bref, je vais utiliser le pattern MvvM, je ne jure que par ça, mais n'ayez crainte ceux qui ne connaissent pas, je vais garder mon exemple à la portée de tous.
Voici mon ViewModel (la donnée qui sera affichée dans ma fenêtre) :
public interface WindowViewModel
{
string Title
{
get;
}
}
Rien de sorcier, j'ai utilisé une interface pour découpler ma vue de ma couche métier qui va être accéder par l'intermédiaire d'une implémentation de cette interface. (Mediator)
On va tester ça !
Pour cela je crée un faux ViewModel :
public class FakeViewModel : WindowViewModel
{
public string Title
{
get
{
return "Hello";
}
}
}
Et je le bind à ma fenêtre.
Window1 window = new Window1();
window.ViewModel = new FakeViewModel();
window.Show();
Pour ne pas dérouter ceux qui ne sont pas habitué au C# je ne détaille pas la création de la propriété ViewModel de ma Window1, et le binding du XAML.
Pas de soucis jusqu'à ici j'ai bien Hello en titre de ma fenêtre. Je vais passer aux choses sérieuse ! Faire une vrai implémentation de ViewModel qui dialoguera avec mon Model.
Voici mon Model :
public class BusinessObject
{
private string _ShortDesc;
public event EventHandler ShortDescriptionChanged;
public BusinessObject()
{
_ShortDesc = "Description par défaut";
}
public String ShortDescription
{
get
{
Thread.Sleep(2000);
return _ShortDesc;
}
set
{
if(_ShortDesc != value)
{
_ShortDesc = value;
if(ShortDescriptionChanged != null)
ShortDescriptionChanged(this, EventArgs.Empty);
}
}
}
}
Il a une seule propriété et un événement qui se déclenche quand cette propriété se met à jour,
cependant l'accès au Get prend deux secondes.
Et mon voici ViewModel :
Il s'abonne à l'événement du changement de description de l'objet du model, et met à jour son titre en conséquence.
Je modifie l'instanciation de ma Window1 :
Tout est beau dans le meilleur des mondes, mon code fonctionne, la fenêtre s'affiche après deux secondes et deux secondes après son titre change en "Hello".
Sauf que ma fenêtre a pris deux secondes à se lancer et mon interface a freezé deux secondes de plus juste après.
Vous seriez peut être prêt à régler le problème à coup de thread/lock/BackgroundWorker et compagnie ! Je vous arrête là... je ne veux pas que mon code devienne compliqué à comprendre et subtile à débugger. Si vous avez déjà essayé de faire des tests unitaires sur une classe qui gére plusieurs threads, je compatis, ce n'est pas drôle et ça ne donne pas envie de vivre.
Notre problème vient du couplage temporelle entre mon objet métier et mon ViewModel, c'est à dire que nous supposons que les 2 objets sont dans le même thread.
Il suffit de découpler les deux objets de façon explicite dans notre design.
Nous allons accéder à notre BusinessObject via un délégué, et notre ViewModel possédera un Dispatcher courant, nous nous en serviront pour traiter les appels du thread du BusinessObject à notre ViewModel.
Voici un Dispatcher :
Et voila l'implémentation du dispatcher pour WPF, puis l'implémentation du dispatcher qui sera non couplé à WPF et synchrone (il sera utilisé dans les tests unitaires).
Maintenant voyons comment découpler temporellement nos deux classes.
L'astuce réside dans le fait que l'on accède à notre BusinessObject qu'à partir d'un Action<Action<BusinessObject>>. Si on souhaite utiliser notre BusinessObject nous devons fournir une methode à executer qui prendra le BusinessObject en paramètre. Tout le code à l'intérieur de cette méthode n'est pas garantie d'être dans le même thread que le ViewModel. On se sert du dispatcher courant de notre ViewModel pour appeler une méthode dans le même thread que notre ViewModel (ici on met à jour le titre de notre ViewModel).
En clair on utilise Action<Action<BusinessObject>> pour faire un appel du thread du ViewModel au thread du Model, et on utilise le IDispatcher pour faire un appel du thread du Model au thread ViewModel.
Il est important de ne pas ajouter une méthode à sémantique synchrone à notre interface IDispatcher, car il y aurait des risque de deadlock.
L'intérêt de cette technique est que notre ViewModel n'est pas chargé de gérer dans quel thread s'execute notre objet métier (dans notre premier exemple, il s'en chargé dans son propre thread).
De cette manière nous pouvons facilement utiliser un Dispatcher synchrone pendant les tests unitaires, et un dispatcher Wpf dans notre application.
Exemple voici notre ViewModel en test unitaire :
Pour WPF, premièrement je déclare mon action (la méthode qui déclenchera la méthode passé en paramètre dans le thread du bo), et je l'initialise dans le thread du BusinessObject :
Puis j'instancie mon ViewModel avec l'action en paramêtre du constructeur, par défaut le ViewModel utilisera le dispatcher courant de WPF, donc je n'ai pas à le spécifier.
Mission accomplie ! ;)
Et mon voici ViewModel :
public class BusinessViewModel : WindowViewModel, INotifyPropertyChanged
{
private BusinessObject _Bo;
private string _Title;
public BusinessViewModel(BusinessObject bo)
{
_Bo = bo;
_Bo.ShortDescriptionChanged += new EventHandler(_Bo_ShortDescriptionChanged);
_Title = _Bo.ShortDescription;
}
void _Bo_ShortDescriptionChanged(object sender, EventArgs e)
{
Title = ((BusinessObject)sender).ShortDescription;
}
public string Title
{
get
{
return _Title;
}
private set
{
if(value != _Title)
{
_Title = value;
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Title"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Il s'abonne à l'événement du changement de description de l'objet du model, et met à jour son titre en conséquence.
Je modifie l'instanciation de ma Window1 :
Window1 window = new Window1();
//window.ViewModel = new FakeViewModel();
BusinessObject bo = new BusinessObject();
window.ViewModel = new BusinessViewModel(bo);
window.Show();
bo.ShortDescription = "Hello";
Tout est beau dans le meilleur des mondes, mon code fonctionne, la fenêtre s'affiche après deux secondes et deux secondes après son titre change en "Hello".
Sauf que ma fenêtre a pris deux secondes à se lancer et mon interface a freezé deux secondes de plus juste après.
Vous seriez peut être prêt à régler le problème à coup de thread/lock/BackgroundWorker et compagnie ! Je vous arrête là... je ne veux pas que mon code devienne compliqué à comprendre et subtile à débugger. Si vous avez déjà essayé de faire des tests unitaires sur une classe qui gére plusieurs threads, je compatis, ce n'est pas drôle et ça ne donne pas envie de vivre.
Notre problème vient du couplage temporelle entre mon objet métier et mon ViewModel, c'est à dire que nous supposons que les 2 objets sont dans le même thread.
Il suffit de découpler les deux objets de façon explicite dans notre design.
Nous allons accéder à notre BusinessObject via un délégué, et notre ViewModel possédera un Dispatcher courant, nous nous en serviront pour traiter les appels du thread du BusinessObject à notre ViewModel.
Voici un Dispatcher :
public interface IDispatcher
{
void BeginInvoke(Delegate method, params Object[] args);
}
Et voila l'implémentation du dispatcher pour WPF, puis l'implémentation du dispatcher qui sera non couplé à WPF et synchrone (il sera utilisé dans les tests unitaires).
public class SynchronizedDispatcher : IDispatcher
{
public void BeginInvoke(Delegate method, params object[] args)
{
method.DynamicInvoke(args);
}
}
public class WpfDispatcher : IDispatcher
{
private Dispatcher _WpfDispatcher;
public WpfDispatcher(): this(null)
{
}
public WpfDispatcher(Dispatcher wpfDispatcher)
{
_WpfDispatcher = wpfDispatcher ?? Dispatcher.CurrentDispatcher;
}
public void BeginInvoke(Delegate method, params object[] args)
{
_WpfDispatcher.BeginInvoke(method, args);
}
}
Maintenant voyons comment découpler temporellement nos deux classes.
public class DispatchedBusinessViewModel : WindowViewModel, INotifyPropertyChanged
{
private String _Title;
private readonly IDispatcher _CurrentDispatcher;
private Action<Action<BusinessObject>> _Bo;
public DispatchedBusinessViewModel(Action<Action<BusinessObject>> businessObject, IDispatcher currentDispatcher)
{
_CurrentDispatcher = currentDispatcher ?? new WpfDispatcher();
_Bo = businessObject;
_Bo((bo) =>
{
string shortDesc = bo.ShortDescription;
_CurrentDispatcher.BeginInvoke(new Action(() =>
{
Title = shortDesc;
}));
bo.ShortDescriptionChanged += new EventHandler(bo_ShortDescriptionChanged);
});
}
void bo_ShortDescriptionChanged(object sender, EventArgs e)
{
BusinessObject bo = (BusinessObject)sender;
string newTitle = bo.ShortDescription;
_CurrentDispatcher.BeginInvoke(new Action(() =>
{
Title = newTitle;
}), null);
}
public DispatchedBusinessViewModel(Action<Action<BusinessObject>> businessObject)
: this(businessObject, null)
{
}
//Identique
public string Title
{
get
{
return _Title;
}
private set
{
if(value != _Title)
{
_Title = value;
if(PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("Title"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
L'astuce réside dans le fait que l'on accède à notre BusinessObject qu'à partir d'un
En clair on utilise Action<Action<BusinessObject>>
Il est important de ne pas ajouter une méthode à sémantique synchrone à notre interface IDispatcher, car il y aurait des risque de deadlock.
L'intérêt de cette technique est que notre ViewModel n'est pas chargé de gérer dans quel thread s'execute notre objet métier (dans notre premier exemple, il s'en chargé dans son propre thread).
De cette manière nous pouvons facilement utiliser un Dispatcher synchrone pendant les tests unitaires, et un dispatcher Wpf dans notre application.
Exemple voici notre ViewModel en test unitaire :
[TestMethod]
public void ShouldMapTitleToShortDescription()
{
BusinessObject bo = new BusinessObject();
Action<Action<BusinessObject>> action = (act) =>
{
act(bo);
};
bo.ShortDescription = "Desc1";
DispatchedBusinessViewModel vm = new DispatchedBusinessViewModel(action, new SynchronizedDispatcher());
Assert.AreEqual("Desc1", vm.Title);
bo.ShortDescription = "Desc2";
Assert.AreEqual("Desc2", vm.Title);
}
Pour WPF, premièrement je déclare mon action (la méthode qui déclenchera la méthode passé en paramètre dans le thread du bo), et je l'initialise dans le thread du BusinessObject :
Action<Action<BusinessObject>> action = null;
Thread boThread = new Thread(() =>
{
BusinessObject bo = new BusinessObject();
Dispatcher boDispatcher = Dispatcher.CurrentDispatcher;
action = (act) =>
{
boDispatcher.BeginInvoke(act, bo);
};
Dispatcher.Run();
});
boThread.Start();
Puis j'instancie mon ViewModel avec l'action en paramêtre du constructeur, par défaut le ViewModel utilisera le dispatcher courant de WPF, donc je n'ai pas à le spécifier.
Window1 window = new Window1();
window.ViewModel = new DispatchedBusinessViewModel(action);
action((bo) =>
{
bo.ShortDescription = "new desc";
});
Mission accomplie ! ;)
0 commentaires:
Enregistrer un commentaire